Compare commits

..

1 Commits

Author SHA1 Message Date
a.bouhuolia
813bed3676 chore: add CONTRIBUTING file 2023-04-27 01:50:09 +02:00
3186 changed files with 79590 additions and 160591 deletions

View File

@@ -1,179 +0,0 @@
{
"files": [
"README.md"
],
"imageSize": 100,
"commit": false,
"commitType": "docs",
"commitConvention": "angular",
"contributors": [
{
"login": "abouolia",
"name": "Ahmed Bouhuolia",
"avatar_url": "https://avatars.githubusercontent.com/u/2197422?v=4",
"profile": "https://github.com/abouolia",
"contributions": [
"code"
]
},
{
"login": "ameir",
"name": "Ameir Abdeldayem",
"avatar_url": "https://avatars.githubusercontent.com/u/374330?v=4",
"profile": "http://ameir.net",
"contributions": [
"bug"
]
},
{
"login": "elforjani13",
"name": "ElforJani13",
"avatar_url": "https://avatars.githubusercontent.com/u/39470382?v=4",
"profile": "https://github.com/elforjani13",
"contributions": [
"code"
]
},
{
"login": "scheibling",
"name": "Lars Scheibling",
"avatar_url": "https://avatars.githubusercontent.com/u/24367830?v=4",
"profile": "https://scheibling.se",
"contributions": [
"bug"
]
},
{
"login": "suhaibaffan",
"name": "Suhaib Affan",
"avatar_url": "https://avatars.githubusercontent.com/u/18115937?v=4",
"profile": "https://github.com/suhaibaffan",
"contributions": [
"code"
]
},
{
"login": "KalliopiPliogka",
"name": "Kalliopi Pliogka",
"avatar_url": "https://avatars.githubusercontent.com/u/81677549?v=4",
"profile": "https://github.com/KalliopiPliogka",
"contributions": [
"bug"
]
},
{
"login": "kochie",
"name": "Robert Koch",
"avatar_url": "https://avatars.githubusercontent.com/u/10809884?v=4",
"profile": "https://me.kochie.io",
"contributions": [
"code"
]
},
{
"login": "cschuijt",
"name": "Casper Schuijt",
"avatar_url": "https://avatars.githubusercontent.com/u/5460015?v=4",
"profile": "http://cschuijt.nl",
"contributions": [
"bug"
]
},
{
"login": "ANasouf",
"name": "ANasouf",
"avatar_url": "https://avatars.githubusercontent.com/u/19536487?v=4",
"profile": "https://github.com/ANasouf",
"contributions": [
"code"
]
},
{
"login": "xprnio",
"name": "Ragnar Laud",
"avatar_url": "https://avatars.githubusercontent.com/u/3042904?v=4",
"profile": "https://ragnarlaud.dev",
"contributions": [
"bug"
]
},
{
"login": "asenawritescode",
"name": "Asena",
"avatar_url": "https://avatars.githubusercontent.com/u/67445192?v=4",
"profile": "https://github.com/asenawritescode",
"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"
]
},
{
"login": "ccantrell72",
"name": "Chris Cantrell",
"avatar_url": "https://avatars.githubusercontent.com/u/104120598?v=4",
"profile": "http://www.pivoten.com",
"contributions": [
"bug"
]
},
{
"login": "oleynikd",
"name": "Denis",
"avatar_url": "https://avatars.githubusercontent.com/u/3976868?v=4",
"profile": "https://github.com/oleynikd",
"contributions": [
"bug"
]
},
{
"login": "mittalsam98",
"name": "Sachin Mittal",
"avatar_url": "https://avatars.githubusercontent.com/u/42431274?v=4",
"profile": "https://myself.vercel.app/",
"contributions": [
"bug"
]
},
{
"login": "Champetaman",
"name": "Camilo Oviedo",
"avatar_url": "https://avatars.githubusercontent.com/u/64604272?v=4",
"profile": "https://www.camilooviedo.com/",
"contributions": [
"code"
]
},
{
"login": "nklmantey",
"name": "Mantey",
"avatar_url": "https://avatars.githubusercontent.com/u/90279429?v=4",
"profile": "https://nklmantey.com/",
"contributions": [
"bug"
]
}
],
"contributorsPerLine": 7,
"skipCi": true,
"repoType": "github",
"repoHost": "https://github.com",
"projectName": "bigcapital",
"projectOwner": "bigcapitalhq"
}

View File

@@ -8,95 +8,27 @@ MAIL_FROM_NAME=
MAIL_FROM_ADDRESS= MAIL_FROM_ADDRESS=
# Database # Database
DB_HOST=localhost DB_USER=
DB_USER=bigcapital DB_HOST=
DB_PASSWORD=bigcapital DB_PASSWORD=
DB_ROOT_PASSWORD=root DB_CHARSET=
DB_CHARSET=utf8
# System database # System database
SYSTEM_DB_NAME=bigcapital_system SYSTEM_DB_NAME=bigcapital_system
# SYSTEM_DB_USER=
# SYSTEM_DB_PASSWORD=
# SYSTEM_DB_NAME=
# SYSTEM_DB_CHARSET=
# Tenant databases # Tenants databases
TENANT_DB_NAME_PERFIX=bigcapital_tenant_ TENANT_DB_NAME_PERFIX=bigcapital_tenant_
# TENANT_DB_HOST=
# TENANT_DB_USER=
# TENANT_DB_PASSWORD=
# TENANT_DB_CHARSET=
# Application # MongoDB
BASE_URL=http://example.com
JWT_SECRET=b0JDZW56RnV6aEthb0RGPXVEcUI
# Jobs MongoDB
MONGODB_DATABASE_URL=mongodb://localhost/bigcapital MONGODB_DATABASE_URL=mongodb://localhost/bigcapital
# App proxy # Authentication
PUBLIC_PROXY_PORT=80 JWT_SECRET=b0JDZW56RnV6aEthb0RGPXVEcUI
PUBLIC_PROXY_SSL_PORT=443
# Application
BASE_URL=https://bigcapital.ly
CONTACT_US_MAIL=support@bigcapital.ly
# Agendash # Agendash
AGENDASH_AUTH_USER=agendash AGENDASH_AUTH_USER=agendash
AGENDASH_AUTH_PASSWORD=123123 AGENDASH_AUTH_PASSWORD=123123
# Sign-up restrictions
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
# Gotenberg API for PDF printing - (production).
GOTENBERG_URL=http://gotenberg:3000
GOTENBERG_DOCS_URL=http://server:3000/public/
# Gotenberg API - (development)
# GOTENBERG_URL=http://localhost:9000
# GOTENBERG_DOCS_URL=http://host.docker.internal:3000/public/
# Exchange Rate Service
EXCHANGE_RATE_SERVICE=open-exchange-rate
# Open Exchange Rate
OPEN_EXCHANGE_RATE_APP_ID=
# The Plaid environment to use ('sandbox' or 'development').
# https://plaid.com/docs/#api-host
PLAID_ENV=sandbox
# Your Plaid keys, which can be found in the Plaid Dashboard.
# https://dashboard.plaid.com/account/keys
PLAID_CLIENT_ID=
PLAID_SECRET=
PLAID_LINK_WEBHOOK=
# https://docs.lemonsqueezy.com/guides/developer-guide/getting-started#create-an-api-key
LEMONSQUEEZY_API_KEY=
LEMONSQUEEZY_STORE_ID=
LEMONSQUEEZY_WEBHOOK_SECRET=
# S3 documents and attachments
S3_REGION=US
S3_ACCESS_KEY_ID=
S3_SECRET_ACCESS_KEY=
S3_ENDPOINT=
S3_BUCKET=
# PostHog
POSTHOG_API_KEY=
POSTHOG_HOST=
# Stripe Payment
STRIPE_PAYMENT_SECRET_KEY=
STRIPE_PAYMENT_PUBLISHABLE_KEY=
STRIPE_PAYMENT_CLIENT_ID=
STRIPE_PAYMENT_WEBHOOKS_SECRET=
STRIPE_PAYMENT_REDIRECT_URL=

2
.gitattributes vendored
View File

@@ -1,2 +0,0 @@
docker/nginx/scripts/build-nginx.sh text eol=lf
docker/mariadb/docker-entrypoint.sh text eol=lf

13
.github/FUNDING.yml vendored
View File

@@ -1,13 +0,0 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: Bigcapital # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

View File

@@ -6,66 +6,43 @@ on:
workflow_dispatch: workflow_dispatch:
env: env:
WEBAPP_IMAGE_NAME: bigcapitalhq/webapp REGISTRY: ghcr.io
SERVER_IMAGE_NAME: bigcapitalhq/server WEBAPP_IMAGE_NAME: bigcapital/bigcapital-webapp
SERVER_IMAGE_NAME: bigcapital/bigcapital-server
jobs: jobs:
build-publish-webapp: build-publish-webapp:
strategy:
fail-fast: false
name: Build and deploy webapp container name: Build and deploy webapp container
runs-on: ubuntu-latest runs-on: ubuntu-latest
environment: production environment: production
steps: steps:
- name: Prepare
run: |
platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
# Login to Container registry. # Login to Container registry.
- name: Log in to the Container registry - name: Log in to the Container registry
uses: docker/login-action@v3 uses: docker/login-action@v1
with: with:
username: ${{ secrets.DOCKER_USERNAME }} registry: ${{ env.REGISTRY }}
password: ${{ secrets.DOCKER_PASSWORD }} username: ${{ github.actor }}
password: ${{ secrets.GH_TOKEN }}
- name: Extract metadata (tags, labels) for Docker - name: Extract metadata (tags, labels) for Docker
id: meta id: meta
uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
with: with:
images: ${{ env.WEBAPP_IMAGE_NAME }} images: ${{ env.REGISTRY }}/${{ env.WEBAPP_IMAGE_NAME }}
# Builds and push the Docker image. # Builds and push the Docker image.
- name: Build and push Docker image - name: Build and push Docker image
uses: docker/build-push-action@v5 uses: docker/build-push-action@v2
id: build
with: with:
context: ./ context: .
file: ./packages/webapp/Dockerfile file: ./packages/webapp/Dockerfile
platforms: linux/amd64,linux/arm64 push: true
push: true tags: ghcr.io/bigcapitalhq/webapp:latest
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
tags: bigcapitalhq/webapp:latest, bigcapitalhq/webapp:${{github.ref_name}}
- 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-webapp
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1
# Send notification to Slack channel. # Send notification to Slack channel.
- name: Slack Notification built and published webapp container successfully. - name: Slack Notification built and published webapp container successfully.
uses: rtCamp/action-slack-notify@v2 uses: rtCamp/action-slack-notify@v2
@@ -76,52 +53,29 @@ jobs:
name: Build and deploy server container name: Build and deploy server container
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Prepare - name: Checkout code
run: | uses: actions/checkout@v2
platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
- name: Checkout
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
# Login to Container registry. # Login to Container registry.
- name: Log in to the Container registry - name: Log in to the Container registry
uses: docker/login-action@v3 uses: docker/login-action@v1
with: with:
username: ${{ secrets.DOCKER_USERNAME }} registry: ${{ env.REGISTRY }}
password: ${{ secrets.DOCKER_PASSWORD }} username: ${{ github.actor }}
password: ${{ secrets.GH_TOKEN }}
# Builds and push the Docker image. # Builds and push the Docker image.
- name: Build and push Docker image - name: Build and push Docker image
uses: docker/build-push-action@v5 uses: docker/build-push-action@v2
id: build
with: with:
context: ./ context: ./
file: ./packages/server/Dockerfile file: ./packages/server/Dockerfile
platforms: linux/amd64,linux/arm64
push: true push: true
tags: bigcapitalhq/server:latest, bigcapitalhq/server:${{github.ref_name}} tags: ghcr.io/bigcapitalhq/server:latest
labels: ${{ steps.meta.outputs.labels }} 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-server
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1
# Send notification to Slack channel. # Send notification to Slack channel.
- name: Slack Notification built and published server container successfully. - name: Slack Notification built and published server container successfully.
uses: rtCamp/action-slack-notify@v2 uses: rtCamp/action-slack-notify@v2
env: env:
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}

View File

@@ -1,127 +0,0 @@
# This workflow will build a docker container, publish it to Github Registry.
name: Build and Deploy Develop Docker Container
on:
push:
branches:
- develop
env:
WEBAPP_IMAGE_NAME: bigcapitalhq/webapp
SERVER_IMAGE_NAME: bigcapitalhq/server
jobs:
build-publish-webapp:
strategy:
fail-fast: false
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@v4
- 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@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
with:
images: ${{ env.WEBAPP_IMAGE_NAME }}
# Builds and push the Docker image.
- name: Build and push Docker image
uses: docker/build-push-action@v5
id: build
with:
context: ./
file: ./packages/webapp/Dockerfile
platforms: linux/amd64
push: true
labels: ${{ steps.meta.outputs.labels }}
tags: bigcapitalhq/webapp:develop
- 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-webapp
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
env:
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
build-publish-server:
name: Build and deploy server container
runs-on: ubuntu-latest
steps:
- name: Prepare
run: |
platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
- name: Checkout
uses: actions/checkout@v4
- 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@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
# Builds and push the Docker image.
- name: Build and push Docker image
uses: docker/build-push-action@v5
id: build
with:
context: ./
file: ./packages/server/Dockerfile
platforms: linux/amd64
push: true
tags: bigcapitalhq/server:develop
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-server
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
env:
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}

View File

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

7
.gitignore vendored
View File

@@ -1,9 +1,4 @@
node_modules/ node_modules/
data
# Docker volumes data directory
/data
# Production env file
.env .env
test-results/ test-results/

View File

@@ -1,22 +0,0 @@
tasks:
- name: Init
init: |
pnpm install &&
cp .env.example .env &&
docker-compose up -d &&
pnpm run build:server &&
node packages/server/build/commands.js system:migrate:latest
command: |
docker-compose up -d &&
pnpm run dev
ports:
- port: 4000
visibility: public
onOpen: open-preview
- port: 3000
visibility: public
onOpen: ignore
- port: 3306
visibility: public
onOpen: ignore

View File

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

View File

