mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-26 01:29:48 +00:00
Compare commits
95 Commits
feature/20
...
feat/accou
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d35915b16b | ||
|
|
b5d1a2c9d0 | ||
|
|
f5e74f3e88 | ||
|
|
c83132b867 | ||
|
|
88ff5db0f3 | ||
|
|
f35e85c3d2 | ||
|
|
29decf9c5a | ||
|
|
f149ff43b4 | ||
|
|
fb05af8c00 | ||
|
|
688b1bfb56 | ||
|
|
0f8147daff | ||
|
|
96b24d4fb9 | ||
|
|
2a87103bc8 | ||
|
|
238b60144f | ||
|
|
fcee85e358 | ||
|
|
64a10053e3 | ||
|
|
ce9f2a238f | ||
|
|
75b98c39d8 | ||
|
|
80e545072d | ||
|
|
de3d4698ea | ||
|
|
171091e0e0 | ||
|
|
78032d7bfc | ||
|
|
06b8a836c5 | ||
|
|
37fa9f9bc6 | ||
|
|
17deeb18e3 | ||
|
|
8416b45f4e | ||
|
|
3cc5aab80e | ||
|
|
93711a7bf4 | ||
|
|
43066c4f1f | ||
|
|
d5402b6a9b | ||
|
|
174aec78ca | ||
|
|
7162e948dd | ||
|
|
8ad1be1d52 | ||
|
|
a96ced10db | ||
|
|
2d39e38578 | ||
|
|
af80afcf59 | ||
|
|
66cb0521e5 | ||
|
|
9204b76346 | ||
|
|
36cbb1eef5 | ||
|
|
441e27581b | ||
|
|
e0d9a56a29 | ||
|
|
5a017104ce | ||
|
|
25ca620836 | ||
|
|
5a3655e093 | ||
|
|
49c2777587 | ||
|
|
a5680c08c2 | ||
|
|
d909dad1bf | ||
|
|
f32cc752ef | ||
|
|
a7f98201cc | ||
|
|
a1d0fc3f0a | ||
|
|
11575cfb96 | ||
|
|
a2160c0595 | ||
|
|
956a9b58dd | ||
|
|
acb701d618 | ||
|
|
09ff72d302 | ||
|
|
7375512fec | ||
|
|
77e65389a4 | ||
|
|
1972861c97 | ||
|
|
c47acdee03 | ||
|
|
8689962bf3 | ||
|
|
3258159474 | ||
|
|
36bfa573ad | ||
|
|
2c05785096 | ||
|
|
6af4be9c6c | ||
|
|
8def1d31d2 | ||
|
|
afab02a053 | ||
|
|
8e925c62f2 | ||
|
|
1b7d513adf | ||
|
|
7d764fb390 | ||
|
|
c571f50a74 | ||
|
|
6549026344 | ||
|
|
0963394b04 | ||
|
|
6cab0651fc | ||
|
|
4af537d6dd | ||
|
|
34db64612c | ||
|
|
10225bbfed | ||
|
|
c3a4fe6b37 | ||
|
|
02be959461 | ||
|
|
d5bf56e333 | ||
|
|
e3182c15b3 | ||
|
|
dfa63ece21 | ||
|
|
6e95bd7da1 | ||
|
|
f51fffa5c7 | ||
|
|
6193358cc3 | ||
|
|
518abcd30d | ||
|
|
7874b9f765 | ||
|
|
02cc7e0c96 | ||
|
|
57cc513873 | ||
|
|
f5bfdede30 | ||
|
|
488556bb59 | ||
|
|
0fc5a66e95 | ||
|
|
d9ae51027e | ||
|
|
a92d6112d9 | ||
|
|
889b0cec4b | ||
|
|
1c4c41ebba |
93
.dockerignore
Normal file
93
.dockerignore
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
**/node_modules/
|
||||||
|
.pnpm-store/
|
||||||
|
|
||||||
|
# Build outputs
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
**/dist/
|
||||||
|
**/build/
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Development files
|
||||||
|
.git/
|
||||||
|
.gitignore
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# Test files
|
||||||
|
test/
|
||||||
|
**/test/
|
||||||
|
**/*.spec.ts
|
||||||
|
**/*.test.ts
|
||||||
|
**/*.e2e-spec.ts
|
||||||
|
coverage/
|
||||||
|
.nyc_output/
|
||||||
|
test-results/
|
||||||
|
playwright-report/
|
||||||
|
|
||||||
|
# Documentation
|
||||||
|
*.md
|
||||||
|
!README.md
|
||||||
|
docs/
|
||||||
|
CHANGELOG.md
|
||||||
|
CONTRIBUTING.md
|
||||||
|
DISCLAIMER
|
||||||
|
LICENSE
|
||||||
|
|
||||||
|
# CI/CD
|
||||||
|
.github/
|
||||||
|
.gitpod.yml
|
||||||
|
|
||||||
|
# Environment files
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
logs/
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
# OS files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Docker files (don't copy Dockerfiles into themselves)
|
||||||
|
docker-compose*.yml
|
||||||
|
Dockerfile*
|
||||||
|
.dockerignore
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
.cache/
|
||||||
|
.temp/
|
||||||
|
tmp/
|
||||||
|
*.tmp
|
||||||
|
.qodo/
|
||||||
|
e2e/
|
||||||
|
playwright.config.ts
|
||||||
|
|
||||||
|
# Source maps (not needed in production)
|
||||||
|
*.map
|
||||||
|
|
||||||
|
# TypeScript configs (not needed at runtime)
|
||||||
|
tsconfig*.json
|
||||||
|
!tsconfig.json
|
||||||
|
|
||||||
|
# Linting/formatting
|
||||||
|
.eslintrc*
|
||||||
|
.prettierrc*
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Package manager locks (we copy them explicitly)
|
||||||
|
# pnpm-lock.yaml
|
||||||
@@ -58,6 +58,12 @@ services:
|
|||||||
# System database
|
# System database
|
||||||
- SYSTEM_DB_NAME=${SYSTEM_DB_NAME}
|
- SYSTEM_DB_NAME=${SYSTEM_DB_NAME}
|
||||||
|
|
||||||
|
# Redis
|
||||||
|
- REDIS_HOST=redis
|
||||||
|
- REDIS_PORT=6379
|
||||||
|
- QUEUE_HOST=redis
|
||||||
|
- QUEUE_PORT=6379
|
||||||
|
|
||||||
# Tenants databases
|
# Tenants databases
|
||||||
- TENANT_DB_NAME_PERFIX=${TENANT_DB_NAME_PERFIX}
|
- TENANT_DB_NAME_PERFIX=${TENANT_DB_NAME_PERFIX}
|
||||||
|
|
||||||
|
|||||||
@@ -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 -- sh -c "node dist/cli.js system:migrate:latest && node dist/cli.js tenants:migrate:latest"
|
||||||
|
|||||||
@@ -1,100 +1,102 @@
|
|||||||
FROM node:18.16.0-alpine as build
|
# Stage 1: Build
|
||||||
|
FROM node:18.16.0-alpine AS builder
|
||||||
|
|
||||||
USER root
|
|
||||||
|
|
||||||
ARG MAIL_HOST= \
|
|
||||||
MAIL_USERNAME= \
|
|
||||||
MAIL_PASSWORD= \
|
|
||||||
MAIL_PORT= \
|
|
||||||
MAIL_SECURE= \
|
|
||||||
MAIL_FROM_NAME= \
|
|
||||||
MAIL_FROM_ADDRESS= \
|
|
||||||
# Database
|
|
||||||
DB_HOST= \
|
|
||||||
DB_USER= \
|
|
||||||
DB_PASSWORD= \
|
|
||||||
DB_CHARSET= \
|
|
||||||
# System database.
|
|
||||||
SYSTEM_DB_NAME= \
|
|
||||||
SYSTEM_DB_PASSWORD= \
|
|
||||||
SYSTEM_DB_USER= \
|
|
||||||
SYSTEM_DB_HOST= \
|
|
||||||
SYSTEM_DB_CHARSET= \
|
|
||||||
# Tenant databases.
|
|
||||||
TENANT_DB_USER= \
|
|
||||||
TENANT_DB_PASSWORD= \
|
|
||||||
TENANT_DB_HOST= \
|
|
||||||
TENANT_DB_NAME_PERFIX= \
|
|
||||||
TENANT_DB_CHARSET= \
|
|
||||||
# Authentication
|
|
||||||
JWT_SECRET= \
|
|
||||||
# Application
|
|
||||||
BASE_URL= \
|
|
||||||
# Sign-up restriction
|
|
||||||
SIGNUP_DISABLED= \
|
|
||||||
SIGNUP_ALLOWED_DOMAINS= \
|
|
||||||
SIGNUP_ALLOWED_EMAILS=
|
|
||||||
|
|
||||||
ENV MAIL_HOST=$MAIL_HOST \
|
|
||||||
MAIL_USERNAME=$MAIL_USERNAME \
|
|
||||||
MAIL_PASSWORD=$MAIL_PASSWORD \
|
|
||||||
MAIL_PORT=$MAIL_PORT \
|
|
||||||
MAIL_SECURE=$MAIL_SECURE \
|
|
||||||
MAIL_FROM_NAME=$MAIL_FROM_NAME \
|
|
||||||
MAIL_FROM_ADDRESS=$MAIL_FROM_ADDRESS \
|
|
||||||
# Database
|
|
||||||
DB_HOST=$DB_HOST \
|
|
||||||
DB_USER=$DB_USER \
|
|
||||||
DB_PASSWORD=$DB_PASSWORD \
|
|
||||||
DB_CHARSET=$DB_CHARSET \
|
|
||||||
# System database.
|
|
||||||
SYSTEM_DB_HOST=$SYSTEM_DB_HOST \
|
|
||||||
SYSTEM_DB_USER=$SYSTEM_DB_USER \
|
|
||||||
SYSTEM_DB_PASSWORD=$SYSTEM_DB_PASSWORD \
|
|
||||||
SYSTEM_DB_NAME=$SYSTEM_DB_NAME \
|
|
||||||
SYSTEM_DB_CHARSET=$SYSTEM_DB_CHARSET \
|
|
||||||
# Tenant databases.
|
|
||||||
TENANT_DB_NAME_PERFIX=$TENANT_DB_NAME_PERFIX \
|
|
||||||
TENANT_DB_HOST=$TENANT_DB_HOST \
|
|
||||||
TENANT_DB_PASSWORD=$TENANT_DB_PASSWORD \
|
|
||||||
TENANT_DB_USER=$TENANT_DB_USER \
|
|
||||||
TENANT_DB_CHARSET=$TENANT_DB_CHARSET \
|
|
||||||
# Authentication
|
|
||||||
JWT_SECRET=$JWT_SECRET \
|
|
||||||
# Application
|
|
||||||
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.
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN chown node:node /
|
|
||||||
|
|
||||||
# Install pnpm
|
# Install pnpm
|
||||||
RUN npm install -g pnpm
|
RUN npm install -g pnpm@8.10.2
|
||||||
|
|
||||||
# Copy application dependency manifests to the container image.
|
# Install build dependencies
|
||||||
COPY --chown=node:node ./ ./
|
RUN apk add --no-cache python3 build-base chromium
|
||||||
|
|
||||||
# Install application dependencies
|
# Set Python environment
|
||||||
RUN apk update
|
|
||||||
RUN apk add python3 build-base chromium
|
|
||||||
|
|
||||||
# Set PYHTON env
|
|
||||||
ENV PYTHON=/usr/bin/python3
|
ENV PYTHON=/usr/bin/python3
|
||||||
|
|
||||||
# Install packages dependencies for production.
|
# Copy package files for dependency installation
|
||||||
RUN pnpm install
|
COPY --chown=node:node package.json pnpm-lock.yaml pnpm-workspace.yaml lerna.json ./
|
||||||
|
COPY --chown=node:node packages/server/package.json ./packages/server/
|
||||||
|
COPY --chown=node:node shared/bigcapital-utils/package.json ./shared/bigcapital-utils/
|
||||||
|
COPY --chown=node:node shared/pdf-templates/package.json ./shared/pdf-templates/
|
||||||
|
COPY --chown=node:node shared/email-components/package.json ./shared/email-components/
|
||||||
|
|
||||||
|
# Install all dependencies (including devDependencies for build)
|
||||||
|
RUN pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
# Copy source code
|
||||||
COPY --chown=node:node ./packages/server ./packages/server
|
COPY --chown=node:node ./packages/server ./packages/server
|
||||||
|
COPY --chown=node:node ./shared/bigcapital-utils ./shared/bigcapital-utils
|
||||||
|
COPY --chown=node:node ./shared/pdf-templates ./shared/pdf-templates
|
||||||
|
COPY --chown=node:node ./shared/email-components ./shared/email-components
|
||||||
|
|
||||||
# # Creates a "dist" folder with the production build
|
# Build NestJS application
|
||||||
RUN pnpm run build:server --skip-nx-cache
|
RUN pnpm run build:server --skip-nx-cache
|
||||||
|
|
||||||
CMD [ "node", "./packages/server/build/index.js" ]
|
# Stage 2: Production
|
||||||
|
FROM node:18.16.0-alpine AS production
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install pnpm for production
|
||||||
|
RUN npm install -g pnpm@8.10.2
|
||||||
|
|
||||||
|
# Create non-root user
|
||||||
|
RUN addgroup -g 1001 -S nodejs && \
|
||||||
|
adduser -S nodejs -u 1001
|
||||||
|
|
||||||
|
# Install build dependencies for native modules (bcrypt, etc.)
|
||||||
|
RUN apk add --no-cache python3 build-base
|
||||||
|
|
||||||
|
# Set Python environment
|
||||||
|
ENV PYTHON=/usr/bin/python3
|
||||||
|
|
||||||
|
# Copy package files for production dependency installation
|
||||||
|
COPY --chown=nodejs:nodejs package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
||||||
|
COPY --chown=nodejs:nodejs packages/server/package.json ./packages/server/
|
||||||
|
COPY --chown=nodejs:nodejs shared/bigcapital-utils/package.json ./shared/bigcapital-utils/
|
||||||
|
COPY --chown=nodejs:nodejs shared/pdf-templates/package.json ./shared/pdf-templates/
|
||||||
|
COPY --chown=nodejs:nodejs shared/email-components/package.json ./shared/email-components/
|
||||||
|
|
||||||
|
# Copy .husky directory (needed for husky install command)
|
||||||
|
COPY --chown=nodejs:nodejs .husky ./.husky
|
||||||
|
|
||||||
|
# Install only production dependencies
|
||||||
|
# Install husky temporarily so prepare script can run, then remove it
|
||||||
|
RUN pnpm add -D -w husky && \
|
||||||
|
pnpm install --prod --frozen-lockfile && \
|
||||||
|
pnpm remove -w husky && \
|
||||||
|
# Remove build dependencies to reduce image size
|
||||||
|
apk del python3 build-base
|
||||||
|
|
||||||
|
# Copy built application from builder stage
|
||||||
|
COPY --from=builder --chown=nodejs:nodejs /app/packages/server/dist ./packages/server/dist
|
||||||
|
|
||||||
|
# Copy static assets (i18n, public, static directories)
|
||||||
|
COPY --from=builder --chown=nodejs:nodejs /app/packages/server/src/i18n ./packages/server/dist/i18n
|
||||||
|
COPY --from=builder --chown=nodejs:nodejs /app/packages/server/public ./packages/server/public
|
||||||
|
COPY --from=builder --chown=nodejs:nodejs /app/packages/server/static ./packages/server/static
|
||||||
|
|
||||||
|
# Copy database migration files (needed for running migrations)
|
||||||
|
COPY --from=builder --chown=nodejs:nodejs /app/packages/server/src/database ./packages/server/src/database
|
||||||
|
|
||||||
|
# Copy built shared packages (dist folders and package.json for module resolution)
|
||||||
|
COPY --from=builder --chown=nodejs:nodejs /app/shared/bigcapital-utils/dist ./shared/bigcapital-utils/dist
|
||||||
|
COPY --from=builder --chown=nodejs:nodejs /app/shared/pdf-templates/dist ./shared/pdf-templates/dist
|
||||||
|
COPY --from=builder --chown=nodejs:nodejs /app/shared/email-components/dist ./shared/email-components/dist
|
||||||
|
|
||||||
|
# Set runtime environment variables (these should be provided at runtime via docker-compose or k8s)
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
ENV NEW_RELIC_NO_CONFIG_FILE=true
|
||||||
|
ENV PORT=3000
|
||||||
|
|
||||||
|
# Switch to non-root user
|
||||||
|
USER nodejs
|
||||||
|
|
||||||
|
# Expose port
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
# Health check - uses /api/system_db ping endpoint
|
||||||
|
HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \
|
||||||
|
CMD node -e "require('http').get('http://localhost:3000/api/system_db', (r) => {process.exit(r.statusCode >= 200 && r.statusCode < 300 ? 0 : 1)}).on('error', () => process.exit(1))"
|
||||||
|
|
||||||
|
# Start the application
|
||||||
|
CMD [ "node", "packages/server/dist/main.js" ]
|
||||||
|
|||||||
@@ -2,10 +2,23 @@
|
|||||||
"$schema": "https://json.schemastore.org/nest-cli",
|
"$schema": "https://json.schemastore.org/nest-cli",
|
||||||
"collection": "@nestjs/schematics",
|
"collection": "@nestjs/schematics",
|
||||||
"sourceRoot": "src",
|
"sourceRoot": "src",
|
||||||
|
"entryFile": "main",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"deleteOutDir": true,
|
"deleteOutDir": true,
|
||||||
"assets": [
|
"assets": [
|
||||||
{ "include": "i18n/**/*", "watchAssets": true }
|
{ "include": "i18n/**/*", "watchAssets": true },
|
||||||
|
{ "include": "database/**/*", "exclude": "**/*.ts", "watchAssets": true }
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"projects": {
|
||||||
|
"cli": {
|
||||||
|
"type": "application",
|
||||||
|
"root": "src",
|
||||||
|
"entryFile": "cli",
|
||||||
|
"sourceRoot": "src",
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsConfigPath": "tsconfig.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,9 @@
|
|||||||
"@lemonsqueezy/lemonsqueezy.js": "^2.2.0",
|
"@lemonsqueezy/lemonsqueezy.js": "^2.2.0",
|
||||||
"@liaoliaots/nestjs-redis": "^10.0.0",
|
"@liaoliaots/nestjs-redis": "^10.0.0",
|
||||||
"@nest-lab/throttler-storage-redis": "^1.1.0",
|
"@nest-lab/throttler-storage-redis": "^1.1.0",
|
||||||
|
"@bull-board/api": "^5.22.0",
|
||||||
|
"@bull-board/express": "^5.22.0",
|
||||||
|
"@bull-board/nestjs": "^5.22.0",
|
||||||
"@nestjs/bull": "^10.2.1",
|
"@nestjs/bull": "^10.2.1",
|
||||||
"@nestjs/bullmq": "^10.2.2",
|
"@nestjs/bullmq": "^10.2.2",
|
||||||
"@nestjs/cache-manager": "^2.2.2",
|
"@nestjs/cache-manager": "^2.2.2",
|
||||||
|
|||||||
8
packages/server/src/common/config/bull-board.ts
Normal file
8
packages/server/src/common/config/bull-board.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { registerAs } from '@nestjs/config';
|
||||||
|
import { parseBoolean } from '@/utils/parse-boolean';
|
||||||
|
|
||||||
|
export default registerAs('bullBoard', () => ({
|
||||||
|
enabled: parseBoolean<boolean>(process.env.BULL_BOARD_ENABLED, false),
|
||||||
|
username: process.env.BULL_BOARD_USERNAME,
|
||||||
|
password: process.env.BULL_BOARD_PASSWORD,
|
||||||
|
}));
|
||||||
@@ -17,6 +17,9 @@ import loops from './loops';
|
|||||||
import bankfeed from './bankfeed';
|
import bankfeed from './bankfeed';
|
||||||
import throttle from './throttle';
|
import throttle from './throttle';
|
||||||
import cloud from './cloud';
|
import cloud from './cloud';
|
||||||
|
import redis from './redis';
|
||||||
|
import queue from './queue';
|
||||||
|
import bullBoard from './bull-board';
|
||||||
|
|
||||||
export const config = [
|
export const config = [
|
||||||
app,
|
app,
|
||||||
@@ -38,4 +41,7 @@ export const config = [
|
|||||||
loops,
|
loops,
|
||||||
bankfeed,
|
bankfeed,
|
||||||
throttle,
|
throttle,
|
||||||
|
redis,
|
||||||
|
queue,
|
||||||
|
bullBoard,
|
||||||
];
|
];
|
||||||
|
|||||||
6
packages/server/src/common/config/queue.ts
Normal file
6
packages/server/src/common/config/queue.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { registerAs } from '@nestjs/config';
|
||||||
|
|
||||||
|
export default registerAs('queue', () => ({
|
||||||
|
host: process.env.QUEUE_HOST || 'localhost',
|
||||||
|
port: parseInt(process.env.QUEUE_PORT, 10) || 6379,
|
||||||
|
}));
|
||||||
@@ -6,4 +6,5 @@ export default registerAs('s3', () => ({
|
|||||||
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
|
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
|
||||||
endpoint: process.env.S3_ENDPOINT,
|
endpoint: process.env.S3_ENDPOINT,
|
||||||
bucket: process.env.S3_BUCKET,
|
bucket: process.env.S3_BUCKET,
|
||||||
|
forcePathStyle: process.env.S3_FORCE_PATH_STYLE === 'true',
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -9,5 +9,10 @@
|
|||||||
"net_cash_financing": "Net cash provided by financing activities",
|
"net_cash_financing": "Net cash provided by financing activities",
|
||||||
"cash_beginning_period": "Cash at beginning of period",
|
"cash_beginning_period": "Cash at beginning of period",
|
||||||
"net_cash_increase": "NET CASH INCREASE FOR PERIOD",
|
"net_cash_increase": "NET CASH INCREASE FOR PERIOD",
|
||||||
"cash_end_period": "CASH AT END OF PERIOD"
|
"cash_end_period": "CASH AT END OF PERIOD",
|
||||||
|
"account_name": "Account name",
|
||||||
|
"total": "Total",
|
||||||
|
"sheet_name": "Statement of Cash Flow",
|
||||||
|
"from_date": "From",
|
||||||
|
"to_date": "To"
|
||||||
}
|
}
|
||||||
|
|||||||
5
packages/server/src/i18n/en/contact_summary_balance.json
Normal file
5
packages/server/src/i18n/en/contact_summary_balance.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"account_name": "Account name",
|
||||||
|
"total": "Total",
|
||||||
|
"percentage_column": "% of Column"
|
||||||
|
}
|
||||||
14
packages/server/src/i18n/en/inventory_item_details.json
Normal file
14
packages/server/src/i18n/en/inventory_item_details.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"opening_balance": "Opening balance",
|
||||||
|
"closing_balance": "Closing balance",
|
||||||
|
"date": "Date",
|
||||||
|
"transaction_type": "Transaction type",
|
||||||
|
"transaction_number": "Transaction #",
|
||||||
|
"quantity": "Quantity",
|
||||||
|
"rate": "Rate",
|
||||||
|
"total": "Total",
|
||||||
|
"value": "Value",
|
||||||
|
"profit_margin": "Profit Margin",
|
||||||
|
"running_quantity": "Running quantity",
|
||||||
|
"running_value": "Running Value"
|
||||||
|
}
|
||||||
4
packages/server/src/i18n/en/transactions_by_contact.json
Normal file
4
packages/server/src/i18n/en/transactions_by_contact.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"opening_balance": "Opening balance",
|
||||||
|
"closing_balance": "Closing balance"
|
||||||
|
}
|
||||||
6
packages/server/src/i18n/en/trial_balance_sheet.json
Normal file
6
packages/server/src/i18n/en/trial_balance_sheet.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"account": "Account",
|
||||||
|
"debit": "Debit",
|
||||||
|
"credit": "Credit",
|
||||||
|
"total": "Total"
|
||||||
|
}
|
||||||
59
packages/server/src/middleware/bull-board-auth.middleware.ts
Normal file
59
packages/server/src/middleware/bull-board-auth.middleware.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates Express middleware for the Bull Board UI:
|
||||||
|
* - When disabled: responds with 404.
|
||||||
|
* - When enabled and username/password are set: enforces HTTP Basic Auth (401 if invalid).
|
||||||
|
* - When enabled and credentials are not set: allows access (no auth).
|
||||||
|
*/
|
||||||
|
export function createBullBoardAuthMiddleware(
|
||||||
|
enabled: boolean,
|
||||||
|
username: string | undefined,
|
||||||
|
password: string | undefined,
|
||||||
|
): (req: Request, res: Response, next: NextFunction) => void {
|
||||||
|
return (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
if (!enabled) {
|
||||||
|
res.status(404).send('Not Found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!username || !password) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
const authHeader = req.headers.authorization;
|
||||||
|
if (!authHeader || !authHeader.startsWith('Basic ')) {
|
||||||
|
res.setHeader('WWW-Authenticate', 'Basic realm="Bull Board"');
|
||||||
|
res.status(401).send('Authentication required');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const base64Credentials = authHeader.slice(6);
|
||||||
|
let decoded: string;
|
||||||
|
try {
|
||||||
|
decoded = Buffer.from(base64Credentials, 'base64').toString('utf8');
|
||||||
|
} catch {
|
||||||
|
res.setHeader('WWW-Authenticate', 'Basic realm="Bull Board"');
|
||||||
|
res.status(401).send('Invalid credentials');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const colonIndex = decoded.indexOf(':');
|
||||||
|
if (colonIndex === -1) {
|
||||||
|
res.setHeader('WWW-Authenticate', 'Basic realm="Bull Board"');
|
||||||
|
res.status(401).send('Invalid credentials');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const reqUsername = decoded.slice(0, colonIndex);
|
||||||
|
const reqPassword = decoded.slice(colonIndex + 1);
|
||||||
|
|
||||||
|
if (reqUsername !== username || reqPassword !== password) {
|
||||||
|
res.setHeader('WWW-Authenticate', 'Basic realm="Bull Board"');
|
||||||
|
res.status(401).send('Invalid credentials');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
ParseIntPipe,
|
ParseIntPipe,
|
||||||
Put,
|
Put,
|
||||||
HttpCode,
|
HttpCode,
|
||||||
|
UseGuards,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { AccountsApplication } from './AccountsApplication.service';
|
import { AccountsApplication } from './AccountsApplication.service';
|
||||||
import { CreateAccountDTO } from './CreateAccount.dto';
|
import { CreateAccountDTO } from './CreateAccount.dto';
|
||||||
@@ -32,6 +33,11 @@ import {
|
|||||||
BulkDeleteDto,
|
BulkDeleteDto,
|
||||||
ValidateBulkDeleteResponseDto,
|
ValidateBulkDeleteResponseDto,
|
||||||
} from '@/common/dtos/BulkDelete.dto';
|
} from '@/common/dtos/BulkDelete.dto';
|
||||||
|
import { RequirePermission } from '@/modules/Roles/RequirePermission.decorator';
|
||||||
|
import { PermissionGuard } from '@/modules/Roles/Permission.guard';
|
||||||
|
import { AuthorizationGuard } from '@/modules/Roles/Authorization.guard';
|
||||||
|
import { AbilitySubject } from '@/modules/Roles/Roles.types';
|
||||||
|
import { AccountAction } from './Accounts.types';
|
||||||
|
|
||||||
@Controller('accounts')
|
@Controller('accounts')
|
||||||
@ApiTags('Accounts')
|
@ApiTags('Accounts')
|
||||||
@@ -40,11 +46,13 @@ import {
|
|||||||
@ApiExtraModels(GetAccountTransactionResponseDto)
|
@ApiExtraModels(GetAccountTransactionResponseDto)
|
||||||
@ApiExtraModels(ValidateBulkDeleteResponseDto)
|
@ApiExtraModels(ValidateBulkDeleteResponseDto)
|
||||||
@ApiCommonHeaders()
|
@ApiCommonHeaders()
|
||||||
|
@UseGuards(AuthorizationGuard, PermissionGuard)
|
||||||
export class AccountsController {
|
export class AccountsController {
|
||||||
constructor(private readonly accountsApplication: AccountsApplication) { }
|
constructor(private readonly accountsApplication: AccountsApplication) { }
|
||||||
|
|
||||||
@Post('validate-bulk-delete')
|
@Post('validate-bulk-delete')
|
||||||
@HttpCode(200)
|
@HttpCode(200)
|
||||||
|
@RequirePermission(AccountAction.DELETE, AbilitySubject.Account)
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary:
|
summary:
|
||||||
'Validates which accounts can be deleted and returns counts of deletable and non-deletable accounts.',
|
'Validates which accounts can be deleted and returns counts of deletable and non-deletable accounts.',
|
||||||
@@ -67,6 +75,7 @@ export class AccountsController {
|
|||||||
|
|
||||||
@Post('bulk-delete')
|
@Post('bulk-delete')
|
||||||
@HttpCode(200)
|
@HttpCode(200)
|
||||||
|
@RequirePermission(AccountAction.DELETE, AbilitySubject.Account)
|
||||||
@ApiOperation({ summary: 'Deletes multiple accounts in bulk.' })
|
@ApiOperation({ summary: 'Deletes multiple accounts in bulk.' })
|
||||||
@ApiResponse({
|
@ApiResponse({
|
||||||
status: 200,
|
status: 200,
|
||||||
@@ -81,6 +90,7 @@ export class AccountsController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
|
@RequirePermission(AccountAction.CREATE, AbilitySubject.Account)
|
||||||
@ApiOperation({ summary: 'Create an account' })
|
@ApiOperation({ summary: 'Create an account' })
|
||||||
@ApiResponse({
|
@ApiResponse({
|
||||||
status: 200,
|
status: 200,
|
||||||
@@ -91,6 +101,7 @@ export class AccountsController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Put(':id')
|
@Put(':id')
|
||||||
|
@RequirePermission(AccountAction.EDIT, AbilitySubject.Account)
|
||||||
@ApiOperation({ summary: 'Edit the given account.' })
|
@ApiOperation({ summary: 'Edit the given account.' })
|
||||||
@ApiResponse({
|
@ApiResponse({
|
||||||
status: 200,
|
status: 200,
|
||||||
@@ -111,6 +122,7 @@ export class AccountsController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
|
@RequirePermission(AccountAction.DELETE, AbilitySubject.Account)
|
||||||
@ApiOperation({ summary: 'Delete the given account.' })
|
@ApiOperation({ summary: 'Delete the given account.' })
|
||||||
@ApiResponse({
|
@ApiResponse({
|
||||||
status: 200,
|
status: 200,
|
||||||
@@ -129,6 +141,7 @@ export class AccountsController {
|
|||||||
|
|
||||||
@Post(':id/activate')
|
@Post(':id/activate')
|
||||||
@HttpCode(200)
|
@HttpCode(200)
|
||||||
|
@RequirePermission(AccountAction.EDIT, AbilitySubject.Account)
|
||||||
@ApiOperation({ summary: 'Activate the given account.' })
|
@ApiOperation({ summary: 'Activate the given account.' })
|
||||||
@ApiResponse({
|
@ApiResponse({
|
||||||
status: 200,
|
status: 200,
|
||||||
@@ -147,6 +160,7 @@ export class AccountsController {
|
|||||||
|
|
||||||
@Post(':id/inactivate')
|
@Post(':id/inactivate')
|
||||||
@HttpCode(200)
|
@HttpCode(200)
|
||||||
|
@RequirePermission(AccountAction.EDIT, AbilitySubject.Account)
|
||||||
@ApiOperation({ summary: 'Inactivate the given account.' })
|
@ApiOperation({ summary: 'Inactivate the given account.' })
|
||||||
@ApiResponse({
|
@ApiResponse({
|
||||||
status: 200,
|
status: 200,
|
||||||
@@ -164,6 +178,7 @@ export class AccountsController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Get('types')
|
@Get('types')
|
||||||
|
@RequirePermission(AccountAction.VIEW, AbilitySubject.Account)
|
||||||
@ApiOperation({ summary: 'Retrieves the account types.' })
|
@ApiOperation({ summary: 'Retrieves the account types.' })
|
||||||
@ApiResponse({
|
@ApiResponse({
|
||||||
status: 200,
|
status: 200,
|
||||||
@@ -180,6 +195,7 @@ export class AccountsController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Get('transactions')
|
@Get('transactions')
|
||||||
|
@RequirePermission(AccountAction.VIEW, AbilitySubject.Account)
|
||||||
@ApiOperation({ summary: 'Retrieves the account transactions.' })
|
@ApiOperation({ summary: 'Retrieves the account transactions.' })
|
||||||
@ApiResponse({
|
@ApiResponse({
|
||||||
status: 200,
|
status: 200,
|
||||||
@@ -198,6 +214,7 @@ export class AccountsController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
|
@RequirePermission(AccountAction.VIEW, AbilitySubject.Account)
|
||||||
@ApiOperation({ summary: 'Retrieves the account details.' })
|
@ApiOperation({ summary: 'Retrieves the account details.' })
|
||||||
@ApiResponse({
|
@ApiResponse({
|
||||||
status: 200,
|
status: 200,
|
||||||
@@ -216,6 +233,7 @@ export class AccountsController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
|
@RequirePermission(AccountAction.VIEW, AbilitySubject.Account)
|
||||||
@ApiOperation({ summary: 'Retrieves the accounts.' })
|
@ApiOperation({ summary: 'Retrieves the accounts.' })
|
||||||
@ApiResponse({
|
@ApiResponse({
|
||||||
status: 200,
|
status: 200,
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import { AccountsExportable } from './AccountsExportable.service';
|
|||||||
import { AccountsImportable } from './AccountsImportable.service';
|
import { AccountsImportable } from './AccountsImportable.service';
|
||||||
import { BulkDeleteAccountsService } from './BulkDeleteAccounts.service';
|
import { BulkDeleteAccountsService } from './BulkDeleteAccounts.service';
|
||||||
import { ValidateBulkDeleteAccountsService } from './ValidateBulkDeleteAccounts.service';
|
import { ValidateBulkDeleteAccountsService } from './ValidateBulkDeleteAccounts.service';
|
||||||
|
import { AccountsSettingsService } from './AccountsSettings.service';
|
||||||
|
|
||||||
const models = [RegisterTenancyModel(BankAccount)];
|
const models = [RegisterTenancyModel(BankAccount)];
|
||||||
|
|
||||||
@@ -29,6 +30,7 @@ const models = [RegisterTenancyModel(BankAccount)];
|
|||||||
controllers: [AccountsController],
|
controllers: [AccountsController],
|
||||||
providers: [
|
providers: [
|
||||||
AccountsApplication,
|
AccountsApplication,
|
||||||
|
AccountsSettingsService,
|
||||||
CreateAccountService,
|
CreateAccountService,
|
||||||
TenancyContext,
|
TenancyContext,
|
||||||
CommandAccountValidators,
|
CommandAccountValidators,
|
||||||
@@ -49,9 +51,10 @@ const models = [RegisterTenancyModel(BankAccount)];
|
|||||||
exports: [
|
exports: [
|
||||||
AccountRepository,
|
AccountRepository,
|
||||||
CreateAccountService,
|
CreateAccountService,
|
||||||
|
AccountsSettingsService,
|
||||||
...models,
|
...models,
|
||||||
AccountsExportable,
|
AccountsExportable,
|
||||||
AccountsImportable
|
AccountsImportable,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AccountsModule {}
|
export class AccountsModule {}
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { SettingsStore } from '../Settings/SettingsStore';
|
||||||
|
import { SETTINGS_PROVIDER } from '../Settings/Settings.types';
|
||||||
|
|
||||||
|
export interface IAccountsSettings {
|
||||||
|
accountCodeRequired: boolean;
|
||||||
|
accountCodeUnique: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AccountsSettingsService {
|
||||||
|
constructor(
|
||||||
|
@Inject(SETTINGS_PROVIDER)
|
||||||
|
private readonly settingsStore: () => SettingsStore,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves account settings (account code required, account code unique).
|
||||||
|
*/
|
||||||
|
public async getAccountsSettings(): Promise<IAccountsSettings> {
|
||||||
|
const settingsStore = await this.settingsStore();
|
||||||
|
return {
|
||||||
|
accountCodeRequired: settingsStore.get(
|
||||||
|
{ group: 'accounts', key: 'account_code_required' },
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
accountCodeUnique: settingsStore.get(
|
||||||
|
{ group: 'accounts', key: 'account_code_unique' },
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -106,6 +106,20 @@ export class CommandAccountValidators {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Throws error if account code is missing or blank when required.
|
||||||
|
* @param {string|undefined} code - Account code.
|
||||||
|
*/
|
||||||
|
public validateAccountCodeRequiredOrThrow(code: string | undefined) {
|
||||||
|
const trimmed = typeof code === 'string' ? code.trim() : '';
|
||||||
|
if (!trimmed) {
|
||||||
|
throw new ServiceError(
|
||||||
|
ERRORS.ACCOUNT_CODE_REQUIRED,
|
||||||
|
'Account code is required.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates the account name uniquiness.
|
* Validates the account name uniquiness.
|
||||||
* @param {string} accountName - Account name.
|
* @param {string} accountName - Account name.
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { events } from '@/common/events/events';
|
|||||||
import { CreateAccountDTO } from './CreateAccount.dto';
|
import { CreateAccountDTO } from './CreateAccount.dto';
|
||||||
import { PartialModelObject } from 'objection';
|
import { PartialModelObject } from 'objection';
|
||||||
import { TenantModelProxy } from '../System/models/TenantBaseModel';
|
import { TenantModelProxy } from '../System/models/TenantBaseModel';
|
||||||
|
import { AccountsSettingsService } from './AccountsSettings.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CreateAccountService {
|
export class CreateAccountService {
|
||||||
@@ -32,6 +33,7 @@ export class CreateAccountService {
|
|||||||
private readonly uow: UnitOfWork,
|
private readonly uow: UnitOfWork,
|
||||||
private readonly validator: CommandAccountValidators,
|
private readonly validator: CommandAccountValidators,
|
||||||
private readonly tenancyContext: TenancyContext,
|
private readonly tenancyContext: TenancyContext,
|
||||||
|
private readonly accountsSettings: AccountsSettingsService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -43,14 +45,21 @@ export class CreateAccountService {
|
|||||||
baseCurrency: string,
|
baseCurrency: string,
|
||||||
params?: CreateAccountParams,
|
params?: CreateAccountParams,
|
||||||
) => {
|
) => {
|
||||||
|
const { accountCodeRequired, accountCodeUnique } =
|
||||||
|
await this.accountsSettings.getAccountsSettings();
|
||||||
|
|
||||||
|
// Validate account code required when setting is enabled.
|
||||||
|
if (accountCodeRequired) {
|
||||||
|
this.validator.validateAccountCodeRequiredOrThrow(accountDTO.code);
|
||||||
|
}
|
||||||
|
// Validate the account code uniquiness when setting is enabled.
|
||||||
|
if (accountCodeUnique && accountDTO.code?.trim()) {
|
||||||
|
await this.validator.isAccountCodeUniqueOrThrowError(accountDTO.code);
|
||||||
|
}
|
||||||
// Validate account name uniquiness.
|
// Validate account name uniquiness.
|
||||||
if (!params.ignoreUniqueName) {
|
if (!params.ignoreUniqueName) {
|
||||||
await this.validator.validateAccountNameUniquiness(accountDTO.name);
|
await this.validator.validateAccountNameUniquiness(accountDTO.name);
|
||||||
}
|
}
|
||||||
// Validate the account code uniquiness.
|
|
||||||
if (accountDTO.code) {
|
|
||||||
await this.validator.isAccountCodeUniqueOrThrowError(accountDTO.code);
|
|
||||||
}
|
|
||||||
// Retrieve the account type meta or throw service error if not found.
|
// Retrieve the account type meta or throw service error if not found.
|
||||||
this.validator.getAccountTypeOrThrowError(accountDTO.accountType);
|
this.validator.getAccountTypeOrThrowError(accountDTO.accountType);
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { UnitOfWork } from '../Tenancy/TenancyDB/UnitOfWork.service';
|
|||||||
import { events } from '@/common/events/events';
|
import { events } from '@/common/events/events';
|
||||||
import { EditAccountDTO } from './EditAccount.dto';
|
import { EditAccountDTO } from './EditAccount.dto';
|
||||||
import { TenantModelProxy } from '../System/models/TenantBaseModel';
|
import { TenantModelProxy } from '../System/models/TenantBaseModel';
|
||||||
|
import { AccountsSettingsService } from './AccountsSettings.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class EditAccount {
|
export class EditAccount {
|
||||||
@@ -17,7 +18,8 @@ export class EditAccount {
|
|||||||
|
|
||||||
@Inject(Account.name)
|
@Inject(Account.name)
|
||||||
private readonly accountModel: TenantModelProxy<typeof Account>,
|
private readonly accountModel: TenantModelProxy<typeof Account>,
|
||||||
) { }
|
private readonly accountsSettings: AccountsSettingsService,
|
||||||
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Authorize the account editing.
|
* Authorize the account editing.
|
||||||
@@ -30,6 +32,24 @@ export class EditAccount {
|
|||||||
accountDTO: EditAccountDTO,
|
accountDTO: EditAccountDTO,
|
||||||
oldAccount: Account,
|
oldAccount: Account,
|
||||||
) => {
|
) => {
|
||||||
|
const { accountCodeRequired, accountCodeUnique } =
|
||||||
|
await this.accountsSettings.getAccountsSettings();
|
||||||
|
|
||||||
|
// Validate account code required when setting is enabled.
|
||||||
|
if (accountCodeRequired) {
|
||||||
|
this.validator.validateAccountCodeRequiredOrThrow(accountDTO.code);
|
||||||
|
}
|
||||||
|
// Validate the account code uniquiness when setting is enabled.
|
||||||
|
if (
|
||||||
|
accountCodeUnique &&
|
||||||
|
accountDTO.code?.trim() &&
|
||||||
|
accountDTO.code !== oldAccount.code
|
||||||
|
) {
|
||||||
|
await this.validator.isAccountCodeUniqueOrThrowError(
|
||||||
|
accountDTO.code,
|
||||||
|
oldAccount.id,
|
||||||
|
);
|
||||||
|
}
|
||||||
// Validate account name uniquiness.
|
// Validate account name uniquiness.
|
||||||
await this.validator.validateAccountNameUniquiness(
|
await this.validator.validateAccountNameUniquiness(
|
||||||
accountDTO.name,
|
accountDTO.name,
|
||||||
@@ -40,13 +60,6 @@ export class EditAccount {
|
|||||||
oldAccount,
|
oldAccount,
|
||||||
accountDTO,
|
accountDTO,
|
||||||
);
|
);
|
||||||
// Validate the account code not exists on the storage.
|
|
||||||
if (accountDTO.code && accountDTO.code !== oldAccount.code) {
|
|
||||||
await this.validator.isAccountCodeUniqueOrThrowError(
|
|
||||||
accountDTO.code,
|
|
||||||
oldAccount.id,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// Retrieve the parent account of throw not found service error.
|
// Retrieve the parent account of throw not found service error.
|
||||||
if (accountDTO.parentAccountId) {
|
if (accountDTO.parentAccountId) {
|
||||||
const parentAccount = await this.validator.getParentAccountOrThrowError(
|
const parentAccount = await this.validator.getParentAccountOrThrowError(
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ export const ERRORS = {
|
|||||||
ACCOUNT_TYPE_NOT_FOUND: 'account_type_not_found',
|
ACCOUNT_TYPE_NOT_FOUND: 'account_type_not_found',
|
||||||
PARENT_ACCOUNT_NOT_FOUND: 'parent_account_not_found',
|
PARENT_ACCOUNT_NOT_FOUND: 'parent_account_not_found',
|
||||||
ACCOUNT_CODE_NOT_UNIQUE: 'account_code_not_unique',
|
ACCOUNT_CODE_NOT_UNIQUE: 'account_code_not_unique',
|
||||||
|
ACCOUNT_CODE_REQUIRED: 'account_code_required',
|
||||||
ACCOUNT_NAME_NOT_UNIQUE: 'account_name_not_unqiue',
|
ACCOUNT_NAME_NOT_UNIQUE: 'account_name_not_unqiue',
|
||||||
PARENT_ACCOUNT_HAS_DIFFERENT_TYPE: 'parent_has_different_type',
|
PARENT_ACCOUNT_HAS_DIFFERENT_TYPE: 'parent_has_different_type',
|
||||||
ACCOUNT_TYPE_NOT_ALLOWED_TO_CHANGE: 'account_type_not_allowed_to_changed',
|
ACCOUNT_TYPE_NOT_ALLOWED_TO_CHANGE: 'account_type_not_allowed_to_changed',
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ export class AccountTransaction extends BaseModel {
|
|||||||
public readonly userId!: number;
|
public readonly userId!: number;
|
||||||
public readonly itemId!: number;
|
public readonly itemId!: number;
|
||||||
public readonly projectId!: number;
|
public readonly projectId!: number;
|
||||||
|
public readonly costable!: boolean;
|
||||||
public readonly account: Account;
|
public readonly account: Account;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ import {
|
|||||||
I18nModule,
|
I18nModule,
|
||||||
QueryResolver,
|
QueryResolver,
|
||||||
} from 'nestjs-i18n';
|
} from 'nestjs-i18n';
|
||||||
|
import { BullBoardModule } from '@bull-board/nestjs';
|
||||||
|
import { ExpressAdapter } from '@bull-board/express';
|
||||||
|
import { createBullBoardAuthMiddleware } from '@/middleware/bull-board-auth.middleware';
|
||||||
import { BullModule } from '@nestjs/bullmq';
|
import { BullModule } from '@nestjs/bullmq';
|
||||||
import { ScheduleModule } from '@nestjs/schedule';
|
import { ScheduleModule } from '@nestjs/schedule';
|
||||||
import { PassportModule } from '@nestjs/passport';
|
import { PassportModule } from '@nestjs/passport';
|
||||||
@@ -137,12 +140,30 @@ import { AppThrottleModule } from './AppThrottle.module';
|
|||||||
imports: [ConfigModule],
|
imports: [ConfigModule],
|
||||||
useFactory: async (configService: ConfigService) => ({
|
useFactory: async (configService: ConfigService) => ({
|
||||||
connection: {
|
connection: {
|
||||||
host: configService.get('QUEUE_HOST'),
|
host: configService.get('queue.host'),
|
||||||
port: configService.get('QUEUE_PORT'),
|
port: configService.get('queue.port'),
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
inject: [ConfigService],
|
inject: [ConfigService],
|
||||||
}),
|
}),
|
||||||
|
BullBoardModule.forRootAsync({
|
||||||
|
imports: [ConfigModule],
|
||||||
|
useFactory: (configService: ConfigService) => {
|
||||||
|
const enabled = configService.get<boolean>('bullBoard.enabled');
|
||||||
|
const username = configService.get<string>('bullBoard.username');
|
||||||
|
const password = configService.get<string>('bullBoard.password');
|
||||||
|
return {
|
||||||
|
route: '/queues',
|
||||||
|
adapter: ExpressAdapter,
|
||||||
|
middleware: createBullBoardAuthMiddleware(
|
||||||
|
enabled,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
inject: [ConfigService],
|
||||||
|
}),
|
||||||
ClsModule.forRoot({
|
ClsModule.forRoot({
|
||||||
global: true,
|
global: true,
|
||||||
middleware: {
|
middleware: {
|
||||||
@@ -158,8 +179,8 @@ import { AppThrottleModule } from './AppThrottle.module';
|
|||||||
imports: [ConfigModule],
|
imports: [ConfigModule],
|
||||||
useFactory: (configService: ConfigService) => ({
|
useFactory: (configService: ConfigService) => ({
|
||||||
config: {
|
config: {
|
||||||
host: configService.get('redis.host') || 'localhost',
|
host: configService.get('redis.host'),
|
||||||
port: configService.get('redis.port') || 6379,
|
port: configService.get('redis.port'),
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
inject: [ConfigService],
|
inject: [ConfigService],
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ModuleRef } from '@nestjs/core';
|
import { ModuleRef } from '@nestjs/core';
|
||||||
import bluebird from 'bluebird';
|
import * as bluebird from 'bluebird';
|
||||||
import { Knex } from 'knex';
|
import { Knex } from 'knex';
|
||||||
import {
|
import {
|
||||||
validateLinkModelEntryExists,
|
validateLinkModelEntryExists,
|
||||||
@@ -53,7 +53,8 @@ export class LinkAttachment {
|
|||||||
const foundLinkModel = await LinkModel().query(trx).findById(modelId);
|
const foundLinkModel = await LinkModel().query(trx).findById(modelId);
|
||||||
validateLinkModelEntryExists(foundLinkModel);
|
validateLinkModelEntryExists(foundLinkModel);
|
||||||
|
|
||||||
const foundLinks = await this.documentLinkModel().query(trx)
|
const foundLinks = await this.documentLinkModel()
|
||||||
|
.query(trx)
|
||||||
.where('modelRef', modelRef)
|
.where('modelRef', modelRef)
|
||||||
.where('modelId', modelId)
|
.where('modelId', modelId)
|
||||||
.where('documentId', foundFile.id);
|
.where('documentId', foundFile.id);
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ export class AuthController {
|
|||||||
return this.authApp.signUp(signupDto);
|
return this.authApp.signUp(signupDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('/signup/confirm')
|
@Post('/signup/verify')
|
||||||
@ApiOperation({ summary: 'Confirm user signup' })
|
@ApiOperation({ summary: 'Confirm user signup' })
|
||||||
@ApiBody({
|
@ApiBody({
|
||||||
schema: {
|
schema: {
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import { PassportModule } from '@nestjs/passport';
|
|||||||
import { APP_GUARD } from '@nestjs/core';
|
import { APP_GUARD } from '@nestjs/core';
|
||||||
import { JwtAuthGuard } from './guards/jwt.guard';
|
import { JwtAuthGuard } from './guards/jwt.guard';
|
||||||
import { AuthMailSubscriber } from './subscribers/AuthMail.subscriber';
|
import { AuthMailSubscriber } from './subscribers/AuthMail.subscriber';
|
||||||
|
import { BullBoardModule } from '@bull-board/nestjs';
|
||||||
|
import { BullMQAdapter } from '@bull-board/api/bullMQAdapter';
|
||||||
import { BullModule } from '@nestjs/bullmq';
|
import { BullModule } from '@nestjs/bullmq';
|
||||||
import {
|
import {
|
||||||
SendResetPasswordMailQueue,
|
SendResetPasswordMailQueue,
|
||||||
@@ -63,6 +65,14 @@ const models = [
|
|||||||
TenancyModule,
|
TenancyModule,
|
||||||
BullModule.registerQueue({ name: SendResetPasswordMailQueue }),
|
BullModule.registerQueue({ name: SendResetPasswordMailQueue }),
|
||||||
BullModule.registerQueue({ name: SendSignupVerificationMailQueue }),
|
BullModule.registerQueue({ name: SendSignupVerificationMailQueue }),
|
||||||
|
BullBoardModule.forFeature({
|
||||||
|
name: SendResetPasswordMailQueue,
|
||||||
|
adapter: BullMQAdapter,
|
||||||
|
}),
|
||||||
|
BullBoardModule.forFeature({
|
||||||
|
name: SendSignupVerificationMailQueue,
|
||||||
|
adapter: BullMQAdapter,
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
exports: [...models],
|
exports: [...models],
|
||||||
providers: [
|
providers: [
|
||||||
@@ -98,4 +108,4 @@ const models = [
|
|||||||
AuthMailSubscriber,
|
AuthMailSubscriber,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AuthModule { }
|
export class AuthModule {}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export class AuthenticationMailMesssages {
|
|||||||
* @returns {Mail}
|
* @returns {Mail}
|
||||||
*/
|
*/
|
||||||
resetPasswordMessage(user: ModelObject<SystemUser>, token: string) {
|
resetPasswordMessage(user: ModelObject<SystemUser>, token: string) {
|
||||||
const baseURL = this.configService.get('baseURL');
|
const baseURL = this.configService.get('app.baseUrl');
|
||||||
|
|
||||||
return new Mail()
|
return new Mail()
|
||||||
.setSubject('Bigcapital - Password Reset')
|
.setSubject('Bigcapital - Password Reset')
|
||||||
@@ -54,7 +54,7 @@ export class AuthenticationMailMesssages {
|
|||||||
* @returns {Mail}
|
* @returns {Mail}
|
||||||
*/
|
*/
|
||||||
signupVerificationMail(email: string, fullName: string, token: string) {
|
signupVerificationMail(email: string, fullName: string, token: string) {
|
||||||
const baseURL = this.configService.get('baseURL');
|
const baseURL = this.configService.get('app.baseUrl');
|
||||||
const verifyUrl = `${baseURL}/auth/email_confirmation?token=${token}&email=${email}`;
|
const verifyUrl = `${baseURL}/auth/email_confirmation?token=${token}&email=${email}`;
|
||||||
|
|
||||||
return new Mail()
|
return new Mail()
|
||||||
|
|||||||
@@ -7,17 +7,13 @@ import {
|
|||||||
import { GetAuthenticatedAccount } from './queries/GetAuthedAccount.service';
|
import { GetAuthenticatedAccount } from './queries/GetAuthedAccount.service';
|
||||||
import { Controller, Get, Post } from '@nestjs/common';
|
import { Controller, Get, Post } from '@nestjs/common';
|
||||||
import { Throttle } from '@nestjs/throttler';
|
import { Throttle } from '@nestjs/throttler';
|
||||||
import { IgnoreTenantSeededRoute } from '../Tenancy/EnsureTenantIsSeeded.guards';
|
import { TenantAgnosticRoute } from '../Tenancy/TenancyGlobal.guard';
|
||||||
import { IgnoreTenantInitializedRoute } from '../Tenancy/EnsureTenantIsInitialized.guard';
|
|
||||||
import { AuthenticationApplication } from './AuthApplication.sevice';
|
import { AuthenticationApplication } from './AuthApplication.sevice';
|
||||||
import { TenancyContext } from '../Tenancy/TenancyContext.service';
|
|
||||||
import { IgnoreUserVerifiedRoute } from './guards/EnsureUserVerified.guard';
|
import { IgnoreUserVerifiedRoute } from './guards/EnsureUserVerified.guard';
|
||||||
|
|
||||||
@Controller('/auth')
|
@Controller('/auth')
|
||||||
@ApiTags('Auth')
|
@ApiTags('Auth')
|
||||||
@ApiExcludeController()
|
@TenantAgnosticRoute()
|
||||||
@IgnoreTenantSeededRoute()
|
|
||||||
@IgnoreTenantInitializedRoute()
|
|
||||||
@IgnoreUserVerifiedRoute()
|
@IgnoreUserVerifiedRoute()
|
||||||
@Throttle({ auth: {} })
|
@Throttle({ auth: {} })
|
||||||
export class AuthedController {
|
export class AuthedController {
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import {
|
|||||||
IAuthSignedUpEventPayload,
|
IAuthSignedUpEventPayload,
|
||||||
IAuthSigningUpEventPayload,
|
IAuthSigningUpEventPayload,
|
||||||
} from '../Auth.interfaces';
|
} from '../Auth.interfaces';
|
||||||
import { defaultTo } from 'ramda';
|
|
||||||
import { ERRORS } from '../Auth.constants';
|
import { ERRORS } from '../Auth.constants';
|
||||||
import { hashPassword } from '../Auth.utils';
|
import { hashPassword } from '../Auth.utils';
|
||||||
import { ClsService } from 'nestjs-cls';
|
import { ClsService } from 'nestjs-cls';
|
||||||
@@ -51,7 +50,7 @@ export class AuthSignupService {
|
|||||||
const signupConfirmation = this.configService.get('signupConfirmation');
|
const signupConfirmation = this.configService.get('signupConfirmation');
|
||||||
|
|
||||||
const verifyTokenCrypto = crypto.randomBytes(64).toString('hex');
|
const verifyTokenCrypto = crypto.randomBytes(64).toString('hex');
|
||||||
const verifiedEnabed = defaultTo(signupConfirmation.enabled, false);
|
const verifiedEnabed = signupConfirmation.enabled ?? false;
|
||||||
const verifyToken = verifiedEnabed ? verifyTokenCrypto : '';
|
const verifyToken = verifiedEnabed ? verifyTokenCrypto : '';
|
||||||
const verified = !verifiedEnabed;
|
const verified = !verifiedEnabed;
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { SystemUser } from '@/modules/System/models/SystemUser';
|
|||||||
import { ServiceError } from '@/modules/Items/ServiceError';
|
import { ServiceError } from '@/modules/Items/ServiceError';
|
||||||
import { ERRORS } from '../Auth.constants';
|
import { ERRORS } from '../Auth.constants';
|
||||||
import { events } from '@/common/events/events';
|
import { events } from '@/common/events/events';
|
||||||
import { ModelObject } from 'objection';
|
|
||||||
import { ISignUpConfigmResendedEventPayload } from '../Auth.interfaces';
|
import { ISignUpConfigmResendedEventPayload } from '../Auth.interfaces';
|
||||||
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
|
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
import { Processor, WorkerHost } from '@nestjs/bullmq';
|
import { Processor, WorkerHost } from '@nestjs/bullmq';
|
||||||
import { Scope } from '@nestjs/common';
|
import { Scope } from '@nestjs/common';
|
||||||
import {
|
import { SendResetPasswordMailQueue } from '../Auth.constants';
|
||||||
SendResetPasswordMailJob,
|
|
||||||
SendResetPasswordMailQueue,
|
|
||||||
} from '../Auth.constants';
|
|
||||||
import { Process } from '@nestjs/bull';
|
|
||||||
import { Job } from 'bullmq';
|
import { Job } from 'bullmq';
|
||||||
import { AuthenticationMailMesssages } from '../AuthMailMessages.esrvice';
|
import { AuthenticationMailMesssages } from '../AuthMailMessages.esrvice';
|
||||||
import { MailTransporter } from '@/modules/Mail/MailTransporter.service';
|
import { MailTransporter } from '@/modules/Mail/MailTransporter.service';
|
||||||
@@ -23,7 +19,6 @@ export class SendResetPasswordMailProcessor extends WorkerHost {
|
|||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Process(SendResetPasswordMailJob)
|
|
||||||
async process(job: Job<SendResetPasswordMailJobPayload>) {
|
async process(job: Job<SendResetPasswordMailJobPayload>) {
|
||||||
try {
|
try {
|
||||||
await this.authMailMesssages.sendResetPasswordMail(
|
await this.authMailMesssages.sendResetPasswordMail(
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
import { Scope } from '@nestjs/common';
|
import { Scope } from '@nestjs/common';
|
||||||
import { Job } from 'bullmq';
|
import { Job } from 'bullmq';
|
||||||
import { Process } from '@nestjs/bull';
|
|
||||||
import { Processor, WorkerHost } from '@nestjs/bullmq';
|
import { Processor, WorkerHost } from '@nestjs/bullmq';
|
||||||
import {
|
import { SendSignupVerificationMailQueue } from '../Auth.constants';
|
||||||
SendSignupVerificationMailJob,
|
|
||||||
SendSignupVerificationMailQueue,
|
|
||||||
} from '../Auth.constants';
|
|
||||||
import { MailTransporter } from '@/modules/Mail/MailTransporter.service';
|
import { MailTransporter } from '@/modules/Mail/MailTransporter.service';
|
||||||
import { AuthenticationMailMesssages } from '../AuthMailMessages.esrvice';
|
import { AuthenticationMailMesssages } from '../AuthMailMessages.esrvice';
|
||||||
|
|
||||||
@@ -21,7 +17,6 @@ export class SendSignupVerificationMailProcessor extends WorkerHost {
|
|||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Process(SendSignupVerificationMailJob)
|
|
||||||
async process(job: Job<SendSignupVerificationMailJobPayload>) {
|
async process(job: Job<SendSignupVerificationMailJobPayload>) {
|
||||||
try {
|
try {
|
||||||
await this.authMailMesssages.sendSignupVerificationMail(
|
await this.authMailMesssages.sendSignupVerificationMail(
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import { ToNumber } from '@/common/decorators/Validators';
|
|||||||
|
|
||||||
class BankRuleConditionDto {
|
class BankRuleConditionDto {
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@IsIn(['description', 'amount'])
|
@IsIn(['description', 'amount', 'payee'])
|
||||||
field: string;
|
field: string;
|
||||||
|
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { BullBoardModule } from '@bull-board/nestjs';
|
||||||
|
import { BullMQAdapter } from '@bull-board/api/bullMQAdapter';
|
||||||
import { BullModule } from '@nestjs/bullmq';
|
import { BullModule } from '@nestjs/bullmq';
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { SocketModule } from '../Socket/Socket.module';
|
import { SocketModule } from '../Socket/Socket.module';
|
||||||
@@ -33,6 +35,10 @@ const models = [RegisterTenancyModel(PlaidItem)];
|
|||||||
BankingCategorizeModule,
|
BankingCategorizeModule,
|
||||||
BankingTransactionsModule,
|
BankingTransactionsModule,
|
||||||
BullModule.registerQueue({ name: UpdateBankingPlaidTransitionsQueueJob }),
|
BullModule.registerQueue({ name: UpdateBankingPlaidTransitionsQueueJob }),
|
||||||
|
BullBoardModule.forFeature({
|
||||||
|
name: UpdateBankingPlaidTransitionsQueueJob,
|
||||||
|
adapter: BullMQAdapter,
|
||||||
|
}),
|
||||||
...models,
|
...models,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
@@ -51,4 +57,4 @@ const models = [RegisterTenancyModel(PlaidItem)];
|
|||||||
exports: [...models],
|
exports: [...models],
|
||||||
controllers: [BankingPlaidController, BankingPlaidWebhooksController],
|
controllers: [BankingPlaidController, BankingPlaidWebhooksController],
|
||||||
})
|
})
|
||||||
export class BankingPlaidModule { }
|
export class BankingPlaidModule {}
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import { Process } from '@nestjs/bull';
|
|
||||||
import { UseCls } from 'nestjs-cls';
|
import { UseCls } from 'nestjs-cls';
|
||||||
import { Processor, WorkerHost } from '@nestjs/bullmq';
|
import { Processor, WorkerHost } from '@nestjs/bullmq';
|
||||||
import { Scope } from '@nestjs/common';
|
import { Scope } from '@nestjs/common';
|
||||||
import { Job } from 'bullmq';
|
import { Job } from 'bullmq';
|
||||||
import {
|
import {
|
||||||
PlaidFetchTransitonsEventPayload,
|
PlaidFetchTransitonsEventPayload,
|
||||||
UpdateBankingPlaidTransitionsJob,
|
|
||||||
UpdateBankingPlaidTransitionsQueueJob,
|
UpdateBankingPlaidTransitionsQueueJob,
|
||||||
} from '../types/BankingPlaid.types';
|
} from '../types/BankingPlaid.types';
|
||||||
import { PlaidUpdateTransactions } from '../command/PlaidUpdateTransactions';
|
import { PlaidUpdateTransactions } from '../command/PlaidUpdateTransactions';
|
||||||
@@ -28,7 +26,6 @@ export class PlaidFetchTransactionsProcessor extends WorkerHost {
|
|||||||
/**
|
/**
|
||||||
* Triggers the function.
|
* Triggers the function.
|
||||||
*/
|
*/
|
||||||
@Process(UpdateBankingPlaidTransitionsJob)
|
|
||||||
@UseCls()
|
@UseCls()
|
||||||
async process(job: Job<PlaidFetchTransitonsEventPayload>) {
|
async process(job: Job<PlaidFetchTransitonsEventPayload>) {
|
||||||
const { plaidItemId } = job.data;
|
const { plaidItemId } = job.data;
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import { BankingRecognizedTransactionsController } from './BankingRecognizedTran
|
|||||||
import { RecognizedTransactionsApplication } from './RecognizedTransactions.application';
|
import { RecognizedTransactionsApplication } from './RecognizedTransactions.application';
|
||||||
import { GetRecognizedTransactionsService } from './GetRecongizedTransactions';
|
import { GetRecognizedTransactionsService } from './GetRecongizedTransactions';
|
||||||
import { GetRecognizedTransactionService } from './queries/GetRecognizedTransaction.service';
|
import { GetRecognizedTransactionService } from './queries/GetRecognizedTransaction.service';
|
||||||
|
import { BullBoardModule } from '@bull-board/nestjs';
|
||||||
|
import { BullMQAdapter } from '@bull-board/api/bullMQAdapter';
|
||||||
import { BullModule } from '@nestjs/bullmq';
|
import { BullModule } from '@nestjs/bullmq';
|
||||||
import { RecognizeUncategorizedTransactionsQueue } from './_types';
|
import { RecognizeUncategorizedTransactionsQueue } from './_types';
|
||||||
import { RegonizeTransactionsPrcessor } from './jobs/RecognizeTransactionsJob';
|
import { RegonizeTransactionsPrcessor } from './jobs/RecognizeTransactionsJob';
|
||||||
@@ -25,6 +27,10 @@ const models = [RegisterTenancyModel(RecognizedBankTransaction)];
|
|||||||
BullModule.registerQueue({
|
BullModule.registerQueue({
|
||||||
name: RecognizeUncategorizedTransactionsQueue,
|
name: RecognizeUncategorizedTransactionsQueue,
|
||||||
}),
|
}),
|
||||||
|
BullBoardModule.forFeature({
|
||||||
|
name: RecognizeUncategorizedTransactionsQueue,
|
||||||
|
adapter: BullMQAdapter,
|
||||||
|
}),
|
||||||
...models,
|
...models,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
|
|||||||
@@ -15,8 +15,13 @@ export const RecognizeUncategorizedTransactionsJob =
|
|||||||
export const RecognizeUncategorizedTransactionsQueue =
|
export const RecognizeUncategorizedTransactionsQueue =
|
||||||
'recognize-uncategorized-transactions-queue';
|
'recognize-uncategorized-transactions-queue';
|
||||||
|
|
||||||
|
|
||||||
export interface RecognizeUncategorizedTransactionsJobPayload extends TenantJobPayload {
|
export interface RecognizeUncategorizedTransactionsJobPayload extends TenantJobPayload {
|
||||||
ruleId: number,
|
ruleId: number,
|
||||||
transactionsCriteria: any;
|
transactionsCriteria?: RecognizeTransactionsCriteria;
|
||||||
|
/**
|
||||||
|
* When true, first reverts recognized transactions before recognizing again.
|
||||||
|
* Used when a bank rule is edited to ensure transactions previously recognized
|
||||||
|
* by lower-priority rules are re-evaluated against the updated rule.
|
||||||
|
*/
|
||||||
|
shouldRevert?: boolean;
|
||||||
}
|
}
|
||||||
@@ -93,6 +93,10 @@ export class RecognizeTranasctionsService {
|
|||||||
q.whereIn('id', rulesIds);
|
q.whereIn('id', rulesIds);
|
||||||
}
|
}
|
||||||
q.withGraphFetched('conditions');
|
q.withGraphFetched('conditions');
|
||||||
|
|
||||||
|
// Order by the 'order' field to ensure higher priority rules (lower order values)
|
||||||
|
// are matched first.
|
||||||
|
q.orderBy('order', 'asc');
|
||||||
});
|
});
|
||||||
|
|
||||||
const bankRulesByAccountId = transformToMapBy(
|
const bankRulesByAccountId = transformToMapBy(
|
||||||
|
|||||||
@@ -69,10 +69,13 @@ export class TriggerRecognizedTransactionsSubscriber {
|
|||||||
const tenantPayload = await this.tenancyContect.getTenantJobPayload();
|
const tenantPayload = await this.tenancyContect.getTenantJobPayload();
|
||||||
const payload = {
|
const payload = {
|
||||||
ruleId: bankRule.id,
|
ruleId: bankRule.id,
|
||||||
|
shouldRevert: true,
|
||||||
...tenantPayload,
|
...tenantPayload,
|
||||||
} as RecognizeUncategorizedTransactionsJobPayload;
|
} as RecognizeUncategorizedTransactionsJobPayload;
|
||||||
|
|
||||||
// Re-recognize the transactions based on the new rules.
|
// Re-recognize the transactions based on the new rules.
|
||||||
|
// Setting shouldRevert to true ensures that transactions previously recognized
|
||||||
|
// by this or lower-priority rules are re-evaluated against the updated rule.
|
||||||
await this.recognizeTransactionsQueue.add(
|
await this.recognizeTransactionsQueue.add(
|
||||||
RecognizeUncategorizedTransactionsJob,
|
RecognizeUncategorizedTransactionsJob,
|
||||||
payload,
|
payload,
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ import { Processor, WorkerHost } from '@nestjs/bullmq';
|
|||||||
import { Scope } from '@nestjs/common';
|
import { Scope } from '@nestjs/common';
|
||||||
import { ClsService, UseCls } from 'nestjs-cls';
|
import { ClsService, UseCls } from 'nestjs-cls';
|
||||||
import { RecognizeTranasctionsService } from '../commands/RecognizeTranasctions.service';
|
import { RecognizeTranasctionsService } from '../commands/RecognizeTranasctions.service';
|
||||||
|
import { RevertRecognizedTransactionsService } from '../commands/RevertRecognizedTransactions.service';
|
||||||
import {
|
import {
|
||||||
RecognizeUncategorizedTransactionsJobPayload,
|
RecognizeUncategorizedTransactionsJobPayload,
|
||||||
RecognizeUncategorizedTransactionsQueue,
|
RecognizeUncategorizedTransactionsQueue,
|
||||||
} from '../_types';
|
} from '../_types';
|
||||||
import { Process } from '@nestjs/bull';
|
|
||||||
|
|
||||||
@Processor({
|
@Processor({
|
||||||
name: RecognizeUncategorizedTransactionsQueue,
|
name: RecognizeUncategorizedTransactionsQueue,
|
||||||
@@ -16,10 +16,12 @@ import { Process } from '@nestjs/bull';
|
|||||||
export class RegonizeTransactionsPrcessor extends WorkerHost {
|
export class RegonizeTransactionsPrcessor extends WorkerHost {
|
||||||
/**
|
/**
|
||||||
* @param {RecognizeTranasctionsService} recognizeTranasctionsService -
|
* @param {RecognizeTranasctionsService} recognizeTranasctionsService -
|
||||||
|
* @param {RevertRecognizedTransactionsService} revertRecognizedTransactionsService -
|
||||||
* @param {ClsService} clsService -
|
* @param {ClsService} clsService -
|
||||||
*/
|
*/
|
||||||
constructor(
|
constructor(
|
||||||
private readonly recognizeTranasctionsService: RecognizeTranasctionsService,
|
private readonly recognizeTranasctionsService: RecognizeTranasctionsService,
|
||||||
|
private readonly revertRecognizedTransactionsService: RevertRecognizedTransactionsService,
|
||||||
private readonly clsService: ClsService,
|
private readonly clsService: ClsService,
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
@@ -28,15 +30,23 @@ export class RegonizeTransactionsPrcessor extends WorkerHost {
|
|||||||
/**
|
/**
|
||||||
* Triggers sending invoice mail.
|
* Triggers sending invoice mail.
|
||||||
*/
|
*/
|
||||||
@Process(RecognizeUncategorizedTransactionsQueue)
|
|
||||||
@UseCls()
|
@UseCls()
|
||||||
async process(job: Job<RecognizeUncategorizedTransactionsJobPayload>) {
|
async process(job: Job<RecognizeUncategorizedTransactionsJobPayload>) {
|
||||||
const { ruleId, transactionsCriteria } = job.data;
|
const { ruleId, transactionsCriteria, shouldRevert } = job.data;
|
||||||
|
|
||||||
this.clsService.set('organizationId', job.data.organizationId);
|
this.clsService.set('organizationId', job.data.organizationId);
|
||||||
this.clsService.set('userId', job.data.userId);
|
this.clsService.set('userId', job.data.userId);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// If shouldRevert is true, first revert recognized transactions before re-recognizing.
|
||||||
|
// This is used when a bank rule is edited to ensure transactions previously recognized
|
||||||
|
// by lower-priority rules are re-evaluated against the updated rule.
|
||||||
|
if (shouldRevert) {
|
||||||
|
await this.revertRecognizedTransactionsService.revertRecognizedTransactions(
|
||||||
|
ruleId,
|
||||||
|
transactionsCriteria,
|
||||||
|
);
|
||||||
|
}
|
||||||
await this.recognizeTranasctionsService.recognizeTransactions(
|
await this.recognizeTranasctionsService.recognizeTransactions(
|
||||||
ruleId,
|
ruleId,
|
||||||
transactionsCriteria,
|
transactionsCriteria,
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export enum CASHFLOW_DIRECTION {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export enum CASHFLOW_TRANSACTION_TYPE {
|
export enum CASHFLOW_TRANSACTION_TYPE {
|
||||||
ONWERS_DRAWING = 'OwnerDrawing',
|
OWNERS_DRAWING = 'OwnerDrawing',
|
||||||
OWNER_CONTRIBUTION = 'OwnerContribution',
|
OWNER_CONTRIBUTION = 'OwnerContribution',
|
||||||
OTHER_INCOME = 'OtherIncome',
|
OTHER_INCOME = 'OtherIncome',
|
||||||
TRANSFER_FROM_ACCOUNT = 'TransferFromAccount',
|
TRANSFER_FROM_ACCOUNT = 'TransferFromAccount',
|
||||||
@@ -36,7 +36,7 @@ export enum CASHFLOW_TRANSACTION_TYPE {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const CASHFLOW_TRANSACTION_TYPE_META = {
|
export const CASHFLOW_TRANSACTION_TYPE_META = {
|
||||||
[`${CASHFLOW_TRANSACTION_TYPE.ONWERS_DRAWING}`]: {
|
[`${CASHFLOW_TRANSACTION_TYPE.OWNERS_DRAWING}`]: {
|
||||||
type: 'OwnerDrawing',
|
type: 'OwnerDrawing',
|
||||||
direction: CASHFLOW_DIRECTION.OUT,
|
direction: CASHFLOW_DIRECTION.OUT,
|
||||||
creditType: [ACCOUNT_TYPE.EQUITY],
|
creditType: [ACCOUNT_TYPE.EQUITY],
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
Post,
|
Post,
|
||||||
Put,
|
Put,
|
||||||
Query,
|
Query,
|
||||||
|
UseGuards,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { BillPaymentsApplication } from './BillPaymentsApplication.service';
|
import { BillPaymentsApplication } from './BillPaymentsApplication.service';
|
||||||
import {
|
import {
|
||||||
@@ -26,12 +27,18 @@ import { BillPaymentsPages } from './commands/BillPaymentsPages.service';
|
|||||||
import { BillPaymentResponseDto } from './dtos/BillPaymentResponse.dto';
|
import { BillPaymentResponseDto } from './dtos/BillPaymentResponse.dto';
|
||||||
import { PaginatedResponseDto } from '@/common/dtos/PaginatedResults.dto';
|
import { PaginatedResponseDto } from '@/common/dtos/PaginatedResults.dto';
|
||||||
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
|
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
|
||||||
|
import { RequirePermission } from '@/modules/Roles/RequirePermission.decorator';
|
||||||
|
import { PermissionGuard } from '@/modules/Roles/Permission.guard';
|
||||||
|
import { AuthorizationGuard } from '@/modules/Roles/Authorization.guard';
|
||||||
|
import { AbilitySubject } from '@/modules/Roles/Roles.types';
|
||||||
|
import { IPaymentMadeAction } from './types/BillPayments.types';
|
||||||
|
|
||||||
@Controller('bill-payments')
|
@Controller('bill-payments')
|
||||||
@ApiTags('Bill Payments')
|
@ApiTags('Bill Payments')
|
||||||
@ApiExtraModels(BillPaymentResponseDto)
|
@ApiExtraModels(BillPaymentResponseDto)
|
||||||
@ApiExtraModels(PaginatedResponseDto)
|
@ApiExtraModels(PaginatedResponseDto)
|
||||||
@ApiCommonHeaders()
|
@ApiCommonHeaders()
|
||||||
|
@UseGuards(AuthorizationGuard, PermissionGuard)
|
||||||
export class BillPaymentsController {
|
export class BillPaymentsController {
|
||||||
constructor(
|
constructor(
|
||||||
private billPaymentsApplication: BillPaymentsApplication,
|
private billPaymentsApplication: BillPaymentsApplication,
|
||||||
@@ -39,12 +46,14 @@ export class BillPaymentsController {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
|
@RequirePermission(IPaymentMadeAction.Create, AbilitySubject.PaymentMade)
|
||||||
@ApiOperation({ summary: 'Create a new bill payment.' })
|
@ApiOperation({ summary: 'Create a new bill payment.' })
|
||||||
public createBillPayment(@Body() billPaymentDTO: CreateBillPaymentDto) {
|
public createBillPayment(@Body() billPaymentDTO: CreateBillPaymentDto) {
|
||||||
return this.billPaymentsApplication.createBillPayment(billPaymentDTO);
|
return this.billPaymentsApplication.createBillPayment(billPaymentDTO);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete(':billPaymentId')
|
@Delete(':billPaymentId')
|
||||||
|
@RequirePermission(IPaymentMadeAction.Delete, AbilitySubject.PaymentMade)
|
||||||
@ApiOperation({ summary: 'Delete the given bill payment.' })
|
@ApiOperation({ summary: 'Delete the given bill payment.' })
|
||||||
@ApiParam({
|
@ApiParam({
|
||||||
name: 'billPaymentId',
|
name: 'billPaymentId',
|
||||||
@@ -59,6 +68,7 @@ export class BillPaymentsController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Put(':billPaymentId')
|
@Put(':billPaymentId')
|
||||||
|
@RequirePermission(IPaymentMadeAction.Edit, AbilitySubject.PaymentMade)
|
||||||
@ApiOperation({ summary: 'Edit the given bill payment.' })
|
@ApiOperation({ summary: 'Edit the given bill payment.' })
|
||||||
@ApiParam({
|
@ApiParam({
|
||||||
name: 'billPaymentId',
|
name: 'billPaymentId',
|
||||||
@@ -77,6 +87,7 @@ export class BillPaymentsController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Get('/new-page/entries')
|
@Get('/new-page/entries')
|
||||||
|
@RequirePermission(IPaymentMadeAction.View, AbilitySubject.PaymentMade)
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary:
|
summary:
|
||||||
'Retrieves the payable entries of the new page once vendor be selected.',
|
'Retrieves the payable entries of the new page once vendor be selected.',
|
||||||
@@ -95,6 +106,7 @@ export class BillPaymentsController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Get(':billPaymentId/bills')
|
@Get(':billPaymentId/bills')
|
||||||
|
@RequirePermission(IPaymentMadeAction.View, AbilitySubject.PaymentMade)
|
||||||
@ApiOperation({ summary: 'Retrieves the bills of the given bill payment.' })
|
@ApiOperation({ summary: 'Retrieves the bills of the given bill payment.' })
|
||||||
@ApiParam({
|
@ApiParam({
|
||||||
name: 'billPaymentId',
|
name: 'billPaymentId',
|
||||||
@@ -107,6 +119,7 @@ export class BillPaymentsController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Get('/:billPaymentId/edit-page')
|
@Get('/:billPaymentId/edit-page')
|
||||||
|
@RequirePermission(IPaymentMadeAction.View, AbilitySubject.PaymentMade)
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: 'Retrieves the edit page of the given bill payment.',
|
summary: 'Retrieves the edit page of the given bill payment.',
|
||||||
})
|
})
|
||||||
@@ -126,6 +139,7 @@ export class BillPaymentsController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Get(':billPaymentId')
|
@Get(':billPaymentId')
|
||||||
|
@RequirePermission(IPaymentMadeAction.View, AbilitySubject.PaymentMade)
|
||||||
@ApiOperation({ summary: 'Retrieves the bill payment details.' })
|
@ApiOperation({ summary: 'Retrieves the bill payment details.' })
|
||||||
@ApiResponse({
|
@ApiResponse({
|
||||||
status: 200,
|
status: 200,
|
||||||
@@ -145,6 +159,7 @@ export class BillPaymentsController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
|
@RequirePermission(IPaymentMadeAction.View, AbilitySubject.PaymentMade)
|
||||||
@ApiOperation({ summary: 'Retrieves the bill payments list.' })
|
@ApiOperation({ summary: 'Retrieves the bill payments list.' })
|
||||||
@ApiResponse({
|
@ApiResponse({
|
||||||
status: 200,
|
status: 200,
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
Get,
|
Get,
|
||||||
Query,
|
Query,
|
||||||
HttpCode,
|
HttpCode,
|
||||||
|
UseGuards,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { BillsApplication } from './Bills.application';
|
import { BillsApplication } from './Bills.application';
|
||||||
import { IBillsFilter } from './Bills.types';
|
import { IBillsFilter } from './Bills.types';
|
||||||
@@ -28,6 +29,11 @@ import {
|
|||||||
BulkDeleteDto,
|
BulkDeleteDto,
|
||||||
ValidateBulkDeleteResponseDto,
|
ValidateBulkDeleteResponseDto,
|
||||||
} from '@/common/dtos/BulkDelete.dto';
|
} from '@/common/dtos/BulkDelete.dto';
|
||||||
|
import { RequirePermission } from '@/modules/Roles/RequirePermission.decorator';
|
||||||
|
import { PermissionGuard } from '@/modules/Roles/Permission.guard';
|
||||||
|
import { AuthorizationGuard } from '@/modules/Roles/Authorization.guard';
|
||||||
|
import { AbilitySubject } from '@/modules/Roles/Roles.types';
|
||||||
|
import { BillAction } from './Bills.types';
|
||||||
|
|
||||||
@Controller('bills')
|
@Controller('bills')
|
||||||
@ApiTags('Bills')
|
@ApiTags('Bills')
|
||||||
@@ -35,10 +41,12 @@ import {
|
|||||||
@ApiExtraModels(PaginatedResponseDto)
|
@ApiExtraModels(PaginatedResponseDto)
|
||||||
@ApiCommonHeaders()
|
@ApiCommonHeaders()
|
||||||
@ApiExtraModels(ValidateBulkDeleteResponseDto)
|
@ApiExtraModels(ValidateBulkDeleteResponseDto)
|
||||||
|
@UseGuards(AuthorizationGuard, PermissionGuard)
|
||||||
export class BillsController {
|
export class BillsController {
|
||||||
constructor(private billsApplication: BillsApplication) { }
|
constructor(private billsApplication: BillsApplication) { }
|
||||||
|
|
||||||
@Post('validate-bulk-delete')
|
@Post('validate-bulk-delete')
|
||||||
|
@RequirePermission(BillAction.Delete, AbilitySubject.Bill)
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: 'Validate which bills can be deleted and return the results.',
|
summary: 'Validate which bills can be deleted and return the results.',
|
||||||
})
|
})
|
||||||
@@ -58,6 +66,7 @@ export class BillsController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post('bulk-delete')
|
@Post('bulk-delete')
|
||||||
|
@RequirePermission(BillAction.Delete, AbilitySubject.Bill)
|
||||||
@ApiOperation({ summary: 'Deletes multiple bills.' })
|
@ApiOperation({ summary: 'Deletes multiple bills.' })
|
||||||
@HttpCode(200)
|
@HttpCode(200)
|
||||||
@ApiResponse({
|
@ApiResponse({
|
||||||
@@ -73,12 +82,14 @@ export class BillsController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
|
@RequirePermission(BillAction.Create, AbilitySubject.Bill)
|
||||||
@ApiOperation({ summary: 'Create a new bill.' })
|
@ApiOperation({ summary: 'Create a new bill.' })
|
||||||
createBill(@Body() billDTO: CreateBillDto) {
|
createBill(@Body() billDTO: CreateBillDto) {
|
||||||
return this.billsApplication.createBill(billDTO);
|
return this.billsApplication.createBill(billDTO);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Put(':id')
|
@Put(':id')
|
||||||
|
@RequirePermission(BillAction.Edit, AbilitySubject.Bill)
|
||||||
@ApiOperation({ summary: 'Edit the given bill.' })
|
@ApiOperation({ summary: 'Edit the given bill.' })
|
||||||
@ApiParam({
|
@ApiParam({
|
||||||
name: 'id',
|
name: 'id',
|
||||||
@@ -91,6 +102,7 @@ export class BillsController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
|
@RequirePermission(BillAction.Delete, AbilitySubject.Bill)
|
||||||
@ApiOperation({ summary: 'Delete the given bill.' })
|
@ApiOperation({ summary: 'Delete the given bill.' })
|
||||||
@ApiParam({
|
@ApiParam({
|
||||||
name: 'id',
|
name: 'id',
|
||||||
@@ -103,6 +115,7 @@ export class BillsController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
|
@RequirePermission(BillAction.View, AbilitySubject.Bill)
|
||||||
@ApiOperation({ summary: 'Retrieves the bills.' })
|
@ApiOperation({ summary: 'Retrieves the bills.' })
|
||||||
@ApiResponse({
|
@ApiResponse({
|
||||||
status: 200,
|
status: 200,
|
||||||
@@ -132,6 +145,7 @@ export class BillsController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id/payment-transactions')
|
@Get(':id/payment-transactions')
|
||||||
|
@RequirePermission(BillAction.View, AbilitySubject.Bill)
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: 'Retrieve the specific bill associated payment transactions.',
|
summary: 'Retrieve the specific bill associated payment transactions.',
|
||||||
})
|
})
|
||||||
@@ -146,6 +160,7 @@ export class BillsController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
|
@RequirePermission(BillAction.View, AbilitySubject.Bill)
|
||||||
@ApiOperation({ summary: 'Retrieves the bill details.' })
|
@ApiOperation({ summary: 'Retrieves the bill details.' })
|
||||||
@ApiResponse({
|
@ApiResponse({
|
||||||
status: 200,
|
status: 200,
|
||||||
@@ -165,6 +180,7 @@ export class BillsController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Patch(':id/open')
|
@Patch(':id/open')
|
||||||
|
@RequirePermission(BillAction.Edit, AbilitySubject.Bill)
|
||||||
@ApiOperation({ summary: 'Open the given bill.' })
|
@ApiOperation({ summary: 'Open the given bill.' })
|
||||||
@ApiParam({
|
@ApiParam({
|
||||||
name: 'id',
|
name: 'id',
|
||||||
@@ -177,6 +193,7 @@ export class BillsController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Get('due')
|
@Get('due')
|
||||||
|
@RequirePermission(BillAction.View, AbilitySubject.Bill)
|
||||||
@ApiOperation({ summary: 'Retrieves the due bills.' })
|
@ApiOperation({ summary: 'Retrieves the due bills.' })
|
||||||
getDueBills(@Body('vendorId') vendorId?: number) {
|
getDueBills(@Body('vendorId') vendorId?: number) {
|
||||||
return this.billsApplication.getDueBills(vendorId);
|
return this.billsApplication.getDueBills(vendorId);
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { Type } from 'class-transformer';
|
||||||
import { ItemEntryDto } from '@/modules/TransactionItemEntry/dto/ItemEntry.dto';
|
import { ItemEntryDto } from '@/modules/TransactionItemEntry/dto/ItemEntry.dto';
|
||||||
import { AttachmentLinkDto } from '@/modules/Attachments/dtos/Attachment.dto';
|
import { AttachmentLinkDto } from '@/modules/Attachments/dtos/Attachment.dto';
|
||||||
|
import { BranchResponseDto } from '@/modules/Branches/dtos/BranchResponse.dto';
|
||||||
import { DiscountType } from '@/common/types/Discount';
|
import { DiscountType } from '@/common/types/Discount';
|
||||||
|
|
||||||
export class BillResponseDto {
|
export class BillResponseDto {
|
||||||
@@ -89,6 +91,14 @@ export class BillResponseDto {
|
|||||||
})
|
})
|
||||||
branchId?: number;
|
branchId?: number;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Branch details',
|
||||||
|
type: () => BranchResponseDto,
|
||||||
|
required: false,
|
||||||
|
})
|
||||||
|
@Type(() => BranchResponseDto)
|
||||||
|
branch?: BranchResponseDto;
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
description: 'The ID of the project',
|
description: 'The ID of the project',
|
||||||
example: 301,
|
example: 301,
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export class BillTransformer extends Transformer {
|
|||||||
'taxes',
|
'taxes',
|
||||||
'entries',
|
'entries',
|
||||||
'attachments',
|
'attachments',
|
||||||
|
'branch',
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,12 @@ import { ValidateBranchExistance } from './integrations/ValidateBranchExistance'
|
|||||||
import { ManualJournalBranchesValidator } from './integrations/ManualJournals/ManualJournalsBranchesValidator';
|
import { ManualJournalBranchesValidator } from './integrations/ManualJournals/ManualJournalsBranchesValidator';
|
||||||
import { CashflowTransactionsActivateBranches } from './integrations/Cashflow/CashflowActivateBranches';
|
import { CashflowTransactionsActivateBranches } from './integrations/Cashflow/CashflowActivateBranches';
|
||||||
import { ExpensesActivateBranches } from './integrations/Expense/ExpensesActivateBranches';
|
import { ExpensesActivateBranches } from './integrations/Expense/ExpensesActivateBranches';
|
||||||
|
import { BillActivateBranches } from './integrations/Purchases/BillBranchesActivate';
|
||||||
|
import { VendorCreditActivateBranches } from './integrations/Purchases/VendorCreditBranchesActivate';
|
||||||
|
import { BillPaymentsActivateBranches } from './integrations/Purchases/PaymentMadeBranchesActivate';
|
||||||
|
import { BillBranchesActivateSubscriber } from './subscribers/Activate/BillBranchesActivateSubscriber';
|
||||||
|
import { VendorCreditBranchesActivateSubscriber } from './subscribers/Activate/VendorCreditBranchesActivateSubscriber';
|
||||||
|
import { PaymentMadeActivateBranchesSubscriber } from './subscribers/Activate/PaymentMadeBranchesActivateSubscriber';
|
||||||
import { FeaturesModule } from '../Features/Features.module';
|
import { FeaturesModule } from '../Features/Features.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
@@ -66,7 +72,13 @@ import { FeaturesModule } from '../Features/Features.module';
|
|||||||
ValidateBranchExistance,
|
ValidateBranchExistance,
|
||||||
ManualJournalBranchesValidator,
|
ManualJournalBranchesValidator,
|
||||||
CashflowTransactionsActivateBranches,
|
CashflowTransactionsActivateBranches,
|
||||||
ExpensesActivateBranches
|
ExpensesActivateBranches,
|
||||||
|
BillActivateBranches,
|
||||||
|
VendorCreditActivateBranches,
|
||||||
|
BillPaymentsActivateBranches,
|
||||||
|
BillBranchesActivateSubscriber,
|
||||||
|
VendorCreditBranchesActivateSubscriber,
|
||||||
|
PaymentMadeActivateBranchesSubscriber
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
BranchesSettingsService,
|
BranchesSettingsService,
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
import { Knex } from 'knex';
|
import { Knex } from 'knex';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||||
import { Bill } from '@/modules/Bills/models/Bill';
|
import { Bill } from '@/modules/Bills/models/Bill';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class BillActivateBranches {
|
export class BillActivateBranches {
|
||||||
constructor(private readonly billModel: TenantModelProxy<typeof Bill>) {}
|
constructor(
|
||||||
|
@Inject(Bill.name)
|
||||||
|
private readonly billModel: TenantModelProxy<typeof Bill>,
|
||||||
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates all bills transactions with the primary branch.
|
* Updates all bills transactions with the primary branch.
|
||||||
@@ -17,7 +20,7 @@ export class BillActivateBranches {
|
|||||||
primaryBranchId: number,
|
primaryBranchId: number,
|
||||||
trx?: Knex.Transaction,
|
trx?: Knex.Transaction,
|
||||||
) => {
|
) => {
|
||||||
// Updates the sale invoice with primary branch.
|
// Updates the bills with primary branch.
|
||||||
await Bill.query(trx).update({ branchId: primaryBranchId });
|
await this.billModel().query(trx).update({ branchId: primaryBranchId });
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { Knex } from 'knex';
|
import { Knex } from 'knex';
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||||
import { BillPayment } from '@/modules/BillPayments/models/BillPayment';
|
import { BillPayment } from '@/modules/BillPayments/models/BillPayment';
|
||||||
import { Injectable } from '@nestjs/common';
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class BillPaymentsActivateBranches {
|
export class BillPaymentsActivateBranches {
|
||||||
constructor(
|
constructor(
|
||||||
|
@Inject(BillPayment.name)
|
||||||
private readonly billPaymentModel: TenantModelProxy<typeof BillPayment>,
|
private readonly billPaymentModel: TenantModelProxy<typeof BillPayment>,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { Knex } from 'knex';
|
import { Knex } from 'knex';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||||
import { VendorCredit } from '@/modules/VendorCredit/models/VendorCredit';
|
import { VendorCredit } from '@/modules/VendorCredit/models/VendorCredit';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class VendorCreditActivateBranches {
|
export class VendorCreditActivateBranches {
|
||||||
constructor(
|
constructor(
|
||||||
|
@Inject(VendorCredit.name)
|
||||||
private readonly vendorCreditModel: TenantModelProxy<typeof VendorCredit>,
|
private readonly vendorCreditModel: TenantModelProxy<typeof VendorCredit>,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { IBranchesActivatedPayload } from '../../Branches.types';
|
||||||
|
import { events } from '@/common/events/events';
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { BillActivateBranches } from '../../integrations/Purchases/BillBranchesActivate';
|
||||||
|
import { OnEvent } from '@nestjs/event-emitter';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class BillBranchesActivateSubscriber {
|
||||||
|
constructor(
|
||||||
|
private readonly billActivateBranches: BillActivateBranches,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates bills transactions with the primary branch once
|
||||||
|
* the multi-branches is activated.
|
||||||
|
* @param {IBranchesActivatedPayload}
|
||||||
|
*/
|
||||||
|
@OnEvent(events.branch.onActivated)
|
||||||
|
async updateBillsWithBranchOnActivated({
|
||||||
|
primaryBranch,
|
||||||
|
trx,
|
||||||
|
}: IBranchesActivatedPayload) {
|
||||||
|
await this.billActivateBranches.updateBillsWithBranch(
|
||||||
|
primaryBranch.id,
|
||||||
|
trx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { IBranchesActivatedPayload } from '../../Branches.types';
|
||||||
|
import { events } from '@/common/events/events';
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { VendorCreditActivateBranches } from '../../integrations/Purchases/VendorCreditBranchesActivate';
|
||||||
|
import { OnEvent } from '@nestjs/event-emitter';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class VendorCreditBranchesActivateSubscriber {
|
||||||
|
constructor(
|
||||||
|
private readonly vendorCreditActivateBranches: VendorCreditActivateBranches,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates vendor credits transactions with the primary branch once
|
||||||
|
* the multi-branches is activated.
|
||||||
|
* @param {IBranchesActivatedPayload}
|
||||||
|
*/
|
||||||
|
@OnEvent(events.branch.onActivated)
|
||||||
|
async updateVendorCreditsWithBranchOnActivated({
|
||||||
|
primaryBranch,
|
||||||
|
trx,
|
||||||
|
}: IBranchesActivatedPayload) {
|
||||||
|
await this.vendorCreditActivateBranches.updateVendorCreditsWithBranch(
|
||||||
|
primaryBranch.id,
|
||||||
|
trx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,6 +22,7 @@ export abstract class BaseCommand extends CommandRunner {
|
|||||||
},
|
},
|
||||||
migrations: {
|
migrations: {
|
||||||
directory: this.configService.get('systemDatabase.migrationDir'),
|
directory: this.configService.get('systemDatabase.migrationDir'),
|
||||||
|
loadExtensions: ['.js'],
|
||||||
},
|
},
|
||||||
seeds: {
|
seeds: {
|
||||||
directory: this.configService.get('systemDatabase.seedsDir'),
|
directory: this.configService.get('systemDatabase.seedsDir'),
|
||||||
@@ -43,6 +44,7 @@ export abstract class BaseCommand extends CommandRunner {
|
|||||||
},
|
},
|
||||||
migrations: {
|
migrations: {
|
||||||
directory: this.configService.get('tenantDatabase.migrationsDir') || './src/database/migrations',
|
directory: this.configService.get('tenantDatabase.migrationsDir') || './src/database/migrations',
|
||||||
|
loadExtensions: ['.js'],
|
||||||
},
|
},
|
||||||
seeds: {
|
seeds: {
|
||||||
directory: this.configService.get('tenantDatabase.seedsDir') || './src/database/seeds/core',
|
directory: this.configService.get('tenantDatabase.seedsDir') || './src/database/seeds/core',
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export interface IContactAddress {
|
|||||||
billingAddressCity: string;
|
billingAddressCity: string;
|
||||||
billingAddressCountry: string;
|
billingAddressCountry: string;
|
||||||
billingAddressEmail: string;
|
billingAddressEmail: string;
|
||||||
billingAddressZipcode: string;
|
billingAddressPostcode: string;
|
||||||
billingAddressPhone: string;
|
billingAddressPhone: string;
|
||||||
billingAddressState: string;
|
billingAddressState: string;
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ export interface IContactAddress {
|
|||||||
shippingAddressCity: string;
|
shippingAddressCity: string;
|
||||||
shippingAddressCountry: string;
|
shippingAddressCountry: string;
|
||||||
shippingAddressEmail: string;
|
shippingAddressEmail: string;
|
||||||
shippingAddressZipcode: string;
|
shippingAddressPostcode: string;
|
||||||
shippingAddressPhone: string;
|
shippingAddressPhone: string;
|
||||||
shippingAddressState: string;
|
shippingAddressState: string;
|
||||||
}
|
}
|
||||||
@@ -29,7 +29,7 @@ export interface IContactAddressDTO {
|
|||||||
billingAddressCity?: string;
|
billingAddressCity?: string;
|
||||||
billingAddressCountry?: string;
|
billingAddressCountry?: string;
|
||||||
billingAddressEmail?: string;
|
billingAddressEmail?: string;
|
||||||
billingAddressZipcode?: string;
|
billingAddressPostcode?: string;
|
||||||
billingAddressPhone?: string;
|
billingAddressPhone?: string;
|
||||||
billingAddressState?: string;
|
billingAddressState?: string;
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ export interface IContactAddressDTO {
|
|||||||
shippingAddressCity?: string;
|
shippingAddressCity?: string;
|
||||||
shippingAddressCountry?: string;
|
shippingAddressCountry?: string;
|
||||||
shippingAddressEmail?: string;
|
shippingAddressEmail?: string;
|
||||||
shippingAddressZipcode?: string;
|
shippingAddressPostcode?: string;
|
||||||
shippingAddressPhone?: string;
|
shippingAddressPhone?: string;
|
||||||
shippingAddressState?: string;
|
shippingAddressState?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,35 @@
|
|||||||
import { ApiOperation, ApiTags } from '@nestjs/swagger';
|
import { ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||||
import { Body, Controller, Delete, Get, Param, Post } from '@nestjs/common';
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
Delete,
|
||||||
|
Get,
|
||||||
|
Param,
|
||||||
|
Post,
|
||||||
|
UseGuards,
|
||||||
|
} from '@nestjs/common';
|
||||||
import { ICreditNoteRefundDTO } from '../CreditNotes/types/CreditNotes.types';
|
import { ICreditNoteRefundDTO } from '../CreditNotes/types/CreditNotes.types';
|
||||||
import { CreditNotesRefundsApplication } from './CreditNotesRefundsApplication.service';
|
import { CreditNotesRefundsApplication } from './CreditNotesRefundsApplication.service';
|
||||||
import { RefundCreditNote } from './models/RefundCreditNote';
|
import { RefundCreditNote } from './models/RefundCreditNote';
|
||||||
import { CreditNoteRefundDto } from './dto/CreditNoteRefund.dto';
|
import { CreditNoteRefundDto } from './dto/CreditNoteRefund.dto';
|
||||||
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
|
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
|
||||||
|
import { RequirePermission } from '@/modules/Roles/RequirePermission.decorator';
|
||||||
|
import { PermissionGuard } from '@/modules/Roles/Permission.guard';
|
||||||
|
import { AuthorizationGuard } from '@/modules/Roles/Authorization.guard';
|
||||||
|
import { AbilitySubject } from '@/modules/Roles/Roles.types';
|
||||||
|
import { CreditNoteAction } from '../CreditNotes/types/CreditNotes.types';
|
||||||
|
|
||||||
@Controller('credit-notes')
|
@Controller('credit-notes')
|
||||||
@ApiTags('Credit Note Refunds')
|
@ApiTags('Credit Note Refunds')
|
||||||
@ApiCommonHeaders()
|
@ApiCommonHeaders()
|
||||||
|
@UseGuards(AuthorizationGuard, PermissionGuard)
|
||||||
export class CreditNoteRefundsController {
|
export class CreditNoteRefundsController {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly creditNotesRefundsApplication: CreditNotesRefundsApplication,
|
private readonly creditNotesRefundsApplication: CreditNotesRefundsApplication,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Get(':creditNoteId/refunds')
|
@Get(':creditNoteId/refunds')
|
||||||
|
@RequirePermission(CreditNoteAction.View, AbilitySubject.CreditNote)
|
||||||
@ApiOperation({ summary: 'Retrieve the credit note graph.' })
|
@ApiOperation({ summary: 'Retrieve the credit note graph.' })
|
||||||
getCreditNoteRefunds(@Param('creditNoteId') creditNoteId: number) {
|
getCreditNoteRefunds(@Param('creditNoteId') creditNoteId: number) {
|
||||||
return this.creditNotesRefundsApplication.getCreditNoteRefunds(
|
return this.creditNotesRefundsApplication.getCreditNoteRefunds(
|
||||||
@@ -29,6 +44,7 @@ export class CreditNoteRefundsController {
|
|||||||
* @returns {Promise<RefundCreditNote>}
|
* @returns {Promise<RefundCreditNote>}
|
||||||
*/
|
*/
|
||||||
@Post(':creditNoteId/refunds')
|
@Post(':creditNoteId/refunds')
|
||||||
|
@RequirePermission(CreditNoteAction.Refund, AbilitySubject.CreditNote)
|
||||||
@ApiOperation({ summary: 'Create a refund for the given credit note.' })
|
@ApiOperation({ summary: 'Create a refund for the given credit note.' })
|
||||||
createRefundCreditNote(
|
createRefundCreditNote(
|
||||||
@Param('creditNoteId') creditNoteId: number,
|
@Param('creditNoteId') creditNoteId: number,
|
||||||
@@ -46,6 +62,7 @@ export class CreditNoteRefundsController {
|
|||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
@Delete('refunds/:refundCreditId')
|
@Delete('refunds/:refundCreditId')
|
||||||
|
@RequirePermission(CreditNoteAction.Refund, AbilitySubject.CreditNote)
|
||||||
@ApiOperation({ summary: 'Delete a refund for the given credit note.' })
|
@ApiOperation({ summary: 'Delete a refund for the given credit note.' })
|
||||||
deleteRefundCreditNote(
|
deleteRefundCreditNote(
|
||||||
@Param('refundCreditId') refundCreditId: number,
|
@Param('refundCreditId') refundCreditId: number,
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
Put,
|
Put,
|
||||||
Query,
|
Query,
|
||||||
Res,
|
Res,
|
||||||
|
UseGuards,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { CreditNoteApplication } from './CreditNoteApplication.service';
|
import { CreditNoteApplication } from './CreditNoteApplication.service';
|
||||||
import { ICreditNotesQueryDTO } from './types/CreditNotes.types';
|
import { ICreditNotesQueryDTO } from './types/CreditNotes.types';
|
||||||
@@ -30,6 +31,11 @@ import {
|
|||||||
ValidateBulkDeleteResponseDto,
|
ValidateBulkDeleteResponseDto,
|
||||||
} from '@/common/dtos/BulkDelete.dto';
|
} from '@/common/dtos/BulkDelete.dto';
|
||||||
import { AcceptType } from '@/constants/accept-type';
|
import { AcceptType } from '@/constants/accept-type';
|
||||||
|
import { RequirePermission } from '@/modules/Roles/RequirePermission.decorator';
|
||||||
|
import { PermissionGuard } from '@/modules/Roles/Permission.guard';
|
||||||
|
import { AuthorizationGuard } from '@/modules/Roles/Authorization.guard';
|
||||||
|
import { AbilitySubject } from '@/modules/Roles/Roles.types';
|
||||||
|
import { CreditNoteAction } from './types/CreditNotes.types';
|
||||||
|
|
||||||
@Controller('credit-notes')
|
@Controller('credit-notes')
|
||||||
@ApiTags('Credit Notes')
|
@ApiTags('Credit Notes')
|
||||||
@@ -37,6 +43,7 @@ import { AcceptType } from '@/constants/accept-type';
|
|||||||
@ApiExtraModels(PaginatedResponseDto)
|
@ApiExtraModels(PaginatedResponseDto)
|
||||||
@ApiExtraModels(ValidateBulkDeleteResponseDto)
|
@ApiExtraModels(ValidateBulkDeleteResponseDto)
|
||||||
@ApiCommonHeaders()
|
@ApiCommonHeaders()
|
||||||
|
@UseGuards(AuthorizationGuard, PermissionGuard)
|
||||||
export class CreditNotesController {
|
export class CreditNotesController {
|
||||||
/**
|
/**
|
||||||
* @param {CreditNoteApplication} creditNoteApplication - The credit note application service.
|
* @param {CreditNoteApplication} creditNoteApplication - The credit note application service.
|
||||||
@@ -44,6 +51,7 @@ export class CreditNotesController {
|
|||||||
constructor(private creditNoteApplication: CreditNoteApplication) { }
|
constructor(private creditNoteApplication: CreditNoteApplication) { }
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
|
@RequirePermission(CreditNoteAction.Create, AbilitySubject.CreditNote)
|
||||||
@ApiOperation({ summary: 'Create a new credit note' })
|
@ApiOperation({ summary: 'Create a new credit note' })
|
||||||
@ApiResponse({ status: 201, description: 'Credit note successfully created' })
|
@ApiResponse({ status: 201, description: 'Credit note successfully created' })
|
||||||
@ApiResponse({ status: 400, description: 'Invalid input data' })
|
@ApiResponse({ status: 400, description: 'Invalid input data' })
|
||||||
@@ -52,6 +60,7 @@ export class CreditNotesController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Get('state')
|
@Get('state')
|
||||||
|
@RequirePermission(CreditNoteAction.View, AbilitySubject.CreditNote)
|
||||||
@ApiOperation({ summary: 'Get credit note state' })
|
@ApiOperation({ summary: 'Get credit note state' })
|
||||||
@ApiResponse({ status: 200, description: 'Returns the credit note state' })
|
@ApiResponse({ status: 200, description: 'Returns the credit note state' })
|
||||||
getCreditNoteState() {
|
getCreditNoteState() {
|
||||||
@@ -59,6 +68,7 @@ export class CreditNotesController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
|
@RequirePermission(CreditNoteAction.View, AbilitySubject.CreditNote)
|
||||||
@ApiOperation({ summary: 'Get a specific credit note by ID' })
|
@ApiOperation({ summary: 'Get a specific credit note by ID' })
|
||||||
@ApiParam({ name: 'id', description: 'Credit note ID', type: 'number' })
|
@ApiParam({ name: 'id', description: 'Credit note ID', type: 'number' })
|
||||||
@ApiResponse({
|
@ApiResponse({
|
||||||
@@ -92,6 +102,7 @@ export class CreditNotesController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
|
@RequirePermission(CreditNoteAction.View, AbilitySubject.CreditNote)
|
||||||
@ApiOperation({ summary: 'Get all credit notes' })
|
@ApiOperation({ summary: 'Get all credit notes' })
|
||||||
@ApiResponse({
|
@ApiResponse({
|
||||||
status: 200,
|
status: 200,
|
||||||
@@ -115,6 +126,7 @@ export class CreditNotesController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Put(':id')
|
@Put(':id')
|
||||||
|
@RequirePermission(CreditNoteAction.Edit, AbilitySubject.CreditNote)
|
||||||
@ApiOperation({ summary: 'Update a credit note' })
|
@ApiOperation({ summary: 'Update a credit note' })
|
||||||
@ApiParam({ name: 'id', description: 'Credit note ID', type: 'number' })
|
@ApiParam({ name: 'id', description: 'Credit note ID', type: 'number' })
|
||||||
@ApiResponse({ status: 200, description: 'Credit note successfully updated' })
|
@ApiResponse({ status: 200, description: 'Credit note successfully updated' })
|
||||||
@@ -131,6 +143,7 @@ export class CreditNotesController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Put(':id/open')
|
@Put(':id/open')
|
||||||
|
@RequirePermission(CreditNoteAction.Edit, AbilitySubject.CreditNote)
|
||||||
@ApiOperation({ summary: 'Open a credit note' })
|
@ApiOperation({ summary: 'Open a credit note' })
|
||||||
@ApiParam({ name: 'id', description: 'Credit note ID', type: 'number' })
|
@ApiParam({ name: 'id', description: 'Credit note ID', type: 'number' })
|
||||||
@ApiResponse({ status: 200, description: 'Credit note successfully opened' })
|
@ApiResponse({ status: 200, description: 'Credit note successfully opened' })
|
||||||
@@ -140,6 +153,7 @@ export class CreditNotesController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post('validate-bulk-delete')
|
@Post('validate-bulk-delete')
|
||||||
|
@RequirePermission(CreditNoteAction.Delete, AbilitySubject.CreditNote)
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary:
|
summary:
|
||||||
'Validates which credit notes can be deleted and returns the results.',
|
'Validates which credit notes can be deleted and returns the results.',
|
||||||
@@ -161,6 +175,7 @@ export class CreditNotesController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post('bulk-delete')
|
@Post('bulk-delete')
|
||||||
|
@RequirePermission(CreditNoteAction.Delete, AbilitySubject.CreditNote)
|
||||||
@ApiOperation({ summary: 'Deletes multiple credit notes.' })
|
@ApiOperation({ summary: 'Deletes multiple credit notes.' })
|
||||||
@ApiResponse({
|
@ApiResponse({
|
||||||
status: 200,
|
status: 200,
|
||||||
@@ -173,6 +188,7 @@ export class CreditNotesController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
|
@RequirePermission(CreditNoteAction.Delete, AbilitySubject.CreditNote)
|
||||||
@ApiOperation({ summary: 'Delete a credit note' })
|
@ApiOperation({ summary: 'Delete a credit note' })
|
||||||
@ApiParam({ name: 'id', description: 'Credit note ID', type: 'number' })
|
@ApiParam({ name: 'id', description: 'Credit note ID', type: 'number' })
|
||||||
@ApiResponse({ status: 200, description: 'Credit note successfully deleted' })
|
@ApiResponse({ status: 200, description: 'Credit note successfully deleted' })
|
||||||
|
|||||||
@@ -1,15 +1,39 @@
|
|||||||
import { Body, Controller, Get, Param, Post } from '@nestjs/common';
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
Delete,
|
||||||
|
Get,
|
||||||
|
Param,
|
||||||
|
Post,
|
||||||
|
UseGuards,
|
||||||
|
} from '@nestjs/common';
|
||||||
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
|
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||||
import { GetCreditNoteAssociatedAppliedInvoices } from './queries/GetCreditNoteAssociatedAppliedInvoices.service';
|
import { GetCreditNoteAssociatedAppliedInvoices } from './queries/GetCreditNoteAssociatedAppliedInvoices.service';
|
||||||
|
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
|
||||||
|
import { RequirePermission } from '@/modules/Roles/RequirePermission.decorator';
|
||||||
|
import { PermissionGuard } from '@/modules/Roles/Permission.guard';
|
||||||
|
import { AuthorizationGuard } from '@/modules/Roles/Authorization.guard';
|
||||||
|
import { AbilitySubject } from '@/modules/Roles/Roles.types';
|
||||||
|
import { CreditNoteAction } from '../CreditNotes/types/CreditNotes.types';
|
||||||
|
import { GetCreditNoteAssociatedInvoicesToApply } from './queries/GetCreditNoteAssociatedInvoicesToApply.service';
|
||||||
|
import { CreditNoteApplyToInvoices } from './commands/CreditNoteApplyToInvoices.service';
|
||||||
|
import { DeleteCreditNoteApplyToInvoices } from './commands/DeleteCreditNoteApplyToInvoices.service';
|
||||||
|
import { ApplyCreditNoteToInvoicesDto } from './dtos/ApplyCreditNoteToInvoices.dto';
|
||||||
|
|
||||||
@Controller('credit-notes')
|
@Controller('credit-notes')
|
||||||
@ApiTags('Credit Notes Apply Invoice')
|
@ApiTags('Credit Notes Apply Invoice')
|
||||||
|
@ApiCommonHeaders()
|
||||||
|
@UseGuards(AuthorizationGuard, PermissionGuard)
|
||||||
export class CreditNotesApplyInvoiceController {
|
export class CreditNotesApplyInvoiceController {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly getCreditNoteAssociatedAppliedInvoicesService: GetCreditNoteAssociatedAppliedInvoices,
|
private readonly getCreditNoteAssociatedAppliedInvoicesService: GetCreditNoteAssociatedAppliedInvoices,
|
||||||
|
private readonly getCreditNoteAssociatedInvoicesToApplyService: GetCreditNoteAssociatedInvoicesToApply,
|
||||||
|
private readonly creditNoteApplyToInvoicesService: CreditNoteApplyToInvoices,
|
||||||
|
private readonly deleteCreditNoteApplyToInvoicesService: DeleteCreditNoteApplyToInvoices,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Get(':creditNoteId/applied-invoices')
|
@Get(':creditNoteId/applied-invoices')
|
||||||
|
@RequirePermission(CreditNoteAction.View, AbilitySubject.CreditNote)
|
||||||
@ApiOperation({ summary: 'Applied credit note to invoices' })
|
@ApiOperation({ summary: 'Applied credit note to invoices' })
|
||||||
@ApiResponse({
|
@ApiResponse({
|
||||||
status: 200,
|
status: 200,
|
||||||
@@ -23,7 +47,25 @@ export class CreditNotesApplyInvoiceController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get(':creditNoteId/apply-invoices')
|
||||||
|
@RequirePermission(CreditNoteAction.View, AbilitySubject.CreditNote)
|
||||||
|
@ApiOperation({ summary: 'Get credit note associated invoices to apply' })
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Credit note associated invoices to apply',
|
||||||
|
})
|
||||||
|
@ApiResponse({ status: 404, description: 'Credit note not found' })
|
||||||
|
@ApiResponse({ status: 400, description: 'Invalid input data' })
|
||||||
|
getCreditNoteAssociatedInvoicesToApply(
|
||||||
|
@Param('creditNoteId') creditNoteId: number,
|
||||||
|
) {
|
||||||
|
return this.getCreditNoteAssociatedInvoicesToApplyService.getCreditAssociatedInvoicesToApply(
|
||||||
|
creditNoteId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@Post(':creditNoteId/apply-invoices')
|
@Post(':creditNoteId/apply-invoices')
|
||||||
|
@RequirePermission(CreditNoteAction.Edit, AbilitySubject.CreditNote)
|
||||||
@ApiOperation({ summary: 'Apply credit note to invoices' })
|
@ApiOperation({ summary: 'Apply credit note to invoices' })
|
||||||
@ApiResponse({
|
@ApiResponse({
|
||||||
status: 200,
|
status: 200,
|
||||||
@@ -31,9 +73,32 @@ export class CreditNotesApplyInvoiceController {
|
|||||||
})
|
})
|
||||||
@ApiResponse({ status: 404, description: 'Credit note not found' })
|
@ApiResponse({ status: 404, description: 'Credit note not found' })
|
||||||
@ApiResponse({ status: 400, description: 'Invalid input data' })
|
@ApiResponse({ status: 400, description: 'Invalid input data' })
|
||||||
applyCreditNoteToInvoices(@Param('creditNoteId') creditNoteId: number) {
|
applyCreditNoteToInvoices(
|
||||||
return this.getCreditNoteAssociatedAppliedInvoicesService.getCreditAssociatedAppliedInvoices(
|
@Param('creditNoteId') creditNoteId: number,
|
||||||
|
@Body() applyDto: ApplyCreditNoteToInvoicesDto,
|
||||||
|
) {
|
||||||
|
return this.creditNoteApplyToInvoicesService.applyCreditNoteToInvoices(
|
||||||
creditNoteId,
|
creditNoteId,
|
||||||
|
applyDto,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete('applied-invoices/:applyCreditToInvoicesId')
|
||||||
|
@RequirePermission(CreditNoteAction.Edit, AbilitySubject.CreditNote)
|
||||||
|
@ApiOperation({ summary: 'Delete applied credit note to invoice' })
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: 'Credit note application successfully deleted',
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 404,
|
||||||
|
description: 'Credit note application not found',
|
||||||
|
})
|
||||||
|
deleteApplyCreditNoteToInvoices(
|
||||||
|
@Param('applyCreditToInvoicesId') applyCreditToInvoicesId: number,
|
||||||
|
) {
|
||||||
|
return this.deleteCreditNoteApplyToInvoicesService.deleteApplyCreditNoteToInvoices(
|
||||||
|
applyCreditToInvoicesId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import { CreditNotesModule } from '../CreditNotes/CreditNotes.module';
|
|||||||
import { GetCreditNoteAssociatedAppliedInvoices } from './queries/GetCreditNoteAssociatedAppliedInvoices.service';
|
import { GetCreditNoteAssociatedAppliedInvoices } from './queries/GetCreditNoteAssociatedAppliedInvoices.service';
|
||||||
import { GetCreditNoteAssociatedInvoicesToApply } from './queries/GetCreditNoteAssociatedInvoicesToApply.service';
|
import { GetCreditNoteAssociatedInvoicesToApply } from './queries/GetCreditNoteAssociatedInvoicesToApply.service';
|
||||||
import { CreditNotesApplyInvoiceController } from './CreditNotesApplyInvoice.controller';
|
import { CreditNotesApplyInvoiceController } from './CreditNotesApplyInvoice.controller';
|
||||||
|
import { CreditNoteApplySyncCreditSubscriber } from './subscribers/CreditNoteApplySyncCreditSubscriber';
|
||||||
|
import { CreditNoteApplySyncInvoicesCreditedAmountSubscriber } from './subscribers/CreditNoteApplySyncInvoicesSubscriber';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
providers: [
|
providers: [
|
||||||
@@ -19,6 +21,8 @@ import { CreditNotesApplyInvoiceController } from './CreditNotesApplyInvoice.con
|
|||||||
CreditNoteApplySyncCredit,
|
CreditNoteApplySyncCredit,
|
||||||
GetCreditNoteAssociatedAppliedInvoices,
|
GetCreditNoteAssociatedAppliedInvoices,
|
||||||
GetCreditNoteAssociatedInvoicesToApply,
|
GetCreditNoteAssociatedInvoicesToApply,
|
||||||
|
CreditNoteApplySyncCreditSubscriber,
|
||||||
|
CreditNoteApplySyncInvoicesCreditedAmountSubscriber,
|
||||||
],
|
],
|
||||||
exports: [DeleteCustomerLinkedCreditNoteService],
|
exports: [DeleteCustomerLinkedCreditNoteService],
|
||||||
imports: [PaymentsReceivedModule, forwardRef(() => CreditNotesModule)],
|
imports: [PaymentsReceivedModule, forwardRef(() => CreditNotesModule)],
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Knex } from 'knex';
|
import { Knex } from 'knex';
|
||||||
import { Injectable, Inject } from '@nestjs/common';
|
import { Injectable, Inject } from '@nestjs/common';
|
||||||
import Bluebird from 'bluebird';
|
import * as Bluebird from 'bluebird';
|
||||||
import { ICreditNoteAppliedToInvoice } from '../types/CreditNoteApplyInvoice.types';
|
import { ICreditNoteAppliedToInvoice } from '../types/CreditNoteApplyInvoice.types';
|
||||||
import { SaleInvoice } from '@/modules/SaleInvoices/models/SaleInvoice';
|
import { SaleInvoice } from '@/modules/SaleInvoices/models/SaleInvoice';
|
||||||
import { CreditNoteAppliedInvoice } from '../models/CreditNoteAppliedInvoice';
|
import { CreditNoteAppliedInvoice } from '../models/CreditNoteAppliedInvoice';
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
|||||||
import { Knex } from 'knex';
|
import { Knex } from 'knex';
|
||||||
import { sumBy } from 'lodash';
|
import { sumBy } from 'lodash';
|
||||||
import {
|
import {
|
||||||
|
ICreditNoteAppliedToInvoice,
|
||||||
ICreditNoteAppliedToInvoiceModel,
|
ICreditNoteAppliedToInvoiceModel,
|
||||||
IApplyCreditToInvoicesDTO,
|
IApplyCreditToInvoicesDTO,
|
||||||
IApplyCreditToInvoicesCreatedPayload,
|
IApplyCreditToInvoicesCreatedPayload,
|
||||||
@@ -17,6 +18,7 @@ import { CreditNote } from '@/modules/CreditNotes/models/CreditNote';
|
|||||||
import { CreditNoteAppliedInvoice } from '../models/CreditNoteAppliedInvoice';
|
import { CreditNoteAppliedInvoice } from '../models/CreditNoteAppliedInvoice';
|
||||||
import { CommandCreditNoteDTOTransform } from '@/modules/CreditNotes/commands/CommandCreditNoteDTOTransform.service';
|
import { CommandCreditNoteDTOTransform } from '@/modules/CreditNotes/commands/CommandCreditNoteDTOTransform.service';
|
||||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||||
|
import { ApplyCreditNoteToInvoicesDto } from '../dtos/ApplyCreditNoteToInvoices.dto';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CreditNoteApplyToInvoices {
|
export class CreditNoteApplyToInvoices {
|
||||||
@@ -48,7 +50,7 @@ export class CreditNoteApplyToInvoices {
|
|||||||
*/
|
*/
|
||||||
public async applyCreditNoteToInvoices(
|
public async applyCreditNoteToInvoices(
|
||||||
creditNoteId: number,
|
creditNoteId: number,
|
||||||
applyCreditToInvoicesDTO: IApplyCreditToInvoicesDTO,
|
applyCreditToInvoicesDTO: ApplyCreditNoteToInvoicesDto,
|
||||||
): Promise<CreditNoteAppliedInvoice[]> {
|
): Promise<CreditNoteAppliedInvoice[]> {
|
||||||
// Saves the credit note or throw not found service error.
|
// Saves the credit note or throw not found service error.
|
||||||
const creditNote = await this.creditNoteModel()
|
const creditNote = await this.creditNoteModel()
|
||||||
@@ -71,7 +73,7 @@ export class CreditNoteApplyToInvoices {
|
|||||||
// Validate invoices has remaining amount to apply.
|
// Validate invoices has remaining amount to apply.
|
||||||
this.validateInvoicesRemainingAmount(
|
this.validateInvoicesRemainingAmount(
|
||||||
appliedInvoicesEntries,
|
appliedInvoicesEntries,
|
||||||
creditNoteAppliedModel.amount,
|
creditNoteAppliedModel.entries,
|
||||||
);
|
);
|
||||||
// Validate the credit note remaining amount.
|
// Validate the credit note remaining amount.
|
||||||
this.creditNoteDTOTransform.validateCreditRemainingAmount(
|
this.creditNoteDTOTransform.validateCreditRemainingAmount(
|
||||||
@@ -122,18 +124,20 @@ export class CreditNoteApplyToInvoices {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate the invoice remaining amount.
|
* Validate each invoice has sufficient remaining amount for the applied credit.
|
||||||
* @param {ISaleInvoice[]} invoices
|
* @param {ISaleInvoice[]} invoices
|
||||||
* @param {number} amount
|
* @param {ICreditNoteAppliedToInvoice[]} entries
|
||||||
*/
|
*/
|
||||||
private validateInvoicesRemainingAmount = (
|
private validateInvoicesRemainingAmount = (
|
||||||
invoices: SaleInvoice[],
|
invoices: SaleInvoice[],
|
||||||
amount: number,
|
entries: ICreditNoteAppliedToInvoice[],
|
||||||
) => {
|
) => {
|
||||||
const invalidInvoices = invoices.filter(
|
const invoiceMap = new Map(invoices.map((inv) => [inv.id, inv]));
|
||||||
(invoice) => invoice.dueAmount < amount,
|
const invalidEntries = entries.filter((entry) => {
|
||||||
);
|
const invoice = invoiceMap.get(entry.invoiceId);
|
||||||
if (invalidInvoices.length > 0) {
|
return invoice != null && invoice.dueAmount < entry.amount;
|
||||||
|
});
|
||||||
|
if (invalidEntries.length > 0) {
|
||||||
throw new ServiceError(ERRORS.INVOICES_HAS_NO_REMAINING_AMOUNT);
|
throw new ServiceError(ERRORS.INVOICES_HAS_NO_REMAINING_AMOUNT);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { Type } from 'class-transformer';
|
||||||
|
import {
|
||||||
|
ArrayMinSize,
|
||||||
|
IsArray,
|
||||||
|
IsInt,
|
||||||
|
IsNotEmpty,
|
||||||
|
IsNumber,
|
||||||
|
ValidateNested,
|
||||||
|
} from 'class-validator';
|
||||||
|
|
||||||
|
export class ApplyCreditNoteInvoiceEntryDto {
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsInt()
|
||||||
|
@ApiProperty({ description: 'Invoice ID to apply credit to', example: 1 })
|
||||||
|
invoiceId: number;
|
||||||
|
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsNumber()
|
||||||
|
@ApiProperty({ description: 'Amount to apply', example: 100.5 })
|
||||||
|
amount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ApplyCreditNoteToInvoicesDto {
|
||||||
|
@IsArray()
|
||||||
|
@ArrayMinSize(1)
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
@Type(() => ApplyCreditNoteInvoiceEntryDto)
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'Entries of invoice ID and amount to apply',
|
||||||
|
type: [ApplyCreditNoteInvoiceEntryDto],
|
||||||
|
example: [
|
||||||
|
{ invoice_id: 1, amount: 100.5 },
|
||||||
|
{ invoice_id: 2, amount: 50 },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
entries: ApplyCreditNoteInvoiceEntryDto[];
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@ import { CreditNoteApplySyncInvoicesCreditedAmount } from '../commands/CreditNot
|
|||||||
import { events } from '@/common/events/events';
|
import { events } from '@/common/events/events';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export default class CreditNoteApplySyncInvoicesCreditedAmountSubscriber {
|
export class CreditNoteApplySyncInvoicesCreditedAmountSubscriber {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly syncInvoicesWithCreditNote: CreditNoteApplySyncInvoicesCreditedAmount,
|
private readonly syncInvoicesWithCreditNote: CreditNoteApplySyncInvoicesCreditedAmount,
|
||||||
) {}
|
) {}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export interface IApplyCreditToInvoicesDeletedPayload {
|
|||||||
export interface ICreditNoteAppliedToInvoice {
|
export interface ICreditNoteAppliedToInvoice {
|
||||||
amount: number;
|
amount: number;
|
||||||
creditNoteId: number;
|
creditNoteId: number;
|
||||||
|
invoiceId: number;
|
||||||
}
|
}
|
||||||
export interface ICreditNoteAppliedToInvoiceModel {
|
export interface ICreditNoteAppliedToInvoiceModel {
|
||||||
amount: number;
|
amount: number;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
Post,
|
Post,
|
||||||
Put,
|
Put,
|
||||||
Query,
|
Query,
|
||||||
|
UseGuards,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { CustomersApplication } from './CustomersApplication.service';
|
import { CustomersApplication } from './CustomersApplication.service';
|
||||||
import { CustomerOpeningBalanceEditDto } from './dtos/CustomerOpeningBalanceEdit.dto';
|
import { CustomerOpeningBalanceEditDto } from './dtos/CustomerOpeningBalanceEdit.dto';
|
||||||
@@ -26,15 +27,22 @@ import {
|
|||||||
ValidateBulkDeleteCustomersResponseDto,
|
ValidateBulkDeleteCustomersResponseDto,
|
||||||
} from './dtos/BulkDeleteCustomers.dto';
|
} from './dtos/BulkDeleteCustomers.dto';
|
||||||
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
|
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
|
||||||
|
import { RequirePermission } from '@/modules/Roles/RequirePermission.decorator';
|
||||||
|
import { PermissionGuard } from '@/modules/Roles/Permission.guard';
|
||||||
|
import { AuthorizationGuard } from '@/modules/Roles/Authorization.guard';
|
||||||
|
import { AbilitySubject } from '@/modules/Roles/Roles.types';
|
||||||
|
import { CustomerAction } from './types/Customers.types';
|
||||||
|
|
||||||
@Controller('customers')
|
@Controller('customers')
|
||||||
@ApiTags('Customers')
|
@ApiTags('Customers')
|
||||||
@ApiExtraModels(CustomerResponseDto)
|
@ApiExtraModels(CustomerResponseDto)
|
||||||
@ApiCommonHeaders()
|
@ApiCommonHeaders()
|
||||||
|
@UseGuards(AuthorizationGuard, PermissionGuard)
|
||||||
export class CustomersController {
|
export class CustomersController {
|
||||||
constructor(private customersApplication: CustomersApplication) { }
|
constructor(private customersApplication: CustomersApplication) { }
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
|
@RequirePermission(CustomerAction.View, AbilitySubject.Customer)
|
||||||
@ApiOperation({ summary: 'Retrieves the customer details.' })
|
@ApiOperation({ summary: 'Retrieves the customer details.' })
|
||||||
@ApiResponse({
|
@ApiResponse({
|
||||||
status: 200,
|
status: 200,
|
||||||
@@ -46,6 +54,7 @@ export class CustomersController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
|
@RequirePermission(CustomerAction.View, AbilitySubject.Customer)
|
||||||
@ApiOperation({ summary: 'Retrieves the customers paginated list.' })
|
@ApiOperation({ summary: 'Retrieves the customers paginated list.' })
|
||||||
@ApiResponse({
|
@ApiResponse({
|
||||||
status: 200,
|
status: 200,
|
||||||
@@ -60,6 +69,7 @@ export class CustomersController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
|
@RequirePermission(CustomerAction.Create, AbilitySubject.Customer)
|
||||||
@ApiOperation({ summary: 'Create a new customer.' })
|
@ApiOperation({ summary: 'Create a new customer.' })
|
||||||
@ApiResponse({
|
@ApiResponse({
|
||||||
status: 201,
|
status: 201,
|
||||||
@@ -71,6 +81,7 @@ export class CustomersController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Put(':id')
|
@Put(':id')
|
||||||
|
@RequirePermission(CustomerAction.Edit, AbilitySubject.Customer)
|
||||||
@ApiOperation({ summary: 'Edit the given customer.' })
|
@ApiOperation({ summary: 'Edit the given customer.' })
|
||||||
@ApiResponse({
|
@ApiResponse({
|
||||||
status: 200,
|
status: 200,
|
||||||
@@ -85,6 +96,7 @@ export class CustomersController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
|
@RequirePermission(CustomerAction.Delete, AbilitySubject.Customer)
|
||||||
@ApiOperation({ summary: 'Delete the given customer.' })
|
@ApiOperation({ summary: 'Delete the given customer.' })
|
||||||
@ApiResponse({
|
@ApiResponse({
|
||||||
status: 200,
|
status: 200,
|
||||||
@@ -95,6 +107,7 @@ export class CustomersController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Put(':id/opening-balance')
|
@Put(':id/opening-balance')
|
||||||
|
@RequirePermission(CustomerAction.Edit, AbilitySubject.Customer)
|
||||||
@ApiOperation({ summary: 'Edit the opening balance of the given customer.' })
|
@ApiOperation({ summary: 'Edit the opening balance of the given customer.' })
|
||||||
@ApiResponse({
|
@ApiResponse({
|
||||||
status: 200,
|
status: 200,
|
||||||
@@ -112,6 +125,7 @@ export class CustomersController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post('validate-bulk-delete')
|
@Post('validate-bulk-delete')
|
||||||
|
@RequirePermission(CustomerAction.Delete, AbilitySubject.Customer)
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary:
|
summary:
|
||||||
'Validates which customers can be deleted and returns counts of deletable and non-deletable customers.',
|
'Validates which customers can be deleted and returns counts of deletable and non-deletable customers.',
|
||||||
@@ -131,6 +145,7 @@ export class CustomersController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post('bulk-delete')
|
@Post('bulk-delete')
|
||||||
|
@RequirePermission(CustomerAction.Delete, AbilitySubject.Customer)
|
||||||
@ApiOperation({ summary: 'Deletes multiple customers in bulk.' })
|
@ApiOperation({ summary: 'Deletes multiple customers in bulk.' })
|
||||||
@ApiResponse({
|
@ApiResponse({
|
||||||
status: 200,
|
status: 200,
|
||||||
|
|||||||
@@ -27,10 +27,10 @@ export class ContactAddressDto {
|
|||||||
@IsEmail()
|
@IsEmail()
|
||||||
billingAddressEmail?: string;
|
billingAddressEmail?: string;
|
||||||
|
|
||||||
@ApiProperty({ required: false, description: 'Billing address zipcode' })
|
@ApiProperty({ required: false, description: 'Billing address postcode' })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
billingAddressZipcode?: string;
|
billingAddressPostcode?: string;
|
||||||
|
|
||||||
@ApiProperty({ required: false, description: 'Billing address phone' })
|
@ApiProperty({ required: false, description: 'Billing address phone' })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@@ -67,10 +67,10 @@ export class ContactAddressDto {
|
|||||||
@IsEmail()
|
@IsEmail()
|
||||||
shippingAddressEmail?: string;
|
shippingAddressEmail?: string;
|
||||||
|
|
||||||
@ApiProperty({ required: false, description: 'Shipping address zipcode' })
|
@ApiProperty({ required: false, description: 'Shipping address postcode' })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
shippingAddressZipcode?: string;
|
shippingAddressPostcode?: string;
|
||||||
|
|
||||||
@ApiProperty({ required: false, description: 'Shipping address phone' })
|
@ApiProperty({ required: false, description: 'Shipping address phone' })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
Post,
|
Post,
|
||||||
Put,
|
Put,
|
||||||
Query,
|
Query,
|
||||||
|
UseGuards,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { ExpensesApplication } from './ExpensesApplication.service';
|
import { ExpensesApplication } from './ExpensesApplication.service';
|
||||||
import { IExpensesFilter } from './Expenses.types';
|
import { IExpensesFilter } from './Expenses.types';
|
||||||
@@ -25,6 +26,11 @@ import {
|
|||||||
BulkDeleteDto,
|
BulkDeleteDto,
|
||||||
ValidateBulkDeleteResponseDto,
|
ValidateBulkDeleteResponseDto,
|
||||||
} from '@/common/dtos/BulkDelete.dto';
|
} from '@/common/dtos/BulkDelete.dto';
|
||||||
|
import { RequirePermission } from '@/modules/Roles/RequirePermission.decorator';
|
||||||
|
import { PermissionGuard } from '@/modules/Roles/Permission.guard';
|
||||||
|
import { AuthorizationGuard } from '@/modules/Roles/Authorization.guard';
|
||||||
|
import { AbilitySubject } from '@/modules/Roles/Roles.types';
|
||||||
|
import { ExpenseAction } from './Expenses.types';
|
||||||
|
|
||||||
@Controller('expenses')
|
@Controller('expenses')
|
||||||
@ApiTags('Expenses')
|
@ApiTags('Expenses')
|
||||||
@@ -34,10 +40,12 @@ import {
|
|||||||
ValidateBulkDeleteResponseDto,
|
ValidateBulkDeleteResponseDto,
|
||||||
)
|
)
|
||||||
@ApiCommonHeaders()
|
@ApiCommonHeaders()
|
||||||
|
@UseGuards(AuthorizationGuard, PermissionGuard)
|
||||||
export class ExpensesController {
|
export class ExpensesController {
|
||||||
constructor(private readonly expensesApplication: ExpensesApplication) { }
|
constructor(private readonly expensesApplication: ExpensesApplication) { }
|
||||||
|
|
||||||
@Post('validate-bulk-delete')
|
@Post('validate-bulk-delete')
|
||||||
|
@RequirePermission(ExpenseAction.Delete, AbilitySubject.Expense)
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: 'Validate which expenses can be deleted and return the results.',
|
summary: 'Validate which expenses can be deleted and return the results.',
|
||||||
})
|
})
|
||||||
@@ -58,6 +66,7 @@ export class ExpensesController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post('bulk-delete')
|
@Post('bulk-delete')
|
||||||
|
@RequirePermission(ExpenseAction.Delete, AbilitySubject.Expense)
|
||||||
@ApiOperation({ summary: 'Deletes multiple expenses.' })
|
@ApiOperation({ summary: 'Deletes multiple expenses.' })
|
||||||
@ApiResponse({
|
@ApiResponse({
|
||||||
status: 200,
|
status: 200,
|
||||||
@@ -76,6 +85,7 @@ export class ExpensesController {
|
|||||||
* @param {IExpenseCreateDTO} expenseDTO
|
* @param {IExpenseCreateDTO} expenseDTO
|
||||||
*/
|
*/
|
||||||
@Post()
|
@Post()
|
||||||
|
@RequirePermission(ExpenseAction.Create, AbilitySubject.Expense)
|
||||||
@ApiOperation({ summary: 'Create a new expense transaction.' })
|
@ApiOperation({ summary: 'Create a new expense transaction.' })
|
||||||
public createExpense(@Body() expenseDTO: CreateExpenseDto) {
|
public createExpense(@Body() expenseDTO: CreateExpenseDto) {
|
||||||
return this.expensesApplication.createExpense(expenseDTO);
|
return this.expensesApplication.createExpense(expenseDTO);
|
||||||
@@ -87,6 +97,7 @@ export class ExpensesController {
|
|||||||
* @param {IExpenseEditDTO} expenseDTO
|
* @param {IExpenseEditDTO} expenseDTO
|
||||||
*/
|
*/
|
||||||
@Put(':id')
|
@Put(':id')
|
||||||
|
@RequirePermission(ExpenseAction.Edit, AbilitySubject.Expense)
|
||||||
@ApiOperation({ summary: 'Edit the given expense transaction.' })
|
@ApiOperation({ summary: 'Edit the given expense transaction.' })
|
||||||
public editExpense(
|
public editExpense(
|
||||||
@Param('id') expenseId: number,
|
@Param('id') expenseId: number,
|
||||||
@@ -100,6 +111,7 @@ export class ExpensesController {
|
|||||||
* @param {number} expenseId
|
* @param {number} expenseId
|
||||||
*/
|
*/
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
|
@RequirePermission(ExpenseAction.Delete, AbilitySubject.Expense)
|
||||||
@ApiOperation({ summary: 'Delete the given expense transaction.' })
|
@ApiOperation({ summary: 'Delete the given expense transaction.' })
|
||||||
public deleteExpense(@Param('id') expenseId: number) {
|
public deleteExpense(@Param('id') expenseId: number) {
|
||||||
return this.expensesApplication.deleteExpense(expenseId);
|
return this.expensesApplication.deleteExpense(expenseId);
|
||||||
@@ -110,6 +122,7 @@ export class ExpensesController {
|
|||||||
* @param {number} expenseId
|
* @param {number} expenseId
|
||||||
*/
|
*/
|
||||||
@Post(':id/publish')
|
@Post(':id/publish')
|
||||||
|
@RequirePermission(ExpenseAction.Edit, AbilitySubject.Expense)
|
||||||
@ApiOperation({ summary: 'Publish the given expense transaction.' })
|
@ApiOperation({ summary: 'Publish the given expense transaction.' })
|
||||||
public publishExpense(@Param('id') expenseId: number) {
|
public publishExpense(@Param('id') expenseId: number) {
|
||||||
return this.expensesApplication.publishExpense(expenseId);
|
return this.expensesApplication.publishExpense(expenseId);
|
||||||
@@ -119,6 +132,7 @@ export class ExpensesController {
|
|||||||
* Get the expense transaction details.
|
* Get the expense transaction details.
|
||||||
*/
|
*/
|
||||||
@Get()
|
@Get()
|
||||||
|
@RequirePermission(ExpenseAction.View, AbilitySubject.Expense)
|
||||||
@ApiOperation({ summary: 'Get the expense transactions.' })
|
@ApiOperation({ summary: 'Get the expense transactions.' })
|
||||||
@ApiResponse({
|
@ApiResponse({
|
||||||
status: 200,
|
status: 200,
|
||||||
@@ -146,6 +160,7 @@ export class ExpensesController {
|
|||||||
* @param {number} expenseId
|
* @param {number} expenseId
|
||||||
*/
|
*/
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
|
@RequirePermission(ExpenseAction.View, AbilitySubject.Expense)
|
||||||
@ApiOperation({ summary: 'Get the expense transaction details.' })
|
@ApiOperation({ summary: 'Get the expense transaction details.' })
|
||||||
@ApiResponse({
|
@ApiResponse({
|
||||||
status: 200,
|
status: 200,
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export class FinancialSheet {
|
|||||||
negativeFormat: 'mines',
|
negativeFormat: 'mines',
|
||||||
};
|
};
|
||||||
public baseCurrency: string;
|
public baseCurrency: string;
|
||||||
|
public dateFormat: string = 'YYYY MMM DD';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transformes the number format query to settings
|
* Transformes the number format query to settings
|
||||||
@@ -140,9 +141,10 @@ export class FinancialSheet {
|
|||||||
* @param {string} format
|
* @param {string} format
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
protected getDateMeta(date: moment.MomentInput, format = 'YYYY-MM-DD') {
|
protected getDateMeta(date: moment.MomentInput, format?: string) {
|
||||||
|
const dateFormat = format || this.dateFormat || 'YYYY MMM DD';
|
||||||
return {
|
return {
|
||||||
formattedDate: moment(date).format(format),
|
formattedDate: moment(date).format(dateFormat),
|
||||||
date: moment(date).toDate(),
|
date: moment(date).toDate(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Response } from 'express';
|
import { Response } from 'express';
|
||||||
import { Controller, Get, Headers, Query, Res } from '@nestjs/common';
|
import { Controller, Get, Headers, Query, Res, UseGuards } from '@nestjs/common';
|
||||||
import { APAgingSummaryApplication } from './APAgingSummaryApplication';
|
import { APAgingSummaryApplication } from './APAgingSummaryApplication';
|
||||||
import { AcceptType } from '@/constants/accept-type';
|
import { AcceptType } from '@/constants/accept-type';
|
||||||
import {
|
import {
|
||||||
@@ -11,14 +11,21 @@ import {
|
|||||||
import { APAgingSummaryQueryDto } from './APAgingSummaryQuery.dto';
|
import { APAgingSummaryQueryDto } from './APAgingSummaryQuery.dto';
|
||||||
import { APAgingSummaryResponseExample } from './APAgingSummary.swagger';
|
import { APAgingSummaryResponseExample } from './APAgingSummary.swagger';
|
||||||
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
|
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
|
||||||
|
import { RequirePermission } from '@/modules/Roles/RequirePermission.decorator';
|
||||||
|
import { PermissionGuard } from '@/modules/Roles/Permission.guard';
|
||||||
|
import { AuthorizationGuard } from '@/modules/Roles/Authorization.guard';
|
||||||
|
import { AbilitySubject } from '@/modules/Roles/Roles.types';
|
||||||
|
import { ReportsAction } from '../../types/Report.types';
|
||||||
|
|
||||||
@Controller('reports/payable-aging-summary')
|
@Controller('reports/payable-aging-summary')
|
||||||
@ApiTags('Reports')
|
@ApiTags('Reports')
|
||||||
@ApiCommonHeaders()
|
@ApiCommonHeaders()
|
||||||
|
@UseGuards(AuthorizationGuard, PermissionGuard)
|
||||||
export class APAgingSummaryController {
|
export class APAgingSummaryController {
|
||||||
constructor(private readonly APAgingSummaryApp: APAgingSummaryApplication) { }
|
constructor(private readonly APAgingSummaryApp: APAgingSummaryApplication) { }
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
|
@RequirePermission(ReportsAction.READ_AP_AGING_SUMMARY, AbilitySubject.Report)
|
||||||
@ApiOperation({ summary: 'Get payable aging summary' })
|
@ApiOperation({ summary: 'Get payable aging summary' })
|
||||||
@ApiResponse({
|
@ApiResponse({
|
||||||
status: 200,
|
status: 200,
|
||||||
|
|||||||
@@ -32,18 +32,19 @@ export class APAgingSummaryService {
|
|||||||
this.APAgingSummaryRepository.setFilter(filter);
|
this.APAgingSummaryRepository.setFilter(filter);
|
||||||
await this.APAgingSummaryRepository.load();
|
await this.APAgingSummaryRepository.load();
|
||||||
|
|
||||||
|
// Retrieve the aging summary report meta first to get date format.
|
||||||
|
const meta = await this.APAgingSummaryMeta.meta(filter);
|
||||||
|
|
||||||
// A/P aging summary report instance.
|
// A/P aging summary report instance.
|
||||||
const APAgingSummaryReport = new APAgingSummarySheet(
|
const APAgingSummaryReport = new APAgingSummarySheet(
|
||||||
filter,
|
filter,
|
||||||
this.APAgingSummaryRepository,
|
this.APAgingSummaryRepository,
|
||||||
|
{ baseCurrency: meta.baseCurrency, dateFormat: meta.dateFormat },
|
||||||
);
|
);
|
||||||
// A/P aging summary report data and columns.
|
// A/P aging summary report data and columns.
|
||||||
const data = APAgingSummaryReport.reportData();
|
const data = APAgingSummaryReport.reportData();
|
||||||
const columns = APAgingSummaryReport.reportColumns();
|
const columns = APAgingSummaryReport.reportColumns();
|
||||||
|
|
||||||
// Retrieve the aging summary report meta.
|
|
||||||
const meta = await this.APAgingSummaryMeta.meta(filter);
|
|
||||||
|
|
||||||
// Triggers `onPayableAgingViewed` event.
|
// Triggers `onPayableAgingViewed` event.
|
||||||
await this.eventPublisher.emitAsync(events.reports.onPayableAgingViewed, {
|
await this.eventPublisher.emitAsync(events.reports.onPayableAgingViewed, {
|
||||||
query,
|
query,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { allPassedConditionsPass } from '@/utils/all-conditions-passed';
|
|||||||
import { APAgingSummaryRepository } from './APAgingSummaryRepository';
|
import { APAgingSummaryRepository } from './APAgingSummaryRepository';
|
||||||
import { Bill } from '@/modules/Bills/models/Bill';
|
import { Bill } from '@/modules/Bills/models/Bill';
|
||||||
import { APAgingSummaryQueryDto } from './APAgingSummaryQuery.dto';
|
import { APAgingSummaryQueryDto } from './APAgingSummaryQuery.dto';
|
||||||
|
import { IFinancialReportMeta, DEFAULT_REPORT_META } from '../../types/Report.types';
|
||||||
|
|
||||||
export class APAgingSummarySheet extends AgingSummaryReport {
|
export class APAgingSummarySheet extends AgingSummaryReport {
|
||||||
readonly repository: APAgingSummaryRepository;
|
readonly repository: APAgingSummaryRepository;
|
||||||
@@ -31,12 +32,14 @@ export class APAgingSummarySheet extends AgingSummaryReport {
|
|||||||
constructor(
|
constructor(
|
||||||
query: APAgingSummaryQueryDto,
|
query: APAgingSummaryQueryDto,
|
||||||
repository: APAgingSummaryRepository,
|
repository: APAgingSummaryRepository,
|
||||||
|
meta: IFinancialReportMeta,
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.query = query;
|
this.query = query;
|
||||||
this.repository = repository;
|
this.repository = repository;
|
||||||
this.numberFormat = this.query.numberFormat;
|
this.numberFormat = this.query.numberFormat;
|
||||||
|
this.dateFormat = meta.dateFormat || DEFAULT_REPORT_META.dateFormat;
|
||||||
|
|
||||||
this.overdueInvoicesByContactId = this.repository.overdueBillsByVendorId;
|
this.overdueInvoicesByContactId = this.repository.overdueBillsByVendorId;
|
||||||
this.currentInvoicesByContactId = this.repository.dueBillsByVendorId;
|
this.currentInvoicesByContactId = this.repository.dueBillsByVendorId;
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Controller, Get, Headers } from '@nestjs/common';
|
import { Controller, Get, Headers, Query, Res, UseGuards } from '@nestjs/common';
|
||||||
import { Query, Res } from '@nestjs/common';
|
|
||||||
import { ARAgingSummaryApplication } from './ARAgingSummaryApplication';
|
import { ARAgingSummaryApplication } from './ARAgingSummaryApplication';
|
||||||
import { AcceptType } from '@/constants/accept-type';
|
import { AcceptType } from '@/constants/accept-type';
|
||||||
import { Response } from 'express';
|
import { Response } from 'express';
|
||||||
@@ -12,14 +11,21 @@ import {
|
|||||||
import { ARAgingSummaryQueryDto } from './ARAgingSummaryQuery.dto';
|
import { ARAgingSummaryQueryDto } from './ARAgingSummaryQuery.dto';
|
||||||
import { ARAgingSummaryResponseExample } from './ARAgingSummary.swagger';
|
import { ARAgingSummaryResponseExample } from './ARAgingSummary.swagger';
|
||||||
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
|
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
|
||||||
|
import { RequirePermission } from '@/modules/Roles/RequirePermission.decorator';
|
||||||
|
import { PermissionGuard } from '@/modules/Roles/Permission.guard';
|
||||||
|
import { AuthorizationGuard } from '@/modules/Roles/Authorization.guard';
|
||||||
|
import { AbilitySubject } from '@/modules/Roles/Roles.types';
|
||||||
|
import { ReportsAction } from '../../types/Report.types';
|
||||||
|
|
||||||
@Controller('reports/receivable-aging-summary')
|
@Controller('reports/receivable-aging-summary')
|
||||||
@ApiTags('Reports')
|
@ApiTags('Reports')
|
||||||
@ApiCommonHeaders()
|
@ApiCommonHeaders()
|
||||||
|
@UseGuards(AuthorizationGuard, PermissionGuard)
|
||||||
export class ARAgingSummaryController {
|
export class ARAgingSummaryController {
|
||||||
constructor(private readonly ARAgingSummaryApp: ARAgingSummaryApplication) {}
|
constructor(private readonly ARAgingSummaryApp: ARAgingSummaryApplication) {}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
|
@RequirePermission(ReportsAction.READ_AR_AGING_SUMMARY, AbilitySubject.Report)
|
||||||
@ApiOperation({ summary: 'Get receivable aging summary' })
|
@ApiOperation({ summary: 'Get receivable aging summary' })
|
||||||
@ApiResponse({
|
@ApiResponse({
|
||||||
status: 200,
|
status: 200,
|
||||||
|
|||||||
@@ -28,18 +28,19 @@ export class ARAgingSummaryService {
|
|||||||
this.ARAgingSummaryRepository.setFilter(filter);
|
this.ARAgingSummaryRepository.setFilter(filter);
|
||||||
await this.ARAgingSummaryRepository.load();
|
await this.ARAgingSummaryRepository.load();
|
||||||
|
|
||||||
|
// Retrieve the aging summary report meta first to get date format.
|
||||||
|
const meta = await this.ARAgingSummaryMeta.meta(filter);
|
||||||
|
|
||||||
// A/R aging summary report instance.
|
// A/R aging summary report instance.
|
||||||
const ARAgingSummaryReport = new ARAgingSummarySheet(
|
const ARAgingSummaryReport = new ARAgingSummarySheet(
|
||||||
filter,
|
filter,
|
||||||
this.ARAgingSummaryRepository,
|
this.ARAgingSummaryRepository,
|
||||||
|
{ baseCurrency: meta.baseCurrency, dateFormat: meta.dateFormat },
|
||||||
);
|
);
|
||||||
// A/R aging summary report data and columns.
|
// A/R aging summary report data and columns.
|
||||||
const data = ARAgingSummaryReport.reportData();
|
const data = ARAgingSummaryReport.reportData();
|
||||||
const columns = ARAgingSummaryReport.reportColumns();
|
const columns = ARAgingSummaryReport.reportColumns();
|
||||||
|
|
||||||
// Retrieve the aging summary report meta.
|
|
||||||
const meta = await this.ARAgingSummaryMeta.meta(filter);
|
|
||||||
|
|
||||||
// Triggers `onReceivableAgingViewed` event.
|
// Triggers `onReceivableAgingViewed` event.
|
||||||
await this.eventPublisher.emitAsync(
|
await this.eventPublisher.emitAsync(
|
||||||
events.reports.onReceivableAgingViewed,
|
events.reports.onReceivableAgingViewed,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { ARAgingSummaryRepository } from './ARAgingSummaryRepository';
|
|||||||
import { Customer } from '@/modules/Customers/models/Customer';
|
import { Customer } from '@/modules/Customers/models/Customer';
|
||||||
import { SaleInvoice } from '@/modules/SaleInvoices/models/SaleInvoice';
|
import { SaleInvoice } from '@/modules/SaleInvoices/models/SaleInvoice';
|
||||||
import { ARAgingSummaryQueryDto } from './ARAgingSummaryQuery.dto';
|
import { ARAgingSummaryQueryDto } from './ARAgingSummaryQuery.dto';
|
||||||
|
import { IFinancialReportMeta, DEFAULT_REPORT_META } from '../../types/Report.types';
|
||||||
|
|
||||||
export class ARAgingSummarySheet extends AgingSummaryReport {
|
export class ARAgingSummarySheet extends AgingSummaryReport {
|
||||||
readonly query: ARAgingSummaryQueryDto;
|
readonly query: ARAgingSummaryQueryDto;
|
||||||
@@ -32,16 +33,19 @@ export class ARAgingSummarySheet extends AgingSummaryReport {
|
|||||||
* Constructor method.
|
* Constructor method.
|
||||||
* @param {ARAgingSummaryQueryDto} query - Query
|
* @param {ARAgingSummaryQueryDto} query - Query
|
||||||
* @param {ARAgingSummaryRepository} repository - Repository.
|
* @param {ARAgingSummaryRepository} repository - Repository.
|
||||||
|
* @param {IFinancialReportMeta} meta - Report meta.
|
||||||
*/
|
*/
|
||||||
constructor(
|
constructor(
|
||||||
query: ARAgingSummaryQueryDto,
|
query: ARAgingSummaryQueryDto,
|
||||||
repository: ARAgingSummaryRepository,
|
repository: ARAgingSummaryRepository,
|
||||||
|
meta: IFinancialReportMeta,
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.query = query;
|
this.query = query;
|
||||||
this.repository = repository;
|
this.repository = repository;
|
||||||
this.numberFormat = this.query.numberFormat;
|
this.numberFormat = this.query.numberFormat;
|
||||||
|
this.dateFormat = meta.dateFormat || DEFAULT_REPORT_META.dateFormat;
|
||||||
|
|
||||||
this.overdueInvoicesByContactId =
|
this.overdueInvoicesByContactId =
|
||||||
this.repository.overdueInvoicesByContactId;
|
this.repository.overdueInvoicesByContactId;
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export class AgingSummaryMeta {
|
|||||||
*/
|
*/
|
||||||
public async meta(query: IAgingSummaryQuery): Promise<IAgingSummaryMeta> {
|
public async meta(query: IAgingSummaryQuery): Promise<IAgingSummaryMeta> {
|
||||||
const commonMeta = await this.financialSheetMeta.meta();
|
const commonMeta = await this.financialSheetMeta.meta();
|
||||||
const formattedAsDate = moment(query.asDate).format('YYYY/MM/DD');
|
const formattedAsDate = moment(query.asDate).format(commonMeta.dateFormat);
|
||||||
const formattedDateRange = `As ${formattedAsDate}`;
|
const formattedDateRange = `As ${formattedAsDate}`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Response } from 'express';
|
import { Response } from 'express';
|
||||||
import { Controller, Get, Headers, Query, Res } from '@nestjs/common';
|
import { Controller, Get, Headers, Query, Res, UseGuards } from '@nestjs/common';
|
||||||
import { AcceptType } from '@/constants/accept-type';
|
import { AcceptType } from '@/constants/accept-type';
|
||||||
import { BalanceSheetApplication } from './BalanceSheetApplication';
|
import { BalanceSheetApplication } from './BalanceSheetApplication';
|
||||||
import {
|
import {
|
||||||
@@ -11,10 +11,16 @@ import {
|
|||||||
import { BalanceSheetQueryDto } from './BalanceSheet.dto';
|
import { BalanceSheetQueryDto } from './BalanceSheet.dto';
|
||||||
import { BalanceSheetResponseExample } from './BalanceSheet.swagger';
|
import { BalanceSheetResponseExample } from './BalanceSheet.swagger';
|
||||||
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
|
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
|
||||||
|
import { RequirePermission } from '@/modules/Roles/RequirePermission.decorator';
|
||||||
|
import { PermissionGuard } from '@/modules/Roles/Permission.guard';
|
||||||
|
import { AuthorizationGuard } from '@/modules/Roles/Authorization.guard';
|
||||||
|
import { AbilitySubject } from '@/modules/Roles/Roles.types';
|
||||||
|
import { ReportsAction } from '../../types/Report.types';
|
||||||
|
|
||||||
@Controller('/reports/balance-sheet')
|
@Controller('/reports/balance-sheet')
|
||||||
@ApiTags('Reports')
|
@ApiTags('Reports')
|
||||||
@ApiCommonHeaders()
|
@ApiCommonHeaders()
|
||||||
|
@UseGuards(AuthorizationGuard, PermissionGuard)
|
||||||
export class BalanceSheetStatementController {
|
export class BalanceSheetStatementController {
|
||||||
constructor(private readonly balanceSheetApp: BalanceSheetApplication) {}
|
constructor(private readonly balanceSheetApp: BalanceSheetApplication) {}
|
||||||
|
|
||||||
@@ -25,6 +31,7 @@ export class BalanceSheetStatementController {
|
|||||||
* @param {string} acceptHeader - Accept header.
|
* @param {string} acceptHeader - Accept header.
|
||||||
*/
|
*/
|
||||||
@Get('')
|
@Get('')
|
||||||
|
@RequirePermission(ReportsAction.READ_BALANCE_SHEET, AbilitySubject.Report)
|
||||||
@ApiOperation({ summary: 'Get balance sheet statement' })
|
@ApiOperation({ summary: 'Get balance sheet statement' })
|
||||||
@ApiResponse({
|
@ApiResponse({
|
||||||
status: 200,
|
status: 200,
|
||||||
|
|||||||
@@ -24,14 +24,14 @@ export class BalanceSheetQueryDto extends FinancialSheetBranchesQueryDto {
|
|||||||
displayColumnsType: 'total' | 'date_periods' = 'total';
|
displayColumnsType: 'total' | 'date_periods' = 'total';
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
enum: ['day', 'month', 'year'],
|
enum: ['day', 'month', 'year', 'quarter'],
|
||||||
default: 'year',
|
default: 'year',
|
||||||
description: 'Time period for column display',
|
description: 'Time period for column display',
|
||||||
})
|
})
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsEnum(['day', 'month', 'year'])
|
@IsEnum(['day', 'month', 'year', 'quarter'])
|
||||||
displayColumnsBy: 'day' | 'month' | 'year' = 'year';
|
displayColumnsBy: 'day' | 'month' | 'year' | 'quarter' = 'year';
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
description: 'Start date for the balance sheet period',
|
description: 'Start date for the balance sheet period',
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { BalanceSheetFiltering } from './BalanceSheetFiltering';
|
|||||||
import { BalanceSheetNetIncome } from './BalanceSheetNetIncome';
|
import { BalanceSheetNetIncome } from './BalanceSheetNetIncome';
|
||||||
import { BalanceSheetAggregators } from './BalanceSheetAggregators';
|
import { BalanceSheetAggregators } from './BalanceSheetAggregators';
|
||||||
import { BalanceSheetAccounts } from './BalanceSheetAccounts';
|
import { BalanceSheetAccounts } from './BalanceSheetAccounts';
|
||||||
import { INumberFormatQuery } from '../../types/Report.types';
|
import { INumberFormatQuery, IFinancialReportMeta, DEFAULT_REPORT_META } from '../../types/Report.types';
|
||||||
import { FinancialSheet } from '../../common/FinancialSheet';
|
import { FinancialSheet } from '../../common/FinancialSheet';
|
||||||
|
|
||||||
export class BalanceSheet extends R.pipe(
|
export class BalanceSheet extends R.pipe(
|
||||||
@@ -66,21 +66,23 @@ export class BalanceSheet extends R.pipe(
|
|||||||
/**
|
/**
|
||||||
* Constructor method.
|
* Constructor method.
|
||||||
* @param {IBalanceSheetQuery} query -
|
* @param {IBalanceSheetQuery} query -
|
||||||
* @param {IAccount[]} accounts -
|
* @param {BalanceSheetRepository} repository -
|
||||||
* @param {string} baseCurrency -
|
* @param {I18nService} i18n -
|
||||||
|
* @param {IFinancialReportMeta} meta -
|
||||||
*/
|
*/
|
||||||
constructor(
|
constructor(
|
||||||
query: IBalanceSheetQuery,
|
query: IBalanceSheetQuery,
|
||||||
repository: BalanceSheetRepository,
|
repository: BalanceSheetRepository,
|
||||||
baseCurrency: string,
|
|
||||||
i18n: I18nService,
|
i18n: I18nService,
|
||||||
|
meta: IFinancialReportMeta,
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.query = new BalanceSheetQuery(query);
|
this.query = new BalanceSheetQuery(query);
|
||||||
this.repository = repository;
|
this.repository = repository;
|
||||||
this.baseCurrency = baseCurrency;
|
this.baseCurrency = meta.baseCurrency;
|
||||||
this.numberFormat = this.query.query.numberFormat;
|
this.numberFormat = this.query.query.numberFormat;
|
||||||
|
this.dateFormat = meta.dateFormat || DEFAULT_REPORT_META.dateFormat;
|
||||||
this.i18n = i18n;
|
this.i18n = i18n;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,19 +40,19 @@ export class BalanceSheetInjectable {
|
|||||||
// Loads all resources.
|
// Loads all resources.
|
||||||
await this.balanceSheetRepository.asyncInitialize(filter);
|
await this.balanceSheetRepository.asyncInitialize(filter);
|
||||||
|
|
||||||
|
// Balance sheet meta first to get date format.
|
||||||
|
const meta = await this.balanceSheetMeta.meta(filter);
|
||||||
|
|
||||||
// Balance sheet report instance.
|
// Balance sheet report instance.
|
||||||
const balanceSheetInstanace = new BalanceSheet(
|
const balanceSheetInstanace = new BalanceSheet(
|
||||||
filter,
|
filter,
|
||||||
this.balanceSheetRepository,
|
this.balanceSheetRepository,
|
||||||
tenantMetadata.baseCurrency,
|
|
||||||
this.i18n,
|
this.i18n,
|
||||||
|
{ baseCurrency: tenantMetadata.baseCurrency, dateFormat: meta.dateFormat },
|
||||||
);
|
);
|
||||||
// Balance sheet data.
|
// Balance sheet data.
|
||||||
const data = balanceSheetInstanace.reportData();
|
const data = balanceSheetInstanace.reportData();
|
||||||
|
|
||||||
// Balance sheet meta.
|
|
||||||
const meta = await this.balanceSheetMeta.meta(filter);
|
|
||||||
|
|
||||||
// Triggers `onBalanceSheetViewed` event.
|
// Triggers `onBalanceSheetViewed` event.
|
||||||
await this.eventPublisher.emitAsync(events.reports.onBalanceSheetViewed, {
|
await this.eventPublisher.emitAsync(events.reports.onBalanceSheetViewed, {
|
||||||
query,
|
query,
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export class BalanceSheetMetaInjectable {
|
|||||||
*/
|
*/
|
||||||
public async meta(query: IBalanceSheetQuery): Promise<IBalanceSheetMeta> {
|
public async meta(query: IBalanceSheetQuery): Promise<IBalanceSheetMeta> {
|
||||||
const commonMeta = await this.financialSheetMeta.meta();
|
const commonMeta = await this.financialSheetMeta.meta();
|
||||||
const formattedAsDate = moment(query.toDate).format('YYYY/MM/DD');
|
const formattedAsDate = moment(query.toDate).format(commonMeta.dateFormat);
|
||||||
const formattedDateRange = `As ${formattedAsDate}`;
|
const formattedDateRange = `As ${formattedAsDate}`;
|
||||||
const sheetName = 'Balance Sheet Statement';
|
const sheetName = 'Balance Sheet Statement';
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import { DISPLAY_COLUMNS_BY } from './constants';
|
|||||||
import { FinancialSheetStructure } from '../../common/FinancialSheetStructure';
|
import { FinancialSheetStructure } from '../../common/FinancialSheetStructure';
|
||||||
import { Account } from '@/modules/Accounts/models/Account.model';
|
import { Account } from '@/modules/Accounts/models/Account.model';
|
||||||
import { ILedger } from '@/modules/Ledger/types/Ledger.types';
|
import { ILedger } from '@/modules/Ledger/types/Ledger.types';
|
||||||
import { INumberFormatQuery } from '../../types/Report.types';
|
import { INumberFormatQuery, IFinancialReportMeta, DEFAULT_REPORT_META } from '../../types/Report.types';
|
||||||
import { transformToMapBy } from '@/utils/transform-to-map-by';
|
import { transformToMapBy } from '@/utils/transform-to-map-by';
|
||||||
import { accumSum } from '@/utils/accum-sum';
|
import { accumSum } from '@/utils/accum-sum';
|
||||||
import { ModelObject } from 'objection';
|
import { ModelObject } from 'objection';
|
||||||
@@ -62,12 +62,12 @@ export class CashFlowStatement extends R.pipe(
|
|||||||
cashLedger: ILedger,
|
cashLedger: ILedger,
|
||||||
netIncomeLedger: ILedger,
|
netIncomeLedger: ILedger,
|
||||||
query: ICashFlowStatementQuery,
|
query: ICashFlowStatementQuery,
|
||||||
baseCurrency: string,
|
|
||||||
i18n: I18nService,
|
i18n: I18nService,
|
||||||
|
meta: IFinancialReportMeta,
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.baseCurrency = baseCurrency;
|
this.baseCurrency = meta.baseCurrency;
|
||||||
this.i18n = i18n;
|
this.i18n = i18n;
|
||||||
this.ledger = ledger;
|
this.ledger = ledger;
|
||||||
this.cashLedger = cashLedger;
|
this.cashLedger = cashLedger;
|
||||||
@@ -76,6 +76,7 @@ export class CashFlowStatement extends R.pipe(
|
|||||||
this.accountsByRootType = transformToMapBy(accounts, 'accountRootType');
|
this.accountsByRootType = transformToMapBy(accounts, 'accountRootType');
|
||||||
this.query = query;
|
this.query = query;
|
||||||
this.numberFormat = this.query.numberFormat;
|
this.numberFormat = this.query.numberFormat;
|
||||||
|
this.dateFormat = meta.dateFormat || DEFAULT_REPORT_META.dateFormat;
|
||||||
this.dateRangeSet = [];
|
this.dateRangeSet = [];
|
||||||
this.comparatorDateType =
|
this.comparatorDateType =
|
||||||
query.displayColumnsType === 'total' ? 'day' : query.displayColumnsBy;
|
query.displayColumnsType === 'total' ? 'day' : query.displayColumnsBy;
|
||||||
|
|||||||
@@ -85,6 +85,9 @@ export class CashFlowStatementService {
|
|||||||
const cashLedger = Ledger.fromTransactions(cashAtBeginningTransactions);
|
const cashLedger = Ledger.fromTransactions(cashAtBeginningTransactions);
|
||||||
const netIncomeLedger = Ledger.fromTransactions(netIncome);
|
const netIncomeLedger = Ledger.fromTransactions(netIncome);
|
||||||
|
|
||||||
|
// Retrieve the cashflow sheet meta first to get date format.
|
||||||
|
const meta = await this.cashflowSheetMeta.meta(filter);
|
||||||
|
|
||||||
// Cash flow statement.
|
// Cash flow statement.
|
||||||
const cashFlowInstance = new CashFlowStatement(
|
const cashFlowInstance = new CashFlowStatement(
|
||||||
accounts,
|
accounts,
|
||||||
@@ -92,11 +95,9 @@ export class CashFlowStatementService {
|
|||||||
cashLedger,
|
cashLedger,
|
||||||
netIncomeLedger,
|
netIncomeLedger,
|
||||||
filter,
|
filter,
|
||||||
tenant.metadata.baseCurrency,
|
|
||||||
this.i18n,
|
this.i18n,
|
||||||
|
{ baseCurrency: tenant.metadata.baseCurrency, dateFormat: meta.dateFormat },
|
||||||
);
|
);
|
||||||
// Retrieve the cashflow sheet meta.
|
|
||||||
const meta = await this.cashflowSheetMeta.meta(filter);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: cashFlowInstance.reportData(),
|
data: cashFlowInstance.reportData(),
|
||||||
|
|||||||
@@ -34,13 +34,13 @@ export class CashFlowStatementQueryDto extends FinancialSheetBranchesQueryDto {
|
|||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
description: 'Display columns by time period',
|
description: 'Display columns by time period',
|
||||||
required: false,
|
required: false,
|
||||||
enum: ['day', 'month', 'year'],
|
enum: ['day', 'month', 'year', 'quarter'],
|
||||||
default: 'year',
|
default: 'year',
|
||||||
})
|
})
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsEnum(['day', 'month', 'year'])
|
@IsEnum(['day', 'month', 'year', 'quarter'])
|
||||||
displayColumnsBy: 'day' | 'month' | 'year' = 'year';
|
displayColumnsBy: 'day' | 'month' | 'year' | 'quarter' = 'year';
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
description: 'Type of column display',
|
description: 'Type of column display',
|
||||||
|
|||||||
@@ -239,7 +239,7 @@ export class CashFlowTable {
|
|||||||
section: ICashFlowStatementSection,
|
section: ICashFlowStatementSection,
|
||||||
): ICashFlowStatementSection => {
|
): ICashFlowStatementSection => {
|
||||||
const label = section.footerLabel
|
const label = section.footerLabel
|
||||||
? section.footerLabel
|
? this.i18n.t(section.footerLabel)
|
||||||
: this.i18n.t('financial_sheet.total_row', {
|
: this.i18n.t('financial_sheet.total_row', {
|
||||||
args: { value: section.label },
|
args: { value: section.label },
|
||||||
});
|
});
|
||||||
@@ -302,7 +302,7 @@ export class CashFlowTable {
|
|||||||
* @returns {ITableColumn}
|
* @returns {ITableColumn}
|
||||||
*/
|
*/
|
||||||
private totalColumns = (): ITableColumn[] => {
|
private totalColumns = (): ITableColumn[] => {
|
||||||
return [{ key: 'total', label: this.i18n.t('Total') }];
|
return [{ key: 'total', label: this.i18n.t('cash_flow_statement.total') }];
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -366,7 +366,7 @@ export class CashFlowTable {
|
|||||||
*/
|
*/
|
||||||
public tableColumns = (): ITableColumn[] => {
|
public tableColumns = (): ITableColumn[] => {
|
||||||
return R.compose(
|
return R.compose(
|
||||||
R.concat([{ key: 'name', label: this.i18n.t('Account name') }]),
|
R.concat([{ key: 'name', label: this.i18n.t('cash_flow_statement.account_name') }]),
|
||||||
R.when(
|
R.when(
|
||||||
R.always(this.isDisplayColumnsBy(DISPLAY_COLUMNS_BY.DATE_PERIODS)),
|
R.always(this.isDisplayColumnsBy(DISPLAY_COLUMNS_BY.DATE_PERIODS)),
|
||||||
R.concat(this.datePeriodsColumns()),
|
R.concat(this.datePeriodsColumns()),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Response } from 'express';
|
import { Response } from 'express';
|
||||||
import { Controller, Get, Headers, Query, Res } from '@nestjs/common';
|
import { Controller, Get, Headers, Query, Res, UseGuards } from '@nestjs/common';
|
||||||
import { AcceptType } from '@/constants/accept-type';
|
import { AcceptType } from '@/constants/accept-type';
|
||||||
import { CashflowSheetApplication } from './CashflowSheetApplication';
|
import { CashflowSheetApplication } from './CashflowSheetApplication';
|
||||||
import {
|
import {
|
||||||
@@ -11,14 +11,21 @@ import {
|
|||||||
import { CashFlowStatementQueryDto } from './CashFlowStatementQuery.dto';
|
import { CashFlowStatementQueryDto } from './CashFlowStatementQuery.dto';
|
||||||
import { CashflowStatementResponseExample } from './CashflowStatement.swagger';
|
import { CashflowStatementResponseExample } from './CashflowStatement.swagger';
|
||||||
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
|
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
|
||||||
|
import { RequirePermission } from '@/modules/Roles/RequirePermission.decorator';
|
||||||
|
import { PermissionGuard } from '@/modules/Roles/Permission.guard';
|
||||||
|
import { AuthorizationGuard } from '@/modules/Roles/Authorization.guard';
|
||||||
|
import { AbilitySubject } from '@/modules/Roles/Roles.types';
|
||||||
|
import { ReportsAction } from '../../types/Report.types';
|
||||||
|
|
||||||
@Controller('reports/cashflow-statement')
|
@Controller('reports/cashflow-statement')
|
||||||
@ApiTags('Reports')
|
@ApiTags('Reports')
|
||||||
@ApiCommonHeaders()
|
@ApiCommonHeaders()
|
||||||
|
@UseGuards(AuthorizationGuard, PermissionGuard)
|
||||||
export class CashflowController {
|
export class CashflowController {
|
||||||
constructor(private readonly cashflowSheetApp: CashflowSheetApplication) { }
|
constructor(private readonly cashflowSheetApp: CashflowSheetApplication) { }
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
|
@RequirePermission(ReportsAction.READ_CASHFLOW, AbilitySubject.Report)
|
||||||
@ApiResponse({
|
@ApiResponse({
|
||||||
status: 200,
|
status: 200,
|
||||||
description: 'Cashflow statement report',
|
description: 'Cashflow statement report',
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import * as moment from 'moment';
|
import * as moment from 'moment';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { I18nService } from 'nestjs-i18n';
|
||||||
import { FinancialSheetMeta } from '../../common/FinancialSheetMeta';
|
import { FinancialSheetMeta } from '../../common/FinancialSheetMeta';
|
||||||
import {
|
import {
|
||||||
ICashFlowStatementMeta,
|
ICashFlowStatementMeta,
|
||||||
@@ -8,7 +9,10 @@ import {
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CashflowSheetMeta {
|
export class CashflowSheetMeta {
|
||||||
constructor(private readonly financialSheetMeta: FinancialSheetMeta) {}
|
constructor(
|
||||||
|
private readonly financialSheetMeta: FinancialSheetMeta,
|
||||||
|
private readonly i18n: I18nService,
|
||||||
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cashflow sheet meta.
|
* Cashflow sheet meta.
|
||||||
@@ -19,11 +23,13 @@ export class CashflowSheetMeta {
|
|||||||
query: ICashFlowStatementQuery,
|
query: ICashFlowStatementQuery,
|
||||||
): Promise<ICashFlowStatementMeta> {
|
): Promise<ICashFlowStatementMeta> {
|
||||||
const meta = await this.financialSheetMeta.meta();
|
const meta = await this.financialSheetMeta.meta();
|
||||||
const formattedToDate = moment(query.toDate).format('YYYY/MM/DD');
|
const formattedToDate = moment(query.toDate).format(meta.dateFormat);
|
||||||
const formattedFromDate = moment(query.fromDate).format('YYYY/MM/DD');
|
const formattedFromDate = moment(query.fromDate).format(meta.dateFormat);
|
||||||
const formattedDateRange = `From ${formattedFromDate} | To ${formattedToDate}`;
|
const fromLabel = this.i18n.t('cash_flow_statement.from_date');
|
||||||
|
const toLabel = this.i18n.t('cash_flow_statement.to_date');
|
||||||
|
const formattedDateRange = `${fromLabel} ${formattedFromDate} | ${toLabel} ${formattedToDate}`;
|
||||||
|
|
||||||
const sheetName = 'Statement of Cash Flow';
|
const sheetName = this.i18n.t('cash_flow_statement.sheet_name');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...meta,
|
...meta,
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
import { ContactBalanceSummaryReport } from '../ContactBalanceSummary/ContactBalanceSummary';
|
import { ContactBalanceSummaryReport } from '../ContactBalanceSummary/ContactBalanceSummary';
|
||||||
import { ILedger } from '@/modules/Ledger/types/Ledger.types';
|
import { ILedger } from '@/modules/Ledger/types/Ledger.types';
|
||||||
import { ModelObject } from 'objection';
|
import { ModelObject } from 'objection';
|
||||||
import { INumberFormatQuery } from '../../types/Report.types';
|
import { INumberFormatQuery, IFinancialReportMeta, DEFAULT_REPORT_META } from '../../types/Report.types';
|
||||||
import { Customer } from '@/modules/Customers/models/Customer';
|
import { Customer } from '@/modules/Customers/models/Customer';
|
||||||
|
|
||||||
export class CustomerBalanceSummaryReport extends ContactBalanceSummaryReport {
|
export class CustomerBalanceSummaryReport extends ContactBalanceSummaryReport {
|
||||||
@@ -23,21 +23,22 @@ export class CustomerBalanceSummaryReport extends ContactBalanceSummaryReport {
|
|||||||
* @param {IJournalPoster} receivableLedger
|
* @param {IJournalPoster} receivableLedger
|
||||||
* @param {ICustomer[]} customers
|
* @param {ICustomer[]} customers
|
||||||
* @param {ICustomerBalanceSummaryQuery} filter
|
* @param {ICustomerBalanceSummaryQuery} filter
|
||||||
* @param {string} baseCurrency
|
* @param {IFinancialReportMeta} meta
|
||||||
*/
|
*/
|
||||||
constructor(
|
constructor(
|
||||||
ledger: ILedger,
|
ledger: ILedger,
|
||||||
customers: ModelObject<Customer>[],
|
customers: ModelObject<Customer>[],
|
||||||
filter: ICustomerBalanceSummaryQuery,
|
filter: ICustomerBalanceSummaryQuery,
|
||||||
baseCurrency: string
|
meta: IFinancialReportMeta,
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.ledger = ledger;
|
this.ledger = ledger;
|
||||||
this.baseCurrency = baseCurrency;
|
this.baseCurrency = meta.baseCurrency;
|
||||||
this.customers = customers;
|
this.customers = customers;
|
||||||
this.filter = filter;
|
this.filter = filter;
|
||||||
this.numberFormat = this.filter.numberFormat;
|
this.numberFormat = this.filter.numberFormat;
|
||||||
|
this.dateFormat = meta.dateFormat || DEFAULT_REPORT_META.dateFormat;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export class CustomerBalanceSummaryMeta {
|
|||||||
query: ICustomerBalanceSummaryQuery,
|
query: ICustomerBalanceSummaryQuery,
|
||||||
): Promise<ICustomerBalanceSummaryMeta> {
|
): Promise<ICustomerBalanceSummaryMeta> {
|
||||||
const commonMeta = await this.financialSheetMeta.meta();
|
const commonMeta = await this.financialSheetMeta.meta();
|
||||||
const formattedAsDate = moment(query.asDate).format('YYYY/MM/DD');
|
const formattedAsDate = moment(query.asDate).format(commonMeta.dateFormat);
|
||||||
const formattedDateRange = `As ${formattedAsDate}`;
|
const formattedDateRange = `As ${formattedAsDate}`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -63,15 +63,16 @@ export class CustomerBalanceSummaryService {
|
|||||||
// Ledger query.
|
// Ledger query.
|
||||||
const ledger = new Ledger(customersEntries);
|
const ledger = new Ledger(customersEntries);
|
||||||
|
|
||||||
|
// Retrieve the customer balance summary meta first to get date format.
|
||||||
|
const meta = await this.customerBalanceSummaryMeta.meta(filter);
|
||||||
|
|
||||||
// Report instance.
|
// Report instance.
|
||||||
const report = new CustomerBalanceSummaryReport(
|
const report = new CustomerBalanceSummaryReport(
|
||||||
ledger,
|
ledger,
|
||||||
customers,
|
customers,
|
||||||
filter,
|
filter,
|
||||||
tenantMetadata.baseCurrency,
|
{ baseCurrency: tenantMetadata.baseCurrency, dateFormat: meta.dateFormat },
|
||||||
);
|
);
|
||||||
// Retrieve the customer balance summary meta.
|
|
||||||
const meta = await this.customerBalanceSummaryMeta.meta(filter);
|
|
||||||
|
|
||||||
// Triggers `onCustomerBalanceSummaryViewed` event.
|
// Triggers `onCustomerBalanceSummaryViewed` event.
|
||||||
await this.eventPublisher.emitAsync(
|
await this.eventPublisher.emitAsync(
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ export class CustomerBalanceSummaryTable {
|
|||||||
*/
|
*/
|
||||||
private getTotalColumnsAccessor = (): IColumnMapperMeta[] => {
|
private getTotalColumnsAccessor = (): IColumnMapperMeta[] => {
|
||||||
const columns = [
|
const columns = [
|
||||||
{ key: 'name', value: this.i18n.t('Total') },
|
{ key: 'name', value: this.i18n.t('contact_summary_balance.total') },
|
||||||
{ key: 'total', accessor: 'total.formattedAmount' },
|
{ key: 'total', accessor: 'total.formattedAmount' },
|
||||||
];
|
];
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
|||||||
@@ -5,22 +5,29 @@ import {
|
|||||||
ApiTags,
|
ApiTags,
|
||||||
} from '@nestjs/swagger';
|
} from '@nestjs/swagger';
|
||||||
import { Response } from 'express';
|
import { Response } from 'express';
|
||||||
import { Controller, Get, Headers, Query, Res } from '@nestjs/common';
|
import { Controller, Get, Headers, Query, Res, UseGuards } from '@nestjs/common';
|
||||||
import { GeneralLedgerApplication } from './GeneralLedgerApplication';
|
import { GeneralLedgerApplication } from './GeneralLedgerApplication';
|
||||||
import { AcceptType } from '@/constants/accept-type';
|
import { AcceptType } from '@/constants/accept-type';
|
||||||
import { GeneralLedgerQueryDto } from './GeneralLedgerQuery.dto';
|
import { GeneralLedgerQueryDto } from './GeneralLedgerQuery.dto';
|
||||||
import { GeneralLedgerResponseExample } from './GeneralLedger.swagger';
|
import { GeneralLedgerResponseExample } from './GeneralLedger.swagger';
|
||||||
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
|
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
|
||||||
|
import { RequirePermission } from '@/modules/Roles/RequirePermission.decorator';
|
||||||
|
import { PermissionGuard } from '@/modules/Roles/Permission.guard';
|
||||||
|
import { AuthorizationGuard } from '@/modules/Roles/Authorization.guard';
|
||||||
|
import { AbilitySubject } from '@/modules/Roles/Roles.types';
|
||||||
|
import { ReportsAction } from '../../types/Report.types';
|
||||||
|
|
||||||
@Controller('/reports/general-ledger')
|
@Controller('/reports/general-ledger')
|
||||||
@ApiTags('Reports')
|
@ApiTags('Reports')
|
||||||
@ApiCommonHeaders()
|
@ApiCommonHeaders()
|
||||||
|
@UseGuards(AuthorizationGuard, PermissionGuard)
|
||||||
export class GeneralLedgerController {
|
export class GeneralLedgerController {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly generalLedgerApplication: GeneralLedgerApplication,
|
private readonly generalLedgerApplication: GeneralLedgerApplication,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
|
@RequirePermission(ReportsAction.READ_GENERAL_LEDGET, AbilitySubject.Report)
|
||||||
@ApiResponse({
|
@ApiResponse({
|
||||||
status: 200,
|
status: 200,
|
||||||
description: 'General ledger report',
|
description: 'General ledger report',
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { Account } from '@/modules/Accounts/models/Account.model';
|
|||||||
import { ModelObject } from 'objection';
|
import { ModelObject } from 'objection';
|
||||||
import { flatToNestedArray } from '@/utils/flat-to-nested-array';
|
import { flatToNestedArray } from '@/utils/flat-to-nested-array';
|
||||||
import { getTransactionTypeLabel } from '@/modules/BankingTransactions/utils';
|
import { getTransactionTypeLabel } from '@/modules/BankingTransactions/utils';
|
||||||
|
import { IFinancialReportMeta, DEFAULT_REPORT_META } from '../../types/Report.types';
|
||||||
|
|
||||||
export class GeneralLedgerSheet extends R.compose(FinancialSheetStructure)(
|
export class GeneralLedgerSheet extends R.compose(FinancialSheetStructure)(
|
||||||
FinancialSheet,
|
FinancialSheet,
|
||||||
@@ -33,18 +34,21 @@ export class GeneralLedgerSheet extends R.compose(FinancialSheetStructure)(
|
|||||||
* @param {IGeneralLedgerSheetQuery} query -
|
* @param {IGeneralLedgerSheetQuery} query -
|
||||||
* @param {GeneralLedgerRepository} repository -
|
* @param {GeneralLedgerRepository} repository -
|
||||||
* @param {I18nService} i18n -
|
* @param {I18nService} i18n -
|
||||||
|
* @param {IFinancialReportMeta} meta -
|
||||||
*/
|
*/
|
||||||
constructor(
|
constructor(
|
||||||
query: IGeneralLedgerSheetQuery,
|
query: IGeneralLedgerSheetQuery,
|
||||||
repository: GeneralLedgerRepository,
|
repository: GeneralLedgerRepository,
|
||||||
i18n: I18nService,
|
i18n: I18nService,
|
||||||
|
meta: IFinancialReportMeta,
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.query = query;
|
this.query = query;
|
||||||
this.numberFormat = this.query.numberFormat;
|
this.numberFormat = this.query.numberFormat;
|
||||||
|
this.dateFormat = meta.dateFormat || DEFAULT_REPORT_META.dateFormat;
|
||||||
this.repository = repository;
|
this.repository = repository;
|
||||||
this.baseCurrency = this.repository.tenant.metadata.baseCurrency;
|
this.baseCurrency = meta.baseCurrency;
|
||||||
this.i18n = i18n;
|
this.i18n = i18n;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,7 +91,7 @@ export class GeneralLedgerSheet extends R.compose(FinancialSheetStructure)(
|
|||||||
return {
|
return {
|
||||||
id: entry.id,
|
id: entry.id,
|
||||||
date: entry.date,
|
date: entry.date,
|
||||||
dateFormatted: moment(entry.date).format('YYYY MMM DD'),
|
dateFormatted: moment(entry.date).format(this.dateFormat),
|
||||||
|
|
||||||
referenceType: entry.transactionType,
|
referenceType: entry.transactionType,
|
||||||
referenceId: entry.transactionId,
|
referenceId: entry.transactionId,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export interface IGeneralLedgerSheetQuery {
|
|||||||
toDate: Date | string;
|
toDate: Date | string;
|
||||||
basis: string;
|
basis: string;
|
||||||
numberFormat: IGeneralLedgerNumberFormat;
|
numberFormat: IGeneralLedgerNumberFormat;
|
||||||
|
dateFormat?: string;
|
||||||
noneTransactions: boolean;
|
noneTransactions: boolean;
|
||||||
accountsIds: number[];
|
accountsIds: number[];
|
||||||
branchesIds?: number[];
|
branchesIds?: number[];
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ export class GeneralLedgerMeta {
|
|||||||
): Promise<IGeneralLedgerMeta> {
|
): Promise<IGeneralLedgerMeta> {
|
||||||
const commonMeta = await this.financialSheetMeta.meta();
|
const commonMeta = await this.financialSheetMeta.meta();
|
||||||
|
|
||||||
const formattedToDate = moment(query.toDate).format('YYYY/MM/DD');
|
const formattedToDate = moment(query.toDate).format(commonMeta.dateFormat);
|
||||||
const formattedFromDate = moment(query.fromDate).format('YYYY/MM/DD');
|
const formattedFromDate = moment(query.fromDate).format(commonMeta.dateFormat);
|
||||||
const formattedDateRange = `From ${formattedFromDate} | To ${formattedToDate}`;
|
const formattedDateRange = `From ${formattedFromDate} | To ${formattedToDate}`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -37,18 +37,19 @@ export class GeneralLedgerService {
|
|||||||
this.generalLedgerRepository.setFilter(filter);
|
this.generalLedgerRepository.setFilter(filter);
|
||||||
await this.generalLedgerRepository.asyncInitialize();
|
await this.generalLedgerRepository.asyncInitialize();
|
||||||
|
|
||||||
|
// Retrieve general ledger report metadata first to get the date format.
|
||||||
|
const meta = await this.generalLedgerMeta.meta(filter);
|
||||||
|
|
||||||
// General ledger report instance.
|
// General ledger report instance.
|
||||||
const generalLedgerInstance = new GeneralLedgerSheet(
|
const generalLedgerInstance = new GeneralLedgerSheet(
|
||||||
filter,
|
filter,
|
||||||
this.generalLedgerRepository,
|
this.generalLedgerRepository,
|
||||||
this.i18n,
|
this.i18n,
|
||||||
|
{ baseCurrency: meta.baseCurrency, dateFormat: meta.dateFormat },
|
||||||
);
|
);
|
||||||
// Retrieve general ledger report data.
|
// Retrieve general ledger report data.
|
||||||
const reportData = generalLedgerInstance.reportData();
|
const reportData = generalLedgerInstance.reportData();
|
||||||
|
|
||||||
// Retrieve general ledger report metadata.
|
|
||||||
const meta = await this.generalLedgerMeta.meta(filter);
|
|
||||||
|
|
||||||
// Triggers `onGeneralLedgerViewed` event.
|
// Triggers `onGeneralLedgerViewed` event.
|
||||||
await this.eventEmitter.emitAsync(events.reports.onGeneralLedgerViewed, {});
|
await this.eventEmitter.emitAsync(events.reports.onGeneralLedgerViewed, {});
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { InventoryDetails } from './InventoryItemDetails';
|
|||||||
import { InventoryItemDetailsRepository } from './InventoryItemDetailsRepository';
|
import { InventoryItemDetailsRepository } from './InventoryItemDetailsRepository';
|
||||||
import { InventoryDetailsMetaInjectable } from './InventoryItemDetailsMeta';
|
import { InventoryDetailsMetaInjectable } from './InventoryItemDetailsMeta';
|
||||||
import { getInventoryItemDetailsDefaultQuery } from './constant';
|
import { getInventoryItemDetailsDefaultQuery } from './constant';
|
||||||
|
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class InventoryDetailsService {
|
export class InventoryDetailsService {
|
||||||
@@ -15,6 +16,7 @@ export class InventoryDetailsService {
|
|||||||
private readonly inventoryItemDetailsRepository: InventoryItemDetailsRepository,
|
private readonly inventoryItemDetailsRepository: InventoryItemDetailsRepository,
|
||||||
private readonly inventoryDetailsMeta: InventoryDetailsMetaInjectable,
|
private readonly inventoryDetailsMeta: InventoryDetailsMetaInjectable,
|
||||||
private readonly i18n: I18nService,
|
private readonly i18n: I18nService,
|
||||||
|
private readonly tenancyContext: TenancyContext,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -34,13 +36,16 @@ export class InventoryDetailsService {
|
|||||||
this.inventoryItemDetailsRepository.setFilter(filter);
|
this.inventoryItemDetailsRepository.setFilter(filter);
|
||||||
await this.inventoryItemDetailsRepository.asyncInit();
|
await this.inventoryItemDetailsRepository.asyncInit();
|
||||||
|
|
||||||
|
// Retrieve the meta first to get date format.
|
||||||
|
const meta = await this.inventoryDetailsMeta.meta(query);
|
||||||
|
|
||||||
// Inventory details report mapper.
|
// Inventory details report mapper.
|
||||||
const inventoryDetailsInstance = new InventoryDetails(
|
const inventoryDetailsInstance = new InventoryDetails(
|
||||||
filter,
|
filter,
|
||||||
this.inventoryItemDetailsRepository,
|
this.inventoryItemDetailsRepository,
|
||||||
this.i18n,
|
this.i18n,
|
||||||
|
{ baseCurrency: meta.baseCurrency, dateFormat: meta.dateFormat },
|
||||||
);
|
);
|
||||||
const meta = await this.inventoryDetailsMeta.meta(query);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: inventoryDetailsInstance.reportData(),
|
data: inventoryDetailsInstance.reportData(),
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import { Item } from '@/modules/Items/models/Item';
|
|||||||
import {
|
import {
|
||||||
IFormatNumberSettings,
|
IFormatNumberSettings,
|
||||||
INumberFormatQuery,
|
INumberFormatQuery,
|
||||||
|
IFinancialReportMeta,
|
||||||
|
DEFAULT_REPORT_META,
|
||||||
} from '../../types/Report.types';
|
} from '../../types/Report.types';
|
||||||
import { InventoryTransaction } from '@/modules/InventoryCost/models/InventoryTransaction';
|
import { InventoryTransaction } from '@/modules/InventoryCost/models/InventoryTransaction';
|
||||||
import { InventoryItemDetailsRepository } from './InventoryItemDetailsRepository';
|
import { InventoryItemDetailsRepository } from './InventoryItemDetailsRepository';
|
||||||
@@ -35,11 +37,13 @@ export class InventoryDetails extends FinancialSheet {
|
|||||||
* Constructor method.
|
* Constructor method.
|
||||||
* @param {InventoryItemDetailsRepository} repository - The repository.
|
* @param {InventoryItemDetailsRepository} repository - The repository.
|
||||||
* @param {I18nService} i18n - The i18n service.
|
* @param {I18nService} i18n - The i18n service.
|
||||||
|
* @param {IFinancialReportMeta} meta - Report meta.
|
||||||
*/
|
*/
|
||||||
constructor(
|
constructor(
|
||||||
filter: IInventoryDetailsQuery,
|
filter: IInventoryDetailsQuery,
|
||||||
repository: InventoryItemDetailsRepository,
|
repository: InventoryItemDetailsRepository,
|
||||||
i18n: I18nService,
|
i18n: I18nService,
|
||||||
|
meta: IFinancialReportMeta,
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
@@ -48,6 +52,7 @@ export class InventoryDetails extends FinancialSheet {
|
|||||||
this.query = filter;
|
this.query = filter;
|
||||||
this.numberFormat = this.query.numberFormat;
|
this.numberFormat = this.query.numberFormat;
|
||||||
this.i18n = i18n;
|
this.i18n = i18n;
|
||||||
|
this.dateFormat = meta.dateFormat || DEFAULT_REPORT_META.dateFormat;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -89,7 +94,7 @@ export class InventoryDetails extends FinancialSheet {
|
|||||||
*/
|
*/
|
||||||
public getDateMeta(date: Date | string): IInventoryDetailsDate {
|
public getDateMeta(date: Date | string): IInventoryDetailsDate {
|
||||||
return {
|
return {
|
||||||
formattedDate: moment(date).format('YYYY-MM-DD'),
|
formattedDate: moment(date).format(this.dateFormat),
|
||||||
date: moment(date).toDate(),
|
date: moment(date).toDate(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ export class InventoryDetailsMetaInjectable {
|
|||||||
): Promise<IInventoryItemDetailMeta> {
|
): Promise<IInventoryItemDetailMeta> {
|
||||||
const commonMeta = await this.financialSheetMeta.meta();
|
const commonMeta = await this.financialSheetMeta.meta();
|
||||||
|
|
||||||
const formattedFromDate = moment(query.fromDate).format('YYYY/MM/DD');
|
const formattedFromDate = moment(query.fromDate).format(commonMeta.dateFormat);
|
||||||
const formattedToDay = moment(query.toDate).format('YYYY/MM/DD');
|
const formattedToDay = moment(query.toDate).format(commonMeta.dateFormat);
|
||||||
const formattedDateRange = `From ${formattedFromDate} | To ${formattedToDay}`;
|
const formattedDateRange = `From ${formattedFromDate} | To ${formattedToDay}`;
|
||||||
|
|
||||||
const sheetName = 'Inventory Item Details';
|
const sheetName = 'Inventory Item Details';
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ export class InventoryItemDetailsTable {
|
|||||||
): ITableRow => {
|
): ITableRow => {
|
||||||
const columns: Array<IColumnMapperMeta> = [
|
const columns: Array<IColumnMapperMeta> = [
|
||||||
{ key: 'date', accessor: 'date.formattedDate' },
|
{ key: 'date', accessor: 'date.formattedDate' },
|
||||||
{ key: 'closing', value: this.i18n.t('Opening balance') },
|
{ key: 'closing', value: this.i18n.t('inventory_item_details.opening_balance') },
|
||||||
{ key: 'empty', value: '' },
|
{ key: 'empty', value: '' },
|
||||||
{ key: 'quantity', accessor: 'quantity.formattedNumber' },
|
{ key: 'quantity', accessor: 'quantity.formattedNumber' },
|
||||||
{ key: 'empty', value: '' },
|
{ key: 'empty', value: '' },
|
||||||
@@ -115,7 +115,7 @@ export class InventoryItemDetailsTable {
|
|||||||
): ITableRow => {
|
): ITableRow => {
|
||||||
const columns: Array<IColumnMapperMeta> = [
|
const columns: Array<IColumnMapperMeta> = [
|
||||||
{ key: 'date', accessor: 'date.formattedDate' },
|
{ key: 'date', accessor: 'date.formattedDate' },
|
||||||
{ key: 'closing', value: this.i18n.t('Closing balance') },
|
{ key: 'closing', value: this.i18n.t('inventory_item_details.closing_balance') },
|
||||||
{ key: 'empty', value: '' },
|
{ key: 'empty', value: '' },
|
||||||
{ key: 'quantity', accessor: 'quantity.formattedNumber' },
|
{ key: 'quantity', accessor: 'quantity.formattedNumber' },
|
||||||
{ key: 'empty', value: '' },
|
{ key: 'empty', value: '' },
|
||||||
@@ -193,16 +193,16 @@ export class InventoryItemDetailsTable {
|
|||||||
*/
|
*/
|
||||||
public tableColumns = (): ITableColumn[] => {
|
public tableColumns = (): ITableColumn[] => {
|
||||||
return [
|
return [
|
||||||
{ key: 'date', label: this.i18n.t('Date') },
|
{ key: 'date', label: this.i18n.t('inventory_item_details.date') },
|
||||||
{ key: 'transaction_type', label: this.i18n.t('Transaction type') },
|
{ key: 'transaction_type', label: this.i18n.t('inventory_item_details.transaction_type') },
|
||||||
{ key: 'transaction_id', label: this.i18n.t('Transaction #') },
|
{ key: 'transaction_id', label: this.i18n.t('inventory_item_details.transaction_number') },
|
||||||
{ key: 'quantity', label: this.i18n.t('Quantity') },
|
{ key: 'quantity', label: this.i18n.t('inventory_item_details.quantity') },
|
||||||
{ key: 'rate', label: this.i18n.t('Rate') },
|
{ key: 'rate', label: this.i18n.t('inventory_item_details.rate') },
|
||||||
{ key: 'total', label: this.i18n.t('Total') },
|
{ key: 'total', label: this.i18n.t('inventory_item_details.total') },
|
||||||
{ key: 'value', label: this.i18n.t('Value') },
|
{ key: 'value', label: this.i18n.t('inventory_item_details.value') },
|
||||||
{ key: 'profit_margin', label: this.i18n.t('Profit Margin') },
|
{ key: 'profit_margin', label: this.i18n.t('inventory_item_details.profit_margin') },
|
||||||
{ key: 'running_quantity', label: this.i18n.t('Running quantity') },
|
{ key: 'running_quantity', label: this.i18n.t('inventory_item_details.running_quantity') },
|
||||||
{ key: 'running_value', label: this.i18n.t('Running Value') },
|
{ key: 'running_value', label: this.i18n.t('inventory_item_details.running_value') },
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { InventoryCostLotTracker } from '@/modules/InventoryCost/models/Inventor
|
|||||||
import { FinancialSheet } from '../../common/FinancialSheet';
|
import { FinancialSheet } from '../../common/FinancialSheet';
|
||||||
import { InventoryValuationSheetRepository } from './InventoryValuationSheetRepository';
|
import { InventoryValuationSheetRepository } from './InventoryValuationSheetRepository';
|
||||||
import { allPassedConditionsPass } from '@/utils/all-conditions-passed';
|
import { allPassedConditionsPass } from '@/utils/all-conditions-passed';
|
||||||
|
import { IFinancialReportMeta, DEFAULT_REPORT_META } from '../../types/Report.types';
|
||||||
|
|
||||||
export class InventoryValuationSheet extends FinancialSheet {
|
export class InventoryValuationSheet extends FinancialSheet {
|
||||||
readonly query: IInventoryValuationReportQuery;
|
readonly query: IInventoryValuationReportQuery;
|
||||||
@@ -21,16 +22,19 @@ export class InventoryValuationSheet extends FinancialSheet {
|
|||||||
* Constructor method.
|
* Constructor method.
|
||||||
* @param {IInventoryValuationReportQuery} query - Inventory valuation query.
|
* @param {IInventoryValuationReportQuery} query - Inventory valuation query.
|
||||||
* @param {InventoryValuationSheetRepository} repository - Inventory valuation sheet repository.
|
* @param {InventoryValuationSheetRepository} repository - Inventory valuation sheet repository.
|
||||||
|
* @param {IFinancialReportMeta} meta - Report meta.
|
||||||
*/
|
*/
|
||||||
constructor(
|
constructor(
|
||||||
query: IInventoryValuationReportQuery,
|
query: IInventoryValuationReportQuery,
|
||||||
repository: InventoryValuationSheetRepository,
|
repository: InventoryValuationSheetRepository,
|
||||||
|
meta: IFinancialReportMeta,
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this.query = query;
|
this.query = query;
|
||||||
this.repository = repository;
|
this.repository = repository;
|
||||||
this.numberFormat = this.query.numberFormat;
|
this.numberFormat = this.query.numberFormat;
|
||||||
|
this.dateFormat = meta.dateFormat || DEFAULT_REPORT_META.dateFormat;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user