@@ -2,480 +2,6 @@
All notable changes to Bigcapital server-side will be in this file. All notable changes to Bigcapital server-side will be in this file.
# [0.20.5]
* fix: Disable tabs of the pdf customization if the first field not filed up by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/701
* fix: Invoice form layout by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/705
* refactor: Invoice, estimate, receipt, credit note and payment received date input fields by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/707
* feat: Add customize templates button to edit forms by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/708
* feat: Track account, invoice and item viewed events by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/709
# [0.20.4]
* fix: Delete company logo from the PDF template by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/699
* fix: Set max width/height to company logo of pdf templates by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/700
# [0.20.3]
* feat: Assign default PDF template automatically by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/687
* fix: pdf template addresses controlling by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/688
* fix: Remove empty lines from address formats by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/690
* fix: Pdf templates layout by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/691
* feat: Download invoice pdf of the payment link by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/689
* fix: Display country name by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/693
* feat: Add shared packages to Docker container by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/694
# [0.20.2]
* feat: Assign default PDF template automatically by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/687
* fix: pdf template addresses controlling by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/688
* fix: Remove empty lines from address formats by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/690
* fix: Pdf templates layout by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/691
* feat: Download invoice pdf of the payment link by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/689
* fix: Display country name by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/693
* feat: Add shared packages to Docker container by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/694
# [0.20.1]
* fix: Getting uploaded object uri by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/684
# [0.20.0]
* feat: Customize pdf templates by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/667
* feat: Onboard accounts to Stripe Connect by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/668
* feat: Upload company logo to invoice templates by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/670
* fix: Invoice pdf customize by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/672
* fix: Invoice customize bugs by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/673
* feat: Clean up payment links endpoints by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/674
* feat: Hook up company logo to server-side pdf templates by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/675
* feat: Company branding preferences by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/677
* feat: Pdf templates customer/company addresses by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/678
* fix: Listen to Stripe session completed event by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/679
* feat: Track pdf templates Posthog events by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/680
* fix: Branding customize content by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/681
* feat: Listen to Stripe integration events by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/682
* feat: Hook up customer/company address to invoice preview of payment page by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/683
# [0.19.17]
* fix: Un-categorize bank transactions by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/663
# [0.19.16]
* feat: Tracking more Posthog events by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/653
* fix: Expense cannot accept credit card as payment account by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/654
* fix: Suspense the lazy loaded components in banking pages by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/657
* feat: Add help dropdown menu by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/656
* feat: Bank pages layout breaking by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/658
* feat: Datatable UI improvements by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/655
* fix: Array cast of recognize function rule ids by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/660
* fix: Payment made filling the form full amount field by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/661
* feat: Tabular number of all money columns by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/659
* refactor: The expense G/L writer by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/662
## [0.19.15] -
* fix: Bank transactions infinity scrolling by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/648
* feat: Integrate multiple branches and warehouses to resource importing by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/645
* fix: Integrate multiple branches with expense resource by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/649
* feat: Cover more tracking events. by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/650
* feat: Track banking service events by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/651
## [0.19.14]
* fix: Import bugs by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/643
* fix: Set default index to transaction entries by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/644
* feat(server): Events tracking using Posthog by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/646
## [0.19.13]
* fix: Subscription middleware by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/624
* fix: Getting the sheet columns in import sheet by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/641
## [0.19.12]
* fix: Typo one-click demo page by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/640
## [0.19.11]
* fix: Avoid running the cost job in import preview by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/635
* fix: Debounce scheduling calculating items cost by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/634
* fix: Expand the resources export page size limitation by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/636
* feat: Optimize loading perf. by splitting big chunks and lazy loading them by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/632
* fix: Use standard ISO 8601 format for exported data by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/638
* fix: Add customer type to customers resource by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/639
## [0.19.10]
* fix: Add subscription plans offer text by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/629
## [0.19.9]
* fix: Make webapp package env variables dynamic by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/628
## [v0.19.8]
* fix: Cannot import items income and cost accounts by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/617
* fix: Some bank account details hidden by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/618
* feat(ee): One-click demo account by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/616
* feat: change banking service language by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/619
* feat(banking): Filter uncategorized bank transactions by date by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/590
* Fix: Syntax error caused error by @wolone in https://github.com/bigcapitalhq/bigcapital/pull/622
* fix: Listen to payment webhooks by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/623
* fix: Add prefix J-00001 to manual journals increments by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/625
* fix: Disable sms service until Twilo integration by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/626
* fix: Style tweaks in onboarding page by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/627
## [0.19.5] - 18-08-2024
* fix: Allow multi-lines to statements transactions by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/594
* feat: Add amount comparators to amount bank rule field by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/595
* fix: Transaction type and description do not show in general ledger. by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/596
* fix: Refresh accounts and account transactions. by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/597
* fix: Typo payments made by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/598
* fix: Typo categories list by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/600
* fix: Autofill the quick created customer/vendor by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/601
* fix: Remove views tabs from receipts list by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/602
* fix: Typo payment receive messages by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/599
* fix: Enhance Dropzone visual of accept and reject modes by @Champetaman in https://github.com/bigcapitalhq/bigcapital/pull/603
* fix: Matching bank transactions should create associate payment transactions by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/606
* fix: Change Dropzone title and subtitle by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/607
* fix: Inconsistance page size of paginated data tables by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/604
* fix: Database connection lost error by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/611
* fix: Language typos by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/613
* Fix: Correctly display Date, Published At, and Created At in ExpenseDrawerHeader by @Champetaman in https://github.com/bigcapitalhq/bigcapital/pull/612
* fix: Delete bank account with uncategorized transactions by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/614
* feat: activate/inactivate account from drawer details by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/615
## [v0.19.4]
* feat: Import and export tax rates by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/591
* feat: Un-categorize bank transactions in bulk by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/587
* feat: Pending bank transactions by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/589
* fix: Update `dev` Script in `package.json` to Use `cross-env` by @Champetaman in https://github.com/bigcapitalhq/bigcapital/pull/588
* fix: Should not load branches on reconcile matching form if the branches not enabled by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/592
* fix: Rounding the total amount the pending and matched transactions by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/593
## [v0.18.0] - 10-08-2024
* feat: Bank rules for automated categorization by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/511
* feat: Categorize & match bank transaction by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/511
* feat: Reconcile match transactions by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/522
* fix: Issues in matching transactions by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/523
* fix: Cashflow transactions types by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/524
## [v0.17.5] - 17-06-2024
* fix: Balance sheet and P/L nested accounts by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/501
* fix: add space between buttons on floating actions bar by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/508
* feat: Migrating to Envoy proxy instead of Nginx by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/509
* fix: Disable email confirmation does not work with invited users by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/497
* feat: Setting up the date format in the whole system dates by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/506
## [0.17.0] - 04-06-2024
### New
* feat: Upload and attach documents by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/461
* feat: Export resource tables to pdf by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/460
* feat: Build and deploy develop Docker container by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/476
* feat: Internal docker virtual network by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/478
### Fixes
* fix: Skip send confirmation email if disabled by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/459
* fix: Lemon Squeezy redirect to base url by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/479
* fix: Organize Plaid env variables for development and sandbox envs by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/480
* fix: Plaid syncs deposit imports as withdrawals by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/481
* fix: Validate the s3 configures exist by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/482
* fix: Run migrations only for initialized tenants by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/484
## [0.16.16] -
* feat: handle http exceptions by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/456
* feat: add the missing Newrelic env vars to docker-compose.prod file by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/457
* fix: add the signup email confirmation env var by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/458
## [0.16.14] -
* fix: Typo in setup wizard by @ccantrell72 in https://github.com/bigcapitalhq/bigcapital/pull/440
* fix: Showing the real mail address on email confirmation view by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/445
* fix: Auto-increment setting parsing by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/453
## [0.16.12] -
* feat: Create a manifest list for `webapp` Docker image and push it to DockerHub. by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/436
* feat: Combine arm64 and amd64 in one Github action runner by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/437
## [0.16.11] - 06-05-2024
### improvements
* feat: Export resource data to csv, xlsx by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/430
* feat: User email verification after signing-up. by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/426
### Fixes
* feat(repo): upgrade to latest lerna v8 and pnpm v9 by @benpsnyder in https://github.com/bigcapitalhq/bigcapital/pull/414
* feat: Update Docker Build-Push Action and Add ARM64 Support by @cloudsbird in https://github.com/bigcapitalhq/bigcapital/pull/412
* feat: Pushing docker containers by version tag by @cloudsbird in https://github.com/bigcapitalhq/bigcapital/pull/421
## [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
* fix: expense amounts should not be rounded by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/339
* feat: get latest exchange rate from third party services by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/340
* fix(webapp): inconsistency in currency of universal search items by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/335
* hotfix: editing sales and expense transactions don't reflect GL entries by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/342
## [0.13.3] - 22-01-2024
* hotfix(server): Unhandled thrown errors of services by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/329
## [0.13.2] - 21-01-2024
* feat: show customer / vendor balance. by @asenawritescode in https://github.com/bigcapitalhq/bigcapital/pull/311
* feat: inventory valuation csv and xlsx export by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/308
* feat: sales by items export csv & xlsx by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/310
* fix(server): the invoice and payment receipt printing by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/315
* fix: get cashflow transaction broken cause transaction type by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/318
* fix: `AccountActivateAlert` import by @xprnio in https://github.com/bigcapitalhq/bigcapital/pull/322
## [0.13.1] - 15-01-2024
* feat(webapp): add approve/reject to action bar of estimate details dr… by @ANasouf in https://github.com/bigcapitalhq/bigcapital/pull/304
* docs: add ANasouf as a contributor for code by @allcontributors in https://github.com/bigcapitalhq/bigcapital/pull/305
* feat: Export general ledger & Journal to CSV and XLSX by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/303
* feat: Auto re-calculate the items rate once changing the invoice exchange rate. by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/270
## [0.13.0] - 31-12-2023
* feat: Send an invoice mail the customer email by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/292
* fix: Allow non-numeric postal codes by @cschuijt in https://github.com/bigcapitalhq/bigcapital/pull/294
* docs: add cschuijt as a contributor for bug by @allcontributors in https://github.com/bigcapitalhq/bigcapital/pull/295
## [0.12.1] - 17-11-2023
* feat: Add default customer message and terms conditions to the transactions by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/291
* fix: The currency code of transaction tax rate entry by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/293
## [0.12.0] - 04-11-2023
* feat: Export reports via CSV and XLSX by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/286
* fix: Axios upgrade by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/288
* fix(server): Allow decimal amount in sale/purchase transactions. by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/289
* feat: Optimize invoice documents printing by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/280
* chore(deps): bump axios from 0.20.0 to 1.6.0 in /packages/server by @dependabot in https://github.com/bigcapitalhq/bigcapital/pull/284
* chore(deps): bump axios from 0.20.0 to 1.6.0 by @dependabot in https://github.com/bigcapitalhq/bigcapital/pull/283
## [0.11.0] - 28-10-2023
* feat: Migrate to pnpm by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/253
* feat: Integrate tax rates to bills by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/260
* feat: Assign default sell/purchase tax rates to items by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/261
* chore(deps-dev): bump @babel/traverse from 7.23.0 to 7.23.2 in /packages/server by @dependabot in https://github.com/bigcapitalhq/bigcapital/pull/272
* feat: Improve financial statements rows color by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/276
* fix: Trial balance sheet adjusted balance by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/273
* feat: Adds tax numbers to organization and customers by @kochie in https://github.com/bigcapitalhq/bigcapital/pull/269
* docs: Add kochie as a contributor for code by @allcontributors in https://github.com/bigcapitalhq/bigcapital/pull/277
* feat: Computed Net Income under Equity in Balance Sheet report. by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/271
* fix: Change Dockerfile files with new pnpm by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/278
## [0.10.2] - 02-10-2023
fix(webapp): Disable tax rates from item entries editor table services do not support tax rates (https://github.com/bigcapitalhq/bigcapital/commit/69afa07e3ba45495a4cab3490c15f2b0c40c4790) by @abouolia
fix(server): Add missing method in ItemEntry model (https://github.com/bigcapitalhq/bigcapital/commit/07628ddc37f46c98959ced0323f28752e0a98944) by @abouolia
## [0.10.1] - 25-09-2023
* Fix: Running tenants migration on Docker migration container by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/242
## [0.10.0] - 24-09-2023
* Added: Tax rates service by @abouolia @elforjani13 in https://github.com/bigcapitalhq/bigcapital/pull/204
* Added: Sales Tax Liability Summary report by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/204
* Added: Tax rates tracking with sale invoices by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/204
* fix(webapp): Table headers sticky for all reports. by @elforjani13 in https://github.com/bigcapitalhq/bigcapital/pull/240
* chore(deps): bump word-wrap from 1.2.3 to 1.2.4 by @dependabot in https://github.com/bigcapitalhq/bigcapital/pull/200
* chore(deps): bump word-wrap from 1.2.3 to 1.2.4 in /packages/webapp by @dependabot in https://github.com/bigcapitalhq/bigcapital/pull/199
* chore(deps): bump mongoose from 5.13.15 to 5.13.20 by @dependabot in https://github.com/bigcapitalhq/bigcapital/pull/197
## [0.9.12] - 29-08-2023
* Refactor: split the services to multiple service classes. (by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/202)
* Fix: create quick customer/vendor by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/206
* Fix: typo in bill success message without bill number by @KalliopiPliogka in https://github.com/bigcapitalhq/bigcapital/pull/219
* Fix: AP/AR aging summary issue by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/229
* Fix: shouldn't write GL entries when save transaction as draft. by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/221
* Fix: Transaction type of credit note and vendor credit are not defined on account transactions by @abouolia in
* Fix: date format of filtering transactions by date range by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/231
* Fix: change the default from/date date value of reports by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/230
* Fix: typos in words start with `A` letter by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/227
* Fix: filter by customers, vendors and items in reports do not work by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/224
https://github.com/bigcapitalhq/bigcapital/pull/225
## [0.9.11] - 23-07-2023
* added: Restart policy to docker compose files. by @suhaibaffan in https://github.com/bigcapitalhq/bigcapital/pull/198
* fix: Expose and expand the rate limit to the env variables by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/195
## [0.9.10] - 18-07-2023
* feat(e2e): E2E onboarding process by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/176
* fix(webapp): Show loading message of cost computing job on financial reports by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/196
* fix(webapp): Change the currency code of sales and purchases transactions with foreign contacts.
## [0.9.9] - 28-06-2023
* refactor: Customer and vendor select component by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/171
* chore: Move auto-increment components in separate files by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/170
* fix: Style of quick item drawer by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/173
* fix: Should not show the form before loading account by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/172
* fix: Payment made form does not handle not unique number an e… by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/177
* fix: Internal note of invoice/bill payment does not saving by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/181
* fix: Storing cash flow transaction description by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/180
* fix: No currency in amount field on money in/out dialogs by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/179
* fix: No default branch for customer/vendor opening balance branch by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/182
## [0.9.8] - 19-06-2023
`bigcapitalhq/webapp`
* add: Inventory Adjustment option to the item drawer by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/158
* fix: use all drawers names from common enum object by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/157
* fix: adjustment type options do not show up by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/159
* fix: change the remove line text to be red to intent as a danger action by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/162
* fix: rename sidebar localization keys names to be keyword path by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/161
* fix: manual journal placeholder text by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/160
* fix: warehouses select component by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/168
`bigcapitalhq/server`
* fix: sending emails on reset password and registration by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/167
## [0.9.7] - 14-06-2023
`@bigcapital/webapp`
* fix: change the footer links of onboarding pages by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/139
`@bigcapital/server`
* fix: expense transaction journal entries by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/155
## [0.9.6] - 12-06-2023
`@bigcapital/webapp`
* fix: remove duplicated form submitting by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/138
* feat: add monorepo version on the application sidebar by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/136
## [0.9.5] - 11-06-2023
`@bigcapital/server`
* fix: filter ledger entries that effect contact balance to AR/AP accounts only by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/132
`@bigcapital/webapp`
* fix: catch journal error when create a journal with accounts have different currency by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/135
* fix: add duplicate icon to context menu of customers and vendors table by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/133
* fix: customer/vendor opening balance with exchange rate by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/134
## [0.9.4] - 08-06-2023
`@bigcapital/monorepo`
- fixed: docker-compose line-ending issue on Windows by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/130
`@bigcapital/server`
- fixed: Disable Webpack minification for JS class name reading.
## [0.9.3] -04-06-2023
`@bigcapital/monorepo`
* Added: Add env variable to customize the proxy public ports by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/125
* Added: Migrate the server database to MariaDB by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/128
## [0.9.2] - 31-05-2023
`@bigcapital/webapp`
- fixed: move `packaeg-lock.json` inside docker container.
- fixed: remove Sentry from the web client.
## [0.9.1] - 28-05-2023
`@bigcapital/server`
- fix: deleting ledger entries of manual journal.
- fix: base currency should be enabled.
- fix: delete invoice transaction issue.
`@bigcapital/webapp`
- fix: general, accountant and items preferences.
- fix: auto-increment sale invoices, estiamtes, credit notes, payments and manual journals.
- refactor: the setup organization form to use binded Formik components.
## [0.9.0] - 06-05-2023
`@bigcapital/server`
- [Sign-up restrictions](https://docs.bigcapital.ly/docs/deployment/signup_restriction) for self-hosting instances to disable signup or control the allowed email addresses and domains that can sign-up.
## [0.8.3] - 06-04-2023 ## [0.8.3] - 06-04-2023
`@bigcaptial/monorepo` `@bigcaptial/monorepo`

View File

@@ -7,7 +7,6 @@ Please read through this document before submitting any issues or pull requests
## Sections ## Sections
- [General Instructions](#general-instructions) - [General Instructions](#general-instructions)
- [Local Setup Prerequisites](#local-setup-prerequisites)
- [Contribute to Backend](#contribute-to-backend) - [Contribute to Backend](#contribute-to-backend)
- [Contribute to Frontend](#contribute-to-frontend) - [Contribute to Frontend](#contribute-to-frontend)
- [Other Ways to Contribute](#other-ways-to-contribute) - [Other Ways to Contribute](#other-ways-to-contribute)
@@ -32,23 +31,14 @@ Contributions via pull requests are much appreciated. Once the approach is agree
--- ---
## Local Setup Prerequisites
- The application currently supports **Node.js v18.x**.
- `pnpm` packages manager, (from pnpm [guide](https://pnpm.io/installation) pick any installation method).
## Contribute to Backend ## Contribute to Backend
- Clone the `bigcapital` repository and `cd` into `bigcapital` directory. - Clone the `bigcapital` repository and `cd` into `bigcapital` directory.
- Create `.env` file by copying `.env.example` file to `.env`. (The ``.env.example`` file has all the necessary values of variables to start development directly).
```
cp .env.example .env
```
- Install all npm dependencies of the monorepo, you don't have to change directory to the `backend` package. just hit the command on root directory and it will install dependencies of all packages. - Install all npm dependencies of the monorepo, you don't have to change directory to the `backend` package. just hit the command on root directory and it will install dependencies of all packages.
``` ```
pnpm install npm install
npm run bootstrap
``` ```
- Run all required docker containers in the development, we already configured all containers under `docker-compose.yml`. - Run all required docker containers in the development, we already configured all containers under `docker-compose.yml`.
@@ -57,7 +47,7 @@ pnpm install
docker-compose up -d docker-compose up -d
``` ```
Wait some seconds, and hit `docker-compose ps` and you should see the same result below. Wait some seconds, and hit `docker-compose ps` to see the result and you should see the same result below.
``` ```
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
@@ -69,7 +59,7 @@ cefa73fe2881 bigcapital-redis "docker-entrypoint.s…" 7 seconds ago Up
- There're some CLI commands we should run before running the server like databaase migration, so we need to build the `server` app first. - There're some CLI commands we should run before running the server like databaase migration, so we need to build the `server` app first.
``` ```
pnpm run build:server npm run build:server
``` ```
- Run the database migration for system database. - Run the database migration for system database.
@@ -87,7 +77,7 @@ Batch 1 run: 6 migrations
- Next, start the webapp application. - Next, start the webapp application.
``` ```
pnpm run dev:server npm run dev:server
``` ```
**[`^top^`](#)** **[`^top^`](#)**
@@ -105,13 +95,14 @@ git clone https://github.com/bigcapital/bigcapital.git && cd bigcaptial
- Install all npm dependencies of the monorepo, you don't have to change directory to the `frontend` package. just hit that command and will install all packages across all application. - Install all npm dependencies of the monorepo, you don't have to change directory to the `frontend` package. just hit that command and will install all packages across all application.
``` ```
pnpm install npm install
npm run bootstrap
``` ```
- Next, start the webapp application. - Next, start the webapp application.
``` ```
pnpm run dev:webapp npm run dev:webapp
``` ```
**[`^top^`](#)** **[`^top^`](#)**
@@ -131,7 +122,7 @@ There are many other ways to get involved with the community and to participate
- Use the product, submitting GitHub issues when a problem is found. - Use the product, submitting GitHub issues when a problem is found.
- Help code review pull requests and participate in issue threads. - Help code review pull requests and participate in issue threads.
- Submit a new feature request as an issue. - Submit a new feature request as an issue.
- Help answer questions on forums such as Bigcapital Community Discord Channel. - Help answer questions on forums such as Stack Overflow and SigNoz Community Slack Channel.
- Tell others about the project on Twitter, your blog, etc. - Tell others about the project on Twitter, your blog, etc.
**[`^top^`](#)** **[`^top^`](#)**

123
README.md
View File

@@ -1,37 +1,12 @@
<p align="center"> <p align="center">
<p align="center"> <p align="center">
<a href="https://bigcapital.app" target="_blank"> <a href="https://bigcapital.ly" target="_blank">
<img src="https://raw.githubusercontent.com/abouolia/blog/main/public/bigcapital.svg" alt="Bigcapital" width="280" height="75"> <img src="https://raw.githubusercontent.com/abouolia/blog/main/public/bigcapital.svg" alt="Bigcapital" width="280" height="75">
</a> </a>
</p> </p>
<p align="center"> <p align="center">
Simple, smart online accounting software for small and medium businesses. Simple, smart online accounting software for small and medium businesses.
</p> </p>
<p align="center">
<a href="https://github.com/bigcapitalhq/bigcapital/commits/develop">
<img src="https://img.shields.io/github/commit-activity/m/bigcapitalhq/bigcapital/develop" />
</a>
<a href="https://hub.docker.com/u/bigcapitalhq">
<img src="https://img.shields.io/docker/pulls/bigcapitalhq/webapp" />
</a>
<a href="https://discord.com/invite/c8nPBJafeb">
<img src="https://img.shields.io/discord/1066514716752625725?label=Discord" alt="" />
</a>
<a href="https://github.com/bigcapitalhq/bigcapital/graphs/contributors">
<img src="https://img.shields.io/github/contributors/bigcapitalhq/bigcapital" alt="" />
</a>
<a href="https://github.com/bigcapitalhq/bigcapital/blob/develop/LICENSE">
<img src="https://img.shields.io/github/license/bigcapitalhq/bigcapital" alt="" />
</a>
<a href="https://twitter.com/bigcapitalhq">
<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> </p>
# What's Bigcapital? # What's Bigcapital?
@@ -44,103 +19,13 @@ Bigcapital is a smart and open-source accounting and inventory software, Bigcapi
<img src="https://raw.githubusercontent.com/abouolia/blog/main/public/screenshot-3.png" width="270"> <img src="https://raw.githubusercontent.com/abouolia/blog/main/public/screenshot-3.png" width="270">
</p> </p>
# Getting Started
We've got serveral options on dev and prod depending on your need to get started quickly with Bigcapital.
## Self-hosted
Bigcapital is available open-source under AGPL license. You can host it on your own servers using Docker.
### 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
### Local Setup
To get started locally, we have a [guide to help you](https://github.com/bigcapitalhq/bigcapital/blob/develop/CONTRIBUTING.md).
### Gitpod
- Click the Gitpod button below to open this project in development mode.
- This will open and configure the workspace in your browser with all the necessary dependencies.
[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/new/#https://github.com/bigcapitalhq/bigcapital)
## Headless Accounting
You can integrate Bigcapital API with your system to organize your transactions in double-entry system to get the best financial reports.
[![Run in Postman](https://run.pstmn.io/button.svg)](https://www.postman.com/bigcapital/workspace/bigcapital-api)
# Resources # Resources
- [Documentation](https://docs.bigcapital.app/) - Learn how to use. - [Documentation](https://docs.bigcapital.ly/) - 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. - [Discord](https://discord.com/invite/c8nPBJafeb) - Ask for help.
- [Bug Tracker](https://github.com/bigcapitalhq/bigcapital/issues) - Notify us new bugs. - [Bug Tracker](https://github.com/bigcapitalhq/bigcapital/issues) - Notify us new bugs.
- [Source Code](https://github.com/bigcapitalhq/bigcapital) - Github repo.
# Changelog # Changlog
Please see [Releases](https://github.com/bigcapitalhq/bigcapital/releases) for more information what has changed recently. Please see [Releases](https://github.com/bigcapitalhq/bigcapital/releases) for more information what has changed recently.
# Contact us
Meet our sales team for any commercial inquiries.
<a target="_blank" href="https://cal.com/ahmed-bouhuolia-ekk3ph/30min"><img src="https://cal.com/book-with-cal-dark.svg" alt="Book us with Cal.com"></a>
# Recognition
<a href="https://news.ycombinator.com/item?id=36118990">
<img
style="width: 250px; height: 54px;" width="250" height="54"
alt="Featured on Hacker News"
src="https://hackernews-badge.vercel.app/api?id=36118990"
/>
</a>
# Contributors
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<table>
<tbody>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/abouolia"><img src="https://avatars.githubusercontent.com/u/2197422?v=4?s=100" width="100px;" alt="Ahmed Bouhuolia"/><br /><sub><b>Ahmed Bouhuolia</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/commits?author=abouolia" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://ameir.net"><img src="https://avatars.githubusercontent.com/u/374330?v=4?s=100" width="100px;" alt="Ameir Abdeldayem"/><br /><sub><b>Ameir Abdeldayem</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/issues?q=author%3Aameir" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/elforjani13"><img src="https://avatars.githubusercontent.com/u/39470382?v=4?s=100" width="100px;" alt="ElforJani13"/><br /><sub><b>ElforJani13</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/commits?author=elforjani13" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://scheibling.se"><img src="https://avatars.githubusercontent.com/u/24367830?v=4?s=100" width="100px;" alt="Lars Scheibling"/><br /><sub><b>Lars Scheibling</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/issues?q=author%3Ascheibling" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/suhaibaffan"><img src="https://avatars.githubusercontent.com/u/18115937?v=4?s=100" width="100px;" alt="Suhaib Affan"/><br /><sub><b>Suhaib Affan</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/commits?author=suhaibaffan" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/KalliopiPliogka"><img src="https://avatars.githubusercontent.com/u/81677549?v=4?s=100" width="100px;" alt="Kalliopi Pliogka"/><br /><sub><b>Kalliopi Pliogka</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/issues?q=author%3AKalliopiPliogka" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://me.kochie.io"><img src="https://avatars.githubusercontent.com/u/10809884?v=4?s=100" width="100px;" alt="Robert Koch"/><br /><sub><b>Robert Koch</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/commits?author=kochie" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="http://cschuijt.nl"><img src="https://avatars.githubusercontent.com/u/5460015?v=4?s=100" width="100px;" alt="Casper Schuijt"/><br /><sub><b>Casper Schuijt</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/issues?q=author%3Acschuijt" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ANasouf"><img src="https://avatars.githubusercontent.com/u/19536487?v=4?s=100" width="100px;" alt="ANasouf"/><br /><sub><b>ANasouf</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/commits?author=ANasouf" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://ragnarlaud.dev"><img src="https://avatars.githubusercontent.com/u/3042904?v=4?s=100" width="100px;" alt="Ragnar Laud"/><br /><sub><b>Ragnar Laud</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/issues?q=author%3Axprnio" title="Bug reports">🐛</a></td>
<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>
<td align="center" valign="top" width="14.28%"><a href="http://www.pivoten.com"><img src="https://avatars.githubusercontent.com/u/104120598?v=4?s=100" width="100px;" alt="Chris Cantrell"/><br /><sub><b>Chris Cantrell</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/issues?q=author%3Accantrell72" title="Bug reports">🐛</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/oleynikd"><img src="https://avatars.githubusercontent.com/u/3976868?v=4?s=100" width="100px;" alt="Denis"/><br /><sub><b>Denis</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/issues?q=author%3Aoleynikd" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://myself.vercel.app/"><img src="https://avatars.githubusercontent.com/u/42431274?v=4?s=100" width="100px;" alt="Sachin Mittal"/><br /><sub><b>Sachin Mittal</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/issues?q=author%3Amittalsam98" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://www.camilooviedo.com/"><img src="https://avatars.githubusercontent.com/u/64604272?v=4?s=100" width="100px;" alt="Camilo Oviedo"/><br /><sub><b>Camilo Oviedo</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/commits?author=Champetaman" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://nklmantey.com/"><img src="https://avatars.githubusercontent.com/u/90279429?v=4?s=100" width="100px;" alt="Mantey"/><br /><sub><b>Mantey</b></sub></a><br /><a href="https://github.com/bigcapitalhq/bigcapital/issues?q=author%3Anklmantey" title="Bug reports">🐛</a></td>
</tr>
</tbody>
</table>
<!-- markdownlint-restore -->
<!-- prettier-ignore-end -->
<!-- ALL-CONTRIBUTORS-LIST:END -->
This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!

View File

@@ -3,33 +3,32 @@
version: '3.3' version: '3.3'
services: services:
proxy: nginx:
image: envoyproxy/envoy:v1.30-latest container_name: bigcapital-nginx-gateway
build:
context: ./docker/nginx
args:
- SERVER_PROXY_PORT=3000
- WEB_SSL=false
- SELF_SIGNED=false
volumes:
- ./data/logs/nginx/:/var/log/nginx
- ./docker/certbot/certs/:/var/certs
ports:
- "80:80"
- "443:443"
tty: true
depends_on: depends_on:
- server - server
- webapp - webapp
ports:
- '${PUBLIC_PROXY_PORT:-80}:80'
- '${PUBLIC_PROXY_SSL_PORT:-443}:443'
tty: true
volumes:
- ./docker/envoy/envoy.yaml:/etc/envoy/envoy.yaml
restart: on-failure
networks:
- bigcapital_network
webapp: webapp:
container_name: bigcapital-webapp container_name: bigcapital-webapp
image: bigcapitalhq/webapp:latest image: ghcr.io/bigcapitalhq/webapp:latest
restart: on-failure
networks:
- bigcapital_network
server: server:
container_name: bigcapital-server container_name: bigcapital-server
image: bigcapitalhq/server:latest image: ghcr.io/bigcapitalhq/server:latest
expose:
- '3000'
links: links:
- mysql - mysql
- mongo - mongo
@@ -38,9 +37,6 @@ services:
- mysql - mysql
- mongo - mongo
- redis - redis
restart: on-failure
networks:
- bigcapital_network
environment: environment:
# Mail # Mail
- MAIL_HOST=${MAIL_HOST} - MAIL_HOST=${MAIL_HOST}
@@ -66,7 +62,7 @@ services:
# Authentication # Authentication
- JWT_SECRET=${JWT_SECRET} - JWT_SECRET=${JWT_SECRET}
# MongoDB # MongoDB
- MONGODB_DATABASE_URL=mongodb://mongo/bigcapital - MONGODB_DATABASE_URL=mongodb://mongo/bigcapital
# Application # Application
@@ -76,118 +72,50 @@ services:
- AGENDASH_AUTH_USER=${AGENDASH_AUTH_USER} - AGENDASH_AUTH_USER=${AGENDASH_AUTH_USER}
- AGENDASH_AUTH_PASSWORD=${AGENDASH_AUTH_PASSWORD} - AGENDASH_AUTH_PASSWORD=${AGENDASH_AUTH_PASSWORD}
# Sign-up restrictions
- SIGNUP_DISABLED=${SIGNUP_DISABLED}
- SIGNUP_ALLOWED_DOMAINS=${SIGNUP_ALLOWED_DOMAINS}
- SIGNUP_ALLOWED_EMAILS=${SIGNUP_ALLOWED_EMAILS}
# Sign-up email confirmation
- SIGNUP_EMAIL_CONFIRMATION=${SIGNUP_EMAIL_CONFIRMATION}
# Gotenberg (Pdf generator)
- GOTENBERG_URL=${GOTENBERG_URL}
- GOTENBERG_DOCS_URL=${GOTENBERG_DOCS_URL}
# Exchange Rate
- EXCHANGE_RATE_SERVICE=${EXCHANGE_RATE_SERVICE}
- OPEN_EXCHANGE_RATE_APP_ID-${OPEN_EXCHANGE_RATE_APP_ID}
# Bank Sync
- BANKING_CONNECT=${BANKING_CONNECT}
# Plaid
- PLAID_ENV=${PLAID_ENV}
- PLAID_CLIENT_ID=${PLAID_CLIENT_ID}
- PLAID_SECRET=${PLAID_SECRET}
- 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}
- NEW_RELIC_LICENSE_KEY=${NEW_RELIC_LICENSE_KEY}
- NEW_RELIC_APP_NAME=${NEW_RELIC_APP_NAME}
# S3
- S3_REGION=${S3_REGION}
- S3_ACCESS_KEY_ID=${S3_ACCESS_KEY_ID}
- S3_SECRET_ACCESS_KEY=${S3_SECRET_ACCESS_KEY}
- S3_ENDPOINT=${S3_ENDPOINT}
- S3_BUCKET=${S3_BUCKET}
database_migration: database_migration:
container_name: bigcapital-database-migration container_name: bigcapital-database-migration
build: build:
context: ./ context: ./
dockerfile: docker/migration/Dockerfile dockerfile: docker/migration/Dockerfile
environment: environment:
# Database
- DB_HOST=mysql - DB_HOST=mysql
- DB_USER=${DB_USER} - DB_USER=${DB_USER}
- DB_PASSWORD=${DB_PASSWORD} - DB_PASSWORD=${DB_PASSWORD}
- DB_CHARSET=${DB_CHARSET} - DB_CHARSET=${DB_CHARSET}
- SYSTEM_DB_NAME=${SYSTEM_DB_NAME} - SYSTEM_DB_NAME=${SYSTEM_DB_NAME}
# Tenants databases
- TENANT_DB_NAME_PERFIX=${TENANT_DB_NAME_PERFIX}
depends_on: depends_on:
- mysql - mysql
networks:
- bigcapital_network
mysql: mysql:
container_name: bigcapital-mysql container_name: bigcapital-mysql
restart: on-failure
build: build:
context: ./docker/mariadb context: ./docker/mysql
environment: environment:
- MYSQL_DATABASE=${SYSTEM_DB_NAME} - MYSQL_DATABASE=${SYSTEM_DB_NAME}
- MYSQL_USER=${DB_USER} - MYSQL_USER=${DB_USER}
- MYSQL_PASSWORD=${DB_PASSWORD} - MYSQL_PASSWORD=${DB_PASSWORD}
- MYSQL_ROOT_PASSWORD=${DB_ROOT_PASSWORD} - MYSQL_ROOT_PASSWORD=${DB_PASSWORD}
volumes: volumes:
- mysql:/var/lib/mysql - mysql:/var/lib/mysql
expose: expose:
- '3306' - '3306'
networks:
- bigcapital_network
mongo: mongo:
container_name: bigcapital-mongo container_name: bigcapital-mongo
restart: on-failure
build: ./docker/mongo build: ./docker/mongo
expose: expose:
- '27017' - '27017'
volumes: volumes:
- mongo:/var/lib/mongodb - mongo:/var/lib/mongodb
networks:
- bigcapital_network
redis: redis:
container_name: bigcapital-redis container_name: bigcapital-redis
restart: on-failure
build: build:
context: ./docker/redis context: ./docker/redis
expose: expose:
- '6379' - "6379"
volumes: volumes:
- redis:/data - redis:/data
networks:
- bigcapital_network
gotenberg:
image: gotenberg/gotenberg:7
expose:
- '9000'
networks:
- bigcapital_network
# Volumes # Volumes
volumes: volumes:
@@ -202,8 +130,3 @@ volumes:
redis: redis:
name: bigcapital_prod_redis name: bigcapital_prod_redis
driver: local driver: local
# Networks
networks:
bigcapital_network:
driver: bridge

View File

@@ -6,23 +6,20 @@
version: '3.3' version: '3.3'
services: services:
mariadb: mysql:
build: build:
context: ./docker/mariadb context: ./docker/mysql
environment: environment:
- MYSQL_DATABASE=${SYSTEM_DB_NAME} - MYSQL_DATABASE=${SYSTEM_DB_NAME}
- MYSQL_USER=${DB_USER} - MYSQL_USER=${DB_USER}
- MYSQL_PASSWORD=${DB_PASSWORD} - MYSQL_PASSWORD=${DB_PASSWORD}
- MYSQL_ROOT_PASSWORD=${DB_ROOT_PASSWORD} - MYSQL_ROOT_PASSWORD=${DB_PASSWORD}
volumes: volumes:
- mysql:/var/lib/mysql - mysql:/var/lib/mysql
expose: expose:
- '3306' - '3306'
ports: ports:
- '3306:3306' - '3306:3306'
deploy:
restart_policy:
condition: unless-stopped
mongo: mongo:
build: ./docker/mongo build: ./docker/mongo
@@ -32,9 +29,6 @@ services:
- mongo:/var/lib/mongodb - mongo:/var/lib/mongodb
ports: ports:
- '27017:27017' - '27017:27017'
deploy:
restart_policy:
condition: unless-stopped
redis: redis:
build: build:
@@ -43,14 +37,6 @@ services:
- "6379" - "6379"
volumes: volumes:
- redis:/data - redis:/data
deploy:
restart_policy:
condition: unless-stopped
gotenberg:
image: gotenberg/gotenberg:7
ports:
- "9000:3000"
# Volumes # Volumes
volumes: volumes:

View File

@@ -1,62 +0,0 @@
static_resources:
listeners:
- name: listener_0
address:
socket_address:
address: 0.0.0.0
port_value: 80
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
'@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
stat_prefix: ingress_http
route_config:
name: local_route
virtual_hosts:
- name: backend
domains: ['*']
routes:
- match:
prefix: '/api'
route:
cluster: dynamic_server
- match:
prefix: '/'
route:
cluster: webapp
http_filters:
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
clusters:
- name: dynamic_server
connect_timeout: 0.25s
type: STRICT_DNS
dns_lookup_family: V4_ONLY
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: dynamic_server
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: server
port_value: 3000
- name: webapp
connect_timeout: 0.25s
type: STRICT_DNS
dns_lookup_family: V4_ONLY
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: webapp
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: webapp
port_value: 80

View File

@@ -1,4 +1,4 @@
FROM bigcapitalhq/server:latest as build FROM ghcr.io/bigcapitalhq/server:latest as build
ARG DB_HOST= \ ARG DB_HOST= \
DB_USER= \ DB_USER= \
@@ -35,4 +35,4 @@ WORKDIR /app/packages/server
RUN git clone https://github.com/vishnubob/wait-for-it.git RUN git clone https://github.com/vishnubob/wait-for-it.git
# Once we listen the mysql port run the migration task. # 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" CMD ["./wait-for-it/wait-for-it.sh", "mysql:3306", "--", "node", "./build/commands.js", "system:migrate:latest"]

View File

@@ -1,4 +1,4 @@
FROM mariadb:10.2 FROM mysql:5.7
USER root USER root
ADD my.cnf /etc/mysql/conf.d/my.cnf ADD my.cnf /etc/mysql/conf.d/my.cnf
@@ -17,7 +17,7 @@ ENV MYSQL_ROOT_PASSWORD=$MYSQL_ROOT_PASSWORD
COPY ./init.sql /scripts/init.template.sql COPY ./init.sql /scripts/init.template.sql
COPY ./docker-entrypoint.sh /docker-entrypoint-initdb.d/docker-initialize.sh COPY ./docker-entrypoint.sh /docker-entrypoint-initdb.d/docker-initialize.sh
# The scripts in the `docker-entrypoint-initdb.d/` directory are executed as # The scripts in the docker-entrypoint-initdb.d/ directory are executed as
# the mysql user inside the MySQL Docker container. # the mysql user inside the MySQL Docker container.
RUN chown -R mysql:root /docker-entrypoint-initdb.d RUN chown -R mysql:root /docker-entrypoint-initdb.d
RUN chown -R mysql:root /scripts RUN chown -R mysql:root /scripts

View File

@@ -1,3 +1,2 @@
GRANT ALL PRIVILEGES ON *.* TO '{MYSQL_USER}'@'%' IDENTIFIED BY '{MYSQL_PASSWORD}' WITH GRANT OPTION; GRANT ALL PRIVILEGES ON *.* TO '{MYSQL_USER}'@'%' IDENTIFIED BY '{MYSQL_PASSWORD}' WITH GRANT OPTION;
FLUSH PRIVILEGES; FLUSH PRIVILEGES;

21
docker/nginx/Dockerfile Normal file
View File

@@ -0,0 +1,21 @@
FROM nginx:1.11
RUN mkdir /etc/nginx/sites-available && rm /etc/nginx/conf.d/default.conf
ADD nginx.conf /etc/nginx/
COPY scripts /root/scripts/
COPY certs /etc/ssl/
COPY sites /etc/nginx/templates
ARG SERVER_PROXY_PORT=3000
ARG WEB_SSL=false
ARG SELF_SIGNED=false
ENV SERVER_PROXY_PORT=$SERVER_PROXY_PORT
ENV WEB_SSL=$WEB_SSL
ENV SELF_SIGNED=$SELF_SIGNED
RUN /bin/bash /root/scripts/build-nginx.sh
CMD nginx

View File

33
docker/nginx/nginx.conf Normal file
View File

@@ -0,0 +1,33 @@
user www-data;
worker_processes auto;
pid /run/nginx.pid;
daemon off;
events {
worker_connections 2048;
use epoll;
}
http {
server_tokens off;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 15;
types_hash_max_size 2048;
client_max_body_size 20M;
open_file_cache max=100;
gzip on;
gzip_disable "msie6";
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers 'ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS';
include /etc/nginx/mime.types;
default_type application/octet-stream;
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-available/*;
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
}

View File

@@ -0,0 +1,9 @@
#!/bin/bash
for conf in /etc/nginx/templates/*.conf; do
mv $conf "/etc/nginx/sites-available/"$(basename $conf) > /dev/null
done
for template in /etc/nginx/templates/*.template; do
envsubst < $template > "/etc/nginx/sites-available/"$(basename $template)".conf"
done

View File

@@ -0,0 +1,16 @@
server {
listen 80 default_server;
location /api {
proxy_pass http://server:${SERVER_PROXY_PORT};
}
location / {
proxy_pass http://webapp;
}
location /.well-known/acme-challenge/ {
root /var/www/letsencrypt/;
log_not_found off;
}
}

View File

@@ -1,13 +0,0 @@
import { Page } from '@playwright/test';
export const clearLocalStorage = (page: Page) => {
return page.evaluate(`window.localStorage.clear()`);
};
export const defaultPageConfig = () => {
return {
extraHTTPHeaders: {
'ngrok-skip-browser-warning': 'any-value',
},
};
};

View File

@@ -1,23 +1,14 @@
import { test, expect, Page } from '@playwright/test'; import { test, expect, Page } from '@playwright/test';
import { faker } from '@faker-js/faker';
import { clearLocalStorage, defaultPageConfig } from './_utils';
let authPage: Page; let authPage: Page;
test.describe('authentication', () => { test.describe('authentication', () => {
test.beforeAll(async ({ browser }) => { test.beforeAll(async ({ browser }) => {
authPage = await browser.newPage({ ...defaultPageConfig() }); authPage = await browser.newPage();
});
test.afterAll(async () => {
await authPage.close();
});
test.afterEach(async ({ context }) => {
context.clearCookies();
await clearLocalStorage(authPage);
}); });
test.describe('login', () => { test.describe('login', () => {
test.beforeEach(async () => { test.beforeAll(async () => {
await authPage.goto('/auth/login'); await authPage.goto('/auth/login');
}); });
test('should show the login page.', async () => { test('should show the login page.', async () => {
@@ -39,23 +30,10 @@ test.describe('authentication', () => {
await authPage.getByRole('link', { name: 'Sign up' }).click(); await authPage.getByRole('link', { name: 'Sign up' }).click();
await expect(authPage.url()).toContain('/auth/register'); await expect(authPage.url()).toContain('/auth/register');
}); });
test('should the email or password is not correct.', async () => {
await authPage.getByLabel('Email Address').click();
await authPage.getByLabel('Email Address').fill(faker.internet.email());
await authPage.getByLabel('Password').click();
await authPage.getByLabel('Password').fill(faker.internet.password());
await authPage.getByRole('button', { name: 'Log in' }).click();
await expect(authPage.locator('body')).toContainText(
'The email and password you entered did not match our records.'
);
});
}); });
test.describe('register', () => { test.describe('register', () => {
test.beforeEach(async () => { test.beforeAll(async () => {
await authPage.goto('/auth/register'); await authPage.goto('/auth/register');
}); });
test('should first name, last name, email and password be required.', async () => { test('should first name, last name, email and password be required.', async () => {
@@ -74,36 +52,10 @@ test.describe('authentication', () => {
'Password is a required field' 'Password is a required field'
); );
}); });
test('should signup successfully.', async () => {
const form = authPage.locator('form');
await form.getByLabel('First Name').click();
await form.getByLabel('First Name').fill(faker.person.firstName());
await form.getByLabel('Email').click();
await form.getByLabel('Email').fill(faker.internet.email());
await form.getByLabel('Last Name').click();
await form.getByLabel('Last Name').fill(faker.person.lastName());
await form.getByLabel('Password').click();
await form.getByLabel('Password').fill(faker.internet.password());
await authPage.getByRole('button', { name: 'Register' }).click();
await expect(authPage.locator('h1')).toContainText(
'Register a New Organization now!'
);
});
}); });
test.describe('reset password', () => { test.describe('reset password', () => {
test.beforeAll(async ({ browser }) => { test.beforeAll(async () => {
authPage = await browser.newPage({ ...defaultPageConfig() });
});
test.afterAll(async () => {
await authPage.close();
});
test.beforeEach(async () => {
await authPage.goto('/auth/send_reset_password'); await authPage.goto('/auth/send_reset_password');
}); });
test('should email be required.', async () => { test('should email be required.', async () => {

View File

@@ -1,7 +0,0 @@
import { test, expect, Page } from '@playwright/test';
test.describe('item', () => {
test('should validate all required fields.', () => {});
test('should save the item successfully.', () => {});
test('should item code be unqiue.', () => {});
});

View File

@@ -1,86 +0,0 @@
import { test, expect, Page } from '@playwright/test';
import { faker } from '@faker-js/faker';
import { defaultPageConfig } from './_utils';
let authPage: Page;
let businessLegalName: string = faker.company.name();
test.describe('onboarding', () => {
test.beforeAll(async ({ browser }) => {
authPage = await browser.newPage({ ...defaultPageConfig() });
await authPage.goto('/auth/register');
const form = authPage.locator('form');
await form.getByLabel('First Name').fill(faker.person.firstName());
await form.getByLabel('Email').fill(faker.internet.email());
await form.getByLabel('Last Name').fill(faker.person.lastName());
await form.getByLabel('Password').fill(faker.internet.password());
await authPage.getByRole('button', { name: 'Register' }).click();
});
test('should validation catch all required fields', async () => {
const form = authPage.locator('form');
await authPage.getByRole('button', { name: 'Save & Continue' }).click();
await expect(form).toContainText('Organization name is a required field');
await expect(form).toContainText('Location is a required field');
await expect(form).toContainText('Base currency is a required field');
await expect(form).toContainText('Fiscal year is a required field');
await expect(form).toContainText('Time zone is a required field');
});
test.describe('after onboarding', () => {
test.beforeAll(async () => {
await authPage.getByLabel('Legal Organization Name').click();
await authPage
.getByLabel('Legal Organization Name')
.fill(businessLegalName);
// Fill Business Location.
await authPage
.getByRole('button', { name: 'Select Business Location...' })
.click();
await authPage.locator('a').filter({ hasText: 'Albania' }).click();
// Fill Base Currency.
await authPage
.getByRole('button', { name: 'Select Base Currency...' })
.click();
await authPage
.locator('a')
.filter({ hasText: 'AED - United Arab Emirates Dirham' })
.click();
// Fill Fasical Year.
await authPage
.getByRole('button', { name: 'Select Fiscal Year...' })
.click();
await authPage.locator('a').filter({ hasText: 'June - May' }).click();
// Fill Timezone.
await authPage
.getByRole('button', { name: 'Select Time Zone...' })
.click();
await authPage.getByText('Pacific/Marquesas-09:30').click();
// Click on Submit button
await authPage.getByRole('button', { name: 'Save & Continue' }).click();
});
test('should onboarding process success', async () => {
await expect(authPage.locator('body')).toContainText(
'Congrats! You are ready to go',
{
timeout: 30000,
}
);
});
test('should go to the dashboard after clicking on "Go to dashboard" button.', async () => {
await authPage.getByRole('button', { name: 'Go to dashboard' }).click();
await expect(
authPage.locator('[data-testId="dashboard-topbar"] h1')
).toContainText(businessLegalName);
});
});
});

View File

@@ -1,9 +1,6 @@
{ {
"$schema": "node_modules/lerna/schemas/lerna-schema.json", "$schema": "node_modules/lerna/schemas/lerna-schema.json",
"version": "independent", "useWorkspaces": true,
"npmClient": "pnpm", "version": "0.0.0",
"packages": [ "npmClient": "npm"
"packages/*",
"shared/*"
]
} }

5720
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -2,35 +2,35 @@
"name": "bigcapital-monorepo", "name": "bigcapital-monorepo",
"private": true, "private": true,
"scripts": { "scripts": {
"bootstrap": "lerna exec npm install",
"dev": "lerna run dev", "dev": "lerna run dev",
"build": "lerna run build", "build": "lerna run build",
"dev:webapp": "lerna run dev --scope \"@bigcapital/webapp\" --scope \"@bigcapital/utils\" --scope \"@bigcapital/pdf-templates\"", "dev:webapp": "lerna run dev --scope \"@bigcapital/webapp\"",
"build:webapp": "lerna run build --scope \"@bigcapital/webapp\" --scope \"@bigcapital/utils\" --scope \"@bigcapital/pdf-templates\"", "build:webapp": "lerna run build --scope \"@bigcapital/webapp\"",
"dev:server": "lerna run dev --scope \"@bigcapital/server\" --scope \"@bigcapital/utils\" --scope \"@bigcapital/pdf-templates\" --scope \"@bigcapital/email-components\"", "dev:server": "lerna run dev --scope \"@bigcapital/server\"",
"build:server": "lerna run build --scope \"@bigcapital/server\" --scope \"@bigcapital/utils\" --scope \"@bigcapital/pdf-templates\" --scope \"@bigcapital/email-components\"", "build:server": "lerna run build --scope \"@bigcapital/server\"",
"serve:server": "lerna run serve --scope \"@bigcapital/server\" --scope \"@bigcapital/utils\"", "serve:server": "lerna run serve --scope \"@bigcapital/server\"",
"test:e2e": "playwright test", "test:e2e": "playwright test",
"prepare": "husky install" "prepare": "husky install"
}, },
"workspaces": [
"packages/*",
"shared/*"
],
"devDependencies": { "devDependencies": {
"@commitlint/cli": "^17.4.2",
"@commitlint/config-conventional": "^17.4.2", "@commitlint/config-conventional": "^17.4.2",
"@commitlint/config-lerna-scopes": "^17.4.2", "@commitlint/config-lerna-scopes": "^17.4.2",
"@faker-js/faker": "^8.0.2",
"@playwright/test": "^1.32.3",
"husky": "^8.0.3", "husky": "^8.0.3",
"lerna": "^8.1.2", "lerna": "^6.4.1",
"pnpm": "^9.0.5" "@commitlint/cli": "^17.4.2",
"@playwright/test": "^1.32.3"
}, },
"engines": { "engines": {
"node": "16.x || 17.x || 18.x" "node": "14.x"
}, },
"husky": { "husky": {
"hooks": { "hooks": {
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS" "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
} }
},
"dependencies": {
"tsup": "^8.3.0"
} }
} }

View File

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

View File

@@ -1,4 +1,4 @@
FROM node:18.16.0-alpine as build FROM node:14.20-alpine as build
USER root USER root
@@ -34,11 +34,7 @@ ARG MAIL_HOST= \
BASE_URL= \ BASE_URL= \
# Agendash # Agendash
AGENDASH_AUTH_USER=agendash \ AGENDASH_AUTH_USER=agendash \
AGENDASH_AUTH_PASSWORD=123123 \ AGENDASH_AUTH_PASSWORD=123123
# Sign-up restriction
SIGNUP_DISABLED= \
SIGNUP_ALLOWED_DOMAINS= \
SIGNUP_ALLOWED_EMAILS=
ENV MAIL_HOST=$MAIL_HOST \ ENV MAIL_HOST=$MAIL_HOST \
MAIL_USERNAME=$MAIL_USERNAME \ MAIL_USERNAME=$MAIL_USERNAME \
@@ -72,39 +68,26 @@ ENV MAIL_HOST=$MAIL_HOST \
# MongoDB # MongoDB
MONGODB_DATABASE_URL=$MONGODB_DATABASE_URL \ MONGODB_DATABASE_URL=$MONGODB_DATABASE_URL \
# Application # Application
BASE_URL=$BASE_URL \ BASE_URL=$BASE_URL
# Sign-up restriction
SIGNUP_DISABLED=$SIGNUP_DISABLED \
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. # Create app directory.
WORKDIR /app WORKDIR /app
RUN chown node:node / RUN chown node:node /
# Install pnpm
RUN npm install -g pnpm
# Copy application dependency manifests to the container image. # Copy application dependency manifests to the container image.
COPY --chown=node:node ./ ./ COPY ./package*.json ./
COPY ./packages/server/package*.json ./packages/server/
# Install application dependencies COPY ./lerna.json ./lerna.json
RUN apk update
RUN apk add python3 build-base chromium
# Set PYHTON env # Install app dependencies for production.
ENV PYTHON=/usr/bin/python3 RUN npm install
RUN npm run bootstrap
# Install packages dependencies for production.
RUN pnpm install
COPY --chown=node:node ./packages/server ./packages/server COPY --chown=node:node ./packages/server ./packages/server
# # Creates a "dist" folder with the production build # # Creates a "dist" folder with the production build
RUN pnpm run build:server --skip-nx-cache RUN npm run build:server --skip-nx-cache
CMD [ "node", "./packages/server/build/index.js" ] CMD [ "node", "./packages/server/build/index.js" ]

View File

@@ -1,6 +1,6 @@
{ {
"name": "@bigcapital/server", "name": "@bigcapital/server",
"version": "0.10.2", "version": "1.7.1",
"description": "", "description": "",
"main": "src/server.ts", "main": "src/server.ts",
"scripts": { "scripts": {
@@ -20,32 +20,21 @@
"bigcapital": "./bin/bigcapital.js" "bigcapital": "./bin/bigcapital.js"
}, },
"dependencies": { "dependencies": {
"@bigcapital/utils": "*",
"@bigcapital/email-components": "*",
"@bigcapital/pdf-templates": "*",
"@aws-sdk/client-s3": "^3.576.0",
"@aws-sdk/s3-request-presigner": "^3.583.0",
"@casl/ability": "^5.4.3", "@casl/ability": "^5.4.3",
"@hapi/boom": "^7.4.3", "@hapi/boom": "^7.4.3",
"@lemonsqueezy/lemonsqueezy.js": "^2.2.0",
"@supercharge/promise-pool": "^3.2.0",
"@types/express": "^4.17.21",
"@types/i18n": "^0.8.7", "@types/i18n": "^0.8.7",
"@types/knex": "^0.16.1", "@types/knex": "^0.16.1",
"@types/mathjs": "^6.0.12", "@types/mathjs": "^6.0.12",
"@types/yup": "^0.29.13",
"accepts": "^1.3.7", "accepts": "^1.3.7",
"accounting": "^0.4.1", "accounting": "^0.4.1",
"agenda": "^4.2.1", "agenda": "^4.2.1",
"agendash": "^3.1.0", "agendash": "^3.1.0",
"app-root-path": "^3.0.0", "app-root-path": "^3.0.0",
"async": "^3.2.0", "async": "^3.2.0",
"async-mutex": "^0.5.0", "axios": "^0.20.0",
"axios": "^1.6.0",
"babel-loader": "^9.1.2", "babel-loader": "^9.1.2",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"bluebird": "^3.7.2", "bluebird": "^3.7.2",
"body-parser": "^1.20.2",
"compression": "^1.7.4", "compression": "^1.7.4",
"country-codes-list": "^1.6.8", "country-codes-list": "^1.6.8",
"cpy": "^8.1.2", "cpy": "^8.1.2",
@@ -53,7 +42,7 @@
"crypto-random-string": "^3.2.0", "crypto-random-string": "^3.2.0",
"csurf": "^1.10.0", "csurf": "^1.10.0",
"deep-map": "^2.0.0", "deep-map": "^2.0.0",
"deepdash": "^5.3.9", "deepdash": "^5.3.7",
"dotenv": "^8.1.0", "dotenv": "^8.1.0",
"errorhandler": "^1.5.1", "errorhandler": "^1.5.1",
"es6-weak-map": "^2.0.3", "es6-weak-map": "^2.0.3",
@@ -63,9 +52,9 @@
"express": "^4.17.1", "express": "^4.17.1",
"express-basic-auth": "^1.2.0", "express-basic-auth": "^1.2.0",
"express-boom": "^3.0.0", "express-boom": "^3.0.0",
"express-fileupload": "^1.1.7-alpha.3",
"express-oauth-server": "^2.0.0", "express-oauth-server": "^2.0.0",
"express-validator": "^6.12.2", "express-validator": "^6.12.2",
"form-data": "^4.0.0",
"gulp": "^4.0.2", "gulp": "^4.0.2",
"gulp-sass": "^5.0.0", "gulp-sass": "^5.0.0",
"helmet": "^3.21.0", "helmet": "^3.21.0",
@@ -73,25 +62,20 @@
"is-my-json-valid": "^2.20.5", "is-my-json-valid": "^2.20.5",
"js-money": "^0.6.3", "js-money": "^0.6.3",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"knex": "^3.1.0", "knex": "^0.95.15",
"knex-cleaner": "^1.3.0", "knex-cleaner": "^1.3.0",
"knex-db-manager": "^0.6.1",
"libphonenumber-js": "^1.9.6", "libphonenumber-js": "^1.9.6",
"lodash": "^4.17.15", "lodash": "^4.17.15",
"lru-cache": "^6.0.0", "lru-cache": "^6.0.0",
"mathjs": "^9.4.0", "mathjs": "^9.4.0",
"memory-cache": "^0.2.0", "memory-cache": "^0.2.0",
"mime-types": "^2.1.35",
"moment": "^2.24.0", "moment": "^2.24.0",
"moment-range": "^4.0.2", "moment-range": "^4.0.2",
"moment-timezone": "^0.5.43",
"mongodb": "^6.1.0",
"mongoose": "^5.10.0", "mongoose": "^5.10.0",
"multer": "1.4.5-lts.1",
"multer-s3": "^3.0.1",
"mustache": "^3.0.3", "mustache": "^3.0.3",
"mysql": "^2.17.1", "mysql": "^2.17.1",
"mysql2": "^1.6.5", "mysql2": "^1.6.5",
"newrelic": "^11.15.0",
"node-cache": "^4.2.1", "node-cache": "^4.2.1",
"nodemailer": "^6.3.0", "nodemailer": "^6.3.0",
"nodemon": "^1.19.1", "nodemon": "^1.19.1",
@@ -100,9 +84,7 @@
"objection-filter": "^4.0.1", "objection-filter": "^4.0.1",
"objection-soft-delete": "^1.0.7", "objection-soft-delete": "^1.0.7",
"objection-unique": "^1.2.2", "objection-unique": "^1.2.2",
"plaid": "^10.3.0",
"pluralize": "^8.0.0", "pluralize": "^8.0.0",
"posthog-node": "^4.2.0",
"pug": "^3.0.2", "pug": "^3.0.2",
"puppeteer": "^10.2.0", "puppeteer": "^10.2.0",
"qim": "0.0.52", "qim": "0.0.52",
@@ -110,22 +92,14 @@
"rate-limiter-flexible": "^2.1.14", "rate-limiter-flexible": "^2.1.14",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"rtl-detect": "^1.0.4", "rtl-detect": "^1.0.4",
"socket.io": "^4.7.4",
"source-map-loader": "^4.0.1",
"stripe": "^16.10.0",
"tmp-promise": "^3.0.3",
"ts-transformer-keys": "^0.4.2", "ts-transformer-keys": "^0.4.2",
"tsyringe": "^4.3.0", "tsyringe": "^4.3.0",
"typedi": "^0.8.0", "typedi": "^0.8.0",
"uniqid": "^5.2.0", "uniqid": "^5.2.0",
"uuid": "^10.0.0", "winston": "^3.2.1"
"winston": "^3.2.1",
"xlsx": "^0.18.5",
"yup": "^0.28.1"
}, },
"devDependencies": { "devDependencies": {
"@types/lodash": "^4.14.158", "@types/lodash": "^4.14.158",
"@types/multer": "^1.4.11",
"@types/ramda": "^0.27.64", "@types/ramda": "^0.27.64",
"@typescript-eslint/eslint-plugin": "^5.50.0", "@typescript-eslint/eslint-plugin": "^5.50.0",
"@typescript-eslint/parser": "^5.50.0", "@typescript-eslint/parser": "^5.50.0",
@@ -157,7 +131,7 @@
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"rtlcss": "^3.3.0", "rtlcss": "^3.3.0",
"run-script-webpack-plugin": "^0.1.1", "run-script-webpack-plugin": "^0.1.1",
"sass": "^1.58.0", "sass": "^1.37.5",
"sinon": "^7.4.2", "sinon": "^7.4.2",
"start-server-webpack-plugin": "^2.2.5", "start-server-webpack-plugin": "^2.2.5",
"ts-loader": "^9.4.2", "ts-loader": "^9.4.2",

Binary file not shown.

Binary file not shown.

View File

@@ -152,7 +152,7 @@
"Opening Balance Liabilities": "رصيد الالتزامات الافتتاحي", "Opening Balance Liabilities": "رصيد الالتزامات الافتتاحي",
"Loan": "اقراض", "Loan": "اقراض",
"Owner A Drawings": "مسحوبات المالك", "Owner A Drawings": "مسحوبات المالك",
"An account that holds valuation of products or goods that available for sale.": "حساب يحمل قيم مخزون البضاعة أو السلع المتاحة للبيع.", "An account that holds valuation of products or goods that availiable for sale.": "حساب يحمل قيم مخزون البضاعة أو السلع المتاحة للبيع.",
"Tracks the gain and losses of the exchange differences.": "يسجل مكاسب وخسائر فروق الصرف.", "Tracks the gain and losses of the exchange differences.": "يسجل مكاسب وخسائر فروق الصرف.",
"Any bank fees levied is recorded into the bank fees and charges account. A bank account maintenance fee, transaction charges, a late payment fee are some examples.": "يتم تسجيل أي رسوم مصرفية يتم فرضها في حساب الرسوم والمصروفات البنكية. ومن الأمثلة على ذلك رسوم صيانة الحساب المصرفي ورسوم المعاملات ورسوم الدفع المتأخر.", "Any bank fees levied is recorded into the bank fees and charges account. A bank account maintenance fee, transaction charges, a late payment fee are some examples.": "يتم تسجيل أي رسوم مصرفية يتم فرضها في حساب الرسوم والمصروفات البنكية. ومن الأمثلة على ذلك رسوم صيانة الحساب المصرفي ورسوم المعاملات ورسوم الدفع المتأخر.",
"The income activities are not associated to the core business.": "لا ترتبط انشطة الدخل إلى الأعمال الأساسية.", "The income activities are not associated to the core business.": "لا ترتبط انشطة الدخل إلى الأعمال الأساسية.",
@@ -242,7 +242,7 @@
"account.field.normal.credit": "دائن", "account.field.normal.credit": "دائن",
"account.field.normal.debit": "مدين", "account.field.normal.debit": "مدين",
"account.field.type": "نوع الحساب", "account.field.type": "نوع الحساب",
"account.field.active": "Active", "account.field.active": "Activity",
"account.field.balance": "الرصيد", "account.field.balance": "الرصيد",
"account.field.created_at": "أنشئت في", "account.field.created_at": "أنشئت في",
"item.field.type": "نوع الصنف", "item.field.type": "نوع الصنف",

View File

@@ -151,7 +151,7 @@
"Opening Balance Liabilities": "Opening Balance Liabilities", "Opening Balance Liabilities": "Opening Balance Liabilities",
"Loan": "Loan", "Loan": "Loan",
"Owner A Drawings": "Owner A Drawings", "Owner A Drawings": "Owner A Drawings",
"An account that holds valuation of products or goods that available for sale.": "An account that holds valuation of products or goods that available for sale.", "An account that holds valuation of products or goods that availiable for sale.": "An account that holds valuation of products or goods that availiable for sale.",
"Tracks the gain and losses of the exchange differences.": "Tracks the gain and losses of the exchange differences.", "Tracks the gain and losses of the exchange differences.": "Tracks the gain and losses of the exchange differences.",
"Any bank fees levied is recorded into the bank fees and charges account. A bank account maintenance fee, transaction charges, a late payment fee are some examples.": "Any bank fees levied is recorded into the bank fees and charges account. A bank account maintenance fee, transaction charges, a late payment fee are some examples.", "Any bank fees levied is recorded into the bank fees and charges account. A bank account maintenance fee, transaction charges, a late payment fee are some examples.": "Any bank fees levied is recorded into the bank fees and charges account. A bank account maintenance fee, transaction charges, a late payment fee are some examples.",
"The income activities are not associated to the core business.": "The income activities are not associated to the core business.", "The income activities are not associated to the core business.": "The income activities are not associated to the core business.",
@@ -176,7 +176,6 @@
"invoice.paper.conditions_title": "Conditions & terms", "invoice.paper.conditions_title": "Conditions & terms",
"invoice.paper.notes_title": "Notes", "invoice.paper.notes_title": "Notes",
"invoice.paper.total": "Total", "invoice.paper.total": "Total",
"invoice.paper.subtotal": "Subtotal",
"invoice.paper.payment_amount": "Payment Amount", "invoice.paper.payment_amount": "Payment Amount",
"invoice.paper.balance_due": "Balance Due", "invoice.paper.balance_due": "Balance Due",
@@ -241,32 +240,28 @@
"account.field.normal.credit": "Credit", "account.field.normal.credit": "Credit",
"account.field.normal.debit": "Debit", "account.field.normal.debit": "Debit",
"account.field.type": "Type", "account.field.type": "Type",
"account.field.active": "Active", "account.field.active": "Activity",
"account.field.currency": "Currency",
"account.field.balance": "Balance", "account.field.balance": "Balance",
"account.field.bank_balance": "Bank Balance",
"account.field.parent_account": "Parent Account",
"account.field.created_at": "Created at", "account.field.created_at": "Created at",
"item.field.type": "Item Type", "item.field.type": "Item type",
"item.field.type.inventory": "Inventory", "item.field.type.inventory": "Inventory",
"item.field.type.service": "Service", "item.field.type.service": "Service",
"item.field.type.non-inventory": "Non Inventory", "item.field.type.non-inventory": "Non inventory",
"item.field.name": "Item Name", "item.field.name": "Name",
"item.field.code": "Item Code", "item.field.code": "Code",
"item.field.sellable": "Sellable", "item.field.sellable": "Sellable",
"item.field.purchasable": "Purchasable", "item.field.purchasable": "Purchasable",
"item.field.cost_price": "Cost Price", "item.field.cost_price": "Cost price",
"item.field.sell_price": "Sell Price", "item.field.cost_account": "Cost account",
"item.field.cost_account": "Cost Account", "item.field.sell_account": "Sell account",
"item.field.sell_account": "Sell Account", "item.field.sell_description": "Sell description",
"item.field.sell_description": "Sell Description", "item.field.inventory_account": "Inventory account",
"item.field.inventory_account": "Inventory Account", "item.field.purchase_description": "Purchase description",
"item.field.purchase_description": "Purchase Description", "item.field.quantity_on_hand": "Quantity on hand",
"item.field.quantity_on_hand": "Quantity on Hand",
"item.field.note": "Note", "item.field.note": "Note",
"item.field.category": "Category", "item.field.category": "Category",
"item.field.active": "Active", "item.field.active": "Active",
"item.field.created_at": "Created At", "item.field.created_at": "Created at",
"item_category.field.name": "Name", "item_category.field.name": "Name",
"item_category.field.description": "Description", "item_category.field.description": "Description",
"item_category.field.count": "Count", "item_category.field.count": "Count",
@@ -279,14 +274,8 @@
"invoice.field.invoice_message": "Invoice message", "invoice.field.invoice_message": "Invoice message",
"invoice.field.terms_conditions": "Terms & conditions", "invoice.field.terms_conditions": "Terms & conditions",
"invoice.field.amount": "Amount", "invoice.field.amount": "Amount",
"invoice.field.exchange_rate": "Exchange Rate",
"invoice.field.payment_amount": "Payment amount", "invoice.field.payment_amount": "Payment amount",
"invoice.field.due_amount": "Due 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": "Status",
"invoice.field.status.paid": "Paid", "invoice.field.status.paid": "Paid",
"invoice.field.status.partially-paid": "Partially paid", "invoice.field.status.partially-paid": "Partially paid",
@@ -295,8 +284,6 @@
"invoice.field.status.delivered": "Delivered", "invoice.field.status.delivered": "Delivered",
"invoice.field.status.draft": "Draft", "invoice.field.status.draft": "Draft",
"invoice.field.created_at": "Created at", "invoice.field.created_at": "Created at",
"invoice.field.currency": "Currency",
"invoice.field.entries": "Entries",
"estimate.field.amount": "Amount", "estimate.field.amount": "Amount",
"estimate.field.estimate_number": "Estimate number", "estimate.field.estimate_number": "Estimate number",
"estimate.field.customer": "Customer", "estimate.field.customer": "Customer",
@@ -311,31 +298,22 @@
"estimate.field.status.approved": "Approved", "estimate.field.status.approved": "Approved",
"estimate.field.status.draft": "Draft", "estimate.field.status.draft": "Draft",
"estimate.field.created_at": "Created at", "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.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.payment_receive_no": "Payment receive No.",
"payment_receive.field.statement": "Statement", "payment_receive.field.statement": "Statement",
"payment_receive.field.created_at": "Created at", "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.vendor": "Vendor",
"bill_payment.field.amount": "Amount", "bill_payment.field.amount": "Amount",
"bill_payment.field.due_amount": "Due Amount", "bill_payment.field.due_amount": "Due amount",
"bill_payment.field.payment_account": "Payment Account", "bill_payment.field.payment_account": "Payment account",
"bill_payment.field.payment_number": "Payment No.", "bill_payment.field.payment_number": "Payment number",
"bill_payment.field.payment_date": "Payment Date", "bill_payment.field.payment_date": "Payment date",
"bill_payment.field.reference_no": "Reference No.", "bill_payment.field.reference_no": "Reference No.",
"bill_payment.field.description": "Description", "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_payment.field.created_at": "Created at",
"bill.field.vendor": "Vendor", "bill.field.vendor": "Vendor",
"bill.field.bill_number": "Bill number", "bill.field.bill_number": "Bill number",
@@ -363,30 +341,22 @@
"inventory_adjustment.field.description": "Description", "inventory_adjustment.field.description": "Description",
"inventory_adjustment.field.published_at": "Published at", "inventory_adjustment.field.published_at": "Published at",
"inventory_adjustment.field.created_at": "Created at", "inventory_adjustment.field.created_at": "Created at",
"expense.field.payment_date": "Payment Date", "expense.field.payment_date": "Payment date",
"expense.field.payment_account": "Payment Account", "expense.field.payment_account": "Payment account",
"expense.field.amount": "Amount", "expense.field.amount": "Amount",
"expense.field.currency_code": "Currency",
"expense.field.exchange_rate": "Exchange Rate",
"expense.field.reference_no": "Reference No.", "expense.field.reference_no": "Reference No.",
"expense.field.description": "Description", "expense.field.description": "Description",
"expense.field.line_description": "Line Description",
"expense.field.published": "Published", "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": "Status",
"expense.field.status.draft": "Draft", "expense.field.status.draft": "Draft",
"expense.field.status.published": "Published", "expense.field.status.published": "Published",
"expense.field.created_at": "Created at", "expense.field.created_at": "Created at",
"manual_journal.field.date": "Date", "manual_journal.field.date": "Date",
"manual_journal.field.journal_number": "Journal No.", "manual_journal.field.journal_number": "Journal number",
"manual_journal.field.reference": "Reference 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.amount": "Amount",
"manual_journal.field.description": "Description", "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.status": "Status",
"manual_journal.field.created_at": "Created at", "manual_journal.field.created_at": "Created at",
"receipt.field.amount": "Amount", "receipt.field.amount": "Amount",
@@ -405,8 +375,8 @@
"customer.field.last_name": "Last name", "customer.field.last_name": "Last name",
"customer.field.display_name": "Display name", "customer.field.display_name": "Display name",
"customer.field.email": "Email", "customer.field.email": "Email",
"customer.field.work_phone": "Work Phone Number", "customer.field.work_phone": "Work phone",
"customer.field.personal_phone": "Personal Phone Number", "customer.field.personal_phone": "Personal phone",
"customer.field.company_name": "Company name", "customer.field.company_name": "Company name",
"customer.field.website": "Website", "customer.field.website": "Website",
"customer.field.opening_balance_at": "Opening balance at", "customer.field.opening_balance_at": "Opening balance at",
@@ -414,7 +384,7 @@
"customer.field.created_at": "Created at", "customer.field.created_at": "Created at",
"customer.field.balance": "Balance", "customer.field.balance": "Balance",
"customer.field.status": "Status", "customer.field.status": "Status",
"customer.field.currency": "Currency", "customer.field.currency": "Curreny",
"customer.field.status.active": "Active", "customer.field.status.active": "Active",
"customer.field.status.inactive": "Inactive", "customer.field.status.inactive": "Inactive",
"customer.field.status.overdue": "Overdue", "customer.field.status.overdue": "Overdue",
@@ -423,8 +393,8 @@
"vendor.field.last_name": "Last name", "vendor.field.last_name": "Last name",
"vendor.field.display_name": "Display name", "vendor.field.display_name": "Display name",
"vendor.field.email": "Email", "vendor.field.email": "Email",
"vendor.field.work_phone": "Work Phone Number", "vendor.field.work_phone": "Work phone",
"vendor.field.personal_phone": "Personal Phone Number", "vendor.field.personal_phone": "Personal phone",
"vendor.field.company_name": "Company name", "vendor.field.company_name": "Company name",
"vendor.field.website": "Website", "vendor.field.website": "Website",
"vendor.field.opening_balance_at": "Opening balance at", "vendor.field.opening_balance_at": "Opening balance at",
@@ -432,16 +402,13 @@
"vendor.field.created_at": "Created at", "vendor.field.created_at": "Created at",
"vendor.field.balance": "Balance", "vendor.field.balance": "Balance",
"vendor.field.status": "Status", "vendor.field.status": "Status",
"vendor.field.note": "Note", "vendor.field.currency": "Curreny",
"vendor.field.currency": "Currency",
"vendor.field.status.active": "Active", "vendor.field.status.active": "Active",
"vendor.field.status.inactive": "Inactive", "vendor.field.status.inactive": "Inactive",
"vendor.field.status.overdue": "Overdue", "vendor.field.status.overdue": "Overdue",
"vendor.field.status.unpaid": "Unpaid", "vendor.field.status.unpaid": "Unpaid",
"Invoice write-off": "Invoice write-off", "Invoice write-off": "Invoice write-off",
"transaction_type.credit_note": "Credit note", "transaction_type.credit_note": "Credit note",
"transaction_type.refund_credit_note": "Refund credit note", "transaction_type.refund_credit_note": "Refund credit note",
"transaction_type.vendor_credit": "Vendor credit", "transaction_type.vendor_credit": "Vendor credit",
@@ -620,7 +587,6 @@
"balance_sheet.long_term_liabilities": "Long-Term Liabilities", "balance_sheet.long_term_liabilities": "Long-Term Liabilities",
"balance_sheet.non_current_liabilities": "Non-Current Liabilities", "balance_sheet.non_current_liabilities": "Non-Current Liabilities",
"balance_sheet.equity": "Equity", "balance_sheet.equity": "Equity",
"balance_sheet.net_income": "Net Income",
"balance_sheet.account_name": "Account name", "balance_sheet.account_name": "Account name",
"balance_sheet.total": "Total", "balance_sheet.total": "Total",

View File

@@ -1,40 +0,0 @@
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100..900;1,100..900&display=swap');
*,
*::before,
*::after {
box-sizing: border-box;
}
th {
text-align: inherit;
text-align: -webkit-match-parent;
}
thead,
tbody,
tfoot,
tr,
td,
th {
border-color: inherit;
border-style: solid;
border-width: 0;
}
body{
margin: 0;
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: #000;
background-color: #fff;
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: transparent;
}
body, h1, h2, h3, h4, h5, h6{
font-family: "Noto Sans", sans-serif;
font-optical-sizing: auto;
font-style: normal;
}

View File

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

View File

@@ -1,38 +0,0 @@
@import "../base.scss";
body {
font-family: system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue","Noto Sans","Liberation Sans",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";
font-size: 12px;
line-height: 1.4;
margin: 0;
}
.sheet__title{
margin-bottom: 18px;
}
.sheet__title h2{
line-height: 1;
margin-top: 0;
margin-bottom: 10px;
font-size: 16px;
}
.sheet__table {
font-size: inherit;
line-height: inherit;
width: 100%;
}
.sheet__table {
table-layout: auto;
border-collapse: collapse;
width: 100%;
}
.sheet__table thead tr th {
border-top: 1px solid #000;
border-bottom: 1px solid #000;
background: #fff;
padding: 8px;
line-height: 1.2;
}
.sheet__table tbody tr td {
padding: 4px 8px;
border-bottom: 1px solid #CCC;
}

View File

@@ -1,57 +0,0 @@
@import "../base.scss";
html,
body {
font-size: 14px;
}
body{
font-weight: 400;
letter-spacing: 0;
line-height: 1.28581;
text-transform: none;
color: #000;
font-family: Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Open Sans, Helvetica Neue, Icons16, sans-serif;
}
.sheet{
padding: 20px;
}
.sheet__company-name{
margin: 0;
font-size: 1.4rem;
}
.sheet__sheet-type {
margin: 0
}
.sheet__sheet-date {
margin-top: 0.35rem;
}
.sheet__header {
text-align: center;
margin-bottom: 1rem;
}
.sheet__table {
border-top: 1px solid #000;
table-layout: fixed;
border-spacing: 0;
text-align: left;
font-size: inherit;
width: 100%;
}
.sheet__table thead th {
color: #000;
border-bottom: 1px solid #000000;
padding: 0.5rem;
}
.sheet__table tbody td {
border-bottom: 0;
padding-top: 0.28rem;
padding-bottom: 0.28rem;
padding-left: 0.5rem;
padding-right: 0.5rem;
color: #252A31;
border-bottom: 1px solid transparent;
}

View File

@@ -137,14 +137,10 @@
tbody tr.payment-amount td:last-child { tbody tr.payment-amount td:last-child {
color: red color: red
} }
tbody tr.blanace-due td{ tbody tr.blanace-due td {
border-top: 3px double #666; border-top: 3px double #666;
font-weight: bold; font-weight: bold;
} }
tbody tr.total td {
border-top: 1px solid #666;
font-weight: bold;
}
} }
} }

View File

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

View File

@@ -1,9 +1,6 @@
html(lang=locale) html(lang=locale)
head head
title My Site - #{title} title My Site - #{title}
style
include ../scss/normalize.css
include ../scss/base.css
block head block head
body body
div.paper-template div.paper-template

View File

@@ -1,227 +1,81 @@
extends ../PaperTemplateLayout.pug extends ../PaperTemplateLayout.pug
block head block head
- var prefix = 'bc' style
style. if (isRtl)
.#{prefix}-root { include ../../css/modules/credit-rtl.css
color: #111; else
padding: 24px 30px; include ../../css/modules/credit.css
font-size: 12px;
position: relative;
box-shadow: inset 0 4px 0px 0 var(--invoice-primary-color);
}
.#{prefix}-header{
box-sizing: border-box;
display: flex;
flex-flow: wrap;
flex: 0 0 auto;
-webkit-box-align: start;
align-items: start;
-webkit-box-pack: start;
justify-content: flex-start;
gap: 10px;
}
.#{prefix}-header-details{
flex: 1;
display: flex;
flex-direction: column;
align-items: stretch;
gap: 20px;
flex: 1 1 0%;
}
.#{prefix}-big-title {
font-size: 30px;
margin: 0;
line-height: 1;
font-weight: 500;
color: #333;
}
.#{prefix}-logo-wrap img {
width: 100%;
height: 100%;
max-width: 260px;
max-height: 100px;
}
.#{prefix}-terms-list {
display: flex;
flex-direction: column;
gap: 4px;
margin-bottom: 24px;
}
.#{prefix}-terms-item {
display: flex;
flex-direction: row;
gap: 12px;
}
.#{prefix}-terms-item__label {
min-width: 120px;
color: #333;
}
.#{prefix}-terms-item__value {
/* Styles for the term value */
}
.#{prefix}-address-section{
box-sizing: border-box;
display: flex;
flex-flow: wrap;
-webkit-box-align: flex-start;
align-items: flex-start;
-webkit-box-pack: start;
justify-content: flex-start;
gap: 10px;
margin-bottom: 24px;
}
.#{prefix}-address-section > * {
flex: 1 1;
}
.#{prefix}-table {
width: 100%;
border-collapse: collapse;
text-align: left;
font-size: inherit;
}
.#{prefix}-table__header {
font-weight: 400;
border-bottom: 1px solid #000;
padding: 2px 10px;
color: #333;
}
.#{prefix}-table__header:first-of-type{
padding-left: 0;
}
.#{prefix}-table__header:last-of-type{
padding-right: 0;
}
.#{prefix}-table__header--right {
text-align: right;
}
.#{prefix}-table__cell {
border-bottom: 1px solid #F6F6F6;
padding: 12px 10px;
}
.#{prefix}-table__cell:first-of-type{
padding-left: 0;
}
.#{prefix}-table__cell:last-of-type {
padding-right: 0;
}
.#{prefix}-table__cell--right {
text-align: right;
}
.#{prefix}-table__cell--item .item {
display: flex;
flex-direction: column;
gap: 2px;
}
.#{prefix}-table__cell--item .item .item__description{
color: #5f6b7c;
}
.#{prefix}-totals {
display: flex;
flex-direction: column;
margin-left: auto;
width: 300px;
margin-bottom: 24px;
}
.#{prefix}-totals__item {
display: flex;
padding: 4px 0;
}
.#{prefix}-totals__item--border-gray {
border-bottom: 1px solid #DADADA;
}
.#{prefix}-totals__item--border-dark {
border-bottom: 1px solid #000;
}
.#{prefix}-totals__item--font-weight-bold {
font-weight: bold;
}
.#{prefix}-totals__item-label {
min-width: 160px;
}
.#{prefix}-totals__item-amount {
flex: 1 1 auto;
text-align: right;
}
.#{prefix}-statement {
margin-bottom: 20px;
}
.#{prefix}-statement__label {
color: #666;
}
.#{prefix}-statement__value {
white-space: pre-line;
}
block content block content
div(class=`${prefix}-root`) div.credit
div.credit__header
//- Header (includes big title, details and logo) div.paper
div(class=`${prefix}-header`) h1.title #{__('credit.paper.credit_note')}
//- Header details (includes big title and details) if creditNote.creditNoteNumber
div(class=`${prefix}-header-details`) span.creditNoteNumber #{creditNote.creditNoteNumber}
div(class=`${prefix}-big-title`) Credit Note
div(class=`${prefix}-terms-list`) div.organization
if showCreditNoteNumber h3.title #{organizationName}
div(class=`${prefix}-terms-item`) if organizationEmail
div(class=`${prefix}-terms-item__label`) #{creditNoteNumberLabel}: span.email #{organizationEmail}
div(class=`${prefix}-terms-item__value`) #{creditNoteNumebr}
if showCreditNoteDate div.credit__full-amount
div(class=`${prefix}-terms-item`) div.label #{__('credit.paper.amount')}
div(class=`${prefix}-terms-item__label`) #{creditNoteDateLabel}: div.amount #{creditNote.formattedAmount}
div(class=`${prefix}-terms-item__value`) #{creditNoteDate}
if showCompanyLogo && companyLogoUri div.credit__meta
div(class=`${prefix}-logo-wrap`) div.credit__meta-item.credit__meta-item--amount
img(src=companyLogoUri alt=`Company Logo`) span.label #{__('credit.paper.remaining')}
span.value #{creditNote.formattedCreditsRemaining}
div(class=`${prefix}-address-section`) div.credit__meta-item.credit__meta-item--billed-to
if showCompanyAddress span.label #{__("credit.paper.billed_to")}
div(class=`${prefix}-address-from`) span.value #{creditNote.customer.displayName}
div !{companyAddress}
if showCustomerAddress div.credit__meta-item.credit__meta-item--credit-date
div(class=`${prefix}-address-to`) span.label #{__("credit.paper.credit_date")}
strong #{billedToLabel} span.value #{creditNote.formattedCreditNoteDate}
div !{customerAddress}
table(class=`${prefix}-table`) div.credit__table
thead table
tr thead
th(class=`${prefix}-table__header ${prefix}-table__header--item`) #{'Item'} tr
th(class=`${prefix}-table__header ${prefix}-table__header--quantity ${prefix}-table__header--right`) #{'Quantity'} th.item #{__("item_entry.paper.item_name")}
th(class=`${prefix}-table__header ${prefix}-table__header--rate ${prefix}-table__header--right`) #{'Rate'} th.rate #{__("item_entry.paper.rate")}
th(class=`${prefix}-table__header ${prefix}-table__header--total ${prefix}-table__header--right`) #{'Total'} th.quantity #{__("item_entry.paper.quantity")}
th.total #{__("item_entry.paper.total")}
tbody tbody
each line in lines each entry in creditNote.entries
tr(class=`${prefix}-table__row`) tr
td(class=`${prefix}-table__cell ${prefix}-table__cell--item ${prefix}-table__cell--item`) td.item
div.item div.title=entry.item.name
div.item__label #{line.item} span.description=entry.description
div.item__description #{line.description} td.rate=entry.rate
td(class=`${prefix}-table__cell ${prefix}-table__cell--quantity ${prefix}-table__cell--right`) #{line.quantity} td.quantity=entry.quantity
td(class=`${prefix}-table__cell ${prefix}-table__cell--rate ${prefix}-table__cell--right`) #{line.rate} td.total=entry.amount
td(class=`${prefix}-table__cell ${prefix}-table__cell--total ${prefix}-table__cell--right`) #{line.total}
div(class=`${prefix}-totals`) div.credit__table-after
if showSubtotal div.credit__table-total
div(class=`${prefix}-totals__item ${prefix}-totals__item--border-gray`) table
div(class=`${prefix}-totals__item-label`) #{subtotallabel} tbody
div(class=`${prefix}-totals__item-amount`) #{subtotal} tr.total
td #{__('credit.paper.total')}
td #{creditNote.formattedAmount}
tr.payment-amount
td #{__('credit.paper.credits_used')}
td #{creditNote.formattedCreditsUsed}
tr.blanace-due
td #{__('credit.paper.credits_remaining')}
td #{creditNote.formattedCreditsRemaining}
if showTotal div.credit__footer
div(class=`${prefix}-totals__item ${prefix}-totals__item--border-dark`) if creditNote.termsConditions
div(class=`${prefix}-totals__item-amount`) #{totalLabel}: div.credit__conditions
div(class=`${prefix}-totals__item-label`) #{total} h3 #{__("credit.paper.terms_conditions")}
p #{creditNote.termsConditions}
if showCustomerNote && customerNote if creditNote.note
div(class=`${prefix}-statement`) div.credit__notes
div(class=`${prefix}-statement__label`) #{customerNoteLabel}: h3 #{__("credit.paper.notes")}
div(class=`${prefix}-statement__value`) #{customerNote} p #{creditNote.note}
if showTermsConditions && termsConditions
div(class=`${prefix}-statement`)
div(class=`${prefix}-statement__label`) #{termsConditionsLabel}:
div(class=`${prefix}-statement__value`) #{termsConditions}

View File

@@ -1,242 +1,82 @@
extends ../PaperTemplateLayout.pug extends ../PaperTemplateLayout.pug
block head block head
- var prefix = 'bc' style
style. if (isRtl)
.#{prefix}-root { include ../../css/modules/estimate-rtl.css
color: #111; else
padding: 24px 30px; include ../../css/modules/estimate.css
font-size: 12px;
position: relative;
box-shadow: inset 0 4px 0px 0 var(--invoice-primary-color);
}
.#{prefix}-header {
box-sizing: border-box;
display: flex;
flex-flow: wrap;
flex: 0 0 auto;
-webkit-box-align: start;
align-items: start;
-webkit-box-pack: start;
justify-content: flex-start;
gap: 10px;
}
.#{prefix}-header-details {
flex: 1;
display: flex;
flex-direction: column;
align-items: stretch;
gap: 20px;
flex: 1 1 0%;
}
.#{prefix}-big-title {
font-size: 30px;
margin: 0;
line-height: 1;
font-weight: 500;
color: #333;
}
.#{prefix}-logo-wrap img {
width: 100%;
height: 100%;
max-width: 260px;
max-height: 100px;
}
.#{prefix}-terms {
display: flex;
flex-direction: column;
gap: 4px;
margin-bottom: 24px;
}
.#{prefix}-terms-item {
display: flex;
flex-direction: row;
gap: 12px;
}
.#{prefix}-terms-item__label {
min-width: 120px;
color: #333;
}
.#{prefix}-terms-item__value {
}
.#{prefix}-addresses{
box-sizing: border-box;
display: flex;
flex-flow: wrap;
align-items: flex-start;
justify-content: flex-start;
gap: 10px;
margin-bottom: 24px;
}
.#{prefix}-addresses > * {
flex: 1 1;
}
.#{prefix}-address {
}
.#{prefix}-address__item {
}
.#{prefix}-table {
width: 100%;
border-collapse: collapse;
text-align: left;
font-size: inherit;
}
.#{prefix}-table__header {
font-weight: 400;
border-bottom: 1px solid #000;
padding: 2px 10px;
color: #333;
}
.#{prefix}-table__header:first-of-type{
padding-left: 0;
}
.#{prefix}-table__header:last-of-type{
padding-right: 0;
}
.#{prefix}-table__header--right {
text-align: right;
}
.#{prefix}-table__header--item{
width: 50%;
}
.#{prefix}-table__cell {
border-bottom: 1px solid #F6F6F6;
padding: 12px 10px;
}
.#{prefix}-table__cell--right{
text-align: right;
}
.#{prefix}-table__cell:first-of-type{
padding-left: 0;
}
.#{prefix}-table__cell:last-of-type {
padding-right: 0;
}
.#{prefix}-table__cell--item .item {
display: flex;
flex-direction: column;
gap: 2px;
}
.#{prefix}-table__cell--item .item .item__description{
color: #5f6b7c;
}
.#{prefix}-totals {
display: flex;
flex-direction: column;
margin-left: auto;
width: 300px;
margin-bottom: 24px;
}
.#{prefix}-totals__item {
display: flex;
padding: 4px 0;
}
.#{prefix}-totals__item--border-gray {
border-bottom: 1px solid #DADADA;
}
.#{prefix}-totals__item--border-dark {
border-bottom: 1px solid #000;
}
.#{prefix}-totals__item--font-weight-bold {
font-weight: bold;
}
.#{prefix}-totals__item-label {
min-width: 160px;
}
.#{prefix}-totals__item-amount {
flex: 1 1 auto;
text-align: right;
}
.#{prefix}-statement {
margin-bottom: 20px;
}
.#{prefix}-statement__label {
color: #666;
}
.#{prefix}-statement__value {
white-space: pre-line;
}
block content block content
div(class=`${prefix}-root`, style=`--invoice-primary-color: ${primaryColor}; --invoice-secondary-color: ${secondaryColor};`) div.estimate
div.estimate__header
div.paper
h1.title #{__("estimate.paper.estimate")}
span.email #{saleEstimate.estimateNumber}
//- Header (invluces big title, details and logo) div.organization
div(class=`${prefix}-header`) h3.title #{organizationName}
if organizationEmail
span.email #{organizationEmail}
//- Header details (includes big title and details ) div.estimate__estimate-amount
div(class=`${prefix}-header-details`) div.label #{__('estimate.paper.estimate_amount')}
h1(class=`${prefix}-big-title`) Estimate div.amount #{saleEstimate.formattedAmount}
//- Terms List div.estimate__meta
div(class=`${prefix}-terms`) if saleEstimate.estimateNumber
if showEstimateNumber div.estimate__meta-item.estimate__meta-item--estimate-number
div(class=`${prefix}-terms-item`) span.label #{__("estimate.paper.estimate_number")}
div(class=`${prefix}-terms-item__label`) #{estimateNumberLabel} span.value #{saleEstimate.estimateNumber}
div(class=`${prefix}-terms-item__value`) #{estimateNumebr}
if showEstimateDate div.estimate__meta-item.estimate__meta-item--billed-to
div(class=`${prefix}-terms-item`) span.label #{__("estimate.paper.billed_to")}
div(class=`${prefix}-terms-item__label`) #{estimateDateLabel} span.value #{saleEstimate.customer.displayName}
div(class=`${prefix}-terms-item__value`) #{estimateDate}
if showExpirationDate div.estimate__meta-item.estimate__meta-item--estimate-date
div(class=`${prefix}-terms-item`) span.label #{__("estimate.paper.estimate_date")}
div(class=`${prefix}-terms-item__label`) #{expirationDateLabel} span.value #{saleEstimate.formattedEstimateDate}
div(class=`${prefix}-terms-item__value`) #{expirationDate}
//- Company logo div.estimate__meta-item.estimate__meta-item--due-date
if showCompanyLogo && companyLogoUri span.label #{__("estimate.paper.expiration_date")}
div(class=`${prefix}-logo-wrap`) span.value #{saleEstimate.formattedExpirationDate}
img(alt="Company logo", src=companyLogoUri)
//- Addresses (Group section) div.estimate__table
div(class=`${prefix}-addresses`) table
if showCompanyAddress thead
div(class=`${prefix}-address-from`) tr
div !{companyAddress} th.item #{__("item_entry.paper.item_name")}
th.rate #{__("item_entry.paper.rate")}
if showCustomerAddress th.quantity #{__("item_entry.paper.quantity")}
div(class=`${prefix}-address-to`) th.total #{__("item_entry.paper.total")}
strong #{billedToLabel}
div !{customerAddress}
//- Table section (Line items)
table(class=`${prefix}-table`)
thead
tr
th(class=`${prefix}-table__header ${prefix}-table__header--item`) Item
th(class=`${prefix}-table__header ${prefix}-table__header--quantity ${prefix}-table__header--right`) Qty
th(class=`${prefix}-table__header ${prefix}-table__header--rate ${prefix}-table__header--right`) Rate
th(class=`${prefix}-table__header ${prefix}-table__header--total ${prefix}-table__header--right`) Total
tbody tbody
each line in lines each entry in saleEstimate.entries
tr tr
td(class=`${prefix}-table__cell ${prefix}-table__cell--item`) td.item
div.item div.title=entry.item.name
div.item__label #{line.item} span.description=entry.description
div.item__description #{line.description} td.rate=entry.rate
td(class=`${prefix}-table__cell ${prefix}-table__cell--quantity ${prefix}-table__cell--right`) #{line.quantity} td.quantity=entry.quantity
td(class=`${prefix}-table__cell ${prefix}-table__cell--rate ${prefix}-table__cell--right`) #{line.rate} td.total=entry.amount
td(class=`${prefix}-table__cell ${prefix}-table__cell--total ${prefix}-table__cell--right`) #{line.total}
//- Totals section div.estimate__table-after
div(class=`${prefix}-totals`) div.estimate__table-total
if showSubtotal table
div(class=`${prefix}-totals__item ${prefix}-totals__item--border-gray`) tbody
div(class=`${prefix}-totals__item-label`) #{subtotalLabel} tr.subtotal
div(class=`${prefix}-totals__item-amount`) #{subtotal} td #{__('estimate.paper.subtotal')}
if showTotal td #{saleEstimate.formattedAmount}
div(class=`${prefix}-totals__item ${prefix}-totals__item--border-dark ${prefix}-totals__item--font-weight-bold`) tr.total
div(class=`${prefix}-totals__item-label`) #{totalLabel} td #{__('estimate.paper.total')}
div(class=`${prefix}-totals__item-amount`) #{total} td #{saleEstimate.formattedAmount}
//- Statements section div.estimate__footer
if showTermsConditions && termsConditions if saleEstimate.termsConditions
div(class=`${prefix}-statement`) div.estimate__conditions
div(class=`${prefix}-statement__label`) #{termsConditionsLabel} h3 #{__("estimate.paper.conditions_title")}
div(class=`${prefix}-statement__value`) #{termsConditions} p #{saleEstimate.termsConditions}
if showCustomerNote && customerNote if saleEstimate.note
div(class=`${prefix}-statement`) div.estimate__notes
div(class=`${prefix}-statement__label`) #{customerNoteLabel} h3 #{__("estimate.paper.notes_title")}
div(class=`${prefix}-statement__value`) #{customerNote} p #{saleEstimate.note}

View File

@@ -1,24 +0,0 @@
block head
style
include ../../css/modules/export-resource-table.css
style.
!{customCSS}
block content
.sheet
.sheet__title
h2.sheetTitle= sheetTitle
p.sheetDesc= sheetDescription
table.sheet__table
thead
tr
each column in table.columns
th(style=column.style class='column--' + column.key)= column.name
tbody
each row in table.rows
tr(class=row.classNames)
each cell in row.cells
td(class='cell--' + cell.key)
span!= cell.value

View File

@@ -1,25 +0,0 @@
block head
style
include ../../css/modules/financial-sheet.css
style.
!{customCSS}
block content
.sheet
.sheet__header
.sheet__company-name=organizationName
.sheet__sheet-type=sheetName
.sheet__sheet-date=sheetDate
table.sheet__table
thead
tr
each column in table.columns
th(style=column.style class='column--' + column.key)= column.label
tbody
each row in table.rows
tr(class=row.classNames)
each cell in row.cells
td(class='cell--' + cell.key)
span!= cell.value

View File

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

View File

@@ -1,272 +0,0 @@
extends ../PaperTemplateLayout.pug
block head
- var prefix = 'bc'
style.
.#{prefix}-root {
color: #111;
padding: 24px 30px;
font-size: 12px;
position: relative;
box-shadow: inset 0 4px 0px 0 var(--invoice-primary-color);
}
.#{prefix}-header{
box-sizing: border-box;
display: flex;
flex-flow: wrap;
flex: 0 0 auto;
-webkit-box-align: start;
align-items: start;
-webkit-box-pack: start;
justify-content: flex-start;
gap: 10px;
}
.#{prefix}-header-details{
flex: 1;
display: flex;
flex-direction: column;
align-items: stretch;
gap: 20px;
flex: 1 1 0%;
}
.#{prefix}-big-title {
font-size: 30px;
margin: 0;
line-height: 1;
font-weight: 500;
color: #333;
}
.#{prefix}-logo-wrap img {
width: 100%;
height: 100%;
max-width: 260px;
max-height: 100px;
}
.#{prefix}-details {
display: flex;
flex-direction: column;
gap: 4px;
margin-bottom: 24px;
}
.#{prefix}-detail {
display: flex;
flex-direction: row;
gap: 12px;
}
.#{prefix}-detail__label {
min-width: 120px;
color: #333;
}
.#{prefix}-detail__value {
/* Styles for detail values */
}
.#{prefix}-address-root {
box-sizing: border-box;
display: flex;
flex-flow: wrap;
align-items: flex-start;
justify-content: flex-start;
gap: 10px;
margin-bottom: 24px;
}
.#{prefix}-address-from {
flex: 1;
}
.#{prefix}-address-from__item {
/* Styles for items in the billed-from address */
}
.#{prefix}-address-to {
flex: 1;
}
.#{prefix}-address-to__item {
/* Styles for items in the billed-to address */
}
.#{prefix}-table {
width: 100%;
border-collapse: collapse;
text-align: left;
font-size: inherit;
}
.#{prefix}-table__header {
font-weight: 400;
border-bottom: 1px solid #000;
padding: 2px 10px;
color: #333;
}
.#{prefix}-table__header:first-of-type{
padding-left: 0;
}
.#{prefix}-table__header:last-of-type{
padding-right: 0;
}
.#{prefix}-table__header--right {
text-align: right;
}
.#{prefix}-table__header--item {
width: 50%;
}
.#{prefix}-table__cell {
border-bottom: 1px solid #F6F6F6;
padding: 12px 10px;
}
.#{prefix}-table__cell:first-of-type{
padding-left: 0;
}
.#{prefix}-table__cell:last-of-type {
padding-right: 0;
}
.#{prefix}-table__cell--right {
text-align: right;
}
.#{prefix}-table__cell--item .item {
display: flex;
flex-direction: column;
gap: 2px;
}
.#{prefix}-table__cell--item .item__description {
color: #5f6b7c;
}
.#{prefix}-totals {
display: flex;
flex-direction: column;
margin-left: auto;
width: 300px;
margin-bottom: 24px;
}
.#{prefix}-totals__item {
display: flex;
padding: 4px 0;
}
.#{prefix}-totals__item--border-gray {
border-bottom: 1px solid #DADADA;
}
.#{prefix}-totals__item--border-dark {
border-bottom: 1px solid #000;
}
.#{prefix}-totals__item--font-weight-bold {
font-weight: bold;
}
.#{prefix}-totals__item-label {
min-width: 160px;
}
.#{prefix}-totals__item-amount {
flex: 1 1 auto;
text-align: right;
}
.#{prefix}-paragraph {
margin-bottom: 20px;
}
.#{prefix}-paragraph__label {
color: #666;
}
.#{prefix}-paragraph__value {
white-space: pre-line;
}
block content
//- block head
div(class=`${prefix}-root`, style=`--invoice-primary-color: ${primaryColor}; --invoice-secondary-color: ${secondaryColor};`)
//- Header (includes big title, details and logo )
div(class=`${prefix}-header`)
//- Header details (includes big title and details )
div(class=`${prefix}-header-details`)
//- Title and company logo
h1(class=`${prefix}-big-title`) Invoice
//- Invoice details
div(class=`${prefix}-details`)
if showInvoiceNumber
div(class=`${prefix}-detail`)
div(class=`${prefix}-detail__label`) #{invoiceNumberLabel}
div(class=`${prefix}-detail__value`) #{invoiceNumber}
if showDateIssue
div(class=`${prefix}-detail`)
div(class=`${prefix}-detail__label`) #{dateIssueLabel}
div(class=`${prefix}-detail__value`) #{dateIssue}
if showDueDate
div(class=`${prefix}-detail`)
div(class=`${prefix}-detail__label`) #{dueDateLabel}
div(class=`${prefix}-detail__value`) #{dueDate}
//- Company logo
if showCompanyLogo && companyLogoUri
div(class=`${prefix}-logo-wrap`)
img(alt="Company logo", src=companyLogoUri)
//- Address section
div(class=`${prefix}-address-root`)
if showCompanyAddress
div(class=`${prefix}-address-from`)
div !{companyAddress}
if showCustomerAddress
div(class=`${prefix}-address-to`)
strong #{billedToLabel}
div !{customerAddress}
//- Invoice table
table(class=`${prefix}-table`)
thead
tr
th(class=`${prefix}-table__header ${prefix}-table__header--item`) #{lineItemLabel}
th(class=`${prefix}-table__header ${prefix}-table__header--quantity ${prefix}-table__header--right`) #{lineQuantityLabel}
th(class=`${prefix}-table__header ${prefix}-table__header--rate ${prefix}-table__header--right`) #{lineRateLabel}
th(class=`${prefix}-table__header ${prefix}-table__header--total ${prefix}-table__header--right`) #{lineTotalLabel}
tbody
each line in lines
tr
td(class=`${prefix}-table__cell ${prefix}-table__cell--item`)
div.item
div.item__label #{line.item}
div.item__description #{line.description}
td(class=`${prefix}-table__cell ${prefix}-table__cell--right`) #{line.quantity}
td(class=`${prefix}-table__cell ${prefix}-table__cell--right`) #{line.rate}
td(class=`${prefix}-table__cell ${prefix}-table__cell--right`) #{line.total}
//- Totals section
div(class=`${prefix}-totals`)
if showSubtotal
div(class=`${prefix}-totals__item ${prefix}-totals__item--border-gray`)
div(class=`${prefix}-totals__item-label`) #{subtotalLabel}
div(class=`${prefix}-totals__item-amount`) #{subtotal}
if showDiscount
div(class=`${prefix}-totals__item`)
div(class=`${prefix}-totals__item-label`) #{discountLabel}
div(class=`${prefix}-totals__item-amount`) #{discount}
if showTaxes
each tax in taxes
div(class=`${prefix}-totals__item`)
div(class=`${prefix}-totals__item-label`) #{tax.label}
div(class=`${prefix}-totals__item-amount`) #{tax.amount}
if showTotal
div(class=`${prefix}-totals__item ${prefix}-totals__item--border-dark ${prefix}-totals__item--font-weight-bold`)
div(class=`${prefix}-totals__item-label`) #{totalLabel}
div(class=`${prefix}-totals__item-amount`) #{total}
if showPaymentMade
div(class=`${prefix}-totals__item`)
div(class=`${prefix}-totals__item-label`) #{paymentMadeLabel}
div(class=`${prefix}-totals__item-amount`) #{paymentMade}
if showBalanceDue
div(class=`${prefix}-totals__item ${prefix}-totals__item--border-dark ${prefix}-totals__item--font-weight-bold`)
div(class=`${prefix}-totals__item-label`) #{balanceDueLabel}
div(class=`${prefix}-totals__item-amount`) #{balanceDue}
//- Footer section
if showTermsConditions && termsConditions
div(class=`${prefix}-paragraph`)
if termsConditionsLabel
div(class=`${prefix}-paragraph__label`) #{termsConditionsLabel}
div(class=`${prefix}-paragraph__value`) #{termsConditions}
if showStatement && statement
div(class=`${prefix}-paragraph`)
if statementLabel
div(class=`${prefix}-paragraph__label`) #{statementLabel}
div(class=`${prefix}-paragraph__value`) #{statement}

View File

@@ -1,192 +1,67 @@
extends ../PaperTemplateLayout.pug extends ../PaperTemplateLayout.pug
block head block head
- var prefix = 'bp3'; style
if (isRtl)
style. include ../../css/modules/payment-rtl.css
.#{prefix}-root{ else
color: #111; include ../../css/modules/payment.css
padding: 24px 30px;
font-size: 12px;
position: relative;
box-shadow: inset 0 4px 0px 0 var(--invoice-primary-color);
}
.#{prefix}-header{
box-sizing: border-box;
display: flex;
flex-flow: wrap;
flex: 0 0 auto;
-webkit-box-align: start;
align-items: start;
-webkit-box-pack: start;
justify-content: flex-start;
gap: 10px;
}
.#{prefix}-header-details{
flex: 1;
display: flex;
flex-direction: column;
align-items: stretch;
gap: 20px;
flex: 1 1 0%;
}
.#{prefix}-big-title{
font-size: 30px;
margin: 0;
line-height: 1;
font-weight: 500;
color: #333;
}
.#{prefix}-logo-wrap img {
width: 100%;
height: 100%;
max-width: 260px;
max-height: 100px;
}
.#{prefix}-terms-list{
display: flex;
flex-direction: column;
gap: 4px;
margin-bottom: 24px;
}
.#{prefix}-terms-item{
display: flex;
flex-direction: row;
gap: 12px;
}
.#{prefix}-terms-item__label{
min-width: 120px;
color: #333;
}
.#{prefix}-addresses{
box-sizing: border-box;
display: flex;
flex-flow: wrap;
align-items: flex-start;
justify-content: flex-start;
gap: 10px;
margin-bottom: 24px;
}
.#{prefix}-addresses > * {
flex: 1 1;
}
.#{prefix}-address__label{
}
.#{prefix}-table {
width: 100%;
border-collapse: collapse;
text-align: left;
font-size: inherit;
}
.#{prefix}-table__header {
font-weight: 400;
border-bottom: 1px solid #000;
padding: 2px 10px;
color: #333;
}
.#{prefix}-table__header:first-of-type{
padding-left: 0;
}
.#{prefix}-table__header:last-of-type{
padding-right: 0;
}
.#{prefix}-table__header--right {
text-align: right;
}
.#{prefix}-table__cell {
border-bottom: 1px solid #F6F6F6;
padding: 12px 10px;
}
.#{prefix}-table__cell:first-of-type{
padding-left: 0;
}
.#{prefix}-table__cell:last-of-type {
padding-right: 0;
}
.#{prefix}-table__cell--right {
text-align: right;
}
.#{prefix}-totals {
display: flex;
flex-direction: column;
margin-left: auto;
width: 300px;
margin-bottom: 24px;
}
.#{prefix}-totals__item {
display: flex;
padding: 4px 0;
}
.#{prefix}-totals__item--gray-border {
border-bottom: 1px solid #DADADA;
}
.#{prefix}-totals__item--dark-border {
border-bottom: 1px solid #000;
}
.#{prefix}-totals__item--bold {
font-weight: bold;
}
.#{prefix}-totals__item-label {
min-width: 160px;
}
.#{prefix}-totals__item-amount {
flex: 1 1 auto;
text-align: right;
}
block content block content
div(class=`${prefix}-root`) div.payment
//- Header (includes big title, details and logo ) div.payment__header
div(class=`${prefix}-header`) div.paper
//- Header details (includes big title and details ) h1.title #{__("payment.paper.payment_receipt")}
div(class=`${prefix}-header-details`) if paymentReceive.paymentReceiveNo
div(class=`${prefix}-big-title`) Payment span.paymentNumber #{paymentReceive.paymentReceiveNo}
div(class=`${prefix}-terms-list`)
if showPaymentReceivedNumber
div(class=`${prefix}-terms-item`)
div(class=`${prefix}-terms-item__label`) #{paymentReceivedNumberLabel}
div(class=`${prefix}-terms-item__value`) #{paymentReceivedNumebr}
if showPaymentReceivedDate div.organization
div(class=`${prefix}-terms-item`) h3.title #{organizationName}
div(class=`${prefix}-terms-item__label`) #{paymentReceivedDateLabel} if organizationEmail
div(class=`${prefix}-terms-item__value`) #{paymentReceivedDate} span.email #{organizationEmail}
if showCompanyLogo && companyLogoUri div.payment__received-amount
div(class=`${prefix}-logo-wrap`) div.label #{__('payment.paper.amount_received')}
img(src=companyLogoUri alt="Company Logo") div.amount #{paymentReceive.formattedAmount}
div(class=`${prefix}-addresses`)
if showCompanyAddress
div(class=`${prefix}-address-from`)
div !{companyAddress}
if showCustomerAddress div.payment__meta
div(class=`${prefix}-address-to`) div.payment__meta-item.payment__meta-item--billed-to
strong #{billedToLabel} span.label #{__("payment.paper.billed_to")}
div !{customerAddress} span.value #{paymentReceive.customer.displayName}
table(class=`${prefix}-table`) div.payment__meta-item.payment__meta-item--payment-date
thead span.label #{__("payment.paper.payment_date")}
tr span.value #{paymentReceive.formattedPaymentDate}
th(class=`${prefix}-table__header`) Invoice #
th(class=`${prefix}-table__header ${prefix}-table__header--right`) Invoice Amount
th(class=`${prefix}-table__header ${prefix}-table__header--right`) Paid Amount
tbody div.payment__table
each line in lines table
tr thead
td(class=`${prefix}-table__cell`) #{line.invoiceNumber} tr
td(class=`${prefix}-table__cell ${prefix}-table__cell--right`) #{line.invoiceAmount} th.item #{__("payment.paper.invoice_number")}
td(class=`${prefix}-table__cell ${prefix}-table__cell--right`) #{line.paidAmount} th.date #{__("payment.paper.invoice_date")}
th.invoiceAmount #{__("payment.paper.invoice_amount")}
th.paymentAmount #{__("payment.paper.payment_amount")}
tbody
each entry in paymentReceive.entries
tr
td.item=entry.invoice.invoiceNo
td.date=entry.invoice.formattedInvoiceDate
td.invoiceAmount=entry.invoice.formattedAmount
td.paymentAmount=entry.invoice.formattedPaymentAmount
div(class=`${prefix}-totals`) div.payment__table-after
if showSubtotal div.payment__table-total
div(class=`${prefix}-totals__item ${prefix}-totals__item--gray-border`) table
div(class=`${prefix}-totals__item-label`) #{subtotalLabel} tbody
div(class=`${prefix}-totals__item-amount`) #{subtotal} tr.payment-amount
td #{__('payment.paper.payment_amount')}
td #{paymentReceive.formattedAmount}
tr.blanace-due
td #{__('payment.paper.balance_due')}
td #{paymentReceive.customer.closingBalance}
if showTotal div.payment__footer
div(class=`${prefix}-totals__item ${prefix}-totals__item--dark-border`) if paymentReceive.statement
div(class=`${prefix}-totals__item-label`) #{totalLabel} div.payment__notes
div(class=`${prefix}-totals__item-amount`) #{total} h3 #{__("payment.paper.statement")}
p #{paymentReceive.statement}

View File

@@ -1,231 +1,77 @@
extends ../PaperTemplateLayout.pug extends ../PaperTemplateLayout.pug
block head block head
- var prefix = 'bc' style
style. if (isRtl)
.#{prefix}-root { include ../../css/modules/receipt-rtl.css
color: #000; else
padding: 24px 30px; include ../../css/modules/receipt.css
font-size: 12px;
position: relative;
box-shadow: inset 0 4px 0px 0 var(--invoice-primary-color);
}
.#{prefix}-header{
box-sizing: border-box;
display: flex;
flex-flow: wrap;
flex: 0 0 auto;
align-items: start;
justify-content: flex-start;
gap: 10px;
}
.#{prefix}-header-details{
flex: 1;
display: flex;
flex-direction: column;
align-items: stretch;
gap: 20px;
flex: 1 1 0%;
}
.#{prefix}-logo-wrap img {
width: 100%;
height: 100%;
max-width: 260px;
max-height: 100px;
}
.#{prefix}-big-title {
font-size: 30px;
margin: 0;
line-height: 1;
font-weight: 500;
color: #333;
}
.#{prefix}-terms-list {
display: flex;
flex-direction: column;
gap: 4px;
margin-bottom: 24px;
}
.#{prefix}-terms-item {
display: flex;
flex-direction: row;
gap: 12px;
}
.#{prefix}-terms-item__label {
min-width: 120px;
color: #333;
}
.#{prefix}-terms-item__value {}
.#{prefix}-address-section {
box-sizing: border-box;
display: flex;
flex-flow: wrap;
-webkit-box-align: flex-start;
align-items: flex-start;
-webkit-box-pack: start;
justify-content: flex-start;
gap: 10px;
margin-bottom: 24px;
}
.#{prefix}-address-section > * {
flex: 1 1 auto;
}
.#{prefix}-address {}
.#{prefix}-table {
width: 100%;
border-collapse: collapse;
text-align: left;
font-size: inherit;
}
.#{prefix}-table__header {
font-weight: 400;
border-bottom: 1px solid #000;
padding: 2px 10px;
color: #333;
}
.#{prefix}-table__header:first-of-type{
padding-left: 0;
}
.#{prefix}-table__header:last-of-type{
padding-right: 0;
}
.#{prefix}-table__header--right {
text-align: right;
}
.#{prefix}-table__header--item{
width: 50%;
}
.#{prefix}-table__cell {
border-bottom: 1px solid #F6F6F6;
padding: 12px 10px;
}
.#{prefix}-table__cell:first-of-type{
padding-left: 0;
}
.#{prefix}-table__cell:last-of-type {
padding-right: 0;
}
.#{prefix}-table__cell--right {
text-align: right;
}
.#{prefix}-table__cell--item .item {
display: flex;
flex-direction: column;
gap: 2px;
}
.#{prefix}-table__cell--item .item .item__description{
color: #5f6b7c;
}
.#{prefix}-totals {
display: flex;
flex-direction: column;
margin-left: auto;
width: 300px;
margin-bottom: 24px;
}
.#{prefix}-totals__line {
display: flex;
padding: 4px 0;
}
.#{prefix}-totals__line--gray-border {
border-bottom: 1px solid #DADADA;
}
.#{prefix}-totals__line--dark-border {
border-bottom: 1px solid #000;
}
.#{prefix}-totals__line__label {
min-width: 160px;
}
.#{prefix}-totals__line__amount {
flex: 1 1 auto;
text-align: right;
}
.#{prefix}-statement {
margin-bottom: 20px;
}
.#{prefix}-statement__label {}
.#{prefix}-statement__value {
white-space: pre-line;
}
block content block content
//- block head div.receipt
div(class=`${prefix}-root`, style=`--invoice-primary-color: ${primaryColor}; --invoice-secondary-color: ${secondaryColor};`) div.receipt__header
div.paper
h1.title #{__("receipt.paper.receipt")}
span.receiptNumber #{saleReceipt.receiptNumber}
//- Header (includes big title, details and logo ) div.organization
div(class=`${prefix}-header`) h3.title #{organizationName}
//- Header details (includes big title and details )
div(class=`${prefix}-header-details`)
//- Title and company logo
h1(class=`${prefix}-big-title`) Receipt
//- Terms List div.receipt__receipt-amount
div(class=`${prefix}-terms-list`) div.label #{__('receipt.paper.receipt_amount')}
if showReceiptNumber div.amount #{saleReceipt.formattedAmount}
div(class=`${prefix}-terms-item`)
span(class=`${prefix}-terms-item__label`)= receiptNumberLabel
span(class=`${prefix}-terms-item__value`)= receiptNumber
if showReceiptDate div.receipt__meta
div(class=`${prefix}-terms-item`) div.receipt__meta-item.receipt__meta-item--billed-to
span(class=`${prefix}-terms-item__label`)= receiptDateLabel span.label #{__("receipt.paper.billed_to")}
span(class=`${prefix}-terms-item__value`)= receiptDate span.value #{saleReceipt.customer.displayName}
//- Company logo div.receipt__meta-item.receipt__meta-item--invoice-date
if showCompanyLogo && companyLogoUri span.label #{__("receipt.paper.receipt_date")}
div(class=`${prefix}-logo-wrap`) span.value #{saleReceipt.formattedReceiptDate}
img(src=companyLogoUri alt=`Company Logo`)
//- Address Section if saleReceipt.receiptNumber
div(class=`${prefix}-address-section`) div.receipt__meta-item.receipt__meta-item--invoice-number
if showCompanyAddress span.label #{__("receipt.paper.receipt_number")}
div(class=`${prefix}-address-from`) span.value #{saleReceipt.receiptNumber}
div !{companyAddress}
if showCustomerAddress div.receipt__table
div(class=`${prefix}-address-to`) table
strong #{billedToLabel} thead
div !{customerAddress} tr
th.item #{__("item_entry.paper.item_name")}
th.rate #{__("item_entry.paper.rate")}
th.quantity #{__("item_entry.paper.quantity")}
th.total #{__("item_entry.paper.total")}
tbody
each entry in saleReceipt.entries
tr
td.item=entry.item.name
td.rate=entry.rate
td.quantity=entry.quantity
td.total=entry.amount
div.receipt__table-after
div.receipt__table-total
table
tbody
tr.total
td #{__('receipt.paper.total')}
td #{saleReceipt.formattedAmount}
tr.payment-amount
td #{__('receipt.paper.payment_amount')}
td #{saleReceipt.formattedAmount}
tr.blanace-due
td #{__('receipt.paper.balance_due')}
td #{'$0'}
//- Table Section div.receipt__footer
table(class=`${prefix}-table`) if saleReceipt.statement
thead(class=`${prefix}-table__header`) div.receipt__conditions
tr h3 #{__("receipt.paper.statement")}
th(class=`${prefix}-table__header ${prefix}-table__header--item`) Item p #{saleReceipt.statement}
th(class=`${prefix}-table__header ${prefix}-table__header--quantity ${prefix}-table__header--right`) Qty
th(class=`${prefix}-table__header ${prefix}-table__header--rate ${prefix}-table__header--right`) Rate
th(class=`${prefix}-table__header ${prefix}-table__header--total ${prefix}-table__header--right`) Total
tbody
each line in lines
tr(class=`${prefix}-table__row`)
td(class=`${prefix}-table__cell ${prefix}-table__cell--item`)
div.item
div.item__label #{line.item}
div.item__description #{line.description}
td(class=`${prefix}-table__cell ${prefix}-table__cell--right`) #{line.quantity}
td(class=`${prefix}-table__cell ${prefix}-table__cell--right`) #{line.rate}
td(class=`${prefix}-table__cell ${prefix}-table__cell--right`) #{line.total}
//- Totals Section if saleReceipt.receiptMessage
div(class=`${prefix}-totals`) div.receipt__notes
if showSubtotal h3 #{__("receipt.paper.notes")}
div(class=`${prefix}-totals__line ${prefix}-totals__line--gray-border`) p #{saleReceipt.receiptMessage}
span(class=`${prefix}-totals__line__label`)= subtotalLabel
span(class=`${prefix}-totals__line__amount`)= subtotal
if showTotal
div(class=`${prefix}-totals__line ${prefix}-totals__line--dark-border`)
span(class=`${prefix}-totals__line__label`)= totalLabel
span(class=`${prefix}-totals__line__amount`)= total
//- Customer Note Section
if showCustomerNote && customerNote
div(class=`${prefix}-statement`)
div(class=`${prefix}-statement__label`)= customerNoteLabel
div(class=`${prefix}-statement__value`)= customerNote
//- Terms & Conditions Section
if showTermsConditions && termsConditions
div(class=`${prefix}-statement`)
div(class=`${prefix}-statement__label`)= termsConditionsLabel
div(class=`${prefix}-statement__value`)= termsConditions

View File

@@ -66,14 +66,12 @@ module.exports = {
// sourcemaps: true, // Allow to enable/disable sourcemaps or pass object to configure it. // sourcemaps: true, // Allow to enable/disable sourcemaps or pass object to configure it.
// minify: true, // Allow to enable/disable minify the source. // minify: true, // Allow to enable/disable minify the source.
}, },
{ // {
src: `${RESOURCES_PATH}/scss/modules/financial-sheet.scss`, // src: './assets/sass/editor-style.scss',
dest: `${RESOURCES_PATH}/css/modules`, // dest: './assets/css',
}, // sourcemaps: true,
{ // minify: true,
src: `${RESOURCES_PATH}/scss/modules/export-resource-table.scss`, // },
dest: `${RESOURCES_PATH}/css/modules`,
},
], ],
// RTL builds. // RTL builds.
rtl: [ rtl: [
@@ -116,7 +114,7 @@ module.exports = {
// SASS Configuration for all builds. // SASS Configuration for all builds.
sass: { sass: {
errLogToConsole: true, errLogToConsole: true,
// outputStyle: 'compact', // outputStyle: 'compact',
}, },
// CSS MQ Packer configuration for all builds and style tasks. // CSS MQ Packer configuration for all builds and style tasks.

View File

@@ -65,9 +65,6 @@ exports.getCommonWebpackOptions = ({
}, },
], ],
}, },
optimization: {
minimize: false,
},
}; };
if (isDev) { if (isDev) {

View File

@@ -3,12 +3,7 @@ import { check, param, query } from 'express-validator';
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import asyncMiddleware from '@/api/middleware/asyncMiddleware'; import asyncMiddleware from '@/api/middleware/asyncMiddleware';
import BaseController from '@/api/controllers/BaseController'; import BaseController from '@/api/controllers/BaseController';
import { import { AbilitySubject, AccountAction, IAccountDTO } from '@/interfaces';
AbilitySubject,
AccountAction,
IAccountDTO,
IAccountsStructureType,
} from '@/interfaces';
import { ServiceError } from '@/exceptions'; import { ServiceError } from '@/exceptions';
import DynamicListingService from '@/services/DynamicListing/DynamicListService'; import DynamicListingService from '@/services/DynamicListing/DynamicListService';
import { DATATYPES_LENGTH } from '@/data/DataTypes'; import { DATATYPES_LENGTH } from '@/data/DataTypes';
@@ -27,7 +22,7 @@ export default class AccountsController extends BaseController {
/** /**
* Router constructor method. * Router constructor method.
*/ */
public router() { router() {
const router = Router(); const router = Router();
router.get( router.get(
@@ -98,25 +93,29 @@ export default class AccountsController extends BaseController {
/** /**
* Create account DTO Schema validation. * Create account DTO Schema validation.
*/ */
private get createAccountDTOSchema() { get createAccountDTOSchema() {
return [ return [
check('name') check('name')
.exists() .exists()
.isLength({ min: 3, max: DATATYPES_LENGTH.STRING }) .isLength({ min: 3, max: DATATYPES_LENGTH.STRING })
.trim(), .trim()
.escape(),
check('code') check('code')
.optional({ nullable: true }) .optional({ nullable: true })
.isLength({ min: 3, max: 6 }) .isLength({ min: 3, max: 6 })
.trim(), .trim()
.escape(),
check('currency_code').optional(), check('currency_code').optional(),
check('account_type') check('account_type')
.exists() .exists()
.isLength({ min: 3, max: DATATYPES_LENGTH.STRING }) .isLength({ min: 3, max: DATATYPES_LENGTH.STRING })
.trim(), .trim()
.escape(),
check('description') check('description')
.optional({ nullable: true }) .optional({ nullable: true })
.isLength({ max: DATATYPES_LENGTH.TEXT }) .isLength({ max: DATATYPES_LENGTH.TEXT })
.trim(), .trim()
.escape(),
check('parent_account_id') check('parent_account_id')
.optional({ nullable: true }) .optional({ nullable: true })
.isInt({ min: 0, max: DATATYPES_LENGTH.INT_10 }) .isInt({ min: 0, max: DATATYPES_LENGTH.INT_10 })
@@ -127,24 +126,28 @@ export default class AccountsController extends BaseController {
/** /**
* Account DTO Schema validation. * Account DTO Schema validation.
*/ */
private get editAccountDTOSchema() { get editAccountDTOSchema() {
return [ return [
check('name') check('name')
.exists() .exists()
.isLength({ min: 3, max: DATATYPES_LENGTH.STRING }) .isLength({ min: 3, max: DATATYPES_LENGTH.STRING })
.trim(), .trim()
.escape(),
check('code') check('code')
.optional({ nullable: true }) .optional({ nullable: true })
.isLength({ min: 3, max: 6 }) .isLength({ min: 3, max: 6 })
.trim(), .trim()
.escape(),
check('account_type') check('account_type')
.exists() .exists()
.isLength({ min: 3, max: DATATYPES_LENGTH.STRING }) .isLength({ min: 3, max: DATATYPES_LENGTH.STRING })
.trim(), .trim()
.escape(),
check('description') check('description')
.optional({ nullable: true }) .optional({ nullable: true })
.isLength({ max: DATATYPES_LENGTH.TEXT }) .isLength({ max: DATATYPES_LENGTH.TEXT })
.trim(), .trim()
.escape(),
check('parent_account_id') check('parent_account_id')
.optional({ nullable: true }) .optional({ nullable: true })
.isInt({ min: 0, max: DATATYPES_LENGTH.INT_10 }) .isInt({ min: 0, max: DATATYPES_LENGTH.INT_10 })
@@ -152,14 +155,14 @@ export default class AccountsController extends BaseController {
]; ];
} }
private get accountParamSchema() { get accountParamSchema() {
return [param('id').exists().isNumeric().toInt()]; return [param('id').exists().isNumeric().toInt()];
} }
/** /**
* Accounts list validation schema. * Accounts list validation schema.
*/ */
private get accountsListSchema() { get accountsListSchema() {
return [ return [
query('view_slug').optional({ nullable: true }).isString().trim(), query('view_slug').optional({ nullable: true }).isString().trim(),
query('stringified_filter_roles').optional().isJSON(), query('stringified_filter_roles').optional().isJSON(),
@@ -169,11 +172,6 @@ export default class AccountsController extends BaseController {
query('inactive_mode').optional().isBoolean().toBoolean(), query('inactive_mode').optional().isBoolean().toBoolean(),
query('search_keyword').optional({ nullable: true }).isString().trim(), query('search_keyword').optional({ nullable: true }).isString().trim(),
query('structure')
.optional()
.isString()
.isIn([IAccountsStructureType.Tree, IAccountsStructureType.Flat]),
]; ];
} }
@@ -199,6 +197,7 @@ export default class AccountsController extends BaseController {
tenantId, tenantId,
accountDTO accountDTO
); );
return res.status(200).send({ return res.status(200).send({
id: account.id, id: account.id,
message: 'The account has been created successfully.', message: 'The account has been created successfully.',
@@ -342,7 +341,6 @@ export default class AccountsController extends BaseController {
sortOrder: 'desc', sortOrder: 'desc',
columnSortBy: 'created_at', columnSortBy: 'created_at',
inactiveMode: false, inactiveMode: false,
structure: IAccountsStructureType.Tree,
...this.matchedQueryData(req), ...this.matchedQueryData(req),
}; };

View File

@@ -1,266 +0,0 @@
import mime from 'mime-types';
import { Service, Inject } from 'typedi';
import { Router, Response, NextFunction, Request } from 'express';
import { body, param } from 'express-validator';
import BaseController from '@/api/controllers/BaseController';
import { AttachmentsApplication } from '@/services/Attachments/AttachmentsApplication';
import { AttachmentUploadPipeline } from '@/services/Attachments/S3UploadPipeline';
@Service()
export class AttachmentsController extends BaseController {
@Inject()
private attachmentsApplication: AttachmentsApplication;
@Inject()
private uploadPipelineService: AttachmentUploadPipeline;
/**
* Router constructor.
*/
public router() {
const router = Router();
router.post(
'/',
this.uploadPipelineService.validateS3Configured,
this.uploadPipelineService.uploadPipeline().single('file'),
this.validateUploadedFileExistance,
this.uploadAttachment.bind(this)
);
router.delete(
'/:id',
[param('id').exists()],
this.validationResult,
this.deleteAttachment.bind(this)
);
router.get(
'/:id',
[param('id').exists()],
this.validationResult,
this.getAttachment.bind(this)
);
router.post(
'/:id/link',
[body('modelRef').exists(), body('modelId').exists()],
this.validationResult
);
router.post(
'/:id/link',
[body('modelRef').exists(), body('modelId').exists()],
this.validationResult,
this.linkDocument.bind(this)
);
router.post(
'/:id/unlink',
[body('modelRef').exists(), body('modelId').exists()],
this.validationResult,
this.unlinkDocument.bind(this)
);
router.get(
'/:id/presigned-url',
[param('id').exists()],
this.validationResult,
this.getAttachmentPresignedUrl.bind(this)
);
return router;
}
/**
* Validates the upload file existance.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @returns {Response|void}
*/
private validateUploadedFileExistance(
req: Request,
res: Response,
next: NextFunction
) {
if (!req.file) {
return res.boom.badRequest(null, {
errorType: 'FILE_UPLOAD_FAILED',
message: 'Now file uploaded.',
});
}
next();
}
/**
* Uploads the attachments to S3 and store the file metadata to DB.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @returns {Response|void}
*/
private async uploadAttachment(
req: Request,
res: Response,
next: NextFunction
): Promise<Response | void> {
const { tenantId } = req;
const file = req.file;
try {
const data = await this.attachmentsApplication.upload(tenantId, file);
return res.status(200).send({
status: 200,
message: 'The document has uploaded successfully.',
data,
});
} catch (error) {
next(error);
}
}
/**
* Retrieves the given attachment key.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @returns {Promise<Response|void>}
*/
private async getAttachment(
req: Request,
res: Response,
next: NextFunction
): Promise<Response | void> {
const { tenantId } = req;
const { id } = req.params;
try {
const data = await this.attachmentsApplication.get(tenantId, id);
const byte = await data.Body.transformToByteArray();
const extension = mime.extension(data.ContentType);
const buffer = Buffer.from(byte);
res.set(
'Content-Disposition',
`filename="${req.params.id}.${extension}"`
);
res.set('Content-Type', data.ContentType);
res.send(buffer);
} catch (error) {
next(error);
}
}
/**
* Deletes the given document key.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @returns {Promise<Response|void>}
*/
private async deleteAttachment(
req: Request,
res: Response,
next: NextFunction
): Promise<Response | void> {
const { tenantId } = req;
const { id: documentId } = req.params;
try {
await this.attachmentsApplication.delete(tenantId, documentId);
return res.status(200).send({
status: 200,
message: 'The document has been delete successfully.',
});
} catch (error) {
next(error);
}
}
/**
* Links the given document key.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @returns {Promise<Response|void>}
*/
private async linkDocument(
req: Request,
res: Response,
next: Function
): Promise<Response | void> {
const { tenantId } = req;
const { id: documentId } = req.params;
const { modelRef, modelId } = this.matchedBodyData(req);
try {
await this.attachmentsApplication.link(
tenantId,
documentId,
modelRef,
modelId
);
return res.status(200).send({
status: 200,
message: 'The document has been linked successfully.',
});
} catch (error) {
next(error);
}
}
/**
* Links the given document key.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @returns {Promise<Response|void>}
*/
private async unlinkDocument(
req: Request,
res: Response,
next: NextFunction
): Promise<Response | void> {
const { tenantId } = req;
const { id: documentId } = req.params;
const { modelRef, modelId } = this.matchedBodyData(req);
try {
await this.attachmentsApplication.link(
tenantId,
documentId,
modelRef,
modelId
);
return res.status(200).send({
status: 200,
message: 'The document has been linked successfully.',
});
} catch (error) {
next(error);
}
}
/**
* Retreives the presigned url of the given attachment key.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @returns {Promise<Response|void>}
*/
private async getAttachmentPresignedUrl(
req: Request,
res: Response,
next: NextFunction
): Promise<Response | void> {
const { tenantId } = req;
const { id: documentKey } = req.params;
try {
const presignedUrl = await this.attachmentsApplication.getPresignedUrl(
tenantId,
documentKey
);
return res.status(200).send({ presignedUrl });
} catch (error) {
next(error);
}
}
}

View File

@@ -9,8 +9,6 @@ import { DATATYPES_LENGTH } from '@/data/DataTypes';
import LoginThrottlerMiddleware from '@/api/middleware/LoginThrottlerMiddleware'; import LoginThrottlerMiddleware from '@/api/middleware/LoginThrottlerMiddleware';
import AuthenticationApplication from '@/services/Authentication/AuthApplication'; import AuthenticationApplication from '@/services/Authentication/AuthApplication';
import JWTAuth from '@/api/middleware/jwtAuth';
import AttachCurrentTenantUser from '@/api/middleware/AttachCurrentTenantUser';
@Service() @Service()
export default class AuthenticationController extends BaseController { export default class AuthenticationController extends BaseController {
@Inject() @Inject()
@@ -30,20 +28,6 @@ export default class AuthenticationController extends BaseController {
asyncMiddleware(this.login.bind(this)), asyncMiddleware(this.login.bind(this)),
this.handlerErrors 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( router.post(
'/register', '/register',
this.registerSchema, this.registerSchema,
@@ -65,7 +49,6 @@ export default class AuthenticationController extends BaseController {
asyncMiddleware(this.resetPassword.bind(this)), asyncMiddleware(this.resetPassword.bind(this)),
this.handlerErrors this.handlerErrors
); );
router.get('/meta', asyncMiddleware(this.getAuthMeta.bind(this)));
return router; return router;
} }
@@ -90,38 +73,30 @@ export default class AuthenticationController extends BaseController {
.exists() .exists()
.isString() .isString()
.trim() .trim()
.escape()
.isLength({ max: DATATYPES_LENGTH.STRING }), .isLength({ max: DATATYPES_LENGTH.STRING }),
check('last_name') check('last_name')
.exists() .exists()
.isString() .isString()
.trim() .trim()
.escape()
.isLength({ max: DATATYPES_LENGTH.STRING }), .isLength({ max: DATATYPES_LENGTH.STRING }),
check('email') check('email')
.exists() .exists()
.isString() .isString()
.isEmail() .isEmail()
.trim() .trim()
.escape()
.isLength({ max: DATATYPES_LENGTH.STRING }), .isLength({ max: DATATYPES_LENGTH.STRING }),
check('password') check('password')
.exists() .exists()
.isString() .isString()
.isLength({ min: 6 })
.trim() .trim()
.escape()
.isLength({ max: DATATYPES_LENGTH.STRING }), .isLength({ max: DATATYPES_LENGTH.STRING }),
]; ];
} }
private get signupVerifySchema(): ValidationChain[] {
return [
check('email')
.exists()
.isString()
.isEmail()
.isLength({ max: DATATYPES_LENGTH.STRING }),
check('token').exists().isString(),
];
}
/** /**
* Reset password schema. * Reset password schema.
* @returns {ValidationChain[]} * @returns {ValidationChain[]}
@@ -130,7 +105,7 @@ export default class AuthenticationController extends BaseController {
return [ return [
check('password') check('password')
.exists() .exists()
.isLength({ min: 6 }) .isLength({ min: 5 })
.custom((value, { req }) => { .custom((value, { req }) => {
if (value !== req.body.confirm_password) { if (value !== req.body.confirm_password) {
throw new Error("Passwords don't match"); throw new Error("Passwords don't match");
@@ -146,7 +121,7 @@ export default class AuthenticationController extends BaseController {
* @returns {ValidationChain[]} * @returns {ValidationChain[]}
*/ */
private get sendResetPasswordSchema(): ValidationChain[] { private get sendResetPasswordSchema(): ValidationChain[] {
return [check('email').exists().isEmail().trim()]; return [check('email').exists().isEmail().trim().escape()];
} }
/** /**
@@ -154,11 +129,7 @@ export default class AuthenticationController extends BaseController {
* @param {Request} req * @param {Request} req
* @param {Response} res * @param {Response} res
*/ */
private async login( private async login(req: Request, res: Response, next: Function): Response {
req: Request,
res: Response,
next: Function
): Promise<Response | null> {
const userDTO: ILoginDTO = this.matchedBodyData(req); const userDTO: ILoginDTO = this.matchedBodyData(req);
try { try {
@@ -193,58 +164,6 @@ 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 * Send reset password handler
* @param {Request} req * @param {Request} req
@@ -288,23 +207,6 @@ export default class AuthenticationController extends BaseController {
} }
} }
/**
* Retrieves the authentication meta for SPA.
* @param {Request} req
* @param {Response} res
* @param {Function} next
* @returns {Response|void}
*/
private async getAuthMeta(req: Request, res: Response, next: Function) {
try {
const meta = await this.authApplication.getAuthMeta();
return res.status(200).send({ meta });
} catch (error) {
next(error);
}
}
/** /**
* Handles the service errors. * Handles the service errors.
*/ */
@@ -345,30 +247,6 @@ export default class AuthenticationController extends BaseController {
errors: [{ type: 'EMAIL.EXISTS', code: 600 }], errors: [{ type: 'EMAIL.EXISTS', code: 600 }],
}); });
} }
if (error.errorType === 'SIGNUP_RESTRICTED') {
return res.status(400).send({
errors: [
{
type: 'SIGNUP_RESTRICTED',
message:
'Sign-up is restricted no one can sign-up to the system.',
code: 700,
},
],
});
}
if (error.errorType === 'SIGNUP_RESTRICTED_NOT_ALLOWED') {
return res.status(400).send({
errors: [
{
type: 'SIGNUP_RESTRICTED_NOT_ALLOWED',
message:
'Sign-up is restricted the given email address is not allowed to sign-up.',
code: 710,
},
],
});
}
} }
next(error); next(error);
} }

View File

@@ -1,218 +0,0 @@
import { Inject, Service } from 'typedi';
import { NextFunction, Request, Response, Router } from 'express';
import { param, query } from 'express-validator';
import BaseController from '@/api/controllers/BaseController';
import { GetBankAccountSummary } from '@/services/Banking/BankAccounts/GetBankAccountSummary';
import { BankAccountsApplication } from '@/services/Banking/BankAccounts/BankAccountsApplication';
import { GetPendingBankAccountTransactions } from '@/services/Cashflow/GetPendingBankAccountTransaction';
@Service()
export class BankAccountsController extends BaseController {
@Inject()
private getBankAccountSummaryService: GetBankAccountSummary;
@Inject()
private bankAccountsApp: BankAccountsApplication;
@Inject()
private getPendingTransactionsService: GetPendingBankAccountTransactions;
/**
* Router constructor.
*/
router() {
const router = Router();
router.get('/:bankAccountId/meta', this.getBankAccountSummary.bind(this));
router.get(
'/pending_transactions',
[
query('account_id').optional().isNumeric().toInt(),
query('page').optional().isNumeric().toInt(),
query('page_size').optional().isNumeric().toInt(),
],
this.validationResult,
this.getBankAccountsPendingTransactions.bind(this)
);
router.post(
'/:bankAccountId/disconnect',
this.disconnectBankAccount.bind(this)
);
router.post('/:bankAccountId/update', this.refreshBankAccount.bind(this));
router.post(
'/:bankAccountId/pause_feeds',
[param('bankAccountId').exists().isNumeric().toInt()],
this.validationResult,
this.pauseBankAccountFeeds.bind(this)
);
router.post(
'/:bankAccountId/resume_feeds',
[param('bankAccountId').exists().isNumeric().toInt()],
this.validationResult,
this.resumeBankAccountFeeds.bind(this)
);
return router;
}
/**
* Retrieves the bank account meta summary.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @returns {Promise<Response|null>}
*/
async getBankAccountSummary(
req: Request<{ bankAccountId: number }>,
res: Response,
next: NextFunction
) {
const { bankAccountId } = req.params;
const { tenantId } = req;
try {
const data =
await this.getBankAccountSummaryService.getBankAccountSummary(
tenantId,
bankAccountId
);
return res.status(200).send({ data });
} catch (error) {
next(error);
}
}
/**
* Retrieves the bank account pending transactions.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
async getBankAccountsPendingTransactions(
req: Request,
res: Response,
next: NextFunction
) {
const { tenantId } = req;
const query = this.matchedQueryData(req);
try {
const data =
await this.getPendingTransactionsService.getPendingTransactions(
tenantId,
query
);
return res.status(200).send(data);
} catch (error) {
next(error);
}
}
/**
* Disonnect the given bank account.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @returns {Promise<Response|null>}
*/
async disconnectBankAccount(
req: Request<{ bankAccountId: number }>,
res: Response,
next: NextFunction
) {
const { bankAccountId } = req.params;
const { tenantId } = req;
try {
await this.bankAccountsApp.disconnectBankAccount(tenantId, bankAccountId);
return res.status(200).send({
id: bankAccountId,
message: 'The bank account has been disconnected.',
});
} catch (error) {
next(error);
}
}
/**
* Refresh the given bank account.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @returns {Promise<Response|null>}
*/
async refreshBankAccount(
req: Request<{ bankAccountId: number }>,
res: Response,
next: NextFunction
) {
const { bankAccountId } = req.params;
const { tenantId } = req;
try {
await this.bankAccountsApp.refreshBankAccount(tenantId, bankAccountId);
return res.status(200).send({
id: bankAccountId,
message: 'The bank account has been disconnected.',
});
} catch (error) {
next(error);
}
}
/**
* Resumes the bank account feeds sync.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @returns {Promise<Response | void>}
*/
async resumeBankAccountFeeds(
req: Request<{ bankAccountId: number }>,
res: Response,
next: NextFunction
) {
const { bankAccountId } = req.params;
const { tenantId } = req;
try {
await this.bankAccountsApp.resumeBankAccount(tenantId, bankAccountId);
return res.status(200).send({
message: 'The bank account feeds syncing has been resumed.',
id: bankAccountId,
});
} catch (error) {
next(error);
}
}
/**
* Pauses the bank account feeds sync.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @returns {Promise<Response | void>}
*/
async pauseBankAccountFeeds(
req: Request<{ bankAccountId: number }>,
res: Response,
next: NextFunction
) {
const { bankAccountId } = req.params;
const { tenantId } = req;
try {
await this.bankAccountsApp.pauseBankAccount(tenantId, bankAccountId);
return res.status(200).send({
message: 'The bank account feeds syncing has been paused.',
id: bankAccountId,
});
} catch (error) {
next(error);
}
}
}

View File

@@ -1,101 +0,0 @@
import { Inject, Service } from 'typedi';
import { body, param } from 'express-validator';
import { NextFunction, Request, Response, Router } from 'express';
import BaseController from '@/api/controllers/BaseController';
import { MatchBankTransactionsApplication } from '@/services/Banking/Matching/MatchBankTransactionsApplication';
@Service()
export class BankTransactionsMatchingController extends BaseController {
@Inject()
private bankTransactionsMatchingApp: MatchBankTransactionsApplication;
/**
* Router constructor.
*/
public router() {
const router = Router();
router.post(
'/unmatch/:transactionId',
[param('transactionId').exists()],
this.validationResult,
this.unmatchMatchedBankTransaction.bind(this)
);
router.post(
'/match',
[
body('uncategorizedTransactions').exists().isArray({ min: 1 }),
body('uncategorizedTransactions.*').isNumeric().toInt(),
body('matchedTransactions').isArray({ min: 1 }),
body('matchedTransactions.*.reference_type').exists(),
body('matchedTransactions.*.reference_id').isNumeric().toInt(),
],
this.validationResult,
this.matchBankTransaction.bind(this)
);
return router;
}
/**
* Matches the given bank transaction.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @returns {Promise<Response|null>}
*/
private async matchBankTransaction(
req: Request<{ transactionId: number }>,
res: Response,
next: NextFunction
): Promise<Response | null> {
const { tenantId } = req;
const bodyData = this.matchedBodyData(req);
const uncategorizedTransactions = bodyData?.uncategorizedTransactions;
const matchedTransactions = bodyData?.matchedTransactions;
try {
await this.bankTransactionsMatchingApp.matchTransaction(
tenantId,
uncategorizedTransactions,
matchedTransactions
);
return res.status(200).send({
ids: uncategorizedTransactions,
message: 'The bank transaction has been matched.',
});
} catch (error) {
next(error);
}
}
/**
* Unmatches the matched bank transaction.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @returns {Promise<Response|null>}
*/
private async unmatchMatchedBankTransaction(
req: Request<{ transactionId: number }>,
res: Response,
next: NextFunction
) {
const { tenantId } = req;
const { transactionId } = req.params;
try {
await this.bankTransactionsMatchingApp.unmatchMatchedTransaction(
tenantId,
transactionId
);
return res.status(200).send({
id: transactionId,
message: 'The bank matched transaction has been unmatched.',
});
} catch (error) {
next(error);
}
}
}

View File

@@ -1,39 +0,0 @@
import Container, { Inject, Service } from 'typedi';
import { Router } from 'express';
import BaseController from '@/api/controllers/BaseController';
import { PlaidBankingController } from './PlaidBankingController';
import { BankingRulesController } from './BankingRulesController';
import { BankTransactionsMatchingController } from './BankTransactionsMatchingController';
import { RecognizedTransactionsController } from './RecognizedTransactionsController';
import { BankAccountsController } from './BankAccountsController';
import { BankingUncategorizedController } from './BankingUncategorizedController';
@Service()
export class BankingController extends BaseController {
/**
* Router constructor.
*/
public router() {
const router = Router();
router.use('/plaid', Container.get(PlaidBankingController).router());
router.use('/rules', Container.get(BankingRulesController).router());
router.use(
'/matches',
Container.get(BankTransactionsMatchingController).router()
);
router.use(
'/recognized',
Container.get(RecognizedTransactionsController).router()
);
router.use(
'/bank_accounts',
Container.get(BankAccountsController).router()
);
router.use(
'/categorize',
Container.get(BankingUncategorizedController).router()
);
return router;
}
}

View File

@@ -1,214 +0,0 @@
import { Inject, Service } from 'typedi';
import { NextFunction, Request, Response, Router } from 'express';
import BaseController from '@/api/controllers/BaseController';
import { BankRulesApplication } from '@/services/Banking/Rules/BankRulesApplication';
import { body, param } from 'express-validator';
import {
ICreateBankRuleDTO,
IEditBankRuleDTO,
} from '@/services/Banking/Rules/types';
@Service()
export class BankingRulesController extends BaseController {
@Inject()
private bankRulesApplication: BankRulesApplication;
/**
* Bank rule DTO validation schema.
*/
private get bankRuleValidationSchema() {
return [
body('name').isString().exists(),
body('order').isInt({ min: 0 }),
// Apply to if transaction is.
body('apply_if_account_id')
.isInt({ min: 0 })
.optional({ nullable: true }),
body('apply_if_transaction_type').isIn(['deposit', 'withdrawal']),
// Conditions
body('conditions_type').isString().isIn(['and', 'or']).default('and'),
body('conditions').isArray({ min: 1 }),
body('conditions.*.field').exists().isIn(['description', 'amount']),
body('conditions.*.comparator')
.exists()
.isIn([
'equals',
'equal',
'contains',
'not_contain',
'bigger',
'bigger_or_equal',
'smaller',
'smaller_or_equal',
])
.default('contain')
.trim(),
body('conditions.*.value').exists().trim(),
// Assign
body('assign_category').isString(),
body('assign_account_id').isInt({ min: 0 }),
body('assign_payee').isString().optional({ nullable: true }),
body('assign_memo').isString().optional({ nullable: true }),
];
}
/**
* Router constructor.
*/
public router() {
const router = Router();
router.post(
'/',
[...this.bankRuleValidationSchema],
this.validationResult,
this.createBankRule.bind(this)
);
router.post(
'/:id',
[param('id').toInt().exists(), ...this.bankRuleValidationSchema],
this.validationResult,
this.editBankRule.bind(this)
);
router.delete(
'/:id',
[param('id').toInt().exists()],
this.validationResult,
this.deleteBankRule.bind(this)
);
router.get(
'/:id',
[param('id').toInt().exists()],
this.validationResult,
this.getBankRule.bind(this)
);
router.get(
'/',
[param('id').toInt().exists()],
this.validationResult,
this.getBankRules.bind(this)
);
return router;
}
/**
* Creates a new bank rule.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
private async createBankRule(
req: Request,
res: Response,
next: NextFunction
) {
const { tenantId } = req;
const createBankRuleDTO = this.matchedBodyData(req) as ICreateBankRuleDTO;
try {
const bankRule = await this.bankRulesApplication.createBankRule(
tenantId,
createBankRuleDTO
);
return res.status(200).send({
id: bankRule.id,
message: 'The bank rule has been created successfully.',
});
} catch (error) {
next(error);
}
}
/**
* Edits the given bank rule.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
private async editBankRule(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const { id: ruleId } = req.params;
const editBankRuleDTO = this.matchedBodyData(req) as IEditBankRuleDTO;
try {
await this.bankRulesApplication.editBankRule(
tenantId,
ruleId,
editBankRuleDTO
);
return res.status(200).send({
id: ruleId,
message: 'The bank rule has been updated successfully.',
});
} catch (error) {
next(error);
}
}
/**
* Deletes the given bank rule.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
private async deleteBankRule(
req: Request<{ id: number }>,
res: Response,
next: NextFunction
) {
const { id: ruleId } = req.params;
const { tenantId } = req;
try {
await this.bankRulesApplication.deleteBankRule(tenantId, ruleId);
return res
.status(200)
.send({ message: 'The bank rule has been deleted.', id: ruleId });
} catch (error) {
next(error);
}
}
/**
* Retrieve the given bank rule.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
private async getBankRule(req: Request, res: Response, next: NextFunction) {
const { id: ruleId } = req.params;
const { tenantId } = req;
try {
const bankRule = await this.bankRulesApplication.getBankRule(
tenantId,
ruleId
);
return res.status(200).send({ bankRule });
} catch (error) {
next(error);
}
}
/**
* Retrieves the bank rules.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
private async getBankRules(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
try {
const bankRules = await this.bankRulesApplication.getBankRules(tenantId);
return res.status(200).send({ bankRules });
} catch (error) {
next(error);
}
}
}

View File

@@ -1,57 +0,0 @@
import { Inject, Service } from 'typedi';
import { NextFunction, Request, Response, Router } from 'express';
import { query } from 'express-validator';
import BaseController from '../BaseController';
import { GetAutofillCategorizeTransaction } from '@/services/Banking/RegonizeTranasctions/GetAutofillCategorizeTransaction';
@Service()
export class BankingUncategorizedController extends BaseController {
@Inject()
private getAutofillCategorizeTransactionService: GetAutofillCategorizeTransaction;
/**
* Router constructor.
*/
router() {
const router = Router();
router.get(
'/autofill',
[
query('uncategorizedTransactionIds').isArray({ min: 1 }),
query('uncategorizedTransactionIds.*').isNumeric().toInt(),
],
this.validationResult,
this.getAutofillCategorizeTransaction.bind(this)
);
return router;
}
/**
* Retrieves the autofill values of the categorize form of the given
* uncategorized transactions.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @returns {Promise<Response | null>}
*/
public async getAutofillCategorizeTransaction(
req: Request,
res: Response,
next: NextFunction
) {
const { tenantId } = req;
const uncategorizedTransactionIds = req.query.uncategorizedTransactionIds;
try {
const data =
await this.getAutofillCategorizeTransactionService.getAutofillCategorizeTransaction(
tenantId,
uncategorizedTransactionIds
);
return res.status(200).send({ data });
} catch (error) {
next(error);
}
}
}

View File

@@ -1,201 +0,0 @@
import { Inject, Service } from 'typedi';
import { body, param, query } from 'express-validator';
import { NextFunction, Request, Response, Router } from 'express';
import BaseController from '../BaseController';
import { ExcludeBankTransactionsApplication } from '@/services/Banking/Exclude/ExcludeBankTransactionsApplication';
import { map, parseInt, trim } from 'lodash';
@Service()
export class ExcludeBankTransactionsController extends BaseController {
@Inject()
private excludeBankTransactionApp: ExcludeBankTransactionsApplication;
/**
* Router constructor.
*/
public router() {
const router = Router();
router.put(
'/transactions/exclude',
[body('ids').exists()],
this.validationResult,
this.excludeBulkBankTransactions.bind(this)
);
router.put(
'/transactions/unexclude',
[body('ids').exists()],
this.validationResult,
this.unexcludeBulkBankTransactins.bind(this)
);
router.put(
'/transactions/:transactionId/exclude',
[param('transactionId').exists().toInt()],
this.validationResult,
this.excludeBankTransaction.bind(this)
);
router.put(
'/transactions/:transactionId/unexclude',
[param('transactionId').exists()],
this.validationResult,
this.unexcludeBankTransaction.bind(this)
);
router.get(
'/excluded',
[
query('account_id').optional().isNumeric().toInt(),
query('page').optional().isNumeric().toInt(),
query('page_size').optional().isNumeric().toInt(),
query('min_date').optional({ nullable: true }).isISO8601().toDate(),
query('max_date').optional({ nullable: true }).isISO8601().toDate(),
query('min_amount').optional({ nullable: true }).isFloat().toFloat(),
query('max_amount').optional({ nullable: true }).isFloat().toFloat(),
],
this.validationResult,
this.getExcludedBankTransactions.bind(this)
);
return router;
}
/**
* Marks a bank transaction as excluded.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @returns
*/
private async excludeBankTransaction(
req: Request,
res: Response,
next: NextFunction
): Promise<Response | void> {
const { tenantId } = req;
const { transactionId } = req.params;
try {
await this.excludeBankTransactionApp.excludeBankTransaction(
tenantId,
transactionId
);
return res.status(200).send({
message: 'The bank transaction has been excluded.',
id: transactionId,
});
} catch (error) {
next(error);
}
}
/**
* Marks a bank transaction as not excluded.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @returns {Promise<Response|void>}
*/
private async unexcludeBankTransaction(
req: Request,
res: Response,
next: NextFunction
): Promise<Response | void> {
const { tenantId } = req;
const { transactionId } = req.params;
try {
await this.excludeBankTransactionApp.unexcludeBankTransaction(
tenantId,
transactionId
);
return res.status(200).send({
message: 'The bank transaction has been unexcluded.',
id: transactionId,
});
} catch (error) {
next(error);
}
}
/**
* Exclude bank transactions in bulk.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
private async excludeBulkBankTransactions(
req: Request,
res: Response,
next: NextFunction
) {
const { tenantId } = req;
const { ids } = this.matchedBodyData(req);
try {
await this.excludeBankTransactionApp.excludeBankTransactions(
tenantId,
ids
);
return res.status(200).send({
message: 'The given bank transactions have been excluded',
ids,
});
} catch (error) {
next(error);
}
}
/**
* Unexclude the given bank transactions in bulk.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @returns {Promise<Response | null>}
*/
private async unexcludeBulkBankTransactins(
req: Request,
res: Response,
next: NextFunction
): Promise<Response | null> {
const { tenantId } = req;
const { ids } = this.matchedBodyData(req);
try {
await this.excludeBankTransactionApp.unexcludeBankTransactions(
tenantId,
ids
);
return res.status(200).send({
message: 'The given bank transactions have been excluded',
ids,
});
} catch (error) {
next(error);
}
}
/**
* Retrieves the excluded uncategorized bank transactions.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @returns {Promise<Response|null>}
*/
private async getExcludedBankTransactions(
req: Request,
res: Response,
next: NextFunction
): Promise<Response | void> {
const { tenantId } = req;
const filter = this.matchedQueryData(req);
try {
const data =
await this.excludeBankTransactionApp.getExcludedBankTransactions(
tenantId,
filter
);
return res.status(200).send(data);
} catch (error) {
next(error);
}
}
}

View File

@@ -1,53 +0,0 @@
import { Inject, Service } from 'typedi';
import { Router, Request, Response } from 'express';
import BaseController from '@/api/controllers/BaseController';
import { PlaidApplication } from '@/services/Banking/Plaid/PlaidApplication';
@Service()
export class PlaidBankingController extends BaseController {
@Inject()
private plaidApp: PlaidApplication;
/**
* Router constructor.
*/
router() {
const router = Router();
router.post('/link-token', this.linkToken.bind(this));
router.post('/exchange-token', this.exchangeToken.bind(this));
return router;
}
/**
* Retrieves the Plaid link token.
* @param {Request} req
* @param {response} res
* @returns {Response}
*/
private async linkToken(req: Request, res: Response) {
const { tenantId } = req;
const linkToken = await this.plaidApp.getLinkToken(tenantId);
return res.status(200).send(linkToken);
}
/**
* Exchanges the given public token.
* @param {Request} req
* @param {response} res
* @returns {Response}
*/
public async exchangeToken(req: Request, res: Response) {
const { tenantId } = req;
const { public_token, institution_id } = req.body;
await this.plaidApp.exchangeToken(tenantId, {
institutionId: institution_id,
publicToken: public_token,
});
return res.status(200).send({});
}
}

View File

@@ -1,91 +0,0 @@
import { Inject, Service } from 'typedi';
import { NextFunction, Request, Response, Router } from 'express';
import { query } from 'express-validator';
import BaseController from '@/api/controllers/BaseController';
import { CashflowApplication } from '@/services/Cashflow/CashflowApplication';
@Service()
export class RecognizedTransactionsController extends BaseController {
@Inject()
private cashflowApplication: CashflowApplication;
/**
* Router constructor.
*/
router() {
const router = Router();
router.get(
'/',
[
query('page').optional().isNumeric().toInt(),
query('page_size').optional().isNumeric().toInt(),
query('account_id').optional().isNumeric().toInt(),
query('min_date').optional({ nullable: true }).isISO8601().toDate(),
query('max_date').optional({ nullable: true }).isISO8601().toDate(),
query('min_amount').optional({ nullable: true }).isFloat().toFloat(),
query('max_amount').optional({ nullable: true }).isFloat().isFloat(),
],
this.validationResult,
this.getRecognizedTransactions.bind(this)
);
router.get(
'/transactions/:uncategorizedTransactionId',
this.getRecognizedTransaction.bind(this)
);
return router;
}
/**
* Retrieves the recognized bank transactions.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @returns {Promise<Response|null>}
*/
async getRecognizedTransactions(
req: Request<{ accountId: number }>,
res: Response,
next: NextFunction
) {
const filter = this.matchedQueryData(req);
const { tenantId } = req;
try {
const data = await this.cashflowApplication.getRecognizedTransactions(
tenantId,
filter
);
return res.status(200).send(data);
} catch (error) {
next(error);
}
}
/**
* Retrieves the recognized transaction of the ginen uncategorized transaction.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @returns {Promise<Response|null>}
*/
async getRecognizedTransaction(
req: Request<{ uncategorizedTransactionId: number }>,
res: Response,
next: NextFunction
) {
const { tenantId } = req;
const { uncategorizedTransactionId } = req.params;
try {
const data = await this.cashflowApplication.getRecognizedTransaction(
tenantId,
uncategorizedTransactionId
);
return res.status(200).send({ data });
} catch (error) {
next(error);
}
}
}

View File

@@ -4,7 +4,6 @@ import CommandCashflowTransaction from './NewCashflowTransaction';
import DeleteCashflowTransaction from './DeleteCashflowTransaction'; import DeleteCashflowTransaction from './DeleteCashflowTransaction';
import GetCashflowTransaction from './GetCashflowTransaction'; import GetCashflowTransaction from './GetCashflowTransaction';
import GetCashflowAccounts from './GetCashflowAccounts'; import GetCashflowAccounts from './GetCashflowAccounts';
import { ExcludeBankTransactionsController } from '../Banking/ExcludeBankTransactionsController';
@Service() @Service()
export default class CashflowController { export default class CashflowController {
@@ -14,10 +13,9 @@ export default class CashflowController {
router() { router() {
const router = Router(); const router = Router();
router.use(Container.get(CommandCashflowTransaction).router());
router.use(Container.get(ExcludeBankTransactionsController).router());
router.use(Container.get(GetCashflowTransaction).router()); router.use(Container.get(GetCashflowTransaction).router());
router.use(Container.get(GetCashflowAccounts).router()); router.use(Container.get(GetCashflowAccounts).router());
router.use(Container.get(CommandCashflowTransaction).router());
router.use(Container.get(DeleteCashflowTransaction).router()); router.use(Container.get(DeleteCashflowTransaction).router());
return router; return router;

View File

@@ -3,15 +3,14 @@ import { Router, Request, Response, NextFunction } from 'express';
import { param } from 'express-validator'; import { param } from 'express-validator';
import BaseController from '../BaseController'; import BaseController from '../BaseController';
import { ServiceError } from '@/exceptions'; import { ServiceError } from '@/exceptions';
import DeleteCashflowTransactionService from '../../../services/Cashflow/DeleteCashflowTransactionService';
import CheckPolicies from '@/api/middleware/CheckPolicies'; import CheckPolicies from '@/api/middleware/CheckPolicies';
import { AbilitySubject, CashflowAction } from '@/interfaces'; import { AbilitySubject, CashflowAction } from '@/interfaces';
import { CashflowApplication } from '@/services/Cashflow/CashflowApplication';
@Service() @Service()
export default class DeleteCashflowTransactionController extends BaseController { export default class DeleteCashflowTransaction extends BaseController {
@Inject() @Inject()
private cashflowApplication: CashflowApplication; deleteCashflowService: DeleteCashflowTransactionService;
/** /**
* Controller router. * Controller router.
@@ -45,7 +44,7 @@ export default class DeleteCashflowTransactionController extends BaseController
try { try {
const { oldCashflowTransaction } = const { oldCashflowTransaction } =
await this.cashflowApplication.deleteTransaction( await this.deleteCashflowService.deleteCashflowTransaction(
tenantId, tenantId,
transactionId transactionId
); );
@@ -93,19 +92,6 @@ export default class DeleteCashflowTransactionController extends BaseController
], ],
}); });
} }
if (
error.errorType ===
'CANNOT_DELETE_TRANSACTION_CONVERTED_FROM_UNCATEGORIZED'
) {
return res.boom.badRequest(null, {
errors: [
{
type: 'CANNOT_DELETE_TRANSACTION_CONVERTED_FROM_UNCATEGORIZED',
code: 4100,
},
],
});
}
} }
next(error); next(error);
} }

View File

@@ -1,16 +1,20 @@
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import { Router, Request, Response, NextFunction } from 'express'; import { Router, Request, Response, NextFunction } from 'express';
import { query } from 'express-validator'; import { param, query } from 'express-validator';
import GetCashflowAccountsService from '@/services/Cashflow/GetCashflowAccountsService';
import BaseController from '../BaseController'; import BaseController from '../BaseController';
import GetCashflowTransactionsService from '@/services/Cashflow/GetCashflowTransactionsService';
import { ServiceError } from '@/exceptions'; import { ServiceError } from '@/exceptions';
import CheckPolicies from '@/api/middleware/CheckPolicies'; import CheckPolicies from '@/api/middleware/CheckPolicies';
import { AbilitySubject, CashflowAction } from '@/interfaces'; import { AbilitySubject, CashflowAction } from '@/interfaces';
import { CashflowApplication } from '@/services/Cashflow/CashflowApplication';
@Service() @Service()
export default class GetCashflowAccounts extends BaseController { export default class GetCashflowAccounts extends BaseController {
@Inject() @Inject()
private cashflowApplication: CashflowApplication; getCashflowAccountsService: GetCashflowAccountsService;
@Inject()
getCashflowTransactionsService: GetCashflowTransactionsService;
/** /**
* Controller router. * Controller router.
@@ -31,6 +35,7 @@ export default class GetCashflowAccounts extends BaseController {
query('search_keyword').optional({ nullable: true }).isString().trim(), query('search_keyword').optional({ nullable: true }).isString().trim(),
], ],
this.asyncMiddleware(this.getCashflowAccounts), this.asyncMiddleware(this.getCashflowAccounts),
this.catchServiceErrors
); );
return router; return router;
} }
@@ -57,7 +62,10 @@ export default class GetCashflowAccounts extends BaseController {
try { try {
const cashflowAccounts = const cashflowAccounts =
await this.cashflowApplication.getCashflowAccounts(tenantId, filter); await this.getCashflowAccountsService.getCashflowAccounts(
tenantId,
filter
);
return res.status(200).send({ return res.status(200).send({
cashflow_accounts: this.transfromToResponse(cashflowAccounts), cashflow_accounts: this.transfromToResponse(cashflowAccounts),
@@ -66,4 +74,22 @@ export default class GetCashflowAccounts extends BaseController {
next(error); next(error);
} }
}; };
/**
* Catches the service errors.
* @param {Error} error - Error.
* @param {Request} req - Request.
* @param {Response} res - Response.
* @param {NextFunction} next -
*/
private catchServiceErrors(
error,
req: Request,
res: Response,
next: NextFunction
) {
if (error instanceof ServiceError) {
}
next(error);
}
} }

View File

@@ -1,21 +1,16 @@
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import { Router, Request, Response, NextFunction } from 'express'; import { Router, Request, Response, NextFunction } from 'express';
import { param, query } from 'express-validator'; import { param } from 'express-validator';
import BaseController from '../BaseController'; import BaseController from '../BaseController';
import GetCashflowTransactionsService from '@/services/Cashflow/GetCashflowTransactionsService';
import { ServiceError } from '@/exceptions'; import { ServiceError } from '@/exceptions';
import CheckPolicies from '@/api/middleware/CheckPolicies'; import CheckPolicies from '@/api/middleware/CheckPolicies';
import { AbilitySubject, CashflowAction } from '@/interfaces'; import { AbilitySubject, CashflowAction } from '@/interfaces';
import { CashflowApplication } from '@/services/Cashflow/CashflowApplication';
import { GetMatchedTransactionsFilter } from '@/services/Banking/Matching/types';
import { MatchBankTransactionsApplication } from '@/services/Banking/Matching/MatchBankTransactionsApplication';
@Service() @Service()
export default class GetCashflowAccounts extends BaseController { export default class GetCashflowAccounts extends BaseController {
@Inject() @Inject()
private cashflowApplication: CashflowApplication; getCashflowTransactionsService: GetCashflowTransactionsService;
@Inject()
private bankTransactionsMatchingApp: MatchBankTransactionsApplication;
/** /**
* Controller router. * Controller router.
@@ -23,15 +18,6 @@ export default class GetCashflowAccounts extends BaseController {
public router() { public router() {
const router = Router(); const router = Router();
router.get(
'/transactions/matches',
[
query('uncategorizeTransactionsIds').exists().isArray({ min: 1 }),
query('uncategorizeTransactionsIds.*').exists().isNumeric().toInt(),
],
this.validationResult,
this.getMatchedTransactions.bind(this)
);
router.get( router.get(
'/transactions/:transactionId', '/transactions/:transactionId',
CheckPolicies(CashflowAction.View, AbilitySubject.Cashflow), CheckPolicies(CashflowAction.View, AbilitySubject.Cashflow),
@@ -49,7 +35,7 @@ export default class GetCashflowAccounts extends BaseController {
* @param {NextFunction} next * @param {NextFunction} next
*/ */
private getCashflowTransaction = async ( private getCashflowTransaction = async (
req: Request<{ transactionId: number }>, req: Request,
res: Response, res: Response,
next: NextFunction next: NextFunction
) => { ) => {
@@ -57,10 +43,12 @@ export default class GetCashflowAccounts extends BaseController {
const { transactionId } = req.params; const { transactionId } = req.params;
try { try {
const cashflowTransaction = await this.cashflowApplication.getTransaction( const cashflowTransaction =
tenantId, await this.getCashflowTransactionsService.getCashflowTransaction(
transactionId tenantId,
); transactionId
);
return res.status(200).send({ return res.status(200).send({
cashflow_transaction: this.transfromToResponse(cashflowTransaction), cashflow_transaction: this.transfromToResponse(cashflowTransaction),
}); });
@@ -69,39 +57,6 @@ export default class GetCashflowAccounts extends BaseController {
} }
}; };
/**
* Retrieves the matched transactions.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
private async getMatchedTransactions(
req: Request<
{ transactionId: number },
null,
null,
{ uncategorizeTransactionsIds: Array<number> }
>,
res: Response,
next: NextFunction
) {
const { tenantId } = req;
const uncategorizeTransactionsIds = req.query.uncategorizeTransactionsIds;
const filter = this.matchedQueryData(req) as GetMatchedTransactionsFilter;
try {
const data =
await this.bankTransactionsMatchingApp.getMatchedTransactions(
tenantId,
uncategorizeTransactionsIds,
filter
);
return res.status(200).send(data);
} catch (error) {
next(error);
}
}
/** /**
* Catches the service errors. * Catches the service errors.
* @param {Error} error - Error. * @param {Error} error - Error.

View File

@@ -1,21 +1,16 @@
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import { ValidationChain, body, check, param, query } from 'express-validator'; import { check } from 'express-validator';
import { Router, Request, Response, NextFunction } from 'express'; import { Router, Request, Response, NextFunction } from 'express';
import { omit } from 'lodash';
import BaseController from '../BaseController'; import BaseController from '../BaseController';
import { ServiceError } from '@/exceptions'; import { ServiceError } from '@/exceptions';
import NewCashflowTransactionService from '@/services/Cashflow/NewCashflowTransactionService';
import CheckPolicies from '@/api/middleware/CheckPolicies'; import CheckPolicies from '@/api/middleware/CheckPolicies';
import { import { AbilitySubject, CashflowAction } from '@/interfaces';
AbilitySubject,
CashflowAction,
ICategorizeCashflowTransactioDTO,
} from '@/interfaces';
import { CashflowApplication } from '@/services/Cashflow/CashflowApplication';
@Service() @Service()
export default class NewCashflowTransactionController extends BaseController { export default class NewCashflowTransactionController extends BaseController {
@Inject() @Inject()
private cashflowApplication: CashflowApplication; private newCashflowTranscationService: NewCashflowTransactionService;
/** /**
* Router constructor. * Router constructor.
@@ -23,18 +18,6 @@ export default class NewCashflowTransactionController extends BaseController {
public router() { public router() {
const router = Router(); const router = Router();
router.get(
'/transactions/uncategorized/:id',
this.asyncMiddleware(this.getUncategorizedCashflowTransaction),
this.catchServiceErrors
);
router.get(
'/transactions/:id/uncategorized',
this.getUncategorizedTransactionsValidationSchema,
this.validationResult,
this.asyncMiddleware(this.getUncategorizedCashflowTransactions),
this.catchServiceErrors
);
router.post( router.post(
'/transactions', '/transactions',
CheckPolicies(CashflowAction.Create, AbilitySubject.Cashflow), CheckPolicies(CashflowAction.Create, AbilitySubject.Cashflow),
@@ -43,94 +26,21 @@ export default class NewCashflowTransactionController extends BaseController {
this.asyncMiddleware(this.newCashflowTransaction), this.asyncMiddleware(this.newCashflowTransaction),
this.catchServiceErrors this.catchServiceErrors
); );
router.post(
'/transactions/uncategorize/bulk',
[
body('ids').isArray({ min: 1 }),
body('ids.*').exists().isNumeric().toInt(),
],
this.validationResult,
this.uncategorizeBulkTransactions.bind(this),
this.catchServiceErrors
);
router.post(
'/transactions/:id/uncategorize',
this.revertCategorizedCashflowTransaction,
this.catchServiceErrors
);
router.post(
'/transactions/categorize',
this.categorizeCashflowTransactionValidationSchema,
this.validationResult,
this.categorizeCashflowTransaction,
this.catchServiceErrors
);
router.post(
'/transaction/:id/categorize/expense',
this.categorizeAsExpenseValidationSchema,
this.validationResult,
this.categorizesCashflowTransactionAsExpense,
this.catchServiceErrors
);
return router; return router;
} }
/**
* Getting uncategorized transactions validation schema.
* @returns {ValidationChain}
*/
public get getUncategorizedTransactionsValidationSchema() {
return [
param('id').exists().isNumeric().toInt(),
query('page').optional().isNumeric().toInt(),
query('page_size').optional().isNumeric().toInt(),
query('min_date').optional({ nullable: true }).isISO8601().toDate(),
query('max_date').optional({ nullable: true }).isISO8601().toDate(),
query('min_amount').optional({ nullable: true }).isFloat().toFloat(),
query('max_amount').optional({ nullable: true }).isFloat().toFloat(),
];
}
/**
* Categorize as expense validation schema.
*/
public get categorizeAsExpenseValidationSchema() {
return [
check('expense_account_id').exists(),
check('date').isISO8601().exists(),
check('reference_no').optional(),
check('exchange_rate').optional().isFloat({ gt: 0 }).toFloat(),
];
}
/**
* Categorize cashflow tranasction validation schema.
*/
public get categorizeCashflowTransactionValidationSchema() {
return [
check('uncategorized_transaction_ids').exists().isArray({ min: 1 }),
check('date').exists().isISO8601().toDate(),
check('credit_account_id').exists().isInt().toInt(),
check('transaction_number').optional(),
check('transaction_type').exists(),
check('reference_no').optional(),
check('exchange_rate').optional().isFloat({ gt: 0 }).toFloat(),
check('description').optional(),
check('branch_id').optional({ nullable: true }).isNumeric().toInt(),
];
}
/** /**
* New cashflow transaction validation schema. * New cashflow transaction validation schema.
*/ */
public get newTransactionValidationSchema() { get newTransactionValidationSchema() {
return [ return [
check('date').exists().isISO8601().toDate(), check('date').exists().isISO8601().toDate(),
check('reference_no').optional({ nullable: true }).trim(), check('reference_no').optional({ nullable: true }).trim().escape(),
check('description') check('description')
.optional({ nullable: true }) .optional({ nullable: true })
.isLength({ min: 3 }) .isLength({ min: 3 })
.trim(), .trim()
.escape(),
check('transaction_type').exists(), check('transaction_type').exists(),
check('amount').exists().isFloat().toFloat(), check('amount').exists().isFloat().toFloat(),
@@ -138,7 +48,9 @@ export default class NewCashflowTransactionController extends BaseController {
check('credit_account_id').exists().isInt().toInt(), check('credit_account_id').exists().isInt().toInt(),
check('exchange_rate').optional().isFloat({ gt: 0 }).toFloat(), check('exchange_rate').optional().isFloat({ gt: 0 }).toFloat(),
check('branch_id').optional({ nullable: true }).isNumeric().toInt(), check('branch_id').optional({ nullable: true }).isNumeric().toInt(),
check('publish').default(false).isBoolean().toBoolean(), check('publish').default(false).isBoolean().toBoolean(),
]; ];
} }
@@ -158,12 +70,13 @@ export default class NewCashflowTransactionController extends BaseController {
const ownerContributionDTO = this.matchedBodyData(req); const ownerContributionDTO = this.matchedBodyData(req);
try { try {
const cashflowTransaction = const { cashflowTransaction } =
await this.cashflowApplication.createTransaction( await this.newCashflowTranscationService.newCashflowTransaction(
tenantId, tenantId,
ownerContributionDTO, ownerContributionDTO,
userId userId
); );
return res.status(200).send({ return res.status(200).send({
id: cashflowTransaction.id, id: cashflowTransaction.id,
message: 'New cashflow transaction has been created successfully.', message: 'New cashflow transaction has been created successfully.',
@@ -173,180 +86,11 @@ export default class NewCashflowTransactionController extends BaseController {
} }
}; };
/**
* Revert the categorized cashflow transaction.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
private revertCategorizedCashflowTransaction = async (
req: Request<{ id: number }>,
res: Response,
next: NextFunction
) => {
const { tenantId } = req;
const { id: cashflowTransactionId } = req.params;
try {
const data = await this.cashflowApplication.uncategorizeTransaction(
tenantId,
cashflowTransactionId
);
return res.status(200).send({ data });
} catch (error) {
next(error);
}
};
/**
* Uncategorize the given transactions in bulk.
* @param {Request<{}>} req
* @param {Response} res
* @param {NextFunction} next
* @returns {Promise<Response | null>}
*/
private uncategorizeBulkTransactions = async (
req: Request<{}>,
res: Response,
next: NextFunction
) => {
const { tenantId } = req;
const { ids: uncategorizedTransactionIds } = this.matchedBodyData(req);
try {
await this.cashflowApplication.uncategorizeTransactions(
tenantId,
uncategorizedTransactionIds
);
return res.status(200).send({
message: 'The given transactions have been uncategorized successfully.',
});
} catch (error) {
next(error);
}
};
/**
* Categorize the cashflow transaction.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
private categorizeCashflowTransaction = async (
req: Request,
res: Response,
next: NextFunction
) => {
const { tenantId } = req;
const matchedObject = this.matchedBodyData(req);
const categorizeDTO = omit(matchedObject, [
'uncategorizedTransactionIds',
]) as ICategorizeCashflowTransactioDTO;
const uncategorizedTransactionIds =
matchedObject.uncategorizedTransactionIds;
try {
await this.cashflowApplication.categorizeTransaction(
tenantId,
uncategorizedTransactionIds,
categorizeDTO
);
return res.status(200).send({
message: 'The cashflow transaction has been created successfully.',
});
} catch (error) {
next(error);
}
};
/**
* Categorize the transaction as expense transaction.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
private categorizesCashflowTransactionAsExpense = async (
req: Request,
res: Response,
next: NextFunction
) => {
const { tenantId } = req;
const { id: cashflowTransactionId } = req.params;
const cashflowTransaction = this.matchedBodyData(req);
try {
await this.cashflowApplication.categorizeAsExpense(
tenantId,
cashflowTransactionId,
cashflowTransaction
);
return res.status(200).send({
message: 'The cashflow transaction has been created successfully.',
});
} catch (error) {
next(error);
}
};
/**
* Retrieves the uncategorized cashflow transactions.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
public getUncategorizedCashflowTransaction = async (
req: Request,
res: Response,
next: NextFunction
) => {
const { tenantId } = req;
const { id: transactionId } = req.params;
try {
const data = await this.cashflowApplication.getUncategorizedTransaction(
tenantId,
transactionId
);
return res.status(200).send({ data });
} catch (error) {
next(error);
}
};
/**
* Retrieves the uncategorized cashflow transactions.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
public getUncategorizedCashflowTransactions = async (
req: Request<{ id: number }>,
res: Response,
next: NextFunction
) => {
const { tenantId } = req;
const { id: accountId } = req.params;
const query = this.matchedQueryData(req);
try {
const data = await this.cashflowApplication.getUncategorizedTransactions(
tenantId,
accountId,
query
);
return res.status(200).send(data);
} catch (error) {
next(error);
}
};
/** /**
* Handle the service errors. * Handle the service errors.
* @param error * @param error
* @param {Request} req * @param req
* @param {res * @param res
* @param next * @param next
* @returns * @returns
*/ */
@@ -396,16 +140,6 @@ export default class NewCashflowTransactionController extends BaseController {
], ],
}); });
} }
if (error.errorType === 'UNCATEGORIZED_TRANSACTION_TYPE_INVALID') {
return res.boom.badRequest(null, {
errors: [
{
type: 'UNCATEGORIZED_TRANSACTION_TYPE_INVALID',
code: 4100,
},
],
});
}
} }
next(error); next(error);
} }

View File

@@ -26,27 +26,27 @@ export default class ContactsController extends BaseController {
[...this.autocompleteQuerySchema], [...this.autocompleteQuerySchema],
this.validationResult, this.validationResult,
this.asyncMiddleware(this.autocompleteContacts.bind(this)), this.asyncMiddleware(this.autocompleteContacts.bind(this)),
this.dynamicListService.handlerErrorsToResponse, this.dynamicListService.handlerErrorsToResponse
); );
router.get( router.get(
'/:id', '/:id',
[param('id').exists().isNumeric().toInt()], [param('id').exists().isNumeric().toInt()],
this.validationResult, this.validationResult,
this.asyncMiddleware(this.getContact.bind(this)), this.asyncMiddleware(this.getContact.bind(this))
); );
router.post( router.post(
'/:id/inactivate', '/:id/inactivate',
[param('id').exists().isNumeric().toInt()], [param('id').exists().isNumeric().toInt()],
this.validationResult, this.validationResult,
this.asyncMiddleware(this.inactivateContact.bind(this)), this.asyncMiddleware(this.inactivateContact.bind(this)),
this.handlerServiceErrors, this.handlerServiceErrors
); );
router.post( router.post(
'/:id/activate', '/:id/activate',
[param('id').exists().isNumeric().toInt()], [param('id').exists().isNumeric().toInt()],
this.validationResult, this.validationResult,
this.asyncMiddleware(this.activateContact.bind(this)), this.asyncMiddleware(this.activateContact.bind(this)),
this.handlerServiceErrors, this.handlerServiceErrors
); );
return router; return router;
} }
@@ -56,7 +56,7 @@ export default class ContactsController extends BaseController {
*/ */
get autocompleteQuerySchema() { get autocompleteQuerySchema() {
return [ return [
query('column_sort_by').optional().trim(), query('column_sort_by').optional().trim().escape(),
query('sort_order').optional().isIn(['desc', 'asc']), query('sort_order').optional().isIn(['desc', 'asc']),
query('stringified_filter_roles').optional().isJSON(), query('stringified_filter_roles').optional().isJSON(),
@@ -77,7 +77,7 @@ export default class ContactsController extends BaseController {
try { try {
const contact = await this.contactsService.getContact( const contact = await this.contactsService.getContact(
tenantId, tenantId,
contactId, contactId
); );
return res.status(200).send({ return res.status(200).send({
customer: this.transfromToResponse(contact), customer: this.transfromToResponse(contact),
@@ -105,7 +105,7 @@ export default class ContactsController extends BaseController {
try { try {
const contacts = await this.contactsService.autocompleteContacts( const contacts = await this.contactsService.autocompleteContacts(
tenantId, tenantId,
filter, filter
); );
return res.status(200).send({ contacts }); return res.status(200).send({ contacts });
} catch (error) { } catch (error) {
@@ -122,32 +122,38 @@ export default class ContactsController extends BaseController {
.optional({ nullable: true }) .optional({ nullable: true })
.isString() .isString()
.trim() .trim()
.escape()
.isLength({ max: DATATYPES_LENGTH.STRING }), .isLength({ max: DATATYPES_LENGTH.STRING }),
check('first_name') check('first_name')
.optional({ nullable: true }) .optional({ nullable: true })
.isString() .isString()
.trim() .trim()
.escape()
.isLength({ max: DATATYPES_LENGTH.STRING }), .isLength({ max: DATATYPES_LENGTH.STRING }),
check('last_name') check('last_name')
.optional({ nullable: true }) .optional({ nullable: true })
.isString() .isString()
.trim() .trim()
.escape()
.isLength({ max: DATATYPES_LENGTH.STRING }), .isLength({ max: DATATYPES_LENGTH.STRING }),
check('company_name') check('company_name')
.optional({ nullable: true }) .optional({ nullable: true })
.isString() .isString()
.trim() .trim()
.escape()
.isLength({ max: DATATYPES_LENGTH.STRING }), .isLength({ max: DATATYPES_LENGTH.STRING }),
check('display_name') check('display_name')
.exists() .exists()
.isString() .isString()
.trim() .trim()
.escape()
.isLength({ max: DATATYPES_LENGTH.STRING }), .isLength({ max: DATATYPES_LENGTH.STRING }),
check('email') check('email')
.optional({ nullable: true }) .optional({ nullable: true })
.isString() .isString()
.normalizeEmail()
.isEmail() .isEmail()
.isLength({ max: DATATYPES_LENGTH.STRING }), .isLength({ max: DATATYPES_LENGTH.STRING }),
check('website') check('website')
@@ -160,101 +166,120 @@ export default class ContactsController extends BaseController {
.optional({ nullable: true }) .optional({ nullable: true })
.isString() .isString()
.trim() .trim()
.escape()
.isLength({ max: DATATYPES_LENGTH.STRING }), .isLength({ max: DATATYPES_LENGTH.STRING }),
check('personal_phone') check('personal_phone')
.optional({ nullable: true }) .optional({ nullable: true })
.isString() .isString()
.trim() .trim()
.escape()
.isLength({ max: DATATYPES_LENGTH.STRING }), .isLength({ max: DATATYPES_LENGTH.STRING }),
check('billing_address_1') check('billing_address_1')
.optional({ nullable: true }) .optional({ nullable: true })
.isString() .isString()
.trim() .trim()
.escape()
.isLength({ max: DATATYPES_LENGTH.STRING }), .isLength({ max: DATATYPES_LENGTH.STRING }),
check('billing_address_2') check('billing_address_2')
.optional({ nullable: true }) .optional({ nullable: true })
.isString() .isString()
.trim() .trim()
.escape()
.isLength({ max: DATATYPES_LENGTH.STRING }), .isLength({ max: DATATYPES_LENGTH.STRING }),
check('billing_address_city') check('billing_address_city')
.optional({ nullable: true }) .optional({ nullable: true })
.isString() .isString()
.trim() .trim()
.escape()
.isLength({ max: DATATYPES_LENGTH.STRING }), .isLength({ max: DATATYPES_LENGTH.STRING }),
check('billing_address_country') check('billing_address_country')
.optional({ nullable: true }) .optional({ nullable: true })
.isString() .isString()
.trim() .trim()
.escape()
.isLength({ max: DATATYPES_LENGTH.STRING }), .isLength({ max: DATATYPES_LENGTH.STRING }),
check('billing_address_email') check('billing_address_email')
.optional({ nullable: true }) .optional({ nullable: true })
.isString() .isString()
.isEmail() .isEmail()
.trim() .trim()
.escape()
.isLength({ max: DATATYPES_LENGTH.STRING }), .isLength({ max: DATATYPES_LENGTH.STRING }),
check('billing_address_postcode') check('billing_address_postcode')
.optional({ nullable: true }) .optional({ nullable: true })
.isString() .isString()
.trim() .trim()
.escape()
.isLength({ max: DATATYPES_LENGTH.STRING }), .isLength({ max: DATATYPES_LENGTH.STRING }),
check('billing_address_phone') check('billing_address_phone')
.optional({ nullable: true }) .optional({ nullable: true })
.isString() .isString()
.trim() .trim()
.escape()
.isLength({ max: DATATYPES_LENGTH.STRING }), .isLength({ max: DATATYPES_LENGTH.STRING }),
check('billing_address_state') check('billing_address_state')
.optional({ nullable: true }) .optional({ nullable: true })
.isString() .isString()
.trim() .trim()
.escape()
.isLength({ max: DATATYPES_LENGTH.STRING }), .isLength({ max: DATATYPES_LENGTH.STRING }),
check('shipping_address_1') check('shipping_address_1')
.optional({ nullable: true }) .optional({ nullable: true })
.isString() .isString()
.trim() .trim()
.escape()
.isLength({ max: DATATYPES_LENGTH.STRING }), .isLength({ max: DATATYPES_LENGTH.STRING }),
check('shipping_address_2') check('shipping_address_2')
.optional({ nullable: true }) .optional({ nullable: true })
.isString() .isString()
.trim() .trim()
.escape()
.isLength({ max: DATATYPES_LENGTH.STRING }), .isLength({ max: DATATYPES_LENGTH.STRING }),
check('shipping_address_city') check('shipping_address_city')
.optional({ nullable: true }) .optional({ nullable: true })
.isString() .isString()
.trim() .trim()
.escape()
.isLength({ max: DATATYPES_LENGTH.STRING }), .isLength({ max: DATATYPES_LENGTH.STRING }),
check('shipping_address_country') check('shipping_address_country')
.optional({ nullable: true }) .optional({ nullable: true })
.isString() .isString()
.trim() .trim()
.escape()
.isLength({ max: DATATYPES_LENGTH.STRING }), .isLength({ max: DATATYPES_LENGTH.STRING }),
check('shipping_address_email') check('shipping_address_email')
.optional({ nullable: true }) .optional({ nullable: true })
.isString() .isString()
.isEmail() .isEmail()
.trim() .trim()
.escape()
.isLength({ max: DATATYPES_LENGTH.STRING }), .isLength({ max: DATATYPES_LENGTH.STRING }),
check('shipping_address_postcode') check('shipping_address_postcode')
.optional({ nullable: true }) .optional({ nullable: true })
.isString() .isString()
.trim() .trim()
.escape()
.isLength({ max: DATATYPES_LENGTH.STRING }), .isLength({ max: DATATYPES_LENGTH.STRING }),
check('shipping_address_phone') check('shipping_address_phone')
.optional({ nullable: true }) .optional({ nullable: true })
.isString() .isString()
.trim() .trim()
.escape()
.isLength({ max: DATATYPES_LENGTH.STRING }), .isLength({ max: DATATYPES_LENGTH.STRING }),
check('shipping_address_state') check('shipping_address_state')
.optional({ nullable: true }) .optional({ nullable: true })
.isString() .isString()
.trim() .trim()
.escape()
.isLength({ max: DATATYPES_LENGTH.STRING }), .isLength({ max: DATATYPES_LENGTH.STRING }),
check('note') check('note')
.optional({ nullable: true }) .optional({ nullable: true })
.isString() .isString()
.trim() .trim()
.escape()
.isLength({ max: DATATYPES_LENGTH.TEXT }), .isLength({ max: DATATYPES_LENGTH.TEXT }),
check('active').optional().isBoolean().toBoolean(), check('active').optional().isBoolean().toBoolean(),
]; ];
@@ -355,7 +380,7 @@ export default class ContactsController extends BaseController {
error: Error, error: Error,
req: Request, req: Request,
res: Response, res: Response,
next: NextFunction, next: NextFunction
) { ) {
if (error instanceof ServiceError) { if (error instanceof ServiceError) {
if (error.errorType === 'contact_not_found') { if (error.errorType === 'contact_not_found') {

View File

@@ -106,7 +106,11 @@ export default class CustomersController extends ContactsController {
*/ */
get customerDTOSchema() { get customerDTOSchema() {
return [ return [
check('customer_type').exists().isIn(['business', 'individual']).trim(), check('customer_type')
.exists()
.isIn(['business', 'individual'])
.trim()
.escape(),
]; ];
} }
@@ -119,6 +123,7 @@ export default class CustomersController extends ContactsController {
.optional({ nullable: true }) .optional({ nullable: true })
.isString() .isString()
.trim() .trim()
.escape()
.isLength({ max: 3 }), .isLength({ max: 3 }),
]; ];
} }
@@ -128,7 +133,7 @@ export default class CustomersController extends ContactsController {
*/ */
get validateListQuerySchema() { get validateListQuerySchema() {
return [ return [
query('column_sort_by').optional().trim(), query('column_sort_by').optional().trim().escape(),
query('sort_order').optional().isIn(['desc', 'asc']), query('sort_order').optional().isIn(['desc', 'asc']),
query('page').optional().isNumeric().toInt(), query('page').optional().isNumeric().toInt(),
@@ -155,8 +160,10 @@ export default class CustomersController extends ContactsController {
try { try {
const contact = await this.customersApplication.createCustomer( const contact = await this.customersApplication.createCustomer(
tenantId, tenantId,
contactDTO contactDTO,
user
); );
return res.status(200).send({ return res.status(200).send({
id: contact.id, id: contact.id,
message: 'The customer has been created successfully.', message: 'The customer has been created successfully.',

View File

@@ -106,6 +106,7 @@ export default class VendorsController extends ContactsController {
.optional({ nullable: true }) .optional({ nullable: true })
.isString() .isString()
.trim() .trim()
.escape()
.isLength({ min: 3, max: 3 }), .isLength({ min: 3, max: 3 }),
]; ];
} }
@@ -143,8 +144,10 @@ export default class VendorsController extends ContactsController {
try { try {
const vendor = await this.vendorsApplication.createVendor( const vendor = await this.vendorsApplication.createVendor(
tenantId, tenantId,
contactDTO contactDTO,
user
); );
return res.status(200).send({ return res.status(200).send({
id: vendor.id, id: vendor.id,
message: 'The vendor has been created successfully.', message: 'The vendor has been created successfully.',

View File

@@ -67,7 +67,7 @@ export default class CurrenciesController extends BaseController {
} }
get currencyParamSchema(): ValidationChain[] { get currencyParamSchema(): ValidationChain[] {
return [param('currency_code').exists().trim()]; return [param('currency_code').exists().trim().escape()];
} }
get listSchema(): ValidationChain[] { get listSchema(): ValidationChain[] {
@@ -187,13 +187,11 @@ export default class CurrenciesController extends BaseController {
} }
if (error.errorType === 'currency_code_exists') { if (error.errorType === 'currency_code_exists') {
return res.boom.badRequest(null, { return res.boom.badRequest(null, {
errors: [ errors: [{
{ type: 'CURRENCY_CODE_EXISTS',
type: 'CURRENCY_CODE_EXISTS', message: 'The given currency code is already exists.',
message: 'The given currency code is already exists.', code: 200,
code: 200, }],
},
],
}); });
} }
if (error.errorType === 'CANNOT_DELETE_BASE_CURRENCY') { if (error.errorType === 'CANNOT_DELETE_BASE_CURRENCY') {

View File

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

View File

@@ -1,16 +1,19 @@
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import { Router, Request, Response, NextFunction } from 'express'; import { Router, Request, Response, NextFunction } from 'express';
import { query, oneOf } from 'express-validator'; import { check, param, query } from 'express-validator';
import asyncMiddleware from '@/api/middleware/asyncMiddleware'; import asyncMiddleware from '@/api/middleware/asyncMiddleware';
import BaseController from './BaseController'; import BaseController from './BaseController';
import { ServiceError } from '@/exceptions'; import { ServiceError } from '@/exceptions';
import { EchangeRateErrors } from '@/lib/ExchangeRate/types'; import ExchangeRatesService from '@/services/ExchangeRates/ExchangeRatesService';
import { ExchangeRateApplication } from '@/services/ExchangeRates/ExchangeRateApplication'; import DynamicListingService from '@/services/DynamicListing/DynamicListService';
@Service() @Service()
export default class ExchangeRatesController extends BaseController { export default class ExchangeRatesController extends BaseController {
@Inject() @Inject()
private exchangeRatesApp: ExchangeRateApplication; exchangeRatesService: ExchangeRatesService;
@Inject()
dynamicListService: DynamicListingService;
/** /**
* Constructor method. * Constructor method.
@@ -19,40 +22,164 @@ export default class ExchangeRatesController extends BaseController {
const router = Router(); const router = Router();
router.get( router.get(
'/latest', '/',
[ [...this.exchangeRatesListSchema],
oneOf([
query('to_currency').exists().isString().isISO4217(),
query('from_currency').exists().isString().isISO4217(),
]),
],
this.validationResult, this.validationResult,
asyncMiddleware(this.latestExchangeRate.bind(this)), asyncMiddleware(this.exchangeRates.bind(this)),
this.dynamicListService.handlerErrorsToResponse,
this.handleServiceError,
);
router.post(
'/',
[...this.exchangeRateDTOSchema],
this.validationResult,
asyncMiddleware(this.addExchangeRate.bind(this)),
this.handleServiceError
);
router.post(
'/:id',
[...this.exchangeRateEditDTOSchema, ...this.exchangeRateIdSchema],
this.validationResult,
asyncMiddleware(this.editExchangeRate.bind(this)),
this.handleServiceError
);
router.delete(
'/:id',
[...this.exchangeRateIdSchema],
this.validationResult,
asyncMiddleware(this.deleteExchangeRate.bind(this)),
this.handleServiceError this.handleServiceError
); );
return router; return router;
} }
get exchangeRatesListSchema() {
return [
query('page').optional().isNumeric().toInt(),
query('page_size').optional().isNumeric().toInt(),
query('column_sort_by').optional(),
query('sort_order').optional().isIn(['desc', 'asc']),
];
}
get exchangeRateDTOSchema() {
return [
check('exchange_rate').exists().isNumeric().toFloat(),
check('currency_code').exists().trim().escape(),
check('date').exists().isISO8601(),
];
}
get exchangeRateEditDTOSchema() {
return [check('exchange_rate').exists().isNumeric().toFloat()];
}
get exchangeRateIdSchema() {
return [param('id').isNumeric().toInt()];
}
get exchangeRatesIdsSchema() {
return [
query('ids').isArray({ min: 2 }),
query('ids.*').isNumeric().toInt(),
];
}
/** /**
* Retrieve exchange rates. * Retrieve exchange rates.
* @param {Request} req * @param {Request} req
* @param {Response} res * @param {Response} res
* @param {NextFunction} next * @param {NextFunction} next
*/ */
private async latestExchangeRate( async exchangeRates(req: Request, res: Response, next: NextFunction) {
req: Request,
res: Response,
next: NextFunction
) {
const { tenantId } = req; const { tenantId } = req;
const exchangeRateQuery = this.matchedQueryData(req); const filter = {
page: 1,
pageSize: 12,
filterRoles: [],
columnSortBy: 'created_at',
sortOrder: 'asc',
...this.matchedQueryData(req),
};
if (filter.stringifiedFilterRoles) {
filter.filterRoles = JSON.parse(filter.stringifiedFilterRoles);
}
try {
const exchangeRates = await this.exchangeRatesService.listExchangeRates(
tenantId,
filter
);
return res.status(200).send({ exchange_rates: exchangeRates });
} catch (error) {
next(error);
}
}
/**
* Adds a new exchange rate on the given date.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
async addExchangeRate(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const exchangeRateDTO = this.matchedBodyData(req);
try { try {
const exchangeRate = await this.exchangeRatesApp.latest( const exchangeRate = await this.exchangeRatesService.newExchangeRate(
tenantId, tenantId,
exchangeRateQuery exchangeRateDTO
); );
return res.status(200).send(exchangeRate); return res.status(200).send({ id: exchangeRate.id });
} catch (error) {
next(error);
}
}
/**
* Edit the given exchange rate.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
async editExchangeRate(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const { id: exchangeRateId } = req.params;
const exchangeRateDTO = this.matchedBodyData(req);
try {
const exchangeRate = await this.exchangeRatesService.editExchangeRate(
tenantId,
exchangeRateId,
exchangeRateDTO
);
return res.status(200).send({
id: exchangeRateId,
message: 'The exchange rate has been edited successfully.',
});
} catch (error) {
next(error);
}
}
/**
* Delete the given exchange rate from the storage.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
async deleteExchangeRate(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const { id: exchangeRateId } = req.params;
try {
await this.exchangeRatesService.deleteExchangeRate(
tenantId,
exchangeRateId
);
return res.status(200).send({ id: exchangeRateId });
} catch (error) { } catch (error) {
next(error); next(error);
} }
@@ -65,56 +192,26 @@ export default class ExchangeRatesController extends BaseController {
* @param {Response} res * @param {Response} res
* @param {NextFunction} next * @param {NextFunction} next
*/ */
private handleServiceError( handleServiceError(
error: Error, error: Error,
req: Request, req: Request,
res: Response, res: Response,
next: NextFunction next: NextFunction
) { ) {
if (error instanceof ServiceError) { if (error instanceof ServiceError) {
if (EchangeRateErrors.EX_RATE_INVALID_BASE_CURRENCY === error.errorType) { if (error.errorType === 'EXCHANGE_RATE_NOT_FOUND') {
return res.status(400).send({ return res.status(404).send({
errors: [ errors: [{ type: 'EXCHANGE.RATE.NOT.FOUND', code: 200 }],
{
type: EchangeRateErrors.EX_RATE_INVALID_BASE_CURRENCY,
code: 100,
message: 'The given base currency is invalid.',
},
],
}); });
} else if ( }
EchangeRateErrors.EX_RATE_SERVICE_NOT_ALLOWED === error.errorType if (error.errorType === 'NOT_FOUND_EXCHANGE_RATES') {
) {
return res.status(400).send({ return res.status(400).send({
errors: [ errors: [{ type: 'EXCHANGE.RATES.IS.NOT.FOUND', code: 100 }],
{
type: EchangeRateErrors.EX_RATE_SERVICE_NOT_ALLOWED,
code: 200,
message: 'The service is not allowed',
},
],
}); });
} else if ( }
EchangeRateErrors.EX_RATE_SERVICE_API_KEY_REQUIRED === error.errorType if (error.errorType === 'EXCHANGE_RATE_PERIOD_EXISTS') {
) {
return res.status(400).send({ return res.status(400).send({
errors: [ errors: [{ type: 'EXCHANGE.RATE.PERIOD.EXISTS', code: 300 }],
{
type: EchangeRateErrors.EX_RATE_SERVICE_API_KEY_REQUIRED,
code: 300,
message: 'The API key is required',
},
],
});
} else if (EchangeRateErrors.EX_RATE_LIMIT_EXCEEDED === error.errorType) {
return res.status(400).send({
errors: [
{
type: EchangeRateErrors.EX_RATE_LIMIT_EXCEEDED,
code: 400,
message: 'The API rate limit has been exceeded',
},
],
}); });
} }
} }

View File

@@ -84,11 +84,12 @@ export class ExpensesController extends BaseController {
/** /**
* Expense DTO schema. * Expense DTO schema.
*/ */
private get expenseDTOSchema() { get expenseDTOSchema() {
return [ return [
check('reference_no') check('reference_no')
.optional({ nullable: true }) .optional({ nullable: true })
.trim() .trim()
.escape()
.isLength({ max: DATATYPES_LENGTH.STRING }), .isLength({ max: DATATYPES_LENGTH.STRING }),
check('payment_date').exists().isISO8601().toDate(), check('payment_date').exists().isISO8601().toDate(),
check('payment_account_id') check('payment_account_id')
@@ -122,15 +123,13 @@ export class ExpensesController extends BaseController {
check('categories.*.description') check('categories.*.description')
.optional() .optional()
.trim() .trim()
.escape()
.isLength({ max: DATATYPES_LENGTH.STRING }), .isLength({ max: DATATYPES_LENGTH.STRING }),
check('categories.*.landed_cost').optional().isBoolean().toBoolean(), check('categories.*.landed_cost').optional().isBoolean().toBoolean(),
check('categories.*.project_id') check('categories.*.project_id')
.optional({ nullable: true }) .optional({ nullable: true })
.isInt({ max: DATATYPES_LENGTH.INT_10 }) .isInt({ max: DATATYPES_LENGTH.INT_10 })
.toInt(), .toInt(),
check('attachments').isArray().optional(),
check('attachments.*.key').exists().isString(),
]; ];
} }
@@ -142,6 +141,7 @@ export class ExpensesController extends BaseController {
check('reference_no') check('reference_no')
.optional({ nullable: true }) .optional({ nullable: true })
.trim() .trim()
.escape()
.isLength({ max: DATATYPES_LENGTH.STRING }), .isLength({ max: DATATYPES_LENGTH.STRING }),
check('payment_date').exists().isISO8601().toDate(), check('payment_date').exists().isISO8601().toDate(),
check('payment_account_id') check('payment_account_id')
@@ -176,15 +176,13 @@ export class ExpensesController extends BaseController {
check('categories.*.description') check('categories.*.description')
.optional() .optional()
.trim() .trim()
.escape()
.isLength({ max: DATATYPES_LENGTH.STRING }), .isLength({ max: DATATYPES_LENGTH.STRING }),
check('categories.*.landed_cost').optional().isBoolean().toBoolean(), check('categories.*.landed_cost').optional().isBoolean().toBoolean(),
check('categories.*.project_id') check('categories.*.project_id')
.optional({ nullable: true }) .optional({ nullable: true })
.isInt({ max: DATATYPES_LENGTH.INT_10 }) .isInt({ max: DATATYPES_LENGTH.INT_10 })
.toInt(), .toInt(),
check('attachments').isArray().optional(),
check('attachments.*.key').exists().isString(),
]; ];
} }
@@ -271,7 +269,7 @@ export class ExpensesController extends BaseController {
* @param {Response} res * @param {Response} res
* @param {NextFunction} next * @param {NextFunction} next
*/ */
private async deleteExpense(req: Request, res: Response, next: NextFunction) { async deleteExpense(req: Request, res: Response, next: NextFunction) {
const { tenantId, user } = req; const { tenantId, user } = req;
const { id: expenseId } = req.params; const { id: expenseId } = req.params;
@@ -293,11 +291,7 @@ export class ExpensesController extends BaseController {
* @param {Response} res * @param {Response} res
* @param {NextFunction} next * @param {NextFunction} next
*/ */
private async publishExpense( async publishExpense(req: Request, res: Response, next: NextFunction) {
req: Request,
res: Response,
next: NextFunction
) {
const { tenantId, user } = req; const { tenantId, user } = req;
const { id: expenseId } = req.params; const { id: expenseId } = req.params;
@@ -319,11 +313,7 @@ export class ExpensesController extends BaseController {
* @param {Response} res * @param {Response} res
* @param {NextFunction} next * @param {NextFunction} next
*/ */
private async getExpensesList( async getExpensesList(req: Request, res: Response, next: NextFunction) {
req: Request,
res: Response,
next: NextFunction
) {
const { tenantId } = req; const { tenantId } = req;
const filter = { const filter = {
sortOrder: 'desc', sortOrder: 'desc',
@@ -353,7 +343,7 @@ export class ExpensesController extends BaseController {
* @param {Response} res * @param {Response} res
* @param {NextFunction} next * @param {NextFunction} next
*/ */
private async getExpense(req: Request, res: Response, next: NextFunction) { async getExpense(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req; const { tenantId } = req;
const { id: expenseId } = req.params; const { id: expenseId } = req.params;

View File

@@ -1,87 +0,0 @@
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';
import { convertAcceptFormatToFormat } from './_utils';
@Service()
export class ExportController extends BaseController {
@Inject()
private exportResourceApp: ExportApplication;
/**
* Router constructor method.
*/
public router() {
const router = Router();
router.get(
'/',
[
query('resource').exists(),
query('format').isIn(['csv', 'xlsx']).optional(),
],
this.validationResult,
this.export.bind(this),
);
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 applicationFormat = convertAcceptFormatToFormat(acceptType);
const data = await this.exportResourceApp.export(
tenantId,
query.resource,
applicationFormat
);
// Retrieves the csv format.
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);
//
} else if (ACCEPT_TYPE.APPLICATION_PDF === acceptType) {
res.set({
'Content-Type': 'application/pdf',
'Content-Length': data.length,
});
res.send(data);
}
} catch (error) {
next(error);
}
}
}

View File

@@ -1,13 +0,0 @@
import { ACCEPT_TYPE } from '@/interfaces/Http';
import { ExportFormat } from '@/services/Export/common';
export const convertAcceptFormatToFormat = (accept: string): ExportFormat => {
switch (accept) {
case ACCEPT_TYPE.APPLICATION_CSV:
return ExportFormat.Csv;
case ACCEPT_TYPE.APPLICATION_PDF:
return ExportFormat.Pdf;
case ACCEPT_TYPE.APPLICATION_XLSX:
return ExportFormat.Xlsx;
}
};

View File

@@ -20,7 +20,6 @@ import InventoryDetailsController from './FinancialStatements/InventoryDetails';
import TransactionsByReferenceController from './FinancialStatements/TransactionsByReference'; import TransactionsByReferenceController from './FinancialStatements/TransactionsByReference';
import CashflowAccountTransactions from './FinancialStatements/CashflowAccountTransactions'; import CashflowAccountTransactions from './FinancialStatements/CashflowAccountTransactions';
import ProjectProfitabilityController from './FinancialStatements/ProjectProfitabilitySummary'; import ProjectProfitabilityController from './FinancialStatements/ProjectProfitabilitySummary';
import SalesTaxLiabilitySummary from './FinancialStatements/SalesTaxLiabilitySummary';
@Service() @Service()
export default class FinancialStatementsService { export default class FinancialStatementsService {
@@ -69,44 +68,40 @@ export default class FinancialStatementsService {
); );
router.use( router.use(
'/customer-balance-summary', '/customer-balance-summary',
Container.get(CustomerBalanceSummaryController).router() Container.get(CustomerBalanceSummaryController).router(),
); );
router.use( router.use(
'/vendor-balance-summary', '/vendor-balance-summary',
Container.get(VendorBalanceSummaryController).router() Container.get(VendorBalanceSummaryController).router(),
); );
router.use( router.use(
'/transactions-by-customers', '/transactions-by-customers',
Container.get(TransactionsByCustomers).router() Container.get(TransactionsByCustomers).router(),
); );
router.use( router.use(
'/transactions-by-vendors', '/transactions-by-vendors',
Container.get(TransactionsByVendors).router() Container.get(TransactionsByVendors).router(),
); );
router.use( router.use(
'/cash-flow', '/cash-flow',
Container.get(CashFlowStatementController).router() Container.get(CashFlowStatementController).router(),
); );
router.use( router.use(
'/inventory-item-details', '/inventory-item-details',
Container.get(InventoryDetailsController).router() Container.get(InventoryDetailsController).router(),
); );
router.use( router.use(
'/transactions-by-reference', '/transactions-by-reference',
Container.get(TransactionsByReferenceController).router() Container.get(TransactionsByReferenceController).router(),
); );
router.use( router.use(
'/cashflow-account-transactions', '/cashflow-account-transactions',
Container.get(CashflowAccountTransactions).router() Container.get(CashflowAccountTransactions).router(),
); );
router.use( router.use(
'/project-profitability-summary', '/project-profitability-summary',
Container.get(ProjectProfitabilityController).router() Container.get(ProjectProfitabilityController).router(),
); )
router.use(
'/sales-tax-liability-summary',
Container.get(SalesTaxLiabilitySummary).router()
);
return router; return router;
} }
} }

View File

@@ -2,20 +2,19 @@ import { Router, Request, Response, NextFunction } from 'express';
import { query } from 'express-validator'; import { query } from 'express-validator';
import { Inject } from 'typedi'; import { Inject } from 'typedi';
import asyncMiddleware from '@/api/middleware/asyncMiddleware'; import asyncMiddleware from '@/api/middleware/asyncMiddleware';
import APAgingSummaryReportService from '@/services/FinancialStatements/AgingSummary/APAgingSummaryService';
import BaseFinancialReportController from './BaseFinancialReportController'; import BaseFinancialReportController from './BaseFinancialReportController';
import { AbilitySubject, ReportsAction } from '@/interfaces'; import { AbilitySubject, ReportsAction } from '@/interfaces';
import CheckPolicies from '@/api/middleware/CheckPolicies'; import CheckPolicies from '@/api/middleware/CheckPolicies';
import { ACCEPT_TYPE } from '@/interfaces/Http';
import { APAgingSummaryApplication } from '@/services/FinancialStatements/AgingSummary/APAgingSummaryApplication';
export default class APAgingSummaryReportController extends BaseFinancialReportController { export default class APAgingSummaryReportController extends BaseFinancialReportController {
@Inject() @Inject()
private APAgingSummaryApp: APAgingSummaryApplication; APAgingSummaryService: APAgingSummaryReportService;
/** /**
* Router constructor. * Router constructor.
*/ */
public router() { router() {
const router = Router(); const router = Router();
router.get( router.get(
@@ -29,19 +28,15 @@ export default class APAgingSummaryReportController extends BaseFinancialReportC
/** /**
* Validation schema. * Validation schema.
* @returns {ValidationChain[]}
*/ */
private get validationSchema() { get validationSchema() {
return [ return [
...this.sheetNumberFormatValidationSchema, ...this.sheetNumberFormatValidationSchema,
query('as_date').optional().isISO8601(), query('as_date').optional().isISO8601(),
query('aging_days_before').optional().isNumeric().toInt(),
query('aging_days_before').default(30).isInt({ max: 500 }).toInt(), query('aging_periods').optional().isNumeric().toInt(),
query('aging_periods').default(3).isInt({ max: 12 }).toInt(),
query('vendors_ids').optional().isArray({ min: 1 }), query('vendors_ids').optional().isArray({ min: 1 }),
query('vendors_ids.*').isInt({ min: 1 }).toInt(), query('vendors_ids.*').isInt({ min: 1 }).toInt(),
query('none_zero').default(true).isBoolean().toBoolean(), query('none_zero').default(true).isBoolean().toBoolean(),
// Filtering by branches. // Filtering by branches.
@@ -51,69 +46,22 @@ export default class APAgingSummaryReportController extends BaseFinancialReportC
} }
/** /**
* Retrieves payable aging summary report. * Retrieve payable aging summary report.
* @param {Request} req -
* @param {Response} res -
* @param {NextFunction} next -
*/ */
private async payableAgingSummary( async payableAgingSummary(req: Request, res: Response, next: NextFunction) {
req: Request, const { tenantId, settings } = req;
res: Response,
next: NextFunction
) {
const { tenantId } = req;
const filter = this.matchedQueryData(req); const filter = this.matchedQueryData(req);
try { try {
const accept = this.accepts(req); const { data, columns, query, meta } =
const acceptType = accept.types([ await this.APAgingSummaryService.APAgingSummary(tenantId, filter);
ACCEPT_TYPE.APPLICATION_JSON,
ACCEPT_TYPE.APPLICATION_JSON_TABLE,
ACCEPT_TYPE.APPLICATION_CSV,
ACCEPT_TYPE.APPLICATION_XLSX,
ACCEPT_TYPE.APPLICATION_PDF
]);
// Retrieves the json table format.
if (ACCEPT_TYPE.APPLICATION_JSON_TABLE === acceptType) {
const table = await this.APAgingSummaryApp.table(tenantId, filter);
return res.status(200).send(table); return res.status(200).send({
// Retrieves the csv format. data: this.transfromToResponse(data),
} else if (ACCEPT_TYPE.APPLICATION_CSV === acceptType) { columns: this.transfromToResponse(columns),
const csv = await this.APAgingSummaryApp.csv(tenantId, filter); query: this.transfromToResponse(query),
meta: this.transfromToResponse(meta),
res.setHeader('Content-Disposition', 'attachment; filename=output.csv'); });
res.setHeader('Content-Type', 'text/csv');
return res.send(csv);
// Retrieves the xlsx format.
} else if (ACCEPT_TYPE.APPLICATION_XLSX === acceptType) {
const buffer = await this.APAgingSummaryApp.xlsx(tenantId, filter);
res.setHeader(
'Content-Disposition',
'attachment; filename=output.xlsx'
);
res.setHeader(
'Content-Type',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
);
return res.send(buffer);
// Retrieves the pdf format.
} else if (ACCEPT_TYPE.APPLICATION_PDF === acceptType) {
const pdfContent = await this.APAgingSummaryApp.pdf(tenantId, filter);
res.set({
'Content-Type': 'application/pdf',
'Content-Length': pdfContent.length,
});
return res.send(pdfContent);
// Retrieves the json format.
} else {
const sheet = await this.APAgingSummaryApp.sheet(tenantId, filter);
return res.status(200).send(sheet);
}
} catch (error) { } catch (error) {
next(error); next(error);
} }

View File

@@ -5,18 +5,16 @@ import ARAgingSummaryService from '@/services/FinancialStatements/AgingSummary/A
import BaseFinancialReportController from './BaseFinancialReportController'; import BaseFinancialReportController from './BaseFinancialReportController';
import { AbilitySubject, ReportsAction } from '@/interfaces'; import { AbilitySubject, ReportsAction } from '@/interfaces';
import CheckPolicies from '@/api/middleware/CheckPolicies'; import CheckPolicies from '@/api/middleware/CheckPolicies';
import { ARAgingSummaryApplication } from '@/services/FinancialStatements/AgingSummary/ARAgingSummaryApplication';
import { ACCEPT_TYPE } from '@/interfaces/Http';
@Service() @Service()
export default class ARAgingSummaryReportController extends BaseFinancialReportController { export default class ARAgingSummaryReportController extends BaseFinancialReportController {
@Inject() @Inject()
private ARAgingSummaryApp: ARAgingSummaryApplication; ARAgingSummaryService: ARAgingSummaryService;
/** /**
* Router constructor. * Router constructor.
*/ */
public router() { router() {
const router = Router(); const router = Router();
router.get( router.get(
@@ -32,14 +30,14 @@ export default class ARAgingSummaryReportController extends BaseFinancialReportC
/** /**
* AR aging summary validation roles. * AR aging summary validation roles.
*/ */
private get validationSchema() { get validationSchema() {
return [ return [
...this.sheetNumberFormatValidationSchema, ...this.sheetNumberFormatValidationSchema,
query('as_date').optional().isISO8601(), query('as_date').optional().isISO8601(),
query('aging_days_before').default(30).isInt({ max: 500 }).toInt(), query('aging_days_before').optional().isInt({ max: 500 }).toInt(),
query('aging_periods').default(3).isInt({ max: 12 }).toInt(), query('aging_periods').optional().isInt({ max: 12 }).toInt(),
query('customers_ids').optional().isArray({ min: 1 }), query('customers_ids').optional().isArray({ min: 1 }),
query('customers_ids.*').isInt({ min: 1 }).toInt(), query('customers_ids.*').isInt({ min: 1 }).toInt(),
@@ -54,64 +52,21 @@ export default class ARAgingSummaryReportController extends BaseFinancialReportC
/** /**
* Retrieve AR aging summary report. * Retrieve AR aging summary report.
* @param {Request} req
* @param {Response} res
*/ */
private async receivableAgingSummary(req: Request, res: Response) { async receivableAgingSummary(req: Request, res: Response) {
const { tenantId } = req; const { tenantId, settings } = req;
const filter = this.matchedQueryData(req); const filter = this.matchedQueryData(req);
try { try {
const accept = this.accepts(req); const { data, columns, query, meta } =
await this.ARAgingSummaryService.ARAgingSummary(tenantId, filter);
const acceptType = accept.types([ return res.status(200).send({
ACCEPT_TYPE.APPLICATION_JSON, data: this.transfromToResponse(data),
ACCEPT_TYPE.APPLICATION_JSON_TABLE, columns: this.transfromToResponse(columns),
ACCEPT_TYPE.APPLICATION_CSV, query: this.transfromToResponse(query),
ACCEPT_TYPE.APPLICATION_XLSX, meta: this.transfromToResponse(meta),
ACCEPT_TYPE.APPLICATION_PDF });
]);
// Retrieves the xlsx format.
if (ACCEPT_TYPE.APPLICATION_XLSX === acceptType) {
const buffer = await this.ARAgingSummaryApp.xlsx(tenantId, filter);
res.setHeader(
'Content-Disposition',
'attachment; filename=output.xlsx'
);
res.setHeader(
'Content-Type',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
);
return res.send(buffer);
// Retrieves the table format.
} else if (ACCEPT_TYPE.APPLICATION_JSON_TABLE === acceptType) {
const table = await this.ARAgingSummaryApp.table(tenantId, filter);
return res.status(200).send(table);
// Retrieves the csv format.
} else if (ACCEPT_TYPE.APPLICATION_CSV === acceptType) {
const buffer = await this.ARAgingSummaryApp.csv(tenantId, filter);
res.setHeader('Content-Disposition', 'attachment; filename=output.csv');
res.setHeader('Content-Type', 'text/csv');
return res.send(buffer);
// Retrieves the pdf format.
} else if (ACCEPT_TYPE.APPLICATION_PDF === acceptType) {
const pdfContent = await this.ARAgingSummaryApp.pdf(tenantId, filter);
res.set({
'Content-Type': 'application/pdf',
'Content-Length': pdfContent.length,
});
return res.send(pdfContent);
// Retrieves the json format.
} else {
const sheet = await this.ARAgingSummaryApp.sheet(tenantId, filter);
return res.status(200).send(sheet);
}
} catch (error) { } catch (error) {
console.log(error); console.log(error);
} }

View File

@@ -3,21 +3,25 @@ import { Router, Request, Response, NextFunction } from 'express';
import { query, ValidationChain } from 'express-validator'; import { query, ValidationChain } from 'express-validator';
import { castArray } from 'lodash'; import { castArray } from 'lodash';
import asyncMiddleware from '@/api/middleware/asyncMiddleware'; import asyncMiddleware from '@/api/middleware/asyncMiddleware';
import BalanceSheetStatementService from '@/services/FinancialStatements/BalanceSheet/BalanceSheetService';
import BaseFinancialReportController from './BaseFinancialReportController'; import BaseFinancialReportController from './BaseFinancialReportController';
import { AbilitySubject, ReportsAction } from '@/interfaces'; import { AbilitySubject, ReportsAction } from '@/interfaces';
import CheckPolicies from '@/api/middleware/CheckPolicies'; import CheckPolicies from '@/api/middleware/CheckPolicies';
import { BalanceSheetApplication } from '@/services/FinancialStatements/BalanceSheet/BalanceSheetApplication'; import BalanceSheetTable from '@/services/FinancialStatements/BalanceSheet/BalanceSheetTable';
import { ACCEPT_TYPE } from '@/interfaces/Http'; import HasTenancyService from '@/services/Tenancy/TenancyService';
@Service() @Service()
export default class BalanceSheetStatementController extends BaseFinancialReportController { export default class BalanceSheetStatementController extends BaseFinancialReportController {
@Inject() @Inject()
private balanceSheetApp: BalanceSheetApplication; balanceSheetService: BalanceSheetStatementService;
@Inject()
tenancy: HasTenancyService;
/** /**
* Router constructor. * Router constructor.
*/ */
public router() { router() {
const router = Router(); const router = Router();
router.get( router.get(
@@ -34,10 +38,10 @@ export default class BalanceSheetStatementController extends BaseFinancialReport
* Balance sheet validation schecma. * Balance sheet validation schecma.
* @returns {ValidationChain[]} * @returns {ValidationChain[]}
*/ */
private get balanceSheetValidationSchema(): ValidationChain[] { get balanceSheetValidationSchema(): ValidationChain[] {
return [ return [
...this.sheetNumberFormatValidationSchema, ...this.sheetNumberFormatValidationSchema,
query('accounting_method').optional().isIn(['cash', 'accrual']), query('accounting_method').optional().isIn(['cash', 'accural']),
query('from_date').optional(), query('from_date').optional(),
query('to_date').optional(), query('to_date').optional(),
@@ -80,12 +84,10 @@ export default class BalanceSheetStatementController extends BaseFinancialReport
/** /**
* Retrieve the balance sheet. * Retrieve the balance sheet.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/ */
private async balanceSheet(req: Request, res: Response, next: NextFunction) { async balanceSheet(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req; const { tenantId, settings } = req;
const i18n = this.tenancy.i18n(tenantId);
let filter = this.matchedQueryData(req); let filter = this.matchedQueryData(req);
@@ -93,55 +95,29 @@ export default class BalanceSheetStatementController extends BaseFinancialReport
...filter, ...filter,
accountsIds: castArray(filter.accountsIds), accountsIds: castArray(filter.accountsIds),
}; };
try { try {
const { data, columns, query, meta } =
await this.balanceSheetService.balanceSheet(tenantId, filter);
const accept = this.accepts(req); const accept = this.accepts(req);
const acceptType = accept.types(['json', 'application/json+table']);
const acceptType = accept.types([ const table = new BalanceSheetTable(data, query, i18n);
ACCEPT_TYPE.APPLICATION_JSON,
ACCEPT_TYPE.APPLICATION_JSON_TABLE,
ACCEPT_TYPE.APPLICATION_XLSX,
ACCEPT_TYPE.APPLICATION_CSV,
ACCEPT_TYPE.APPLICATION_PDF,
]);
// Retrieves the json table format.
if (ACCEPT_TYPE.APPLICATION_JSON_TABLE == acceptType) {
const table = await this.balanceSheetApp.table(tenantId, filter);
return res.status(200).send(table); switch (acceptType) {
// Retrieves the csv format. case 'application/json+table':
} else if (ACCEPT_TYPE.APPLICATION_CSV === acceptType) { return res.status(200).send({
const buffer = await this.balanceSheetApp.csv(tenantId, filter); table: {
rows: table.tableRows(),
res.setHeader('Content-Disposition', 'attachment; filename=output.csv'); columns: table.tableColumns(),
res.setHeader('Content-Type', 'text/csv'); },
query,
return res.send(buffer); meta,
// Retrieves the xlsx format. });
} else if (ACCEPT_TYPE.APPLICATION_XLSX === acceptType) { case 'json':
const buffer = await this.balanceSheetApp.xlsx(tenantId, filter); default:
return res.status(200).send({ data, columns, query, meta });
res.setHeader(
'Content-Disposition',
'attachment; filename=output.xlsx'
);
res.setHeader(
'Content-Type',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
);
return res.send(buffer);
// Retrieves the pdf format.
} else if (ACCEPT_TYPE.APPLICATION_PDF === acceptType) {
const pdfContent = await this.balanceSheetApp.pdf(tenantId, filter);
res.set({
'Content-Type': 'application/pdf',
'Content-Length': pdfContent.length,
});
res.send(pdfContent);
} else {
const sheet = await this.balanceSheetApp.sheet(tenantId, filter);
return res.status(200).send(sheet);
} }
} catch (error) { } catch (error) {
next(error); next(error);

View File

@@ -1,7 +1,9 @@
import { query } from 'express-validator'; import { query } from 'express-validator';
import BaseController from '../BaseController'; import BaseController from "../BaseController";
export default class BaseFinancialReportController extends BaseController { export default class BaseFinancialReportController extends BaseController {
get sheetNumberFormatValidationSchema() { get sheetNumberFormatValidationSchema() {
return [ return [
query('number_format.precision') query('number_format.precision')
@@ -17,7 +19,8 @@ export default class BaseFinancialReportController extends BaseController {
query('number_format.negative_format') query('number_format.negative_format')
.optional() .optional()
.isIn(['parentheses', 'mines']) .isIn(['parentheses', 'mines'])
.trim(), .trim()
.escape(),
]; ];
} }
} }

View File

@@ -8,20 +8,29 @@ import {
ValidationChain, ValidationChain,
} from 'express'; } from 'express';
import BaseFinancialReportController from '../BaseFinancialReportController'; import BaseFinancialReportController from '../BaseFinancialReportController';
import { AbilitySubject, ReportsAction } from '@/interfaces'; import CashFlowStatementService from '@/services/FinancialStatements/CashFlow/CashFlowService';
import {
ICashFlowStatementDOO,
ICashFlowStatement,
AbilitySubject,
ReportsAction,
} from '@/interfaces';
import CashFlowTable from '@/services/FinancialStatements/CashFlow/CashFlowTable';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import CheckPolicies from '@/api/middleware/CheckPolicies'; import CheckPolicies from '@/api/middleware/CheckPolicies';
import { ACCEPT_TYPE } from '@/interfaces/Http';
import { CashflowSheetApplication } from '@/services/FinancialStatements/CashFlow/CashflowSheetApplication';
@Service() @Service()
export default class CashFlowController extends BaseFinancialReportController { export default class CashFlowController extends BaseFinancialReportController {
@Inject() @Inject()
private cashflowSheetApp: CashflowSheetApplication; cashFlowService: CashFlowStatementService;
@Inject()
tenancy: HasTenancyService;
/** /**
* Router constructor. * Router constructor.
*/ */
public router() { router() {
const router = Router(); const router = Router();
router.get( router.get(
@@ -38,7 +47,7 @@ export default class CashFlowController extends BaseFinancialReportController {
* Balance sheet validation schecma. * Balance sheet validation schecma.
* @returns {ValidationChain[]} * @returns {ValidationChain[]}
*/ */
private get cashflowValidationSchema(): ValidationChain[] { get cashflowValidationSchema(): ValidationChain[] {
return [ return [
...this.sheetNumberFormatValidationSchema, ...this.sheetNumberFormatValidationSchema,
query('from_date').optional(), query('from_date').optional(),
@@ -58,6 +67,41 @@ export default class CashFlowController extends BaseFinancialReportController {
]; ];
} }
/**
* Retrieve the cashflow statment to json response.
* @param {ICashFlowStatement} cashFlow -
*/
private transformJsonResponse(cashFlowDOO: ICashFlowStatementDOO) {
const { data, query, meta } = cashFlowDOO;
return {
data: this.transfromToResponse(data),
query: this.transfromToResponse(query),
meta: this.transfromToResponse(meta),
};
}
/**
* Transformes the report statement to table rows.
* @param {ITransactionsByVendorsStatement} statement -
*/
private transformToTableRows(
cashFlowDOO: ICashFlowStatementDOO,
tenantId: number
) {
const i18n = this.tenancy.i18n(tenantId);
const cashFlowTable = new CashFlowTable(cashFlowDOO, i18n);
return {
table: {
data: cashFlowTable.tableRows(),
columns: cashFlowTable.tableColumns(),
},
query: this.transfromToResponse(cashFlowDOO.query),
meta: this.transfromToResponse(cashFlowDOO.meta),
};
}
/** /**
* Retrieve the cash flow statment. * Retrieve the cash flow statment.
* @param {Request} req * @param {Request} req
@@ -65,62 +109,26 @@ export default class CashFlowController extends BaseFinancialReportController {
* @param {NextFunction} next * @param {NextFunction} next
* @returns {Response} * @returns {Response}
*/ */
public async cashFlow(req: Request, res: Response, next: NextFunction) { async cashFlow(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req; const { tenantId, settings } = req;
const filter = { const filter = {
...this.matchedQueryData(req), ...this.matchedQueryData(req),
}; };
try { try {
const cashFlow = await this.cashFlowService.cashFlow(tenantId, filter);
const accept = this.accepts(req); const accept = this.accepts(req);
const acceptType = accept.types(['json', 'application/json+table']);
const acceptType = accept.types([ switch (acceptType) {
ACCEPT_TYPE.APPLICATION_JSON, case 'application/json+table':
ACCEPT_TYPE.APPLICATION_JSON_TABLE, return res
ACCEPT_TYPE.APPLICATION_CSV, .status(200)
ACCEPT_TYPE.APPLICATION_XLSX, .send(this.transformToTableRows(cashFlow, tenantId));
ACCEPT_TYPE.APPLICATION_PDF case 'json':
]); default:
// Retrieves the json table format. return res.status(200).send(this.transformJsonResponse(cashFlow));
if (ACCEPT_TYPE.APPLICATION_JSON_TABLE === acceptType) {
const table = await this.cashflowSheetApp.table(tenantId, filter);
return res.status(200).send(table);
// Retrieves the csv format.
} else if (ACCEPT_TYPE.APPLICATION_CSV === acceptType) {
const buffer = await this.cashflowSheetApp.csv(tenantId, filter);
res.setHeader('Content-Disposition', 'attachment; filename=output.csv');
res.setHeader('Content-Type', 'text/csv');
return res.status(200).send(buffer);
// Retrieves the pdf format.
} else if (ACCEPT_TYPE.APPLICATION_XLSX === acceptType) {
const buffer = await this.cashflowSheetApp.xlsx(tenantId, filter);
res.setHeader(
'Content-Disposition',
'attachment; filename=output.xlsx'
);
res.setHeader(
'Content-Type',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
);
return res.send(buffer);
// Retrieves the pdf format.
} else if (ACCEPT_TYPE.APPLICATION_PDF === acceptType) {
const pdfContent = await this.cashflowSheetApp.pdf(tenantId, filter);
res.set({
'Content-Type': 'application/pdf',
'Content-Length': pdfContent.length,
});
return res.send(pdfContent);
// Retrieves the json format.
} else {
const cashflow = await this.cashflowSheetApp.sheet(tenantId, filter);
return res.status(200).send(cashflow);
} }
} catch (error) { } catch (error) {
next(error); next(error);

View File

@@ -1,21 +1,29 @@
import { Router, Request, Response, NextFunction } from 'express'; import { Router, Request, Response, NextFunction } from 'express';
import { query } from 'express-validator'; import { query } from 'express-validator';
import { Inject } from 'typedi'; import { Inject } from 'typedi';
import { AbilitySubject, ReportsAction } from '@/interfaces'; import {
AbilitySubject,
ICustomerBalanceSummaryStatement,
ReportsAction,
} from '@/interfaces';
import asyncMiddleware from '@/api/middleware/asyncMiddleware'; import asyncMiddleware from '@/api/middleware/asyncMiddleware';
import CustomerBalanceSummary from '@/services/FinancialStatements/CustomerBalanceSummary/CustomerBalanceSummaryService';
import BaseFinancialReportController from '../BaseFinancialReportController'; import BaseFinancialReportController from '../BaseFinancialReportController';
import CustomerBalanceSummaryTableRows from '@/services/FinancialStatements/CustomerBalanceSummary/CustomerBalanceSummaryTableRows';
import CheckPolicies from '@/api/middleware/CheckPolicies'; import CheckPolicies from '@/api/middleware/CheckPolicies';
import { ACCEPT_TYPE } from '@/interfaces/Http'; import HasTenancyService from '@/services/Tenancy/TenancyService';
import { CustomerBalanceSummaryApplication } from '@/services/FinancialStatements/CustomerBalanceSummary/CustomerBalanceSummaryApplication';
export default class CustomerBalanceSummaryReportController extends BaseFinancialReportController { export default class CustomerBalanceSummaryReportController extends BaseFinancialReportController {
@Inject() @Inject()
private customerBalanceSummaryApp: CustomerBalanceSummaryApplication; customerBalanceSummaryService: CustomerBalanceSummary;
@Inject()
tenancy: HasTenancyService;
/** /**
* Router constructor. * Router constructor.
*/ */
public router() { router() {
const router = Router(); const router = Router();
router.get( router.get(
@@ -34,7 +42,7 @@ export default class CustomerBalanceSummaryReportController extends BaseFinancia
/** /**
* Validation schema. * Validation schema.
*/ */
private get validationSchema() { get validationSchema() {
return [ return [
...this.sheetNumberFormatValidationSchema, ...this.sheetNumberFormatValidationSchema,
@@ -54,81 +62,75 @@ export default class CustomerBalanceSummaryReportController extends BaseFinancia
]; ];
} }
/**
* Transformes the balance summary statement to table rows.
* @param {ICustomerBalanceSummaryStatement} statement -
*/
private transformToTableRows(
tenantId,
{ data, query }: ICustomerBalanceSummaryStatement
) {
const i18n = this.tenancy.i18n(tenantId);
const tableRows = new CustomerBalanceSummaryTableRows(data, query, i18n);
return {
table: {
columns: tableRows.tableColumns(),
data: tableRows.tableRows(),
},
query: this.transfromToResponse(query),
};
}
/**
* Transformes the balance summary statement to raw json.
* @param {ICustomerBalanceSummaryStatement} customerBalance -
*/
private transformToJsonResponse({
data,
columns,
query,
}: ICustomerBalanceSummaryStatement) {
return {
data: this.transfromToResponse(data),
columns: this.transfromToResponse(columns),
query: this.transfromToResponse(query),
};
}
/** /**
* Retrieve payable aging summary report. * Retrieve payable aging summary report.
* @param {Request} req - * @param {Request} req -
* @param {Response} res - * @param {Response} res -
* @param {NextFunction} next - * @param {NextFunction} next -
*/ */
private async customerBalanceSummary( async customerBalanceSummary(
req: Request, req: Request,
res: Response, res: Response,
next: NextFunction next: NextFunction
) { ) {
const { tenantId } = req; const { tenantId, settings } = req;
const filter = this.matchedQueryData(req); const filter = this.matchedQueryData(req);
try { try {
const customerBalanceSummary =
await this.customerBalanceSummaryService.customerBalanceSummary(
tenantId,
filter
);
const accept = this.accepts(req); const accept = this.accepts(req);
const acceptType = accept.types([ const acceptType = accept.types(['json', 'application/json+table']);
ACCEPT_TYPE.APPLICATION_JSON,
ACCEPT_TYPE.APPLICATION_JSON_TABLE,
ACCEPT_TYPE.APPLICATION_CSV,
ACCEPT_TYPE.APPLICATION_XLSX,
ACCEPT_TYPE.APPLICATION_PDF,
]);
// Retrieves the xlsx format. switch (acceptType) {
if (ACCEPT_TYPE.APPLICATION_XLSX === acceptType) { case 'application/json+table':
const buffer = await this.customerBalanceSummaryApp.xlsx( return res
tenantId, .status(200)
filter .send(this.transformToTableRows(tenantId, customerBalanceSummary));
); case 'application/json':
res.setHeader( default:
'Content-Disposition', return res
'attachment; filename=output.xlsx' .status(200)
); .send(this.transformToJsonResponse(customerBalanceSummary));
res.setHeader(
'Content-Type',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
);
return res.send(buffer);
// Retrieves the csv format.
} else if (ACCEPT_TYPE.APPLICATION_CSV === acceptType) {
const buffer = await this.customerBalanceSummaryApp.csv(
tenantId,
filter
);
res.setHeader('Content-Disposition', 'attachment; filename=output.csv');
res.setHeader('Content-Type', 'text/csv');
return res.send(buffer);
// Retrieves the json table format.
} else if (ACCEPT_TYPE.APPLICATION_JSON_TABLE === acceptType) {
const table = await this.customerBalanceSummaryApp.table(
tenantId,
filter
);
return res.status(200).send(table);
// Retrieves the pdf format.
} else if (ACCEPT_TYPE.APPLICATION_PDF === acceptType) {
const buffer = await this.customerBalanceSummaryApp.pdf(
tenantId,
filter
);
res.set({
'Content-Type': 'application/pdf',
'Content-Length': buffer.length,
});
return res.send(buffer);
// Retrieves the json format.
} else {
const sheet = await this.customerBalanceSummaryApp.sheet(
tenantId,
filter
);
return res.status(200).send(sheet);
} }
} catch (error) { } catch (error) {
next(error); next(error);

View File

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

View File

@@ -8,20 +8,24 @@ import {
ValidationChain, ValidationChain,
} from 'express'; } from 'express';
import BaseController from '@/api/controllers/BaseController'; import BaseController from '@/api/controllers/BaseController';
import InventoryDetailsService from '@/services/FinancialStatements/InventoryDetails/InventoryDetailsService';
import InventoryDetailsTable from '@/services/FinancialStatements/InventoryDetails/InventoryDetailsTable';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { AbilitySubject, ReportsAction } from '@/interfaces'; import { AbilitySubject, ReportsAction } from '@/interfaces';
import { InventortyDetailsApplication } from '@/services/FinancialStatements/InventoryDetails/InventoryDetailsApplication';
import CheckPolicies from '@/api/middleware/CheckPolicies'; import CheckPolicies from '@/api/middleware/CheckPolicies';
import { ACCEPT_TYPE } from '@/interfaces/Http';
@Service() @Service()
export default class InventoryDetailsController extends BaseController { export default class InventoryDetailsController extends BaseController {
@Inject() @Inject()
private inventoryItemDetailsApp: InventortyDetailsApplication; inventoryDetailsService: InventoryDetailsService;
@Inject()
tenancy: HasTenancyService;
/** /**
* Router constructor. * Router constructor.
*/ */
public router() { router() {
const router = Router(); const router = Router();
router.get( router.get(
@@ -41,7 +45,7 @@ export default class InventoryDetailsController extends BaseController {
* Balance sheet validation schecma. * Balance sheet validation schecma.
* @returns {ValidationChain[]} * @returns {ValidationChain[]}
*/ */
private get validationSchema(): ValidationChain[] { get validationSchema(): ValidationChain[] {
return [ return [
query('number_format.precision') query('number_format.precision')
.optional() .optional()
@@ -51,7 +55,8 @@ export default class InventoryDetailsController extends BaseController {
query('number_format.negative_format') query('number_format.negative_format')
.optional() .optional()
.isIn(['parentheses', 'mines']) .isIn(['parentheses', 'mines'])
.trim(), .trim()
.escape(),
query('from_date').optional(), query('from_date').optional(),
query('to_date').optional(), query('to_date').optional(),
@@ -72,76 +77,69 @@ export default class InventoryDetailsController extends BaseController {
} }
/** /**
* Retrieve the inventory item details sheet. * Retrieve the cashflow statment to json response.
* @param {ICashFlowStatement} cashFlow -
*/
private transformJsonResponse(inventoryDetails) {
const { data, query, meta } = inventoryDetails;
return {
data: this.transfromToResponse(data),
query: this.transfromToResponse(query),
meta: this.transfromToResponse(meta),
};
}
/**
* Transformes the report statement to table rows.
*/
private transformToTableRows(inventoryDetails, tenantId: number) {
const i18n = this.tenancy.i18n(tenantId);
const inventoryDetailsTable = new InventoryDetailsTable(
inventoryDetails,
i18n
);
return {
table: {
data: inventoryDetailsTable.tableData(),
columns: inventoryDetailsTable.tableColumns(),
},
query: this.transfromToResponse(inventoryDetails.query),
meta: this.transfromToResponse(inventoryDetails.meta),
};
}
/**
* Retrieve the cash flow statment.
* @param {Request} req * @param {Request} req
* @param {Response} res * @param {Response} res
* @param {NextFunction} next * @param {NextFunction} next
* @returns {Response} * @returns {Response}
*/ */
private async inventoryDetails( async inventoryDetails(req: Request, res: Response, next: NextFunction) {
req: Request, const { tenantId, settings } = req;
res: Response,
next: NextFunction
) {
const { tenantId } = req;
const filter = { const filter = {
...this.matchedQueryData(req), ...this.matchedQueryData(req),
}; };
try { try {
const inventoryDetails =
await this.inventoryDetailsService.inventoryDetails(tenantId, filter);
const accept = this.accepts(req); const accept = this.accepts(req);
const acceptType = accept.types([ const acceptType = accept.types(['json', 'application/json+table']);
ACCEPT_TYPE.APPLICATION_JSON,
ACCEPT_TYPE.APPLICATION_JSON_TABLE,
ACCEPT_TYPE.APPLICATION_CSV,
ACCEPT_TYPE.APPLICATION_XLSX,
ACCEPT_TYPE.APPLICATION_PDF,
]);
// Retrieves the csv format.
if (acceptType === ACCEPT_TYPE.APPLICATION_CSV) {
const buffer = await this.inventoryItemDetailsApp.csv(tenantId, filter);
res.setHeader('Content-Disposition', 'attachment; filename=output.csv'); switch (acceptType) {
res.setHeader('Content-Type', 'text/csv'); case 'application/json+table':
return res
return res.send(buffer); .status(200)
// Retrieves the xlsx format. .send(this.transformToTableRows(inventoryDetails, tenantId));
} else if (acceptType === ACCEPT_TYPE.APPLICATION_XLSX) { case 'json':
const buffer = await this.inventoryItemDetailsApp.xlsx( default:
tenantId, return res
filter .status(200)
); .send(this.transformJsonResponse(inventoryDetails));
res.setHeader(
'Content-Disposition',
'attachment; filename=output.xlsx'
);
res.setHeader(
'Content-Type',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
);
return res.send(buffer);
// Retrieves the json table format.
} else if (acceptType === ACCEPT_TYPE.APPLICATION_JSON_TABLE) {
const table = await this.inventoryItemDetailsApp.table(
tenantId,
filter
);
return res.status(200).send(table);
// Retrieves the pdf format.
} else if (acceptType === ACCEPT_TYPE.APPLICATION_PDF) {
const buffer = await this.inventoryItemDetailsApp.pdf(tenantId, filter);
res.set({
'Content-Type': 'application/pdf',
'Content-Length': buffer.length,
});
return res.send(buffer);
} else {
const sheet = await this.inventoryItemDetailsApp.sheet(
tenantId,
filter
);
return res.status(200).send(sheet);
} }
} catch (error) { } catch (error) {
next(error); next(error);

View File

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

View File

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

View File

@@ -1,20 +1,24 @@
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import { Router, Request, Response, NextFunction } from 'express'; import { Router, Request, Response, NextFunction } from 'express';
import { query, ValidationChain } from 'express-validator'; import { query, ValidationChain } from 'express-validator';
import ProfitLossSheetService from '@/services/FinancialStatements/ProfitLossSheet/ProfitLossSheetService';
import BaseFinancialReportController from './BaseFinancialReportController'; import BaseFinancialReportController from './BaseFinancialReportController';
import CheckPolicies from '@/api/middleware/CheckPolicies'; import CheckPolicies from '@/api/middleware/CheckPolicies';
import { AbilitySubject, ReportsAction } from '@/interfaces'; import { AbilitySubject, ReportsAction } from '@/interfaces';
import { ACCEPT_TYPE } from '@/interfaces/Http'; import { ProfitLossSheetTable } from '@/services/FinancialStatements/ProfitLossSheet/ProfitLossSheetTable';
import { ProfitLossSheetApplication } from '@/services/FinancialStatements/ProfitLossSheet/ProfitLossSheetApplication'; import HasTenancyService from '@/services/Tenancy/TenancyService';
@Service() @Service()
export default class ProfitLossSheetController extends BaseFinancialReportController { export default class ProfitLossSheetController extends BaseFinancialReportController {
@Inject() @Inject()
private profitLossSheetApp: ProfitLossSheetApplication; profitLossSheetService: ProfitLossSheetService;
@Inject()
tenancy: HasTenancyService;
/** /**
* Router constructor. * Router constructor.
*/ */
public router() { router() {
const router = Router(); const router = Router();
router.get( router.get(
@@ -30,7 +34,7 @@ export default class ProfitLossSheetController extends BaseFinancialReportContro
/** /**
* Validation schema. * Validation schema.
*/ */
private get validationSchema(): ValidationChain[] { get validationSchema(): ValidationChain[] {
return [ return [
...this.sheetNumberFormatValidationSchema, ...this.sheetNumberFormatValidationSchema,
query('basis').optional(), query('basis').optional(),
@@ -81,63 +85,37 @@ export default class ProfitLossSheetController extends BaseFinancialReportContro
* @param {Request} req - * @param {Request} req -
* @param {Response} res - * @param {Response} res -
*/ */
private async profitLossSheet( async profitLossSheet(req: Request, res: Response, next: NextFunction) {
req: Request, const { tenantId, settings } = req;
res: Response, const i18n = this.tenancy.i18n(tenantId);
next: NextFunction
) {
const { tenantId } = req;
const filter = this.matchedQueryData(req); const filter = this.matchedQueryData(req);
const accept = this.accepts(req);
const acceptType = accept.types([
ACCEPT_TYPE.APPLICATION_JSON,
ACCEPT_TYPE.APPLICATION_JSON_TABLE,
ACCEPT_TYPE.APPLICATION_CSV,
ACCEPT_TYPE.APPLICATION_XLSX,
ACCEPT_TYPE.APPLICATION_PDF,
]);
try { try {
// Retrieves the csv format. const { data, query, meta } =
if (acceptType === ACCEPT_TYPE.APPLICATION_CSV) { await this.profitLossSheetService.profitLossSheet(tenantId, filter);
const sheet = await this.profitLossSheetApp.csv(tenantId, filter);
res.setHeader('Content-Disposition', 'attachment; filename=output.csv'); const accept = this.accepts(req);
res.setHeader('Content-Type', 'text/csv'); const acceptType = accept.types(['json', 'application/json+table']);
return res.send(sheet); switch (acceptType) {
// Retrieves the json table format. case 'application/json+table':
} else if (acceptType === ACCEPT_TYPE.APPLICATION_JSON_TABLE) { const table = new ProfitLossSheetTable(data, query, i18n);
const table = await this.profitLossSheetApp.table(tenantId, filter);
return res.status(200).send(table); return res.status(200).send({
// Retrieves the xlsx format. table: {
} else if (acceptType === ACCEPT_TYPE.APPLICATION_XLSX) { rows: table.tableRows(),
const sheet = await this.profitLossSheetApp.xlsx(tenantId, filter); columns: table.tableColumns(),
},
res.setHeader( query,
'Content-Disposition', meta,
'attachment; filename=output.xlsx' });
); case 'json':
res.setHeader( default:
'Content-Type', return res.status(200).send({
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' data,
); query,
return res.send(sheet); meta,
// Retrieves the json format. });
} else if (acceptType === ACCEPT_TYPE.APPLICATION_PDF) {
const pdfContent = await this.profitLossSheetApp.pdf(tenantId, filter);
res.set({
'Content-Type': 'application/pdf',
'Content-Length': pdfContent.length,
});
return res.send(pdfContent);
} else {
const sheet = await this.profitLossSheetApp.sheet(tenantId, filter);
return res.status(200).send(sheet);
} }
} catch (error) { } catch (error) {
next(error); next(error);

View File

@@ -1,18 +1,17 @@
import { Router, Request, Response, NextFunction } from 'express'; import { Router, Request, Response, NextFunction } from 'express';
import { query, ValidationChain } from 'express-validator'; import { query, ValidationChain } from 'express-validator';
import moment from 'moment';
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import asyncMiddleware from '@/api/middleware/asyncMiddleware'; import asyncMiddleware from '@/api/middleware/asyncMiddleware';
import BaseFinancialReportController from './BaseFinancialReportController'; import BaseFinancialReportController from './BaseFinancialReportController';
import { PurchasesByItemsService } from '@/services/FinancialStatements/PurchasesByItems/PurchasesByItemsService'; import PurchasesByItemsService from '@/services/FinancialStatements/PurchasesByItems/PurchasesByItemsService';
import { AbilitySubject, ReportsAction } from '@/interfaces'; import { AbilitySubject, ReportsAction } from '@/interfaces';
import CheckPolicies from '@/api/middleware/CheckPolicies'; import CheckPolicies from '@/api/middleware/CheckPolicies';
import { ACCEPT_TYPE } from '@/interfaces/Http';
import { PurcahsesByItemsApplication } from '@/services/FinancialStatements/PurchasesByItems/PurchasesByItemsApplication';
@Service() @Service()
export default class PurchasesByItemReportController extends BaseFinancialReportController { export default class PurchasesByItemReportController extends BaseFinancialReportController {
@Inject() @Inject()
private purchasesByItemsApp: PurcahsesByItemsApplication; purchasesByItemsService: PurchasesByItemsService;
/** /**
* Router constructor. * Router constructor.
@@ -64,56 +63,20 @@ export default class PurchasesByItemReportController extends BaseFinancialReport
* @param {Request} req - * @param {Request} req -
* @param {Response} res - * @param {Response} res -
*/ */
public async purchasesByItems(req: Request, res: Response) { async purchasesByItems(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req; const { tenantId } = req;
const filter = this.matchedQueryData(req); const filter = this.matchedQueryData(req);
const accept = this.accepts(req); try {
const { data, query, meta } =
const acceptType = accept.types([ await this.purchasesByItemsService.purchasesByItems(tenantId, filter);
ACCEPT_TYPE.APPLICATION_JSON, return res.status(200).send({
ACCEPT_TYPE.APPLICATION_JSON_TABLE, meta: this.transfromToResponse(meta),
ACCEPT_TYPE.APPLICATION_XLSX, data: this.transfromToResponse(data),
ACCEPT_TYPE.APPLICATION_CSV, query: this.transfromToResponse(query),
ACCEPT_TYPE.APPLICATION_PDF,
]);
// JSON table response format.
if (ACCEPT_TYPE.APPLICATION_JSON_TABLE === acceptType) {
const table = await this.purchasesByItemsApp.table(tenantId, filter);
return res.status(200).send(table);
// CSV response format.
} else if (ACCEPT_TYPE.APPLICATION_CSV === acceptType) {
const buffer = await this.purchasesByItemsApp.csv(tenantId, filter);
res.setHeader('Content-Disposition', 'attachment; filename=output.csv');
res.setHeader('Content-Type', 'text/csv');
return res.send(buffer);
// Xlsx response format.
} else if (ACCEPT_TYPE.APPLICATION_XLSX === acceptType) {
const buffer = await this.purchasesByItemsApp.xlsx(tenantId, filter);
res.setHeader('Content-Disposition', 'attachment; filename=output.xlsx');
res.setHeader(
'Content-Type',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
);
return res.send(buffer);
// PDF response format.
} else if (ACCEPT_TYPE.APPLICATION_PDF === acceptType) {
const pdfContent = await this.purchasesByItemsApp.pdf(tenantId, filter);
res.set({
'Content-Type': 'application/pdf',
'Content-Length': pdfContent.length,
}); });
return res.send(pdfContent); } catch (error) {
// Json response format. next(error);
} else {
const sheet = await this.purchasesByItemsApp.sheet(tenantId, filter);
return res.status(200).send(sheet);
} }
} }
} }

View File

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

View File

@@ -1,122 +0,0 @@
import { Inject } from 'typedi';
import { Router, Request, Response, NextFunction } from 'express';
import { query } from 'express-validator';
import asyncMiddleware from '@/api/middleware/asyncMiddleware';
import BaseFinancialReportController from '../BaseFinancialReportController';
import { AbilitySubject, ReportsAction } from '@/interfaces';
import CheckPolicies from '@/api/middleware/CheckPolicies';
import { SalesTaxLiabilitySummaryApplication } from '@/services/FinancialStatements/SalesTaxLiabilitySummary/SalesTaxLiabilitySummaryApplication';
import { ACCEPT_TYPE } from '@/interfaces/Http';
export default class SalesTaxLiabilitySummary extends BaseFinancialReportController {
@Inject()
private salesTaxLiabilitySummaryApp: SalesTaxLiabilitySummaryApplication;
/**
* Router constructor.
*/
public router() {
const router = Router();
router.get(
'/',
CheckPolicies(
ReportsAction.READ_SALES_TAX_LIABILITY_SUMMARY,
AbilitySubject.Report
),
this.validationSchema,
asyncMiddleware(this.salesTaxLiabilitySummary.bind(this))
);
return router;
}
/**
* Validation schema.
* @returns {ValidationChain[]}
*/
private get validationSchema() {
return [
query('from_date').optional().isISO8601(),
query('to_date').optional().isISO8601(),
];
}
/*
* Retrieves the sales tax liability summary.
* @param {Request} req -
* @param {Response} res -
* @param {NextFunction} next -
*/
private async salesTaxLiabilitySummary(
req: Request,
res: Response,
next: NextFunction
) {
const { tenantId } = req;
const filter = this.matchedQueryData(req);
try {
const accept = this.accepts(req);
const acceptType = accept.types([
ACCEPT_TYPE.APPLICATION_JSON,
ACCEPT_TYPE.APPLICATION_JSON_TABLE,
ACCEPT_TYPE.APPLICATION_CSV,
ACCEPT_TYPE.APPLICATION_XLSX,
ACCEPT_TYPE.APPLICATION_PDF,
]);
// Retrieves the json table format.
if (acceptType === ACCEPT_TYPE.APPLICATION_JSON_TABLE) {
const table = await this.salesTaxLiabilitySummaryApp.table(
tenantId,
filter
);
return res.status(200).send(table);
// Retrieves the xlsx format.
} else if (acceptType === ACCEPT_TYPE.APPLICATION_XLSX) {
const buffer = await this.salesTaxLiabilitySummaryApp.xlsx(
tenantId,
filter
);
res.setHeader(
'Content-Disposition',
'attachment; filename=output.xlsx'
);
res.setHeader(
'Content-Type',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
);
return res.send(buffer);
// Retrieves the csv format.
} else if (acceptType === ACCEPT_TYPE.APPLICATION_CSV) {
const buffer = await this.salesTaxLiabilitySummaryApp.csv(
tenantId,
filter
);
res.setHeader('Content-Disposition', 'attachment; filename=output.csv');
res.setHeader('Content-Type', 'text/csv');
return res.send(buffer);
// Retrieves the json format.
} else if (acceptType === ACCEPT_TYPE.APPLICATION_PDF) {
const pdfContent = await this.salesTaxLiabilitySummaryApp.pdf(
tenantId,
filter
);
res.set({
'Content-Type': 'application/pdf',
'Content-Length': pdfContent.length,
});
return res.status(200).send(pdfContent);
} else {
const sheet = await this.salesTaxLiabilitySummaryApp.sheet(
tenantId,
filter
);
return res.status(200).send(sheet);
}
} catch (error) {
next(error);
}
}
}

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