Compare commits

..

37 Commits

Author SHA1 Message Date
Ahmed Bouhuolia
8def1d31d2 Merge pull request #933 from bigcapitalhq/20260205-151219-7770
feat(webapp): add blurry background to sticky data table cells
2026-02-05 15:29:47 +02:00
Ahmed Bouhuolia
afab02a053 feat(webapp): add blurry background to sticky data table cells
Add backdrop-filter blur effect to sticky column cells in financial reports
to prevent content from showing through during horizontal scrolling.
The effect only applies when rows are not hovered to preserve hover
background interactions.
2026-02-05 15:27:45 +02:00
Ahmed Bouhuolia
8e925c62f2 Merge pull request #932 from bigcapitalhq/20260205-151219-7770
fix(server): balance sheet query validation schema
2026-02-05 15:14:45 +02:00
Ahmed Bouhuolia
1b7d513adf fix(server): balance sheet query validation schema 2026-02-05 15:12:54 +02:00
Ahmed Bouhuolia
7d764fb390 Merge pull request #931 from bigcapitalhq/fix/item-error-handling
fix(items): correct error type handling and add swagger documentation
2026-02-04 21:44:45 +02:00
Ahmed Bouhuolia
c571f50a74 fix(items): correct error type handling and add swagger documentation
- Fix error type mismatch: change 'ITEM.NAME.ALREADY.EXISTS' to 'ITEM_NAME_EXISTS'
- Add ItemErrorType constant with UpperCamelCase keys for better maintainability
- Update all error checks to use the new ItemErrorType constant
- Add ItemErrorResponse.dto.ts with documented error types for swagger
- Add @ApiResponse decorators to document 400 validation errors in swagger
2026-02-04 21:42:39 +02:00
Ahmed Bouhuolia
6549026344 Merge pull request #930 from bigcapitalhq/fix/account-delete-error-handling
fix(webapp): account delete error handling response types
2026-02-04 21:31:38 +02:00
Ahmed Bouhuolia
0963394b04 fix(webapp): account delete error handling response types 2026-02-04 21:27:25 +02:00
Ahmed Bouhuolia
6cab0651fc Merge pull request #927 from bigcapitalhq/feature/20260202223150
fix(webapp): darkmode warehouses list page
2026-02-02 22:36:42 +02:00
Ahmed Bouhuolia
4af537d6dd fix(webapp): darkmode warehouses list page 2026-02-02 22:31:53 +02:00
Ahmed Bouhuolia
34db64612c Merge pull request #926 from bigcapitalhq/20260202-185120-9c84
fix(webapp): constrant not found row color
2026-02-02 18:53:48 +02:00
Ahmed Bouhuolia
10225bbfed fix(webapp): constrant not found row color 2026-02-02 18:51:52 +02:00
Ahmed Bouhuolia
c3a4fe6b37 Merge pull request #924 from bigcapitalhq/20260201-180532-f578
fix(webapp): normalize api path
2026-02-01 18:06:51 +02:00
Ahmed Bouhuolia
02be959461 fix(webapp): normalize api path 2026-02-01 18:05:51 +02:00
Ahmed Bouhuolia
d5bf56e333 Merge pull request #923 from bigcapitalhq/20260201-165255-f063
fix(server): copy .js migration files
2026-02-01 16:56:49 +02:00
Ahmed Bouhuolia
e3182c15b3 fix(server): copy .js migration files 2026-02-01 16:53:21 +02:00
Ahmed Bouhuolia
dfa63ece21 Merge pull request #921 from bigcapitalhq/20260131-145158-fd0c
fix(scripts): db migration dockerfile
2026-01-31 15:32:07 +02:00
Ahmed Bouhuolia
6e95bd7da1 fix(scripts): db migration dockerfile 2026-01-31 15:31:17 +02:00
Ahmed Bouhuolia
f51fffa5c7 Merge pull request #918 from bigcapitalhq/20260129-203653-75b0
feat(server): add bull ui board
2026-01-29 20:39:05 +02:00
Ahmed Bouhuolia
6193358cc3 feat(server): add bull ui board 2026-01-29 20:37:04 +02:00
Ahmed Bouhuolia
518abcd30d Merge pull request #917 from bigcapitalhq/20260128-195652-2287
fix: dockerfile build script
2026-01-28 23:42:24 +02:00
Ahmed Bouhuolia
7874b9f765 fix(ci): dockerfile build script 2026-01-28 23:40:32 +02:00
Ahmed Bouhuolia
02cc7e0c96 Merge pull request #916 from bigcapitalhq/20260128-181425-8b6a
fix(webapp): blueprintjs datetime version
2026-01-28 18:17:29 +02:00
Ahmed Bouhuolia
57cc513873 fix(webapp): blueprintjs datetime version 2026-01-28 18:14:44 +02:00
Ahmed Bouhuolia
f5bfdede30 Merge pull request #915 from bigcapitalhq/fix-vendor-customer-edit-opening-balance
fix(webapp): vendor/customer edit opening balance
2026-01-27 22:09:00 +02:00
Ahmed Bouhuolia
488556bb59 fix(webapp): vendor/customer edit opening balance 2026-01-27 22:06:57 +02:00
Ahmed Bouhuolia
0fc5a66e95 Merge pull request #914 from bigcapitalhq/fix-costable-inventory-transactions
fix(server): costable attr of inventory gl entries
2026-01-26 15:02:35 +02:00
Ahmed Bouhuolia
d9ae51027e fix(server): costable attr of inventory gl entries 2026-01-26 15:00:17 +02:00
Ahmed Bouhuolia
a92d6112d9 Merge pull request #913 from bigcapitalhq/feature/20260125222025
fix(server): sale receipt cost gl entries
2026-01-25 22:22:08 +02:00
Ahmed Bouhuolia
889b0cec4b fix(server): sale receipt cost gl entries 2026-01-25 22:20:28 +02:00
Ahmed Bouhuolia
1c4c41ebba Merge pull request #912 from bigcapitalhq/feature/20260125215941
fix(server): mark compute inventory cost flag
2026-01-25 22:02:13 +02:00
Ahmed Bouhuolia
421f0c26a7 fix(server): mark compute inventory cost flag 2026-01-25 21:59:44 +02:00
Ahmed Bouhuolia
f461cc221b Merge pull request #911 from bigcapitalhq/feature/20260125001703
fix(server): landed cost gl transactions
2026-01-25 00:19:07 +02:00
Ahmed Bouhuolia
acae75a912 fix(server): landed cost gl transactions 2026-01-25 00:17:14 +02:00
Ahmed Bouhuolia
b5a69971a9 Merge pull request #910 from bigcapitalhq/feature/20260123174320
fix(server): customer/vendor opening balance
2026-01-24 14:02:17 +02:00
Ahmed Bouhuolia
04d065b969 wip 2026-01-24 13:59:43 +02:00
Ahmed Bouhuolia
ca910ee489 fix(server): customer/vendor opening balance: 2026-01-23 17:43:22 +02:00
119 changed files with 1996 additions and 1516 deletions

93
.dockerignore Normal file
View 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

View File

@@ -58,6 +58,12 @@ services:
# System database
- SYSTEM_DB_NAME=${SYSTEM_DB_NAME}
# Redis
- REDIS_HOST=redis
- REDIS_PORT=6379
- QUEUE_HOST=redis
- QUEUE_PORT=6379
# Tenants databases
- TENANT_DB_NAME_PERFIX=${TENANT_DB_NAME_PERFIX}

View File

@@ -35,4 +35,4 @@ WORKDIR /app/packages/server
RUN git clone https://github.com/vishnubob/wait-for-it.git
# 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"

View File

@@ -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
RUN chown node:node /
# Install pnpm
RUN npm install -g pnpm@8.10.2
# Install pnpm
RUN npm install -g pnpm
# Install build dependencies
RUN apk add --no-cache python3 build-base chromium
# Copy application dependency manifests to the container image.
COPY --chown=node:node ./ ./
# Install application dependencies
RUN apk update
RUN apk add python3 build-base chromium
# Set PYHTON env
# Set Python environment
ENV PYTHON=/usr/bin/python3
# Install packages dependencies for production.
RUN pnpm install
# Copy package files for dependency installation
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 ./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
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" ]

View File

@@ -2,10 +2,23 @@
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"entryFile": "main",
"compilerOptions": {
"deleteOutDir": true,
"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"
}
}
}
}

View File

@@ -40,6 +40,9 @@
"@lemonsqueezy/lemonsqueezy.js": "^2.2.0",
"@liaoliaots/nestjs-redis": "^10.0.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/bullmq": "^10.2.2",
"@nestjs/cache-manager": "^2.2.2",

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

View File

@@ -17,6 +17,9 @@ import loops from './loops';
import bankfeed from './bankfeed';
import throttle from './throttle';
import cloud from './cloud';
import redis from './redis';
import queue from './queue';
import bullBoard from './bull-board';
export const config = [
app,
@@ -38,4 +41,7 @@ export const config = [
loops,
bankfeed,
throttle,
redis,
queue,
bullBoard,
];

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

View 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();
};
}

View File

@@ -33,6 +33,7 @@ export class AccountTransaction extends BaseModel {
public readonly userId!: number;
public readonly itemId!: number;
public readonly projectId!: number;
public readonly costable!: boolean;
public readonly account: Account;
/**

View File

@@ -12,6 +12,9 @@ import {
I18nModule,
QueryResolver,
} 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 { ScheduleModule } from '@nestjs/schedule';
import { PassportModule } from '@nestjs/passport';
@@ -137,12 +140,30 @@ import { AppThrottleModule } from './AppThrottle.module';
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
connection: {
host: configService.get('QUEUE_HOST'),
port: configService.get('QUEUE_PORT'),
host: configService.get('queue.host'),
port: configService.get('queue.port'),
},
}),
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({
global: true,
middleware: {
@@ -158,8 +179,8 @@ import { AppThrottleModule } from './AppThrottle.module';
imports: [ConfigModule],
useFactory: (configService: ConfigService) => ({
config: {
host: configService.get('redis.host') || 'localhost',
port: configService.get('redis.port') || 6379,
host: configService.get('redis.host'),
port: configService.get('redis.port'),
},
}),
inject: [ConfigService],

View File

@@ -17,6 +17,8 @@ import { PassportModule } from '@nestjs/passport';
import { APP_GUARD } from '@nestjs/core';
import { JwtAuthGuard } from './guards/jwt.guard';
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 {
SendResetPasswordMailQueue,
@@ -63,6 +65,14 @@ const models = [
TenancyModule,
BullModule.registerQueue({ name: SendResetPasswordMailQueue }),
BullModule.registerQueue({ name: SendSignupVerificationMailQueue }),
BullBoardModule.forFeature({
name: SendResetPasswordMailQueue,
adapter: BullMQAdapter,
}),
BullBoardModule.forFeature({
name: SendSignupVerificationMailQueue,
adapter: BullMQAdapter,
}),
],
exports: [...models],
providers: [
@@ -98,4 +108,4 @@ const models = [
AuthMailSubscriber,
],
})
export class AuthModule { }
export class AuthModule {}

View File

@@ -1,10 +1,6 @@
import { Processor, WorkerHost } from '@nestjs/bullmq';
import { Scope } from '@nestjs/common';
import {
SendResetPasswordMailJob,
SendResetPasswordMailQueue,
} from '../Auth.constants';
import { Process } from '@nestjs/bull';
import { SendResetPasswordMailQueue } from '../Auth.constants';
import { Job } from 'bullmq';
import { AuthenticationMailMesssages } from '../AuthMailMessages.esrvice';
import { MailTransporter } from '@/modules/Mail/MailTransporter.service';
@@ -23,7 +19,6 @@ export class SendResetPasswordMailProcessor extends WorkerHost {
super();
}
@Process(SendResetPasswordMailJob)
async process(job: Job<SendResetPasswordMailJobPayload>) {
try {
await this.authMailMesssages.sendResetPasswordMail(

View File

@@ -1,11 +1,7 @@
import { Scope } from '@nestjs/common';
import { Job } from 'bullmq';
import { Process } from '@nestjs/bull';
import { Processor, WorkerHost } from '@nestjs/bullmq';
import {
SendSignupVerificationMailJob,
SendSignupVerificationMailQueue,
} from '../Auth.constants';
import { SendSignupVerificationMailQueue } from '../Auth.constants';
import { MailTransporter } from '@/modules/Mail/MailTransporter.service';
import { AuthenticationMailMesssages } from '../AuthMailMessages.esrvice';
@@ -21,7 +17,6 @@ export class SendSignupVerificationMailProcessor extends WorkerHost {
super();
}
@Process(SendSignupVerificationMailJob)
async process(job: Job<SendSignupVerificationMailJobPayload>) {
try {
await this.authMailMesssages.sendSignupVerificationMail(

View File

@@ -1,3 +1,5 @@
import { BullBoardModule } from '@bull-board/nestjs';
import { BullMQAdapter } from '@bull-board/api/bullMQAdapter';
import { BullModule } from '@nestjs/bullmq';
import { Module } from '@nestjs/common';
import { SocketModule } from '../Socket/Socket.module';
@@ -33,6 +35,10 @@ const models = [RegisterTenancyModel(PlaidItem)];
BankingCategorizeModule,
BankingTransactionsModule,
BullModule.registerQueue({ name: UpdateBankingPlaidTransitionsQueueJob }),
BullBoardModule.forFeature({
name: UpdateBankingPlaidTransitionsQueueJob,
adapter: BullMQAdapter,
}),
...models,
],
providers: [
@@ -51,4 +57,4 @@ const models = [RegisterTenancyModel(PlaidItem)];
exports: [...models],
controllers: [BankingPlaidController, BankingPlaidWebhooksController],
})
export class BankingPlaidModule { }
export class BankingPlaidModule {}

View File

@@ -1,11 +1,9 @@
import { Process } from '@nestjs/bull';
import { UseCls } from 'nestjs-cls';
import { Processor, WorkerHost } from '@nestjs/bullmq';
import { Scope } from '@nestjs/common';
import { Job } from 'bullmq';
import {
PlaidFetchTransitonsEventPayload,
UpdateBankingPlaidTransitionsJob,
UpdateBankingPlaidTransitionsQueueJob,
} from '../types/BankingPlaid.types';
import { PlaidUpdateTransactions } from '../command/PlaidUpdateTransactions';
@@ -28,7 +26,6 @@ export class PlaidFetchTransactionsProcessor extends WorkerHost {
/**
* Triggers the function.
*/
@Process(UpdateBankingPlaidTransitionsJob)
@UseCls()
async process(job: Job<PlaidFetchTransitonsEventPayload>) {
const { plaidItemId } = job.data;

View File

@@ -10,6 +10,8 @@ import { BankingRecognizedTransactionsController } from './BankingRecognizedTran
import { RecognizedTransactionsApplication } from './RecognizedTransactions.application';
import { GetRecognizedTransactionsService } from './GetRecongizedTransactions';
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 { RecognizeUncategorizedTransactionsQueue } from './_types';
import { RegonizeTransactionsPrcessor } from './jobs/RecognizeTransactionsJob';
@@ -25,6 +27,10 @@ const models = [RegisterTenancyModel(RecognizedBankTransaction)];
BullModule.registerQueue({
name: RecognizeUncategorizedTransactionsQueue,
}),
BullBoardModule.forFeature({
name: RecognizeUncategorizedTransactionsQueue,
adapter: BullMQAdapter,
}),
...models,
],
providers: [

View File

@@ -7,7 +7,6 @@ import {
RecognizeUncategorizedTransactionsJobPayload,
RecognizeUncategorizedTransactionsQueue,
} from '../_types';
import { Process } from '@nestjs/bull';
@Processor({
name: RecognizeUncategorizedTransactionsQueue,
@@ -28,7 +27,6 @@ export class RegonizeTransactionsPrcessor extends WorkerHost {
/**
* Triggers sending invoice mail.
*/
@Process(RecognizeUncategorizedTransactionsQueue)
@UseCls()
async process(job: Job<RecognizeUncategorizedTransactionsJobPayload>) {
const { ruleId, transactionsCriteria } = job.data;

View File

@@ -2,9 +2,11 @@ import { forwardRef, Module } from '@nestjs/common';
import { TransactionLandedCostEntriesService } from './TransactionLandedCostEntries.service';
import { AllocateLandedCostService } from './commands/AllocateLandedCost.service';
import { LandedCostGLEntriesSubscriber } from './commands/LandedCostGLEntries.subscriber';
// import { LandedCostGLEntries } from './commands/LandedCostGLEntries.service';
import { LandedCostGLEntriesService } from './commands/LandedCostGLEntries.service';
import { LandedCostSyncCostTransactions } from './commands/LandedCostSyncCostTransactions.service';
import { LedgerModule } from '../Ledger/Ledger.module';
import { LandedCostSyncCostTransactionsSubscriber } from './commands/LandedCostSyncCostTransactions.subscriber';
import { LandedCostInventoryTransactionsSubscriber } from './commands/LandedCostInventoryTransactions.subscriber';
import { BillAllocatedLandedCostTransactions } from './commands/BillAllocatedLandedCostTransactions.service';
import { BillAllocateLandedCostController } from './LandedCost.controller';
import { RevertAllocatedLandedCost } from './commands/RevertAllocatedLandedCost.service';
@@ -16,12 +18,12 @@ import { ExpenseLandedCost } from './commands/ExpenseLandedCost.service';
import { BillLandedCost } from './commands/BillLandedCost.service';
@Module({
imports: [forwardRef(() => InventoryCostModule)],
imports: [forwardRef(() => InventoryCostModule), LedgerModule],
providers: [
AllocateLandedCostService,
TransactionLandedCostEntriesService,
BillAllocatedLandedCostTransactions,
LandedCostGLEntriesSubscriber,
LandedCostGLEntriesService,
TransactionLandedCost,
BillLandedCost,
ExpenseLandedCost,
@@ -29,6 +31,8 @@ import { BillLandedCost } from './commands/BillLandedCost.service';
RevertAllocatedLandedCost,
LandedCostInventoryTransactions,
LandedCostTranasctions,
LandedCostGLEntriesSubscriber,
LandedCostInventoryTransactionsSubscriber,
LandedCostSyncCostTransactionsSubscriber,
],
exports: [TransactionLandedCostEntriesService],

View File

@@ -23,7 +23,9 @@ export class AllocateLandedCostService extends BaseLandedCostService {
private readonly billModel: TenantModelProxy<typeof Bill>,
@Inject(BillLandedCost.name)
protected readonly billLandedCostModel: TenantModelProxy<typeof BillLandedCost>
protected readonly billLandedCostModel: TenantModelProxy<
typeof BillLandedCost
>,
) {
super();
}
@@ -54,7 +56,8 @@ export class AllocateLandedCostService extends BaseLandedCostService {
const amount = this.getAllocateItemsCostTotal(allocateCostDTO);
// Retrieve the purchase invoice or throw not found error.
const bill = await this.billModel().query()
const bill = await this.billModel()
.query()
.findById(billId)
.withGraphFetched('entries')
.throwIfNotFound();
@@ -89,8 +92,9 @@ export class AllocateLandedCostService extends BaseLandedCostService {
// unit-of-work eniverment.
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
// Save the bill landed cost model.
const billLandedCost =
await BillLandedCost.query(trx).insertGraph(billLandedCostObj);
const billLandedCost = await this.billLandedCostModel()
.query(trx)
.insertGraph(billLandedCostObj);
// Triggers `onBillLandedCostCreated` event.
await this.eventPublisher.emitAsync(events.billLandedCost.onCreated, {
bill,
@@ -103,5 +107,5 @@ export class AllocateLandedCostService extends BaseLandedCostService {
return billLandedCost;
});
};
}
}

View File

@@ -1,236 +1,188 @@
// import * as R from 'ramda';
// import { Knex } from 'knex';
// import { Inject, Injectable } from '@nestjs/common';
// import { BaseLandedCostService } from '../BaseLandedCost.service';
// import { BillLandedCost } from '../models/BillLandedCost';
// import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
// import { Bill } from '@/modules/Bills/models/Bill';
// import { BillLandedCostEntry } from '../models/BillLandedCostEntry';
// import { ILedger, ILedgerEntry } from '@/modules/Ledger/types/Ledger.types';
// import { Ledger } from '@/modules/Ledger/Ledger';
// import { AccountNormal } from '@/interfaces/Account';
// import { ILandedCostTransactionEntry } from '../types/BillLandedCosts.types';
import { Knex } from 'knex';
import { Inject, Injectable } from '@nestjs/common';
import * as moment from 'moment';
import { BaseLandedCostService } from '../BaseLandedCost.service';
import { BillLandedCost } from '../models/BillLandedCost';
import { Bill } from '@/modules/Bills/models/Bill';
import { BillLandedCostEntry } from '../models/BillLandedCostEntry';
import { ILedgerEntry } from '@/modules/Ledger/types/Ledger.types';
import { Ledger } from '@/modules/Ledger/Ledger';
import { LedgerStorageService } from '@/modules/Ledger/LedgerStorage.service';
import { AccountNormal } from '@/modules/Accounts/Accounts.types';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
// @Injectable()
// export class LandedCostGLEntries extends BaseLandedCostService {
// constructor(
// private readonly journalService: JournalPosterService,
// private readonly ledgerRepository: LedgerRepository,
@Injectable()
export class LandedCostGLEntriesService extends BaseLandedCostService {
constructor(
private readonly ledgerStorage: LedgerStorageService,
// @Inject(BillLandedCost.name)
// private readonly billLandedCostModel: TenantModelProxy<typeof BillLandedCost>,
// ) {
// super();
// }
@Inject(BillLandedCost.name)
protected readonly billLandedCostModel: TenantModelProxy<
typeof BillLandedCost
>,
) {
super();
}
// /**
// * Retrieves the landed cost GL common entry.
// * @param {IBill} bill
// * @param {IBillLandedCost} allocatedLandedCost
// * @returns
// */
// private getLandedCostGLCommonEntry = (
// bill: Bill,
// allocatedLandedCost: BillLandedCost
// ) => {
// return {
// date: bill.billDate,
// currencyCode: allocatedLandedCost.currencyCode,
// exchangeRate: allocatedLandedCost.exchangeRate,
/**
* Retrieves the landed cost GL common entry.
*/
private getLandedCostGLCommonEntry(
bill: Bill,
allocatedLandedCost: BillLandedCost,
) {
return {
date: moment(bill.billDate).format('YYYY-MM-DD'),
currencyCode: allocatedLandedCost.currencyCode,
exchangeRate: allocatedLandedCost.exchangeRate,
// transactionType: 'LandedCost',
// transactionId: allocatedLandedCost.id,
// transactionNumber: bill.billNumber,
transactionType: 'LandedCost',
transactionId: allocatedLandedCost.id,
transactionNumber: bill.billNumber,
// referenceNumber: bill.referenceNo,
referenceNumber: bill.referenceNo,
// credit: 0,
// debit: 0,
// };
// };
branchId: bill.branchId,
projectId: bill.projectId,
// /**
// * Retrieves the landed cost GL inventory entry.
// * @param {IBill} bill
// * @param {IBillLandedCost} allocatedLandedCost
// * @param {IBillLandedCostEntry} allocatedEntry
// * @returns {ILedgerEntry}
// */
// private getLandedCostGLInventoryEntry = (
// bill: Bill,
// allocatedLandedCost: BillLandedCost,
// allocatedEntry: BillLandedCostEntry
// ): ILedgerEntry => {
// const commonEntry = this.getLandedCostGLCommonEntry(
// bill,
// allocatedLandedCost
// );
// return {
// ...commonEntry,
// debit: allocatedLandedCost.localAmount,
// accountId: allocatedEntry.itemEntry.item.inventoryAccountId,
// index: 1,
// accountNormal: AccountNormal.DEBIT,
// };
// };
credit: 0,
debit: 0,
};
}
// /**
// * Retrieves the landed cost GL cost entry.
// * @param {IBill} bill
// * @param {IBillLandedCost} allocatedLandedCost
// * @param {ILandedCostTransactionEntry} fromTransactionEntry
// * @returns {ILedgerEntry}
// */
// private getLandedCostGLCostEntry = (
// bill: Bill,
// allocatedLandedCost: BillLandedCost,
// fromTransactionEntry: ILandedCostTransactionEntry
// ): ILedgerEntry => {
// const commonEntry = this.getLandedCostGLCommonEntry(
// bill,
// allocatedLandedCost
// );
// return {
// ...commonEntry,
// credit: allocatedLandedCost.localAmount,
// accountId: fromTransactionEntry.costAccountId,
// index: 2,
// accountNormal: AccountNormal.CREDIT,
// };
// };
/**
* Retrieves the landed cost GL inventory entry for an allocated item.
*/
private getLandedCostGLInventoryEntry(
bill: Bill,
allocatedLandedCost: BillLandedCost,
allocatedEntry: BillLandedCostEntry,
index: number,
): ILedgerEntry {
const commonEntry = this.getLandedCostGLCommonEntry(
bill,
allocatedLandedCost,
);
const itemEntry = (
allocatedEntry as BillLandedCostEntry & {
itemEntry?: {
item?: { type?: string; inventoryAccountId?: number };
costAccountId?: number;
itemId?: number;
};
}
).itemEntry;
const item = itemEntry?.item;
const isInventory = item && ['inventory'].indexOf(item.type) !== -1;
const accountId = isInventory
? item?.inventoryAccountId
: itemEntry?.costAccountId;
// /**
// * Retrieve allocated landed cost entry GL entries.
// * @param {IBill} bill
// * @param {IBillLandedCost} allocatedLandedCost
// * @param {ILandedCostTransactionEntry} fromTransactionEntry
// * @param {IBillLandedCostEntry} allocatedEntry
// * @returns {ILedgerEntry}
// */
// private getLandedCostGLAllocateEntry = R.curry(
// (
// bill: Bill,
// allocatedLandedCost: BillLandedCost,
// fromTransactionEntry: ILandedCostTransactionEntry,
// allocatedEntry: BillLandedCostEntry
// ): ILedgerEntry[] => {
// const inventoryEntry = this.getLandedCostGLInventoryEntry(
// bill,
// allocatedLandedCost,
// allocatedEntry
// );
// const costEntry = this.getLandedCostGLCostEntry(
// bill,
// allocatedLandedCost,
// fromTransactionEntry
// );
// return [inventoryEntry, costEntry];
// }
// );
if (!accountId) {
throw new Error(
`Cannot determine GL account for landed cost allocate entry (entryId: ${allocatedEntry.entryId})`,
);
}
// /**
// * Compose the landed cost GL entries.
// * @param {BillLandedCost} allocatedLandedCost
// * @param {Bill} bill
// * @param {ILandedCostTransactionEntry} fromTransactionEntry
// * @returns {ILedgerEntry[]}
// */
// public getLandedCostGLEntries = (
// allocatedLandedCost: BillLandedCost,
// bill: Bill,
// fromTransactionEntry: ILandedCostTransactionEntry
// ): ILedgerEntry[] => {
// const getEntry = this.getLandedCostGLAllocateEntry(
// bill,
// allocatedLandedCost,
// fromTransactionEntry
// );
// return allocatedLandedCost.allocateEntries.map(getEntry).flat();
// };
const localAmount =
allocatedEntry.cost * (allocatedLandedCost.exchangeRate || 1);
// /**
// * Retrieves the landed cost GL ledger.
// * @param {BillLandedCost} allocatedLandedCost
// * @param {Bill} bill
// * @param {ILandedCostTransactionEntry} fromTransactionEntry
// * @returns {ILedger}
// */
// public getLandedCostLedger = (
// allocatedLandedCost: BillLandedCost,
// bill: Bill,
// fromTransactionEntry: ILandedCostTransactionEntry
// ): ILedger => {
// const entries = this.getLandedCostGLEntries(
// allocatedLandedCost,
// bill,
// fromTransactionEntry
// );
// return new Ledger(entries);
// };
return {
...commonEntry,
debit: localAmount,
accountId,
index: index + 1,
indexGroup: 10,
itemId: itemEntry?.itemId,
accountNormal: AccountNormal.DEBIT,
};
}
// /**
// * Writes landed cost GL entries to the storage layer.
// * @param {number} tenantId -
// */
// public writeLandedCostGLEntries = async (
// allocatedLandedCost: BillLandedCost,
// bill: Bill,
// fromTransactionEntry: ILandedCostTransactionEntry,
// trx?: Knex.Transaction
// ) => {
// const ledgerEntries = this.getLandedCostGLEntries(
// allocatedLandedCost,
// bill,
// fromTransactionEntry
// );
// await this.ledgerRepository.saveLedgerEntries(ledgerEntries, trx);
// };
/**
* Retrieves the landed cost GL cost entry (credit to cost account).
*/
private getLandedCostGLCostEntry(
bill: Bill,
allocatedLandedCost: BillLandedCost,
): ILedgerEntry {
const commonEntry = this.getLandedCostGLCommonEntry(
bill,
allocatedLandedCost,
);
// /**
// * Generates and writes GL entries of the given landed cost.
// * @param {number} billLandedCostId
// * @param {Knex.Transaction} trx
// */
// public createLandedCostGLEntries = async (
// billLandedCostId: number,
// trx?: Knex.Transaction
// ) => {
// // Retrieve the bill landed cost transacion with associated
// // allocated entries and items.
// const allocatedLandedCost = await this.billLandedCostModel().query(trx)
// .findById(billLandedCostId)
// .withGraphFetched('bill')
// .withGraphFetched('allocateEntries.itemEntry.item');
return {
...commonEntry,
credit: allocatedLandedCost.localAmount,
accountId: allocatedLandedCost.costAccountId,
index: 1,
indexGroup: 20,
accountNormal: AccountNormal.CREDIT,
};
}
// // Retrieve the allocated from transactione entry.
// const transactionEntry = await this.getLandedCostEntry(
// allocatedLandedCost.fromTransactionType,
// allocatedLandedCost.fromTransactionId,
// allocatedLandedCost.fromTransactionEntryId
// );
// // Writes the given landed cost GL entries to the storage layer.
// await this.writeLandedCostGLEntries(
// allocatedLandedCost,
// allocatedLandedCost.bill,
// transactionEntry,
// trx
// );
// };
/**
* Composes the landed cost GL entries.
*/
public getLandedCostGLEntries(
allocatedLandedCost: BillLandedCost,
bill: Bill,
): ILedgerEntry[] {
const inventoryEntries = allocatedLandedCost.allocateEntries.map(
(allocatedEntry, index) =>
this.getLandedCostGLInventoryEntry(
bill,
allocatedLandedCost,
allocatedEntry,
index,
),
);
const costEntry = this.getLandedCostGLCostEntry(bill, allocatedLandedCost);
// /**
// * Reverts GL entries of the given allocated landed cost transaction.
// * @param {number} tenantId
// * @param {number} landedCostId
// * @param {Knex.Transaction} trx
// */
// public revertLandedCostGLEntries = async (
// landedCostId: number,
// trx: Knex.Transaction
// ) => {
// await this.journalService.revertJournalTransactions(
// landedCostId,
// 'LandedCost',
// trx
// );
// };
// }
return [...inventoryEntries, costEntry];
}
/**
* Retrieves the landed cost GL ledger.
*/
public getLandedCostLedger(
allocatedLandedCost: BillLandedCost,
bill: Bill,
): Ledger {
const entries = this.getLandedCostGLEntries(allocatedLandedCost, bill);
return new Ledger(entries);
}
/**
* Generates and writes GL entries of the given landed cost.
*/
public createLandedCostGLEntries = async (
billLandedCostId: number,
trx?: Knex.Transaction,
) => {
const allocatedLandedCost = await this.billLandedCostModel()
.query(trx)
.findById(billLandedCostId)
.withGraphFetched('bill')
.withGraphFetched('allocateEntries.itemEntry.item');
if (!allocatedLandedCost?.bill) {
throw new Error('BillLandedCost or associated Bill not found');
}
const ledger = this.getLandedCostLedger(
allocatedLandedCost,
allocatedLandedCost.bill,
);
await this.ledgerStorage.commit(ledger, trx);
};
/**
* Reverts GL entries of the given allocated landed cost transaction.
*/
public revertLandedCostGLEntries = async (
landedCostId: number,
trx?: Knex.Transaction,
) => {
await this.ledgerStorage.deleteByReference(landedCostId, 'LandedCost', trx);
};
}

View File

@@ -3,14 +3,15 @@ import {
IAllocatedLandedCostDeletedPayload,
} from '../types/BillLandedCosts.types';
import { OnEvent } from '@nestjs/event-emitter';
// import { LandedCostGLEntries } from './LandedCostGLEntries.service';
import { Injectable } from '@nestjs/common';
import { events } from '@/common/events/events';
import { LandedCostGLEntriesService } from './LandedCostGLEntries.service';
@Injectable()
export class LandedCostGLEntriesSubscriber {
constructor() // private readonly billLandedCostGLEntries: LandedCostGLEntries,
{}
constructor(
private readonly landedCostGLEntries: LandedCostGLEntriesService,
) {}
/**
* Writes GL entries once landed cost transaction created.
@@ -21,10 +22,10 @@ export class LandedCostGLEntriesSubscriber {
billLandedCost,
trx,
}: IAllocatedLandedCostCreatedPayload) {
// await this.billLandedCostGLEntries.createLandedCostGLEntries(
// billLandedCost.id,
// trx
// );
await this.landedCostGLEntries.createLandedCostGLEntries(
billLandedCost.id,
trx,
);
}
/**
@@ -32,13 +33,13 @@ export class LandedCostGLEntriesSubscriber {
* @param {IAllocatedLandedCostDeletedPayload} payload -
*/
@OnEvent(events.billLandedCost.onDeleted)
async revertGLEnteriesOnceLandedCostDeleted({
async revertGLEntriesOnceLandedCostDeleted({
oldBillLandedCost,
trx,
}: IAllocatedLandedCostDeletedPayload) {
// await this.billLandedCostGLEntries.revertLandedCostGLEntries(
// oldBillLandedCost.id,
// trx
// );
await this.landedCostGLEntries.revertLandedCostGLEntries(
oldBillLandedCost.id,
trx,
);
}
}

View File

@@ -17,7 +17,7 @@ export class BillBranchValidateSubscriber {
* Validate branch existance on bill creating.
* @param {IBillCreatingPayload} payload
*/
@OnEvent(events.bill.onCreating)
@OnEvent(events.bill.onCreating, { suppressErrors: false })
async validateBranchExistanceOnBillCreating({
billDTO,
}: IBillCreatingPayload) {
@@ -30,7 +30,7 @@ export class BillBranchValidateSubscriber {
* Validate branch existance once bill editing.
* @param {IBillEditingPayload} payload
*/
@OnEvent(events.bill.onEditing)
@OnEvent(events.bill.onEditing, { suppressErrors: false })
async validateBranchExistanceOnBillEditing({ billDTO }: IBillEditingPayload) {
await this.validateBranchExistance.validateTransactionBranchWhenActive(
billDTO.branchId,

View File

@@ -14,7 +14,7 @@ export class CashflowBranchDTOValidatorSubscriber {
* Validate branch existance once cashflow transaction creating.
* @param {ICommandCashflowCreatingPayload} payload
*/
@OnEvent(events.cashflow.onTransactionCreating)
@OnEvent(events.cashflow.onTransactionCreating, { suppressErrors: false })
async validateBranchExistanceOnCashflowTransactionCreating({
newTransactionDTO,
}: ICommandCashflowCreatingPayload) {

View File

@@ -15,13 +15,13 @@ import {
export class ContactBranchValidateSubscriber {
constructor(
private readonly validateBranchExistance: ValidateBranchExistance,
) { }
) {}
/**
* Validate branch existance on customer creating.
* @param {ICustomerEventCreatingPayload} payload
*/
@OnEvent(events.customers.onCreating)
@OnEvent(events.customers.onCreating, { suppressErrors: false })
async validateBranchExistanceOnCustomerCreating({
customerDTO,
}: ICustomerEventCreatingPayload) {
@@ -37,7 +37,7 @@ export class ContactBranchValidateSubscriber {
* Validate branch existance once customer opening balance editing.
* @param {ICustomerOpeningBalanceEditingPayload} payload
*/
@OnEvent(events.customers.onOpeningBalanceChanging)
@OnEvent(events.customers.onOpeningBalanceChanging, { suppressErrors: false })
async validateBranchExistanceOnCustomerOpeningBalanceEditing({
openingBalanceEditDTO,
}: ICustomerOpeningBalanceEditingPayload) {
@@ -52,7 +52,7 @@ export class ContactBranchValidateSubscriber {
* Validates the branch existance on vendor creating.
* @param {IVendorEventCreatingPayload} payload
*/
@OnEvent(events.vendors.onCreating)
@OnEvent(events.vendors.onCreating, { suppressErrors: false })
async validateBranchExistanceonVendorCreating({
vendorDTO,
}: IVendorEventCreatingPayload) {
@@ -68,7 +68,7 @@ export class ContactBranchValidateSubscriber {
* Validate branch existance once the vendor opening balance editing.
* @param {IVendorOpeningBalanceEditingPayload} payload
*/
@OnEvent(events.vendors.onOpeningBalanceChanging)
@OnEvent(events.vendors.onOpeningBalanceChanging, { suppressErrors: false })
async validateBranchExistanceOnVendorOpeningBalanceEditing({
openingBalanceEditDTO,
}: IVendorOpeningBalanceEditingPayload) {

View File

@@ -15,7 +15,7 @@ export class CreditNoteBranchValidateSubscriber {
* Validate branch existance on credit note creating.
* @param {ICreditNoteCreatingPayload} payload
*/
@OnEvent(events.creditNote.onCreating)
@OnEvent(events.creditNote.onCreating, { suppressErrors: false })
async validateBranchExistanceOnCreditCreating({
creditNoteDTO,
}: ICreditNoteCreatingPayload) {
@@ -28,7 +28,7 @@ export class CreditNoteBranchValidateSubscriber {
* Validate branch existance once credit note editing.
* @param {ICreditNoteEditingPayload} payload
*/
@OnEvent(events.creditNote.onEditing)
@OnEvent(events.creditNote.onEditing, { suppressErrors: false })
async validateBranchExistanceOnCreditEditing({
creditNoteEditDTO,
}: ICreditNoteEditingPayload) {

View File

@@ -14,7 +14,7 @@ export class CreditNoteRefundBranchValidateSubscriber {
* Validate branch existance on refund credit note creating.
* @param {IRefundCreditNoteCreatingPayload} payload
*/
@OnEvent(events.creditNote.onRefundCreating)
@OnEvent(events.creditNote.onRefundCreating, { suppressErrors: false })
async validateBranchExistanceOnCreditRefundCreating({
newCreditNoteDTO,
}: IRefundCreditNoteCreatingPayload) {

View File

@@ -16,7 +16,7 @@ export class ExpenseBranchValidateSubscriber {
* Validate branch existance once expense transaction creating.
* @param {IExpenseCreatingPayload} payload
*/
@OnEvent(events.expenses.onCreating)
@OnEvent(events.expenses.onCreating, { suppressErrors: false })
async validateBranchExistanceOnExpenseCreating({
expenseDTO,
}: IExpenseCreatingPayload) {
@@ -29,7 +29,7 @@ export class ExpenseBranchValidateSubscriber {
* Validate branch existance once expense transaction editing.
* @param {IExpenseEventEditingPayload} payload
*/
@OnEvent(events.expenses.onEditing)
@OnEvent(events.expenses.onEditing, { suppressErrors: false })
async validateBranchExistanceOnExpenseEditing({
expenseDTO,
}: IExpenseEventEditingPayload) {

View File

@@ -14,7 +14,7 @@ export class InventoryAdjustmentBranchValidateSubscriber {
* Validate branch existance on inventory adjustment creating.
* @param {IInventoryAdjustmentCreatingPayload} payload
*/
@OnEvent(events.inventoryAdjustment.onQuickCreating)
@OnEvent(events.inventoryAdjustment.onQuickCreating, { suppressErrors: false })
async validateBranchExistanceOnInventoryCreating({
quickAdjustmentDTO,
}: IInventoryAdjustmentCreatingPayload) {

View File

@@ -17,7 +17,7 @@ export class InvoiceBranchValidateSubscriber {
* Validate branch existance on invoice creating.
* @param {ISaleInvoiceCreatingPayload} payload
*/
@OnEvent(events.saleInvoice.onCreating)
@OnEvent(events.saleInvoice.onCreating, { suppressErrors: false })
async validateBranchExistanceOnInvoiceCreating({
saleInvoiceDTO,
}: ISaleInvoiceCreatingPaylaod) {
@@ -30,7 +30,7 @@ export class InvoiceBranchValidateSubscriber {
* Validate branch existance once invoice editing.
* @param {ISaleInvoiceEditingPayload} payload
*/
@OnEvent(events.saleInvoice.onEditing)
@OnEvent(events.saleInvoice.onEditing, { suppressErrors: false })
async validateBranchExistanceOnInvoiceEditing({
saleInvoiceDTO,
}: ISaleInvoiceEditingPayload) {

View File

@@ -17,7 +17,7 @@ export class PaymentMadeBranchValidateSubscriber {
* Validate branch existance on estimate creating.
* @param {ISaleEstimateCreatedPayload} payload
*/
@OnEvent(events.billPayment.onCreating)
@OnEvent(events.billPayment.onCreating, { suppressErrors: false })
async validateBranchExistanceOnPaymentCreating({
billPaymentDTO,
}: IBillPaymentCreatingPayload) {
@@ -30,7 +30,7 @@ export class PaymentMadeBranchValidateSubscriber {
* Validate branch existance once estimate editing.
* @param {ISaleEstimateEditingPayload} payload
*/
@OnEvent(events.billPayment.onEditing)
@OnEvent(events.billPayment.onEditing, { suppressErrors: false })
async validateBranchExistanceOnPaymentEditing({
billPaymentDTO,
}: IBillPaymentEditingPayload) {

View File

@@ -17,7 +17,7 @@ export class PaymentReceiveBranchValidateSubscriber {
* Validate branch existance on estimate creating.
* @param {IPaymentReceivedCreatingPayload} payload
*/
@OnEvent(events.paymentReceive.onCreating)
@OnEvent(events.paymentReceive.onCreating, { suppressErrors: false })
async validateBranchExistanceOnPaymentCreating({
paymentReceiveDTO,
}: IPaymentReceivedCreatingPayload) {
@@ -30,7 +30,7 @@ export class PaymentReceiveBranchValidateSubscriber {
* Validate branch existance once estimate editing.
* @param {IPaymentReceivedEditingPayload} payload
*/
@OnEvent(events.paymentReceive.onEditing)
@OnEvent(events.paymentReceive.onEditing, { suppressErrors: false })
async validateBranchExistanceOnPaymentEditing({
paymentReceiveDTO,
}: IPaymentReceivedEditingPayload) {

View File

@@ -17,7 +17,7 @@ export class SaleEstimateBranchValidateSubscriber {
* Validate branch existance on estimate creating.
* @param {ISaleEstimateCreatedPayload} payload
*/
@OnEvent(events.saleEstimate.onCreating)
@OnEvent(events.saleEstimate.onCreating, { suppressErrors: false })
async validateBranchExistanceOnEstimateCreating({
estimateDTO,
}: ISaleEstimateCreatingPayload) {
@@ -30,7 +30,7 @@ export class SaleEstimateBranchValidateSubscriber {
* Validate branch existance once estimate editing.
* @param {ISaleEstimateEditingPayload} payload
*/
@OnEvent(events.saleEstimate.onEditing)
@OnEvent(events.saleEstimate.onEditing, { suppressErrors: false })
async validateBranchExistanceOnEstimateEditing({
estimateDTO,
}: ISaleEstimateEditingPayload) {

View File

@@ -17,7 +17,7 @@ export class SaleReceiptBranchValidateSubscriber {
* Validate branch existance on estimate creating.
* @param {ISaleReceiptCreatingPayload} payload
*/
@OnEvent(events.saleReceipt.onCreating)
@OnEvent(events.saleReceipt.onCreating, { suppressErrors: false })
async validateBranchExistanceOnInvoiceCreating({
saleReceiptDTO,
}: ISaleReceiptCreatingPayload) {
@@ -30,7 +30,7 @@ export class SaleReceiptBranchValidateSubscriber {
* Validate branch existance once estimate editing.
* @param {ISaleReceiptEditingPayload} payload
*/
@OnEvent(events.saleReceipt.onEditing)
@OnEvent(events.saleReceipt.onEditing, { suppressErrors: false })
async validateBranchExistanceOnInvoiceEditing({
saleReceiptDTO,
}: ISaleReceiptEditingPayload) {

View File

@@ -17,7 +17,7 @@ export class VendorCreditBranchValidateSubscriber {
* Validate branch existance on estimate creating.
* @param {ISaleEstimateCreatedPayload} payload
*/
@OnEvent(events.vendorCredit.onCreating)
@OnEvent(events.vendorCredit.onCreating, { suppressErrors: false })
async validateBranchExistanceOnCreditCreating({
vendorCreditCreateDTO,
}: IVendorCreditCreatingPayload) {
@@ -30,7 +30,7 @@ export class VendorCreditBranchValidateSubscriber {
* Validate branch existance once estimate editing.
* @param {ISaleEstimateEditingPayload} payload
*/
@OnEvent(events.vendorCredit.onEditing)
@OnEvent(events.vendorCredit.onEditing, { suppressErrors: false })
async validateBranchExistanceOnCreditEditing({
vendorCreditDTO,
}: IVendorCreditEditingPayload) {

View File

@@ -14,7 +14,7 @@ export class VendorCreditRefundBranchValidateSubscriber {
* Validate branch existance on refund credit note creating.
* @param {IRefundVendorCreditCreatingPayload} payload
*/
@OnEvent(events.vendorCredit.onRefundCreating)
@OnEvent(events.vendorCredit.onRefundCreating, { suppressErrors: false })
async validateBranchExistanceOnCreditRefundCreating({
refundVendorCreditDTO,
}: IRefundVendorCreditCreatingPayload) {

View File

@@ -22,6 +22,7 @@ export abstract class BaseCommand extends CommandRunner {
},
migrations: {
directory: this.configService.get('systemDatabase.migrationDir'),
loadExtensions: ['.js'],
},
seeds: {
directory: this.configService.get('systemDatabase.seedsDir'),
@@ -43,6 +44,7 @@ export abstract class BaseCommand extends CommandRunner {
},
migrations: {
directory: this.configService.get('tenantDatabase.migrationsDir') || './src/database/migrations',
loadExtensions: ['.js'],
},
seeds: {
directory: this.configService.get('tenantDatabase.seedsDir') || './src/database/seeds/core',

View File

@@ -1,117 +1,103 @@
// import { Service, Inject } from 'typedi';
// import { AccountNormal, ICustomer, ILedgerEntry } from '@/interfaces';
// import Ledger from '@/services/Accounting/Ledger';
import { Injectable } from '@nestjs/common';
import { AccountNormal } from '@/interfaces/Account';
import { ILedgerEntry } from '@/modules/Ledger/types/Ledger.types';
import { Ledger } from '@/modules/Ledger/Ledger';
import { Customer } from './models/Customer';
// @Service()
// export class CustomerGLEntries {
// /**
// * Retrieves the customer opening balance common entry attributes.
// * @param {ICustomer} customer
// */
// private getCustomerOpeningGLCommonEntry = (customer: ICustomer) => {
// return {
// exchangeRate: customer.openingBalanceExchangeRate,
// currencyCode: customer.currencyCode,
@Injectable()
export class CustomerGLEntries {
/**
* Retrieves the customer opening balance common entry attributes.
*/
private getCustomerOpeningGLCommonEntry = (customer: Customer) => {
return {
exchangeRate: customer.openingBalanceExchangeRate,
currencyCode: customer.currencyCode,
// transactionType: 'CustomerOpeningBalance',
// transactionId: customer.id,
transactionType: 'CustomerOpeningBalance',
transactionId: customer.id,
// date: customer.openingBalanceAt,
// userId: customer.userId,
// contactId: customer.id,
date: customer.openingBalanceAt,
contactId: customer.id,
// credit: 0,
// debit: 0,
credit: 0,
debit: 0,
// branchId: customer.openingBalanceBranchId,
// };
// };
branchId: customer.openingBalanceBranchId,
};
};
// /**
// * Retrieves the customer opening GL credit entry.
// * @param {number} ARAccountId
// * @param {ICustomer} customer
// * @returns {ILedgerEntry}
// */
// private getCustomerOpeningGLCreditEntry = (
// ARAccountId: number,
// customer: ICustomer
// ): ILedgerEntry => {
// const commonEntry = this.getCustomerOpeningGLCommonEntry(customer);
/**
* Retrieves the customer opening GL credit entry.
*/
private getCustomerOpeningGLCreditEntry = (
ARAccountId: number,
customer: Customer
): ILedgerEntry => {
const commonEntry = this.getCustomerOpeningGLCommonEntry(customer);
// return {
// ...commonEntry,
// credit: 0,
// debit: customer.localOpeningBalance,
// accountId: ARAccountId,
// accountNormal: AccountNormal.DEBIT,
// index: 1,
// };
// };
return {
...commonEntry,
credit: 0,
debit: customer.localOpeningBalance,
accountId: ARAccountId,
accountNormal: AccountNormal.DEBIT,
index: 1,
};
};
// /**
// * Retrieves the customer opening GL debit entry.
// * @param {number} incomeAccountId
// * @param {ICustomer} customer
// * @returns {ILedgerEntry}
// */
// private getCustomerOpeningGLDebitEntry = (
// incomeAccountId: number,
// customer: ICustomer
// ): ILedgerEntry => {
// const commonEntry = this.getCustomerOpeningGLCommonEntry(customer);
/**
* Retrieves the customer opening GL debit entry.
*/
private getCustomerOpeningGLDebitEntry = (
incomeAccountId: number,
customer: Customer
): ILedgerEntry => {
const commonEntry = this.getCustomerOpeningGLCommonEntry(customer);
// return {
// ...commonEntry,
// credit: customer.localOpeningBalance,
// debit: 0,
// accountId: incomeAccountId,
// accountNormal: AccountNormal.CREDIT,
return {
...commonEntry,
credit: customer.localOpeningBalance,
debit: 0,
accountId: incomeAccountId,
accountNormal: AccountNormal.CREDIT,
// index: 2,
// };
// };
index: 2,
};
};
// /**
// * Retrieves the customer opening GL entries.
// * @param {number} ARAccountId
// * @param {number} incomeAccountId
// * @param {ICustomer} customer
// * @returns {ILedgerEntry[]}
// */
// public getCustomerOpeningGLEntries = (
// ARAccountId: number,
// incomeAccountId: number,
// customer: ICustomer
// ) => {
// const debitEntry = this.getCustomerOpeningGLDebitEntry(
// incomeAccountId,
// customer
// );
// const creditEntry = this.getCustomerOpeningGLCreditEntry(
// ARAccountId,
// customer
// );
// return [debitEntry, creditEntry];
// };
/**
* Retrieves the customer opening GL entries.
*/
public getCustomerOpeningGLEntries = (
ARAccountId: number,
incomeAccountId: number,
customer: Customer
) => {
const debitEntry = this.getCustomerOpeningGLDebitEntry(
incomeAccountId,
customer
);
const creditEntry = this.getCustomerOpeningGLCreditEntry(
ARAccountId,
customer
);
return [debitEntry, creditEntry];
};
// /**
// * Retrieves the customer opening balance ledger.
// * @param {number} ARAccountId
// * @param {number} incomeAccountId
// * @param {ICustomer} customer
// * @returns {ILedger}
// */
// public getCustomerOpeningLedger = (
// ARAccountId: number,
// incomeAccountId: number,
// customer: ICustomer
// ) => {
// const entries = this.getCustomerOpeningGLEntries(
// ARAccountId,
// incomeAccountId,
// customer
// );
// return new Ledger(entries);
// };
// }
/**
* Retrieves the customer opening balance ledger.
*/
public getCustomerOpeningLedger = (
ARAccountId: number,
incomeAccountId: number,
customer: Customer
) => {
const entries = this.getCustomerOpeningGLEntries(
ARAccountId,
incomeAccountId,
customer
);
return new Ledger(entries);
};
}

View File

@@ -1,90 +1,84 @@
// import { Knex } from 'knex';
// import LedgerStorageService from '@/services/Accounting/LedgerStorageService';
// import HasTenancyService from '@/services/Tenancy/TenancyService';
// import { Service, Inject } from 'typedi';
// import { CustomerGLEntries } from './CustomerGLEntries';
import { Knex } from 'knex';
import { Inject, Injectable } from '@nestjs/common';
import { LedgerStorageService } from '@/modules/Ledger/LedgerStorage.service';
import { AccountRepository } from '@/modules/Accounts/repositories/Account.repository';
import { CustomerGLEntries } from './CustomerGLEntries';
import { Customer } from './models/Customer';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { Account } from '../Accounts/models/Account.model';
// @Service()
// export class CustomerGLEntriesStorage {
// @Inject()
// private tenancy: HasTenancyService;
@Injectable()
export class CustomerGLEntriesStorage {
constructor(
private readonly ledgerStorage: LedgerStorageService,
private readonly accountRepository: AccountRepository,
private readonly customerGLEntries: CustomerGLEntries,
// @Inject()
// private ledegrRepository: LedgerStorageService;
@Inject(Account.name)
private readonly accountModel: TenantModelProxy<typeof Account>,
// @Inject()
// private customerGLEntries: CustomerGLEntries;
@Inject(Customer.name)
private readonly customerModel: TenantModelProxy<typeof Customer>,
) { }
// /**
// * Customer opening balance journals.
// * @param {number} tenantId
// * @param {number} customerId
// * @param {Knex.Transaction} trx
// */
// public writeCustomerOpeningBalance = async (
// tenantId: number,
// customerId: number,
// trx?: Knex.Transaction
// ) => {
// const { Customer } = this.tenancy.models(tenantId);
// const { accountRepository } = this.tenancy.repositories(tenantId);
/**
* Customer opening balance journals.
*/
public writeCustomerOpeningBalance = async (
customerId: number,
trx?: Knex.Transaction,
) => {
const customer = await this.customerModel()
.query(trx)
.findById(customerId);
// const customer = await Customer.query(trx).findById(customerId);
// Finds the income account.
const incomeAccount = await this.accountModel()
.query(trx)
.findOne({ slug: 'other-income' });
// // Finds the income account.
// const incomeAccount = await accountRepository.findOne({
// slug: 'other-income',
// });
// // Find or create the A/R account.
// const ARAccount = await accountRepository.findOrCreateAccountReceivable(
// customer.currencyCode,
// {},
// trx
// );
// // Retrieves the customer opening balance ledger.
// const ledger = this.customerGLEntries.getCustomerOpeningLedger(
// ARAccount.id,
// incomeAccount.id,
// customer
// );
// // Commits the ledger entries to the storage.
// await this.ledegrRepository.commit(tenantId, ledger, trx);
// };
// Find or create the A/R account.
const ARAccount =
await this.accountRepository.findOrCreateAccountReceivable(
customer.currencyCode,
{},
trx,
);
// Retrieves the customer opening balance ledger.
const ledger = this.customerGLEntries.getCustomerOpeningLedger(
ARAccount.id,
incomeAccount.id,
customer,
);
// Commits the ledger entries to the storage.
await this.ledgerStorage.commit(ledger, trx);
};
// /**
// * Reverts the customer opening balance GL entries.
// * @param {number} tenantId
// * @param {number} customerId
// * @param {Knex.Transaction} trx
// */
// public revertCustomerOpeningBalance = async (
// tenantId: number,
// customerId: number,
// trx?: Knex.Transaction
// ) => {
// await this.ledegrRepository.deleteByReference(
// tenantId,
// customerId,
// 'CustomerOpeningBalance',
// trx
// );
// };
/**
* Reverts the customer opening balance GL entries.
*/
public revertCustomerOpeningBalance = async (
customerId: number,
trx?: Knex.Transaction,
) => {
await this.ledgerStorage.deleteByReference(
customerId,
'CustomerOpeningBalance',
trx,
);
};
// /**
// * Writes the customer opening balance GL entries.
// * @param {number} tenantId
// * @param {number} customerId
// * @param {Knex.Transaction} trx
// */
// public rewriteCustomerOpeningBalance = async (
// tenantId: number,
// customerId: number,
// trx?: Knex.Transaction
// ) => {
// // Reverts the customer opening balance entries.
// await this.revertCustomerOpeningBalance(tenantId, customerId, trx);
/**
* Writes the customer opening balance GL entries.
*/
public rewriteCustomerOpeningBalance = async (
customerId: number,
trx?: Knex.Transaction,
) => {
// Reverts the customer opening balance entries.
await this.revertCustomerOpeningBalance(customerId, trx);
// // Write the customer opening balance entries.
// await this.writeCustomerOpeningBalance(tenantId, customerId, trx);
// };
// }
// Write the customer opening balance entries.
await this.writeCustomerOpeningBalance(customerId, trx);
};
}

View File

@@ -9,10 +9,7 @@ import {
Query,
} from '@nestjs/common';
import { CustomersApplication } from './CustomersApplication.service';
import {
ICustomerOpeningBalanceEditDTO,
ICustomersFilter,
} from './types/Customers.types';
import { CustomerOpeningBalanceEditDto } from './dtos/CustomerOpeningBalanceEdit.dto';
import {
ApiOperation,
ApiResponse,
@@ -106,7 +103,7 @@ export class CustomersController {
})
editOpeningBalance(
@Param('id') customerId: number,
@Body() openingBalanceDTO: ICustomerOpeningBalanceEditDTO,
@Body() openingBalanceDTO: CustomerOpeningBalanceEditDto,
) {
return this.customersApplication.editOpeningBalance(
customerId,

View File

@@ -18,9 +18,19 @@ import { GetCustomers } from './queries/GetCustomers.service';
import { DynamicListModule } from '../DynamicListing/DynamicList.module';
import { BulkDeleteCustomersService } from './BulkDeleteCustomers.service';
import { ValidateBulkDeleteCustomersService } from './ValidateBulkDeleteCustomers.service';
import { LedgerModule } from '../Ledger/Ledger.module';
import { AccountsModule } from '../Accounts/Accounts.module';
import { CustomerGLEntries } from './CustomerGLEntries';
import { CustomerGLEntriesStorage } from './CustomerGLEntriesStorage';
import { CustomerWriteGLOpeningBalanceSubscriber } from './subscribers/CustomerGLEntriesSubscriber';
@Module({
imports: [TenancyDatabaseModule, DynamicListModule],
imports: [
TenancyDatabaseModule,
DynamicListModule,
LedgerModule,
AccountsModule,
],
controllers: [CustomersController],
providers: [
ActivateCustomer,
@@ -41,6 +51,9 @@ import { ValidateBulkDeleteCustomersService } from './ValidateBulkDeleteCustomer
GetCustomers,
BulkDeleteCustomersService,
ValidateBulkDeleteCustomersService,
CustomerGLEntries,
CustomerGLEntriesStorage,
CustomerWriteGLOpeningBalanceSubscriber,
],
})
export class CustomersModule {}

View File

@@ -4,10 +4,7 @@ import { CreateCustomer } from './commands/CreateCustomer.service';
import { EditCustomer } from './commands/EditCustomer.service';
import { DeleteCustomer } from './commands/DeleteCustomer.service';
import { EditOpeningBalanceCustomer } from './commands/EditOpeningBalanceCustomer.service';
import {
ICustomerOpeningBalanceEditDTO,
ICustomersFilter,
} from './types/Customers.types';
import { CustomerOpeningBalanceEditDto } from './dtos/CustomerOpeningBalanceEdit.dto';
import { CreateCustomerDto } from './dtos/CreateCustomer.dto';
import { EditCustomerDto } from './dtos/EditCustomer.dto';
import { GetCustomers } from './queries/GetCustomers.service';
@@ -18,12 +15,12 @@ import { ValidateBulkDeleteCustomersService } from './ValidateBulkDeleteCustomer
@Injectable()
export class CustomersApplication {
constructor(
private getCustomerService: GetCustomerService,
private createCustomerService: CreateCustomer,
private editCustomerService: EditCustomer,
private deleteCustomerService: DeleteCustomer,
private editOpeningBalanceService: EditOpeningBalanceCustomer,
private getCustomersService: GetCustomers,
private readonly getCustomerService: GetCustomerService,
private readonly createCustomerService: CreateCustomer,
private readonly editCustomerService: EditCustomer,
private readonly deleteCustomerService: DeleteCustomer,
private readonly editOpeningBalanceService: EditOpeningBalanceCustomer,
private readonly getCustomersService: GetCustomers,
private readonly bulkDeleteCustomersService: BulkDeleteCustomersService,
private readonly validateBulkDeleteCustomersService: ValidateBulkDeleteCustomersService,
) {}
@@ -72,7 +69,7 @@ export class CustomersApplication {
*/
public editOpeningBalance = (
customerId: number,
openingBalanceEditDTO: ICustomerOpeningBalanceEditDTO,
openingBalanceEditDTO: CustomerOpeningBalanceEditDto,
) => {
return this.editOpeningBalanceService.changeOpeningBalance(
customerId,
@@ -82,7 +79,7 @@ export class CustomersApplication {
/**
* Retrieve customers paginated list.
* @param {ICustomersFilter} filter - Cusotmers filter.
* @param {GetCustomersQueryDto} filter - Cusotmers filter.
*/
public getCustomers = (filterDTO: GetCustomersQueryDto) => {
return this.getCustomersService.getCustomersList(filterDTO);

View File

@@ -31,7 +31,7 @@ export class CreateCustomer {
/**
* Creates a new customer.
* @param {ICustomerNewDTO} customerDTO
* @param {CreateCustomerDto} customerDTO
* @return {Promise<ICustomer>}
*/
public async createCustomer(

View File

@@ -1,10 +1,10 @@
import { Inject, Injectable } from '@nestjs/common';
import { Knex } from 'knex';
import {
ICustomerOpeningBalanceEditDTO,
ICustomerOpeningBalanceEditedPayload,
ICustomerOpeningBalanceEditingPayload,
} from '../types/Customers.types';
import { CustomerOpeningBalanceEditDto } from '../dtos/CustomerOpeningBalanceEdit.dto';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
import { Customer } from '../models/Customer';
@@ -29,11 +29,11 @@ export class EditOpeningBalanceCustomer {
/**
* Changes the opening balance of the given customer.
* @param {number} customerId - Customer ID.
* @param {ICustomerOpeningBalanceEditDTO} openingBalanceEditDTO
* @param {CustomerOpeningBalanceEditDto} openingBalanceEditDTO
*/
public async changeOpeningBalance(
customerId: number,
openingBalanceEditDTO: ICustomerOpeningBalanceEditDTO,
openingBalanceEditDTO: CustomerOpeningBalanceEditDto,
): Promise<Customer> {
// Retrieves the old customer or throw not found error.
const oldCustomer = await this.customerModel()

View File

@@ -4,6 +4,7 @@ import {
IsNotEmpty,
IsNumber,
IsString,
ValidateIf,
} from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { IsOptional, ToNumber } from '@/common/decorators/Validators';
@@ -40,10 +41,11 @@ export class CreateCustomerDto extends ContactAddressDto {
@ApiProperty({
required: false,
description: 'Opening balance date',
description: 'Opening balance date (required when openingBalance is provided)',
example: '2024-01-01',
})
@IsOptional()
@ValidateIf((o) => o.openingBalance != null)
@IsNotEmpty({ message: 'openingBalanceAt is required when openingBalance is provided' })
@IsString()
openingBalanceAt?: string;

View File

@@ -0,0 +1,44 @@
import { IsNotEmpty, IsNumber, IsString } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { IsOptional, ToNumber } from '@/common/decorators/Validators';
export class CustomerOpeningBalanceEditDto {
@ApiProperty({
required: true,
description: 'Opening balance',
example: 5000.0,
})
@IsNumber()
@IsNotEmpty()
@ToNumber()
openingBalance: number;
@ApiProperty({
required: false,
description: 'Opening balance date',
example: '2024-01-01',
})
@IsOptional()
@IsString()
openingBalanceAt?: string;
@ApiProperty({
required: false,
description: 'Opening balance exchange rate',
example: 1.0,
})
@IsOptional()
@IsNumber()
@ToNumber()
openingBalanceExchangeRate?: number;
@ApiProperty({
required: false,
description: 'Opening balance branch ID',
example: 101,
})
@IsOptional()
@IsNumber()
@ToNumber()
openingBalanceBranchId?: number;
}

View File

@@ -1,91 +1,63 @@
// import { Service, Inject } from 'typedi';
// import {
// ICustomerEventCreatedPayload,
// ICustomerEventDeletedPayload,
// ICustomerOpeningBalanceEditedPayload,
// } from '@/interfaces';
// import events from '@/subscribers/events';
// import { CustomerGLEntriesStorage } from '../CustomerGLEntriesStorage';
import { Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import {
ICustomerEventCreatedPayload,
ICustomerEventDeletedPayload,
ICustomerOpeningBalanceEditedPayload,
} from '../types/Customers.types';
import { events } from '@/common/events/events';
import { CustomerGLEntriesStorage } from '../CustomerGLEntriesStorage';
// @Service()
// export class CustomerWriteGLOpeningBalanceSubscriber {
// @Inject()
// private customerGLEntries: CustomerGLEntriesStorage;
@Injectable()
export class CustomerWriteGLOpeningBalanceSubscriber {
constructor(private readonly customerGLEntries: CustomerGLEntriesStorage) { }
// /**
// * Attaches events with handlers.
// */
// public attach(bus) {
// bus.subscribe(
// events.customers.onCreated,
// this.handleWriteOpenBalanceEntries
// );
// bus.subscribe(
// events.customers.onDeleted,
// this.handleRevertOpeningBalanceEntries
// );
// bus.subscribe(
// events.customers.onOpeningBalanceChanged,
// this.handleRewriteOpeningEntriesOnChanged
// );
// }
/**
* Handles the writing opening balance journal entries once the customer created.
*/
@OnEvent(events.customers.onCreated)
public async handleWriteOpenBalanceEntries({
customer,
trx,
}: ICustomerEventCreatedPayload) {
// Writes the customer opening balance journal entries.
if (customer.openingBalance) {
await this.customerGLEntries.writeCustomerOpeningBalance(
customer.id,
trx,
);
}
}
// /**
// * Handles the writing opening balance journal entries once the customer created.
// * @param {ICustomerEventCreatedPayload} payload -
// */
// private handleWriteOpenBalanceEntries = async ({
// tenantId,
// customer,
// trx,
// }: ICustomerEventCreatedPayload) => {
// // Writes the customer opening balance journal entries.
// if (customer.openingBalance) {
// await this.customerGLEntries.writeCustomerOpeningBalance(
// tenantId,
// customer.id,
// trx
// );
// }
// };
/**
* Handles the deleting opening balance journal entries once the customer deleted.
*/
@OnEvent(events.customers.onDeleted)
public async handleRevertOpeningBalanceEntries({
customerId,
trx,
}: ICustomerEventDeletedPayload) {
await this.customerGLEntries.revertCustomerOpeningBalance(customerId, trx);
}
// /**
// * Handles the deleting opeing balance journal entrise once the customer deleted.
// * @param {ICustomerEventDeletedPayload} payload -
// */
// private handleRevertOpeningBalanceEntries = async ({
// tenantId,
// customerId,
// trx,
// }: ICustomerEventDeletedPayload) => {
// await this.customerGLEntries.revertCustomerOpeningBalance(
// tenantId,
// customerId,
// trx
// );
// };
// /**
// * Handles the rewrite opening balance entries once opening balnace changed.
// * @param {ICustomerOpeningBalanceEditedPayload} payload -
// */
// private handleRewriteOpeningEntriesOnChanged = async ({
// tenantId,
// customer,
// trx,
// }: ICustomerOpeningBalanceEditedPayload) => {
// if (customer.openingBalance) {
// await this.customerGLEntries.rewriteCustomerOpeningBalance(
// tenantId,
// customer.id,
// trx
// );
// } else {
// await this.customerGLEntries.revertCustomerOpeningBalance(
// tenantId,
// customer.id,
// trx
// );
// }
// };
// }
/**
* Handles the rewrite opening balance entries once opening balance changed.
*/
@OnEvent(events.customers.onOpeningBalanceChanged)
public async handleRewriteOpeningEntriesOnChanged({
customer,
trx,
}: ICustomerOpeningBalanceEditedPayload) {
if (customer.openingBalance) {
await this.customerGLEntries.rewriteCustomerOpeningBalance(
customer.id,
trx,
);
} else {
await this.customerGLEntries.revertCustomerOpeningBalance(
customer.id,
trx,
);
}
}
}

View File

@@ -4,6 +4,7 @@ import { IContactAddressDTO } from '@/modules/Contacts/types/Contacts.types';
import { IDynamicListFilter } from '@/modules/DynamicListing/DynamicFilter/DynamicFilter.types';
import { IFilterMeta, IPaginationMeta } from '@/interfaces/Model';
import { CreateCustomerDto } from '../dtos/CreateCustomer.dto';
import { CustomerOpeningBalanceEditDto } from '../dtos/CustomerOpeningBalanceEdit.dto';
import { EditCustomerDto } from '../dtos/EditCustomer.dto';
// Customer Interfaces.
@@ -113,23 +114,16 @@ export enum VendorAction {
View = 'View',
}
export interface ICustomerOpeningBalanceEditDTO {
openingBalance: number;
openingBalanceAt: Date | string;
openingBalanceExchangeRate: number;
openingBalanceBranchId?: number;
}
export interface ICustomerOpeningBalanceEditingPayload {
oldCustomer: Customer;
openingBalanceEditDTO: ICustomerOpeningBalanceEditDTO;
openingBalanceEditDTO: CustomerOpeningBalanceEditDto;
trx?: Knex.Transaction;
}
export interface ICustomerOpeningBalanceEditedPayload {
customer: Customer;
oldCustomer: Customer;
openingBalanceEditDTO: ICustomerOpeningBalanceEditDTO;
openingBalanceEditDTO: CustomerOpeningBalanceEditDto;
trx: Knex.Transaction;
}

View File

@@ -4,14 +4,15 @@ import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
import { TableSheetPdf } from './TableSheetPdf';
import { TemplateInjectableModule } from '@/modules/TemplateInjectable/TemplateInjectable.module';
import { ChromiumlyTenancyModule } from '@/modules/ChromiumlyTenancy/ChromiumlyTenancy.module';
import { InventoryCostModule } from '@/modules/InventoryCost/InventoryCost.module';
@Module({
imports: [TemplateInjectableModule, ChromiumlyTenancyModule],
providers: [
FinancialSheetMeta,
TenancyContext,
TableSheetPdf,
imports: [
TemplateInjectableModule,
ChromiumlyTenancyModule,
InventoryCostModule,
],
providers: [FinancialSheetMeta, TenancyContext, TableSheetPdf],
exports: [FinancialSheetMeta, TableSheetPdf],
})
export class FinancialSheetCommonModule {}

View File

@@ -1,10 +1,14 @@
import { Injectable } from '@nestjs/common';
import { IFinancialSheetCommonMeta } from '../types/Report.types';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
import { InventoryComputeCostService } from '@/modules/InventoryCost/commands/InventoryComputeCost.service';
@Injectable()
export class FinancialSheetMeta {
constructor(private readonly tenancyContext: TenancyContext) {}
constructor(
private readonly tenancyContext: TenancyContext,
private readonly inventoryComputeCostService: InventoryComputeCostService,
) {}
/**
* Retrieves the common meta data of the financial sheet.
@@ -17,10 +21,8 @@ export class FinancialSheetMeta {
const baseCurrency = tenantMetadata.baseCurrency;
const dateFormat = tenantMetadata.dateFormat;
// const isCostComputeRunning =
// this.inventoryService.isItemsCostComputeRunning();
const isCostComputeRunning = false;
const isCostComputeRunning =
await this.inventoryComputeCostService.isItemsCostComputeRunning();
return {
organizationName,

View File

@@ -24,14 +24,14 @@ export class BalanceSheetQueryDto extends FinancialSheetBranchesQueryDto {
displayColumnsType: 'total' | 'date_periods' = 'total';
@ApiProperty({
enum: ['day', 'month', 'year'],
enum: ['day', 'month', 'year', 'quarter'],
default: 'year',
description: 'Time period for column display',
})
@IsString()
@IsOptional()
@IsEnum(['day', 'month', 'year'])
displayColumnsBy: 'day' | 'month' | 'year' = 'year';
@IsEnum(['day', 'month', 'year', 'quarter'])
displayColumnsBy: 'day' | 'month' | 'year' | 'quarter' = 'year';
@ApiProperty({
description: 'Start date for the balance sheet period',

View File

@@ -34,13 +34,13 @@ export class CashFlowStatementQueryDto extends FinancialSheetBranchesQueryDto {
@ApiProperty({
description: 'Display columns by time period',
required: false,
enum: ['day', 'month', 'year'],
enum: ['day', 'month', 'year', 'quarter'],
default: 'year',
})
@IsString()
@IsOptional()
@IsEnum(['day', 'month', 'year'])
displayColumnsBy: 'day' | 'month' | 'year' = 'year';
@IsEnum(['day', 'month', 'year', 'quarter'])
displayColumnsBy: 'day' | 'month' | 'year' | 'quarter' = 'year';
@ApiProperty({
description: 'Type of column display',

View File

@@ -64,10 +64,10 @@ export class ProfitLossSheetQueryDto extends FinancialSheetBranchesQueryDto {
displayColumnsType: 'total' | 'date_periods';
@IsString()
@IsEnum(['day', 'month', 'year'])
@IsEnum(['day', 'month', 'year', 'quarter'])
@IsOptional()
@ApiProperty({ description: 'How to display columns' })
displayColumnsBy: 'day' | 'month' | 'year' = 'year';
displayColumnsBy: 'day' | 'month' | 'year' | 'quarter' = 'year';
@Transform(({ value }) => parseBoolean(value, false))
@IsBoolean()

View File

@@ -16,6 +16,8 @@ import {
ComputeItemCostQueue,
WriteInventoryTransactionsGLEntriesQueue,
} from './types/InventoryCost.types';
import { BullBoardModule } from '@bull-board/nestjs';
import { BullMQAdapter } from '@bull-board/api/bullMQAdapter';
import { BullModule } from '@nestjs/bullmq';
import { InventoryAverageCostMethodService } from './commands/InventoryAverageCostMethod.service';
import { InventoryItemCostService } from './commands/InventoryCosts.service';
@@ -39,6 +41,14 @@ const models = [
BullModule.registerQueue({
name: WriteInventoryTransactionsGLEntriesQueue,
}),
BullBoardModule.forFeature({
name: ComputeItemCostQueue,
adapter: BullMQAdapter,
}),
BullBoardModule.forFeature({
name: WriteInventoryTransactionsGLEntriesQueue,
adapter: BullMQAdapter,
}),
forwardRef(() => SaleInvoicesModule),
ImportModule,
],
@@ -56,7 +66,7 @@ const models = [
InventoryItemCostService,
InventoryItemOpeningAvgCostService,
InventoryCostSubscriber,
GetItemsInventoryValuationListService
GetItemsInventoryValuationListService,
],
exports: [
...models,
@@ -64,6 +74,6 @@ const models = [
InventoryItemCostService,
InventoryComputeCostService,
],
controllers: [InventoryCostController]
controllers: [InventoryCostController],
})
export class InventoryCostModule {}

View File

@@ -7,11 +7,7 @@ import * as moment from 'moment';
import { TenantJobPayload } from '@/interfaces/Tenant';
import { InventoryComputeCostService } from '../commands/InventoryComputeCost.service';
import { events } from '@/common/events/events';
import {
ComputeItemCostQueue,
ComputeItemCostQueueJob,
} from '../types/InventoryCost.types';
import { Process } from '@nestjs/bull';
import { ComputeItemCostQueue } from '../types/InventoryCost.types';
interface ComputeItemCostJobPayload extends TenantJobPayload {
itemId: number;
@@ -39,7 +35,6 @@ export class ComputeItemCostProcessor extends WorkerHost {
* Process the compute item cost job.
* @param {Job<ComputeItemCostJobPayload>} job - The job to process
*/
@Process(ComputeItemCostQueueJob)
@UseCls()
async process(job: Job<ComputeItemCostJobPayload>) {
const { itemId, startingDate, organizationId, userId } = job.data;
@@ -68,6 +63,12 @@ export class ComputeItemCostProcessor extends WorkerHost {
} catch (error) {
console.error(`[error] Error computing item cost for item ${itemId}:`, error);
console.error('Error stack:', error instanceof Error ? error.stack : 'No stack trace');
// Reset cost_compute_running when job fails so it does not stay true indefinitely
try {
await this.inventoryComputeCostService.markItemsCostComputeRunning(false);
} catch (markError) {
console.error('[error] Failed to mark cost compute as not running:', markError);
}
throw error;
}
}

View File

@@ -1,8 +1,4 @@
import { Process } from '@nestjs/bull';
import {
WriteInventoryTransactionsGLEntriesQueue,
WriteInventoryTransactionsGLEntriesQueueJob,
} from '../types/InventoryCost.types';
import { WriteInventoryTransactionsGLEntriesQueue } from '../types/InventoryCost.types';
import { Processor, WorkerHost } from '@nestjs/bullmq';
import { Scope } from '@nestjs/common';
@@ -15,6 +11,5 @@ export class WriteInventoryTransactionsGLEntriesProcessor extends WorkerHost {
super();
}
@Process(WriteInventoryTransactionsGLEntriesQueueJob)
async process() {}
}

View File

@@ -34,6 +34,7 @@ import {
BulkDeleteItemsDto,
ValidateBulkDeleteItemsResponseDto,
} from './dtos/BulkDeleteItems.dto';
import { ItemApiErrorResponseDto } from './dtos/ItemErrorResponse.dto';
@Controller('/items')
@ApiTags('Items')
@@ -45,6 +46,7 @@ import {
@ApiExtraModels(ItemEstimatesResponseDto)
@ApiExtraModels(ItemReceiptsResponseDto)
@ApiExtraModels(ValidateBulkDeleteItemsResponseDto)
@ApiExtraModels(ItemApiErrorResponseDto)
@ApiCommonHeaders()
export class ItemsController extends TenantController {
constructor(private readonly itemsApplication: ItemsApplicationService) {
@@ -147,6 +149,13 @@ export class ItemsController extends TenantController {
status: 200,
description: 'The item has been successfully updated.',
})
@ApiResponse({
status: 400,
description: 'Validation error. Possible error types: ITEM_NAME_EXISTS, INVENTORY_ACCOUNT_CANNOT_MODIFIED, TYPE_CANNOT_CHANGE_WITH_ITEM_HAS_TRANSACTIONS, etc.',
schema: {
$ref: getSchemaPath(ItemApiErrorResponseDto),
},
})
@ApiResponse({ status: 404, description: 'The item not found.' })
@ApiParam({
name: 'id',
@@ -204,6 +213,13 @@ export class ItemsController extends TenantController {
status: 200,
description: 'The item has been successfully created.',
})
@ApiResponse({
status: 400,
description: 'Validation error. Possible error types: ITEM_NAME_EXISTS, ITEM_CATEOGRY_NOT_FOUND, COST_ACCOUNT_NOT_COGS, SELL_ACCOUNT_NOT_INCOME, INVENTORY_ACCOUNT_NOT_INVENTORY, INCOME_ACCOUNT_REQUIRED_WITH_SELLABLE_ITEM, COST_ACCOUNT_REQUIRED_WITH_PURCHASABLE_ITEM, etc.',
schema: {
$ref: getSchemaPath(ItemApiErrorResponseDto),
},
})
// @UsePipes(new ZodValidationPipe(createItemSchema))
async createItem(
@Body() createItemDto: CreateItemDto,
@@ -219,6 +235,13 @@ export class ItemsController extends TenantController {
status: 200,
description: 'The item has been successfully deleted.',
})
@ApiResponse({
status: 400,
description: 'Cannot delete item. Possible error types: ITEM_HAS_ASSOCIATED_TRANSACTINS, ITEM_HAS_ASSOCIATED_INVENTORY_ADJUSTMENT, etc.',
schema: {
$ref: getSchemaPath(ItemApiErrorResponseDto),
},
})
@ApiResponse({ status: 404, description: 'The item not found.' })
@ApiParam({
name: 'id',

View File

@@ -0,0 +1,112 @@
import { ApiProperty } from '@nestjs/swagger';
/**
* Item API Error Types
* These error types are returned when item operations fail validation
*/
export enum ItemErrorType {
/** Item name already exists in the system */
ItemNameExists = 'ITEM_NAME_EXISTS',
/** Item category was not found */
ItemCategoryNotFound = 'ITEM_CATEOGRY_NOT_FOUND',
/** Cost account is not a Cost of Goods Sold account */
CostAccountNotCogs = 'COST_ACCOUNT_NOT_COGS',
/** Cost account was not found */
CostAccountNotFound = 'COST_ACCOUNT_NOT_FOUMD',
/** Sell account was not found */
SellAccountNotFound = 'SELL_ACCOUNT_NOT_FOUND',
/** Sell account is not an income account */
SellAccountNotIncome = 'SELL_ACCOUNT_NOT_INCOME',
/** Inventory account was not found */
InventoryAccountNotFound = 'INVENTORY_ACCOUNT_NOT_FOUND',
/** Account is not an inventory type account */
InventoryAccountNotInventory = 'INVENTORY_ACCOUNT_NOT_INVENTORY',
/** Multiple items have associated transactions */
ItemsHaveAssociatedTransactions = 'ITEMS_HAVE_ASSOCIATED_TRANSACTIONS',
/** Item has associated transactions (singular) */
ItemHasAssociatedTransactions = 'ITEM_HAS_ASSOCIATED_TRANSACTINS',
/** Item has associated inventory adjustments */
ItemHasAssociatedInventoryAdjustment = 'ITEM_HAS_ASSOCIATED_INVENTORY_ADJUSTMENT',
/** Cannot change item type to inventory */
ItemCannotChangeInventoryType = 'ITEM_CANNOT_CHANGE_INVENTORY_TYPE',
/** Cannot change type when item has transactions */
TypeCannotChangeWithItemHasTransactions = 'TYPE_CANNOT_CHANGE_WITH_ITEM_HAS_TRANSACTIONS',
/** Inventory account cannot be modified */
InventoryAccountCannotModified = 'INVENTORY_ACCOUNT_CANNOT_MODIFIED',
/** Purchase tax rate was not found */
PurchaseTaxRateNotFound = 'PURCHASE_TAX_RATE_NOT_FOUND',
/** Sell tax rate was not found */
SellTaxRateNotFound = 'SELL_TAX_RATE_NOT_FOUND',
/** Income account is required for sellable items */
IncomeAccountRequiredWithSellableItem = 'INCOME_ACCOUNT_REQUIRED_WITH_SELLABLE_ITEM',
/** Cost account is required for purchasable items */
CostAccountRequiredWithPurchasableItem = 'COST_ACCOUNT_REQUIRED_WITH_PURCHASABLE_ITEM',
/** Item not found */
NotFound = 'NOT_FOUND',
/** Items not found */
ItemsNotFound = 'ITEMS_NOT_FOUND',
}
/**
* Item API Error Response
* Returned when an item operation fails
*/
export class ItemErrorResponseDto {
@ApiProperty({
description: 'HTTP status code',
example: 400,
})
statusCode: number;
@ApiProperty({
description: 'Error type identifier',
enum: ItemErrorType,
example: ItemErrorType.ItemNameExists,
})
type: ItemErrorType;
@ApiProperty({
description: 'Human-readable error message',
example: 'The item name is already exist.',
required: false,
nullable: true,
})
message: string | null;
@ApiProperty({
description: 'Additional error payload data',
required: false,
nullable: true,
})
payload: any;
}
/**
* Item API Error Response Wrapper
*/
export class ItemApiErrorResponseDto {
@ApiProperty({
description: 'Array of error details',
type: [ItemErrorResponseDto],
})
errors: ItemErrorResponseDto[];
}

View File

@@ -35,7 +35,7 @@ export const transformLedgerEntryToTransaction = (
itemId: entry.itemId,
projectId: entry.projectId,
// costable: entry.costable,
costable: entry.costable,
taxRateId: entry.taxRateId,
taxRate: entry.taxRate,

View File

@@ -3,6 +3,8 @@ import { GetCurrentOrganizationService } from './queries/GetCurrentOrganization.
import { BuildOrganizationService } from './commands/BuildOrganization.service';
import { UpdateOrganizationService } from './commands/UpdateOrganization.service';
import { OrganizationController } from './Organization.controller';
import { BullBoardModule } from '@bull-board/nestjs';
import { BullMQAdapter } from '@bull-board/api/bullMQAdapter';
import { BullModule } from '@nestjs/bullmq';
import { OrganizationBuildQueue } from './Organization.types';
import { OrganizationBuildProcessor } from './processors/OrganizationBuild.processor';
@@ -25,10 +27,14 @@ import { GetBuildOrganizationBuildJob } from './commands/GetBuildOrganizationJob
OrganizationBaseCurrencyLocking,
SyncSystemUserToTenantService,
SyncSystemUserToTenantSubscriber,
GetBuildOrganizationBuildJob
GetBuildOrganizationBuildJob,
],
imports: [
BullModule.registerQueue({ name: OrganizationBuildQueue }),
BullBoardModule.forFeature({
name: OrganizationBuildQueue,
adapter: BullMQAdapter,
}),
TenantDBManagerModule,
],
controllers: [OrganizationController],

View File

@@ -2,10 +2,8 @@ import { Processor, WorkerHost } from '@nestjs/bullmq';
import { Scope } from '@nestjs/common';
import { Job } from 'bullmq';
import { ClsService, UseCls } from 'nestjs-cls';
import { Process } from '@nestjs/bull';
import {
OrganizationBuildQueue,
OrganizationBuildQueueJob,
OrganizationBuildQueueJobPayload,
} from '../Organization.types';
import { BuildOrganizationService } from '../commands/BuildOrganization.service';
@@ -22,7 +20,6 @@ export class OrganizationBuildProcessor extends WorkerHost {
super();
}
@Process(OrganizationBuildQueueJob)
@UseCls()
async process(job: Job<OrganizationBuildQueueJobPayload>) {
console.log('Processing organization build job:', job.id);

View File

@@ -1,4 +1,6 @@
import { Module } from '@nestjs/common';
import { BullBoardModule } from '@bull-board/nestjs';
import { BullAdapter } from '@bull-board/api/bullAdapter';
import { BullModule } from '@nestjs/bull';
import { PaymentReceivesController } from './PaymentsReceived.controller';
import { PaymentReceivesApplication } from './PaymentReceived.application';
@@ -95,6 +97,10 @@ import { ValidateBulkDeletePaymentReceivedService } from './ValidateBulkDeletePa
DynamicListModule,
MailModule,
BullModule.registerQueue({ name: SEND_PAYMENT_RECEIVED_MAIL_QUEUE }),
BullBoardModule.forFeature({
name: SEND_PAYMENT_RECEIVED_MAIL_QUEUE,
adapter: BullAdapter,
}),
],
})
export class PaymentsReceivedModule {}

View File

@@ -1,4 +1,6 @@
import { Module } from '@nestjs/common';
import { BullBoardModule } from '@bull-board/nestjs';
import { BullAdapter } from '@bull-board/api/bullAdapter';
import { BullModule } from '@nestjs/bull';
import { TenancyContext } from '../Tenancy/TenancyContext.service';
import { TenancyDatabaseModule } from '../Tenancy/TenancyDB/TenancyDB.module';
@@ -54,6 +56,10 @@ import { SendSaleEstimateMailProcess } from './processes/SendSaleEstimateMail.pr
TemplateInjectableModule,
PdfTemplatesModule,
BullModule.registerQueue({ name: SendSaleEstimateMailQueue }),
BullBoardModule.forFeature({
name: SendSaleEstimateMailQueue,
adapter: BullAdapter,
}),
],
controllers: [SaleEstimatesController],
providers: [
@@ -99,4 +105,4 @@ import { SendSaleEstimateMailProcess } from './processes/SendSaleEstimateMail.pr
GetSaleEstimateMailTemplateService,
],
})
export class SaleEstimatesModule { }
export class SaleEstimatesModule {}

View File

@@ -45,6 +45,8 @@ import { SendSaleInvoiceMailCommon } from './commands/SendInvoiceInvoiceMailComm
import { DynamicListModule } from '../DynamicListing/DynamicList.module';
import { MailNotificationModule } from '../MailNotification/MailNotification.module';
import { SendSaleInvoiceMailProcessor } from './processors/SendSaleInvoiceMail.processor';
import { BullBoardModule } from '@bull-board/nestjs';
import { BullAdapter } from '@bull-board/api/bullAdapter';
import { BullModule } from '@nestjs/bull';
import { SendSaleInvoiceQueue } from './constants';
import { InvoicePaymentIntegrationSubscriber } from './subscribers/InvoicePaymentIntegrationSubscriber';
@@ -81,6 +83,10 @@ import { ValidateBulkDeleteSaleInvoicesService } from './ValidateBulkDeleteSaleI
forwardRef(() => PaymentLinksModule),
DynamicListModule,
BullModule.registerQueue({ name: SendSaleInvoiceQueue }),
BullBoardModule.forFeature({
name: SendSaleInvoiceQueue,
adapter: BullAdapter,
}),
],
controllers: [SaleInvoicesController],
providers: [
@@ -139,4 +145,4 @@ import { ValidateBulkDeleteSaleInvoicesService } from './ValidateBulkDeleteSaleI
SaleInvoicesImportable,
],
})
export class SaleInvoicesModule { }
export class SaleInvoicesModule {}

View File

@@ -114,7 +114,6 @@ export class SaleInvoicesCost {
*/
scheduleWriteJournalEntries(startingDate?: Date) {
// const agenda = Container.get('agenda');
// return agenda.schedule('in 3 seconds', 'rewrite-invoices-journal-entries', {
// startingDate,
// tenantId,
@@ -123,12 +122,11 @@ export class SaleInvoicesCost {
/**
* Writes cost GL entries from the inventory cost lots.
* @param {number} tenantId -
* @param {Date} startingDate -
* @param {Date} startingDate - Starting date.
* @returns {Promise<void>}
*/
public writeCostLotsGLEntries = (startingDate: Date) => {
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
public writeCostLotsGLEntries = async (startingDate: Date) => {
await this.uow.withTransaction(async (trx: Knex.Transaction) => {
// Triggers event `onInventoryCostLotsGLEntriesBeforeWrite`.
await this.eventPublisher.emitAsync(
events.inventory.onCostLotsGLEntriesBeforeWrite,
@@ -146,5 +144,10 @@ export class SaleInvoicesCost {
} as IInventoryCostLotsGLEntriesWriteEvent,
);
});
// Signal that cost entries have been written so cost_compute_running can be set to false.
await this.eventPublisher.emitAsync(
events.inventory.onInventoryCostEntriesWritten,
{},
);
};
}

View File

@@ -0,0 +1,143 @@
import * as R from 'ramda';
import { Knex } from 'knex';
import { Inject, Injectable } from '@nestjs/common';
import { TenantModelProxy } from '../System/models/TenantBaseModel';
import { InventoryCostLotTracker } from '../InventoryCost/models/InventoryCostLotTracker';
import { LedgerStorageService } from '../Ledger/LedgerStorage.service';
import { groupInventoryTransactionsByTypeId } from '../InventoryCost/utils';
import { Ledger } from '../Ledger/Ledger';
import { AccountNormal } from '@/interfaces/Account';
import { ILedgerEntry } from '../Ledger/types/Ledger.types';
import { increment } from '@/utils/increment';
@Injectable()
export class SaleReceiptCostGLEntries {
constructor(
private readonly ledgerStorage: LedgerStorageService,
@Inject(InventoryCostLotTracker.name)
private readonly inventoryCostLotTracker: TenantModelProxy<
typeof InventoryCostLotTracker
>,
) {}
/**
* Writes journal entries from sales receipts.
* @param {Date} startingDate - Starting date.
* @param {Knex.Transaction} trx - Transaction.
*/
public writeInventoryCostJournalEntries = async (
startingDate: Date,
trx?: Knex.Transaction,
): Promise<void> => {
const inventoryCostLotTrans = await this.inventoryCostLotTracker()
.query()
.where('direction', 'OUT')
.where('transaction_type', 'SaleReceipt')
.where('cost', '>', 0)
.modify('filterDateRange', startingDate)
.orderBy('date', 'ASC')
.withGraphFetched('receipt')
.withGraphFetched('item')
.withGraphFetched('itemEntry');
const ledger = this.getInventoryCostLotsLedger(inventoryCostLotTrans);
await this.ledgerStorage.commit(ledger, trx);
};
/**
* Retrieves the inventory cost lots ledger.
*/
private getInventoryCostLotsLedger = (
inventoryCostLots: InventoryCostLotTracker[],
) => {
const inventoryTransactions =
groupInventoryTransactionsByTypeId(inventoryCostLots);
const entries = inventoryTransactions
.map(this.getSaleReceiptCostGLEntries)
.flat();
return new Ledger(entries);
};
/**
* Builds the common GL entry fields for a sale receipt cost.
*/
private getReceiptCostGLCommonEntry = (
inventoryCostLot: InventoryCostLotTracker,
) => {
return {
currencyCode: inventoryCostLot.receipt.currencyCode,
exchangeRate: inventoryCostLot.receipt.exchangeRate,
transactionType: inventoryCostLot.transactionType,
transactionId: inventoryCostLot.transactionId,
transactionNumber: inventoryCostLot.receipt.receiptNumber,
referenceNumber: inventoryCostLot.receipt.referenceNo,
date: inventoryCostLot.date,
indexGroup: 20,
costable: true,
createdAt: inventoryCostLot.createdAt,
debit: 0,
credit: 0,
branchId: inventoryCostLot.receipt.branchId,
};
};
/**
* Retrieves the inventory cost GL entry for a single lot.
*/
private getInventoryCostGLEntry = R.curry(
(
getIndexIncrement: () => number,
inventoryCostLot: InventoryCostLotTracker,
): ILedgerEntry[] => {
const commonEntry = this.getReceiptCostGLCommonEntry(inventoryCostLot);
const costAccountId =
inventoryCostLot.costAccountId || inventoryCostLot.item.costAccountId;
const description = inventoryCostLot.itemEntry?.description || null;
const costEntry = {
...commonEntry,
debit: inventoryCostLot.cost,
accountId: costAccountId,
accountNormal: AccountNormal.DEBIT,
itemId: inventoryCostLot.itemId,
note: description,
index: getIndexIncrement(),
};
const inventoryEntry = {
...commonEntry,
credit: inventoryCostLot.cost,
accountId: inventoryCostLot.item.inventoryAccountId,
accountNormal: AccountNormal.DEBIT,
itemId: inventoryCostLot.itemId,
note: description,
index: getIndexIncrement(),
};
return [costEntry, inventoryEntry];
},
);
/**
* Builds GL entries for a group of sale receipt cost lots.
* - Cost of goods sold -> Debit
* - Inventory assets -> Credit
*/
public getSaleReceiptCostGLEntries = (
inventoryCostLots: InventoryCostLotTracker[],
): ILedgerEntry[] => {
const getIndexIncrement = increment(0);
const getInventoryLotEntry =
this.getInventoryCostGLEntry(getIndexIncrement);
return inventoryCostLots.map((t) => getInventoryLotEntry(t)).flat();
};
}

View File

@@ -1,4 +1,6 @@
import { Module } from '@nestjs/common';
import { BullBoardModule } from '@bull-board/nestjs';
import { BullAdapter } from '@bull-board/api/bullAdapter';
import { BullModule } from '@nestjs/bull';
import { SaleReceiptApplication } from './SaleReceiptApplication.service';
import { CreateSaleReceipt } from './commands/CreateSaleReceipt.service';
@@ -40,6 +42,8 @@ import { SaleReceiptsImportable } from './commands/SaleReceiptsImportable';
import { GetSaleReceiptMailStateService } from './queries/GetSaleReceiptMailState.service';
import { GetSaleReceiptMailTemplateService } from './queries/GetSaleReceiptMailTemplate.service';
import { SaleReceiptAutoIncrementSubscriber } from './subscribers/SaleReceiptAutoIncrementSubscriber';
import { SaleReceiptCostGLEntriesSubscriber } from './subscribers/SaleReceiptCostGLEntriesSubscriber';
import { SaleReceiptCostGLEntries } from './SaleReceiptCostGLEntries';
import { BulkDeleteSaleReceiptsService } from './BulkDeleteSaleReceipts.service';
import { ValidateBulkDeleteSaleReceiptsService } from './ValidateBulkDeleteSaleReceipts.service';
@@ -60,6 +64,10 @@ import { ValidateBulkDeleteSaleReceiptsService } from './ValidateBulkDeleteSaleR
MailModule,
MailNotificationModule,
BullModule.registerQueue({ name: SendSaleReceiptMailQueue }),
BullBoardModule.forFeature({
name: SendSaleReceiptMailQueue,
adapter: BullAdapter,
}),
],
providers: [
TenancyContext,
@@ -87,8 +95,10 @@ import { ValidateBulkDeleteSaleReceiptsService } from './ValidateBulkDeleteSaleR
GetSaleReceiptMailStateService,
GetSaleReceiptMailTemplateService,
SaleReceiptAutoIncrementSubscriber,
SaleReceiptCostGLEntries,
SaleReceiptCostGLEntriesSubscriber,
BulkDeleteSaleReceiptsService,
ValidateBulkDeleteSaleReceiptsService,
],
})
export class SaleReceiptsModule { }
export class SaleReceiptsModule {}

View File

@@ -1,148 +0,0 @@
// import { Service, Inject } from 'typedi';
// import * as R from 'ramda';
// import { Knex } from 'knex';
// import { AccountNormal, IInventoryLotCost, ILedgerEntry } from '@/interfaces';
// import { increment } from 'utils';
// import HasTenancyService from '@/services/Tenancy/TenancyService';
// import Ledger from '@/services/Accounting/Ledger';
// import LedgerStorageService from '@/services/Accounting/LedgerStorageService';
// import { groupInventoryTransactionsByTypeId } from '../../Inventory/utils';
// @Service()
// export class SaleReceiptCostGLEntries {
// @Inject()
// private tenancy: HasTenancyService;
// @Inject()
// private ledgerStorage: LedgerStorageService;
// /**
// * Writes journal entries from sales invoices.
// * @param {number} tenantId - The tenant id.
// * @param {Date} startingDate - Starting date.
// * @param {boolean} override
// */
// public writeInventoryCostJournalEntries = async (
// tenantId: number,
// startingDate: Date,
// trx?: Knex.Transaction
// ): Promise<void> => {
// const { InventoryCostLotTracker } = this.tenancy.models(tenantId);
// const inventoryCostLotTrans = await InventoryCostLotTracker.query()
// .where('direction', 'OUT')
// .where('transaction_type', 'SaleReceipt')
// .where('cost', '>', 0)
// .modify('filterDateRange', startingDate)
// .orderBy('date', 'ASC')
// .withGraphFetched('receipt')
// .withGraphFetched('item');
// const ledger = this.getInventoryCostLotsLedger(inventoryCostLotTrans);
// // Commit the ledger to the storage.
// await this.ledgerStorage.commit(tenantId, ledger, trx);
// };
// /**
// * Retrieves the inventory cost lots ledger.
// * @param {} inventoryCostLots
// * @returns {Ledger}
// */
// private getInventoryCostLotsLedger = (
// inventoryCostLots: IInventoryLotCost[]
// ) => {
// // Groups the inventory cost lots transactions.
// const inventoryTransactions =
// groupInventoryTransactionsByTypeId(inventoryCostLots);
// //
// const entries = inventoryTransactions
// .map(this.getSaleInvoiceCostGLEntries)
// .flat();
// return new Ledger(entries);
// };
// /**
// *
// * @param {IInventoryLotCost} inventoryCostLot
// * @returns {}
// */
// private getInvoiceCostGLCommonEntry = (
// inventoryCostLot: IInventoryLotCost
// ) => {
// return {
// currencyCode: inventoryCostLot.receipt.currencyCode,
// exchangeRate: inventoryCostLot.receipt.exchangeRate,
// transactionType: inventoryCostLot.transactionType,
// transactionId: inventoryCostLot.transactionId,
// date: inventoryCostLot.date,
// indexGroup: 20,
// costable: true,
// createdAt: inventoryCostLot.createdAt,
// debit: 0,
// credit: 0,
// branchId: inventoryCostLot.receipt.branchId,
// };
// };
// /**
// * Retrieves the inventory cost GL entry.
// * @param {IInventoryLotCost} inventoryLotCost
// * @returns {ILedgerEntry[]}
// */
// private getInventoryCostGLEntry = R.curry(
// (
// getIndexIncrement,
// inventoryCostLot: IInventoryLotCost
// ): ILedgerEntry[] => {
// const commonEntry = this.getInvoiceCostGLCommonEntry(inventoryCostLot);
// const costAccountId =
// inventoryCostLot.costAccountId || inventoryCostLot.item.costAccountId;
// // XXX Debit - Cost account.
// const costEntry = {
// ...commonEntry,
// debit: inventoryCostLot.cost,
// accountId: costAccountId,
// accountNormal: AccountNormal.DEBIT,
// itemId: inventoryCostLot.itemId,
// index: getIndexIncrement(),
// };
// // XXX Credit - Inventory account.
// const inventoryEntry = {
// ...commonEntry,
// credit: inventoryCostLot.cost,
// accountId: inventoryCostLot.item.inventoryAccountId,
// accountNormal: AccountNormal.DEBIT,
// itemId: inventoryCostLot.itemId,
// index: getIndexIncrement(),
// };
// return [costEntry, inventoryEntry];
// }
// );
// /**
// * Writes journal entries for given sale invoice.
// * -------
// * - Cost of goods sold -> Debit -> YYYY
// * - Inventory assets -> Credit -> YYYY
// * --------
// * @param {ISaleInvoice} saleInvoice
// * @param {JournalPoster} journal
// */
// public getSaleInvoiceCostGLEntries = (
// inventoryCostLots: IInventoryLotCost[]
// ): ILedgerEntry[] => {
// const getIndexIncrement = increment(0);
// const getInventoryLotEntry =
// this.getInventoryCostGLEntry(getIndexIncrement);
// return inventoryCostLots.map(getInventoryLotEntry).flat();
// };
// }

View File

@@ -1,36 +1,26 @@
// import { Inject, Service } from 'typedi';
// import events from '@/subscribers/events';
// import { IInventoryCostLotsGLEntriesWriteEvent } from '@/interfaces';
// import { SaleReceiptCostGLEntries } from '../SaleReceiptCostGLEntries';
import { Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { events } from '@/common/events/events';
import { IInventoryCostLotsGLEntriesWriteEvent } from '@/modules/InventoryCost/types/InventoryCost.types';
import { SaleReceiptCostGLEntries } from '../SaleReceiptCostGLEntries';
// @Service()
// export class SaleReceiptCostGLEntriesSubscriber {
// @Inject()
// private saleReceiptCostEntries: SaleReceiptCostGLEntries;
@Injectable()
export class SaleReceiptCostGLEntriesSubscriber {
constructor(
private readonly saleReceiptCostEntries: SaleReceiptCostGLEntries,
) {}
// /**
// * Attaches events.
// */
// public attach(bus) {
// bus.subscribe(
// events.inventory.onCostLotsGLEntriesWrite,
// this.writeJournalEntriesOnceWriteoffCreate
// );
// }
// /**
// * Writes the receipts cost GL entries once the inventory cost lots be written.
// * @param {IInventoryCostLotsGLEntriesWriteEvent}
// */
// private writeJournalEntriesOnceWriteoffCreate = async ({
// trx,
// startingDate,
// tenantId,
// }: IInventoryCostLotsGLEntriesWriteEvent) => {
// await this.saleReceiptCostEntries.writeInventoryCostJournalEntries(
// tenantId,
// startingDate,
// trx
// );
// };
// }
/**
* Writes the receipts cost GL entries once the inventory cost lots are written.
*/
@OnEvent(events.inventory.onCostLotsGLEntriesWrite)
async writeReceiptsCostEntriesOnCostLotsWritten({
trx,
startingDate,
}: IInventoryCostLotsGLEntriesWriteEvent) {
await this.saleReceiptCostEntries.writeInventoryCostJournalEntries(
startingDate,
trx,
);
}
}

View File

@@ -22,6 +22,7 @@ const providers = [
},
migrations: {
directory: configService.get('systemDatabase.migrationDir'),
loadExtensions: ['.js'],
},
seeds: {
directory: configService.get('systemDatabase.seedsDir'),

View File

@@ -33,6 +33,7 @@ export const TenancyDatabaseProxyProvider = ClsModule.forFeatureAsync({
},
migrations: {
directory: configService.get('tenantDatabase.migrationsDir'),
loadExtensions: ['.js'],
},
seeds: {
directory: configService.get('tenantDatabase.seedsDir'),

View File

@@ -1,115 +1,116 @@
// import { Service } from 'typedi';
// import { IVendor, AccountNormal, ILedgerEntry } from '@/interfaces';
// import Ledger from '@/services/Accounting/Ledger';
import { Injectable } from '@nestjs/common';
import { AccountNormal } from '@/interfaces/Account';
import { ILedgerEntry } from '@/modules/Ledger/types/Ledger.types';
import { Ledger } from '@/modules/Ledger/Ledger';
import { Vendor } from './models/Vendor';
// @Service()
// export class VendorGLEntries {
// /**
// * Retrieves the opening balance GL common entry.
// * @param {IVendor} vendor -
// */
// private getOpeningBalanceGLCommonEntry = (vendor: IVendor) => {
// return {
// exchangeRate: vendor.openingBalanceExchangeRate,
// currencyCode: vendor.currencyCode,
@Injectable()
export class VendorGLEntries {
/**
* Retrieves the opening balance GL common entry.
* @param {Vendor} vendor -
*/
private getOpeningBalanceGLCommonEntry = (vendor: Vendor) => {
return {
exchangeRate: vendor.openingBalanceExchangeRate,
currencyCode: vendor.currencyCode,
// transactionType: 'VendorOpeningBalance',
// transactionId: vendor.id,
transactionType: 'VendorOpeningBalance',
transactionId: vendor.id,
// date: vendor.openingBalanceAt,
// userId: vendor.userId,
// contactId: vendor.id,
date: vendor.openingBalanceAt,
contactId: vendor.id,
// credit: 0,
// debit: 0,
credit: 0,
debit: 0,
// branchId: vendor.openingBalanceBranchId,
// };
// };
branchId: vendor.openingBalanceBranchId,
};
};
// /**
// * Retrieves the opening balance GL debit entry.
// * @param {number} costAccountId -
// * @param {IVendor} vendor
// * @returns {ILedgerEntry}
// */
// private getOpeningBalanceGLDebitEntry = (
// costAccountId: number,
// vendor: IVendor
// ): ILedgerEntry => {
// const commonEntry = this.getOpeningBalanceGLCommonEntry(vendor);
/**
* Retrieves the opening balance GL debit entry.
* @param {number} costAccountId -
* @param {Vendor} vendor
* @returns {ILedgerEntry}
*/
private getOpeningBalanceGLDebitEntry = (
costAccountId: number,
vendor: Vendor
): ILedgerEntry => {
const commonEntry = this.getOpeningBalanceGLCommonEntry(vendor);
// return {
// ...commonEntry,
// accountId: costAccountId,
// accountNormal: AccountNormal.DEBIT,
// debit: vendor.localOpeningBalance,
// credit: 0,
// index: 2,
// };
// };
return {
...commonEntry,
accountId: costAccountId,
accountNormal: AccountNormal.DEBIT,
debit: vendor.localOpeningBalance,
credit: 0,
index: 2,
};
};
// /**
// * Retrieves the opening balance GL credit entry.
// * @param {number} APAccountId
// * @param {IVendor} vendor
// * @returns {ILedgerEntry}
// */
// private getOpeningBalanceGLCreditEntry = (
// APAccountId: number,
// vendor: IVendor
// ): ILedgerEntry => {
// const commonEntry = this.getOpeningBalanceGLCommonEntry(vendor);
/**
* Retrieves the opening balance GL credit entry.
* @param {number} APAccountId
* @param {Vendor} vendor
* @returns {ILedgerEntry}
*/
private getOpeningBalanceGLCreditEntry = (
APAccountId: number,
vendor: Vendor
): ILedgerEntry => {
const commonEntry = this.getOpeningBalanceGLCommonEntry(vendor);
// return {
// ...commonEntry,
// accountId: APAccountId,
// accountNormal: AccountNormal.CREDIT,
// credit: vendor.localOpeningBalance,
// index: 1,
// };
// };
return {
...commonEntry,
accountId: APAccountId,
accountNormal: AccountNormal.CREDIT,
credit: vendor.localOpeningBalance,
index: 1,
};
};
// /**
// * Retrieves the opening balance GL entries.
// * @param {number} APAccountId
// * @param {number} costAccountId -
// * @param {IVendor} vendor
// * @returns {ILedgerEntry[]}
// */
// public getOpeningBalanceGLEntries = (
// APAccountId: number,
// costAccountId: number,
// vendor: IVendor
// ): ILedgerEntry[] => {
// const debitEntry = this.getOpeningBalanceGLDebitEntry(
// costAccountId,
// vendor
// );
// const creditEntry = this.getOpeningBalanceGLCreditEntry(
// APAccountId,
// vendor
// );
// return [debitEntry, creditEntry];
// };
/**
* Retrieves the opening balance GL entries.
* @param {number} APAccountId
* @param {number} costAccountId -
* @param {Vendor} vendor
* @returns {ILedgerEntry[]}
*/
public getOpeningBalanceGLEntries = (
APAccountId: number,
costAccountId: number,
vendor: Vendor
): ILedgerEntry[] => {
const debitEntry = this.getOpeningBalanceGLDebitEntry(
costAccountId,
vendor
);
const creditEntry = this.getOpeningBalanceGLCreditEntry(
APAccountId,
vendor
);
return [debitEntry, creditEntry];
};
// /**
// * Retrieves the opening balance ledger.
// * @param {number} APAccountId
// * @param {number} costAccountId -
// * @param {IVendor} vendor
// * @returns {Ledger}
// */
// public getOpeningBalanceLedger = (
// APAccountId: number,
// costAccountId: number,
// vendor: IVendor
// ) => {
// const entries = this.getOpeningBalanceGLEntries(
// APAccountId,
// costAccountId,
// vendor
// );
// return new Ledger(entries);
// };
// }
/**
* Retrieves the opening balance ledger.
* @param {number} APAccountId
* @param {number} costAccountId -
* @param {Vendor} vendor
* @returns {Ledger}
*/
public getOpeningBalanceLedger = (
APAccountId: number,
costAccountId: number,
vendor: Vendor
) => {
const entries = this.getOpeningBalanceGLEntries(
APAccountId,
costAccountId,
vendor
);
return new Ledger(entries);
};
}

View File

@@ -1,88 +1,86 @@
// import { Knex } from 'knex';
// import { Service, Inject } from 'typedi';
// import LedgerStorageService from '@/services/Accounting/LedgerStorageService';
// import HasTenancyService from '@/services/Tenancy/TenancyService';
// import { VendorGLEntries } from './VendorGLEntries';
import { Knex } from 'knex';
import { Inject, Injectable } from '@nestjs/common';
import { LedgerStorageService } from '@/modules/Ledger/LedgerStorage.service';
import { AccountRepository } from '@/modules/Accounts/repositories/Account.repository';
import { VendorGLEntries } from './VendorGLEntries';
import { Vendor } from './models/Vendor';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
// @Service()
// export class VendorGLEntriesStorage {
// @Inject()
// private tenancy: HasTenancyService;
@Injectable()
export class VendorGLEntriesStorage {
constructor(
private readonly ledgerStorage: LedgerStorageService,
private readonly accountRepository: AccountRepository,
private readonly vendorGLEntries: VendorGLEntries,
// @Inject()
// private ledegrRepository: LedgerStorageService;
@Inject(Vendor.name)
private readonly vendorModel: TenantModelProxy<typeof Vendor>,
) { }
// @Inject()
// private vendorGLEntries: VendorGLEntries;
/**
* Vendor opening balance journals.
* @param {number} vendorId
* @param {Knex.Transaction} trx
*/
public writeVendorOpeningBalance = async (
vendorId: number,
trx?: Knex.Transaction,
) => {
const vendor = await this.vendorModel()
.query(trx)
.findById(vendorId);
// /**
// * Vendor opening balance journals.
// * @param {number} tenantId
// * @param {number} vendorId
// * @param {Knex.Transaction} trx
// */
// public writeVendorOpeningBalance = async (
// tenantId: number,
// vendorId: number,
// trx?: Knex.Transaction
// ) => {
// const { Vendor } = this.tenancy.models(tenantId);
// const { accountRepository } = this.tenancy.repositories(tenantId);
// Finds the expense account.
const expenseAccount = await this.accountRepository.findOrCreateOtherExpensesAccount(
{},
trx,
);
// Find or create the A/P account.
const APAccount =
await this.accountRepository.findOrCreateAccountsPayable(
vendor.currencyCode,
{},
trx,
);
// Retrieves the vendor opening balance ledger.
const ledger = this.vendorGLEntries.getOpeningBalanceLedger(
APAccount.id,
expenseAccount.id,
vendor,
);
// Commits the ledger entries to the storage.
await this.ledgerStorage.commit(ledger, trx);
};
// const vendor = await Vendor.query(trx).findById(vendorId);
/**
* Reverts the vendor opening balance GL entries.
* @param {number} vendorId
* @param {Knex.Transaction} trx
*/
public revertVendorOpeningBalance = async (
vendorId: number,
trx?: Knex.Transaction,
) => {
await this.ledgerStorage.deleteByReference(
vendorId,
'VendorOpeningBalance',
trx,
);
};
// // Finds the expense account.
// const expenseAccount = await accountRepository.findOne({
// slug: 'other-expenses',
// });
// // Find or create the A/P account.
// const APAccount = await accountRepository.findOrCreateAccountsPayable(
// vendor.currencyCode,
// {},
// trx
// );
// // Retrieves the vendor opening balance ledger.
// const ledger = this.vendorGLEntries.getOpeningBalanceLedger(
// APAccount.id,
// expenseAccount.id,
// vendor
// );
// // Commits the ledger entries to the storage.
// await this.ledegrRepository.commit(tenantId, ledger, trx);
// };
/**
* Writes the vendor opening balance GL entries.
* @param {number} vendorId
* @param {Knex.Transaction} trx
*/
public rewriteVendorOpeningBalance = async (
vendorId: number,
trx?: Knex.Transaction,
) => {
// Reverts the vendor opening balance entries first.
await this.revertVendorOpeningBalance(vendorId, trx);
// /**
// * Reverts the vendor opening balance GL entries.
// * @param {number} tenantId
// * @param {number} vendorId
// * @param {Knex.Transaction} trx
// */
// public revertVendorOpeningBalance = async (
// tenantId: number,
// vendorId: number,
// trx?: Knex.Transaction
// ) => {
// await this.ledegrRepository.deleteByReference(
// tenantId,
// vendorId,
// 'VendorOpeningBalance',
// trx
// );
// };
// /**
// * Writes the vendor opening balance GL entries.
// * @param {number} tenantId
// * @param {number} vendorId
// * @param {Knex.Transaction} trx
// */
// public rewriteVendorOpeningBalance = async (
// tenantId: number,
// vendorId: number,
// trx?: Knex.Transaction
// ) => {
// await this.writeVendorOpeningBalance(tenantId, vendorId, trx);
// await this.revertVendorOpeningBalance(tenantId, vendorId, trx);
// };
// }
// Write the vendor opening balance entries.
await this.writeVendorOpeningBalance(vendorId, trx);
};
}

View File

@@ -9,10 +9,7 @@ import {
Query,
} from '@nestjs/common';
import { VendorsApplication } from './VendorsApplication.service';
import {
IVendorOpeningBalanceEditDTO,
IVendorsFilter,
} from './types/Vendors.types';
import { VendorOpeningBalanceEditDto } from './dtos/VendorOpeningBalanceEdit.dto';
import {
ApiOperation,
ApiResponse,
@@ -68,7 +65,7 @@ export class VendorsController {
@ApiOperation({ summary: 'Edit the given vendor opening balance.' })
editOpeningBalance(
@Param('id') vendorId: number,
@Body() openingBalanceDTO: IVendorOpeningBalanceEditDTO,
@Body() openingBalanceDTO: VendorOpeningBalanceEditDto,
) {
return this.vendorsApplication.editOpeningBalance(
vendorId,

View File

@@ -18,9 +18,14 @@ import { VendorsExportable } from './VendorsExportable';
import { VendorsImportable } from './VendorsImportable';
import { BulkDeleteVendorsService } from './BulkDeleteVendors.service';
import { ValidateBulkDeleteVendorsService } from './ValidateBulkDeleteVendors.service';
import { LedgerModule } from '../Ledger/Ledger.module';
import { AccountsModule } from '../Accounts/Accounts.module';
import { VendorGLEntries } from './VendorGLEntries';
import { VendorGLEntriesStorage } from './VendorGLEntriesStorage';
import { VendorsWriteGLOpeningSubscriber } from './subscribers/VendorGLEntriesSubscriber';
@Module({
imports: [TenancyDatabaseModule, DynamicListModule],
imports: [TenancyDatabaseModule, DynamicListModule, LedgerModule, AccountsModule],
controllers: [VendorsController],
providers: [
ActivateVendorService,
@@ -38,7 +43,10 @@ import { ValidateBulkDeleteVendorsService } from './ValidateBulkDeleteVendors.se
TransformerInjectable,
TenancyContext,
VendorsExportable,
VendorsImportable
VendorsImportable,
VendorGLEntries,
VendorGLEntriesStorage,
VendorsWriteGLOpeningSubscriber,
],
})
export class VendorsModule {}
export class VendorsModule { }

View File

@@ -5,10 +5,7 @@ import { EditVendorService } from './commands/EditVendor.service';
import { DeleteVendorService } from './commands/DeleteVendor.service';
import { EditOpeningBalanceVendorService } from './commands/EditOpeningBalanceVendor.service';
import { GetVendorService } from './queries/GetVendor';
import {
IVendorOpeningBalanceEditDTO,
IVendorsFilter,
} from './types/Vendors.types';
import { VendorOpeningBalanceEditDto } from './dtos/VendorOpeningBalanceEdit.dto';
import { GetVendorsService } from './queries/GetVendors.service';
import { CreateVendorDto } from './dtos/CreateVendor.dto';
import { EditVendorDto } from './dtos/EditVendor.dto';
@@ -58,14 +55,14 @@ export class VendorsApplication {
}
/**
* Changes the opening balance of the given customer.
* @param {number} vendorId
* @param {IVendorOpeningBalanceEditDTO} openingBalanceEditDTO
* Changes the opening balance of the given vendor.
* @param {number} vendorId
* @param {VendorOpeningBalanceEditDto} openingBalanceEditDTO
* @returns {Promise<IVendor>}
*/
public editOpeningBalance(
vendorId: number,
openingBalanceEditDTO: IVendorOpeningBalanceEditDTO,
openingBalanceEditDTO: VendorOpeningBalanceEditDto,
) {
return this.editOpeningBalanceService.editOpeningBalance(
vendorId,
@@ -95,10 +92,7 @@ export class VendorsApplication {
vendorIds: number[],
options?: { skipUndeletable?: boolean },
) {
return this.bulkDeleteVendorsService.bulkDeleteVendors(
vendorIds,
options,
);
return this.bulkDeleteVendorsService.bulkDeleteVendors(vendorIds, options);
}
public validateBulkDeleteVendors(vendorIds: number[]) {

View File

@@ -2,10 +2,10 @@ import { Knex } from 'knex';
import { Inject, Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import {
IVendorOpeningBalanceEditDTO,
IVendorOpeningBalanceEditedPayload,
IVendorOpeningBalanceEditingPayload,
} from '../types/Vendors.types';
import { VendorOpeningBalanceEditDto } from '../dtos/VendorOpeningBalanceEdit.dto';
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
import { Vendor } from '../models/Vendor';
import { events } from '@/common/events/events';
@@ -29,12 +29,12 @@ export class EditOpeningBalanceVendorService {
/**
* Changes the opening balance of the given customer.
* @param {number} vendorId
* @param {IVendorOpeningBalanceEditDTO} openingBalanceEditDTO
* @param {VendorOpeningBalanceEditDto} openingBalanceEditDTO
* @returns {Promise<IVendor>}
*/
public async editOpeningBalance(
vendorId: number,
openingBalanceEditDTO: IVendorOpeningBalanceEditDTO,
openingBalanceEditDTO: VendorOpeningBalanceEditDto,
) {
// Retrieves the old vendor or throw not found error.
const oldVendor = await this.vendorModel()

View File

@@ -2,11 +2,13 @@ import { ApiProperty } from '@nestjs/swagger';
import {
IsISO8601,
IsInt,
IsNotEmpty,
IsNumber,
Min,
IsBoolean,
IsEmail,
IsString,
ValidateIf,
} from 'class-validator';
import { ContactAddressDto } from '@/modules/Customers/dtos/ContactAddress.dto';
import { IsOptional, ToNumber } from '@/common/decorators/Validators';
@@ -30,8 +32,12 @@ export class CreateVendorDto extends ContactAddressDto {
@ToNumber()
openingBalanceExchangeRate?: number;
@ApiProperty({ required: false, description: 'Date of the opening balance' })
@IsOptional()
@ApiProperty({
required: false,
description: 'Date of the opening balance (required when openingBalance is provided)',
})
@ValidateIf((o) => o.openingBalance != null)
@IsNotEmpty({ message: 'openingBalanceAt is required when openingBalance is provided' })
@IsISO8601()
openingBalanceAt?: Date;

View File

@@ -0,0 +1,44 @@
import { IsNotEmpty, IsNumber, IsString } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { IsOptional, ToNumber } from '@/common/decorators/Validators';
export class VendorOpeningBalanceEditDto {
@ApiProperty({
required: true,
description: 'Opening balance',
example: 5000.0,
})
@IsNumber()
@IsNotEmpty()
@ToNumber()
openingBalance: number;
@ApiProperty({
required: false,
description: 'Opening balance date',
example: '2024-01-01',
})
@IsOptional()
@IsString()
openingBalanceAt?: string;
@ApiProperty({
required: false,
description: 'Opening balance exchange rate',
example: 1.0,
})
@IsOptional()
@IsNumber()
@ToNumber()
openingBalanceExchangeRate?: number;
@ApiProperty({
required: false,
description: 'Opening balance branch ID',
example: 101,
})
@IsOptional()
@IsNumber()
@ToNumber()
openingBalanceBranchId?: number;
}

View File

@@ -36,6 +36,7 @@ export class Vendor extends TenantBaseModel {
openingBalance: number;
openingBalanceExchangeRate: number;
openingBalanceAt: Date | string;
openingBalanceBranchId?: number;
salutation: string;
firstName: string;

View File

@@ -1,91 +1,71 @@
// import { Inject, Service } from 'typedi';
// import events from '@/subscribers/events';
// import { VendorGLEntriesStorage } from '../VendorGLEntriesStorage';
// import {
// IVendorEventCreatedPayload,
// IVendorEventDeletedPayload,
// IVendorOpeningBalanceEditedPayload,
// } from '@/interfaces';
import { Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { events } from '@/common/events/events';
import { VendorGLEntriesStorage } from '../VendorGLEntriesStorage';
import {
IVendorEventCreatedPayload,
IVendorEventDeletedPayload,
IVendorOpeningBalanceEditedPayload,
} from '../types/Vendors.types';
// @Service()
// export class VendorsWriteGLOpeningSubscriber {
// @Inject()
// private vendorGLEntriesStorage: VendorGLEntriesStorage;
@Injectable()
export class VendorsWriteGLOpeningSubscriber {
constructor(
private readonly vendorGLEntriesStorage: VendorGLEntriesStorage,
) {}
// /**
// * Constructor method.
// */
// public attach(bus) {
// bus.subscribe(
// events.vendors.onCreated,
// this.handleWriteOpeningBalanceEntries
// );
// bus.subscribe(
// events.vendors.onDeleted,
// this.handleRevertOpeningBalanceEntries
// );
// bus.subscribe(
// events.vendors.onOpeningBalanceChanged,
// this.handleRewriteOpeningEntriesOnChanged
// );
// }
/**
* Writes the open balance journal entries once the vendor created.
* @param {IVendorEventCreatedPayload} payload -
*/
@OnEvent(events.vendors.onCreated)
public async handleWriteOpeningBalanceEntries({
vendor,
trx,
}: IVendorEventCreatedPayload) {
// Writes the vendor opening balance journal entries.
if (vendor.openingBalance) {
await this.vendorGLEntriesStorage.writeVendorOpeningBalance(
vendor.id,
trx,
);
}
}
// /**
// * Writes the open balance journal entries once the vendor created.
// * @param {IVendorEventCreatedPayload} payload -
// */
// private handleWriteOpeningBalanceEntries = async ({
// tenantId,
// vendor,
// trx,
// }: IVendorEventCreatedPayload) => {
// // Writes the vendor opening balance journal entries.
// if (vendor.openingBalance) {
// await this.vendorGLEntriesStorage.writeVendorOpeningBalance(
// tenantId,
// vendor.id,
// trx
// );
// }
// };
/**
* Revert the opening balance journal entries once the vendor deleted.
* @param {IVendorEventDeletedPayload} payload -
*/
@OnEvent(events.vendors.onDeleted)
public async handleRevertOpeningBalanceEntries({
vendorId,
trx,
}: IVendorEventDeletedPayload) {
await this.vendorGLEntriesStorage.revertVendorOpeningBalance(
vendorId,
trx,
);
}
// /**
// * Revert the opening balance journal entries once the vendor deleted.
// * @param {IVendorEventDeletedPayload} payload -
// */
// private handleRevertOpeningBalanceEntries = async ({
// tenantId,
// vendorId,
// trx,
// }: IVendorEventDeletedPayload) => {
// await this.vendorGLEntriesStorage.revertVendorOpeningBalance(
// tenantId,
// vendorId,
// trx
// );
// };
// /**
// * Handles the rewrite opening balance entries once opening balnace changed.
// * @param {ICustomerOpeningBalanceEditedPayload} payload -
// */
// private handleRewriteOpeningEntriesOnChanged = async ({
// tenantId,
// vendor,
// trx,
// }: IVendorOpeningBalanceEditedPayload) => {
// if (vendor.openingBalance) {
// await this.vendorGLEntriesStorage.rewriteVendorOpeningBalance(
// tenantId,
// vendor.id,
// trx
// );
// } else {
// await this.vendorGLEntriesStorage.revertVendorOpeningBalance(
// tenantId,
// vendor.id,
// trx
// );
// }
// };
// }
/**
* Handles the rewrite opening balance entries once opening balance changed.
* @param {IVendorOpeningBalanceEditedPayload} payload -
*/
@OnEvent(events.vendors.onOpeningBalanceChanged)
public async handleRewriteOpeningEntriesOnChanged({
vendor,
trx,
}: IVendorOpeningBalanceEditedPayload) {
if (vendor.openingBalance) {
await this.vendorGLEntriesStorage.rewriteVendorOpeningBalance(
vendor.id,
trx,
);
} else {
await this.vendorGLEntriesStorage.revertVendorOpeningBalance(
vendor.id,
trx,
);
}
}
}

View File

@@ -7,6 +7,7 @@ import { IDynamicListFilter } from '@/modules/DynamicListing/DynamicFilter/Dynam
import { IFilterMeta, IPaginationMeta } from '@/interfaces/Model';
import { CreateVendorDto } from '../dtos/CreateVendor.dto';
import { EditVendorDto } from '../dtos/EditVendor.dto';
import { VendorOpeningBalanceEditDto } from '../dtos/VendorOpeningBalanceEdit.dto';
// ----------------------------------
export interface IVendorNewDTO extends IContactAddressDTO {
@@ -92,23 +93,16 @@ export interface IVendorEventEditedPayload {
trx?: Knex.Transaction;
}
export interface IVendorOpeningBalanceEditDTO {
openingBalance: number;
openingBalanceAt: Date | string;
openingBalanceExchangeRate: number;
openingBalanceBranchId?: number;
}
export interface IVendorOpeningBalanceEditingPayload {
oldVendor: Vendor;
openingBalanceEditDTO: IVendorOpeningBalanceEditDTO;
openingBalanceEditDTO: VendorOpeningBalanceEditDto;
trx?: Knex.Transaction;
}
export interface IVendorOpeningBalanceEditedPayload {
vendor: Vendor;
oldVendor: Vendor;
openingBalanceEditDTO: IVendorOpeningBalanceEditDTO;
openingBalanceEditDTO: VendorOpeningBalanceEditDto;
trx: Knex.Transaction;
}

View File

@@ -1,27 +1,47 @@
FROM node:18.16.0-alpine as build
USER root
# Stage 1: Build
FROM node:18.16.0-alpine AS builder
WORKDIR /app
# Copy application dependency manifests to the container image.
COPY . .
# Install pnpm
RUN npm install -g pnpm@8.10.2
# Install application dependencies
RUN apk update
RUN apk add python3 build-base chromium
# Install build dependencies
RUN apk add --no-cache python3 build-base chromium
# Set PYHTON env
# Set Python environment
ENV PYTHON=/usr/bin/python3
# Install pnpm packages dependencies
RUN npm install -g pnpm
# Copy package files for dependency installation
COPY --chown=node:node package.json pnpm-lock.yaml pnpm-workspace.yaml lerna.json ./
COPY --chown=node:node packages/webapp/package.json ./packages/webapp/
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
# Copy source code for webapp and dependencies
COPY --chown=node:node ./packages/webapp ./packages/webapp
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
# Build webapp package
RUN pnpm run build:webapp
FROM nginx
# Stage 2: Nginx
FROM nginx:alpine
COPY ./packages/webapp/nginx/sites/default.conf /etc/nginx/conf.d/default.conf
COPY --from=build /app/packages/webapp/dist /usr/share/nginx/html
# Copy nginx configuration
COPY --chown=root:root ./packages/webapp/nginx/sites/default.conf /etc/nginx/conf.d/default.conf
# Copy built webapp assets from builder stage
COPY --from=builder --chown=nginx:nginx /app/packages/webapp/dist /usr/share/nginx/html
# Expose port
EXPOSE 80
# Nginx runs as nginx user by default, which is good for security
# No CMD needed as nginx base image already has it

View File

@@ -12,7 +12,7 @@
"@blueprintjs/colors": "4.1.19",
"@blueprintjs/core": "^4.20.2",
"@blueprintjs/datetime": "^4.4.37",
"@blueprintjs/datetime2": "^3.0.10",
"@blueprintjs/datetime2": "^0.9.0",
"@blueprintjs/popover2": "^1.14.11",
"@blueprintjs/select": "^4.9.24",
"@blueprintjs/table": "^4.10.12",

View File

@@ -4,7 +4,12 @@ import styled from 'styled-components';
import { DataTable } from '../Datatable';
export const ReportDataTable = styled(DataTable)`
--x-table-no-results-border-color: #ddd;
.bp4-dark & {
--x-table-no-results-border-color: var(--color-dark-gray5);
}
.table .tbody .tr.no-results:last-of-type .td {
border-bottom: 1px solid #ddd;
border-bottom: 1px solid var(--x-table-no-results-border-color);
}
`;

View File

@@ -3,7 +3,7 @@ import React from 'react';
import { Menu, MenuItem, MenuDivider } from '@blueprintjs/core';
import { useHistory, useLocation } from 'react-router-dom';
import { FormattedMessage as T } from '@/components';
import preferencesMenu from '@/constants/preferencesMenu';
import { PreferencesMenu } from '@/constants/preferencesMenu';
import PreferencesSidebarContainer from './PreferencesSidebarContainer';
import '@/style/pages/Preferences/Sidebar.scss';
@@ -15,7 +15,7 @@ export default function PreferencesSidebar() {
const history = useHistory();
const location = useLocation();
const items = preferencesMenu.map((item) =>
const items = PreferencesMenu.map((item) =>
item.divider ? (
<MenuDivider title={item.title} />
) : (

View File

@@ -1,7 +1,7 @@
// @ts-nocheck
import intl from 'react-intl-universal';
export default [
export const AllocateLandedCostType = [
{ name: intl.get('bills'), value: 'Bill' },
{ name: intl.get('expenses'), value: 'Expense' },
];

View File

@@ -1,5 +1,5 @@
// @ts-nocheck
export default {
export const App = {
"app_name": "BigCapital",
"app_version": "0.0.1 (build 12344)",
}

View File

@@ -2,7 +2,7 @@
import React from 'react';
import intl from 'react-intl-universal';
export default [
export const ContactsOptions = [
{ name: intl.get('customer'), path: 'customers' },
{ name: intl.get('vendor'), path: 'vendors' },
];

View File

@@ -16,7 +16,7 @@ import {
VendorAction,
} from './abilityOption';
export default [
export const KeyboardShortcutsOptions = [
{
shortcut_key: 'Shift + I',
description: intl.get('jump_to_the_invoices'),

View File

@@ -2,7 +2,7 @@
import React from 'react';
import { FormattedMessage as T } from '@/components';
export default [
export const PreferencesMenu = [
{
text: <T id={'general'} />,
disabled: false,
@@ -13,10 +13,10 @@ export default [
disabled: false,
href: '/preferences/branding',
},
{
text: 'Billing',
href: '/preferences/billing',
},
// {
// text: 'Billing',
// href: '/preferences/billing',
// },
{
text: <T id={'users'} />,
href: '/preferences/users',
@@ -63,11 +63,11 @@ export default [
disabled: false,
href: '/preferences/items',
},
{
text: 'Integrations',
disabled: false,
href: '/preferences/integrations'
},
// {
// text: 'Integrations',
// disabled: false,
// href: '/preferences/integrations'
// },
{
text: 'API Keys',
disabled: false,

View File

@@ -8,6 +8,11 @@ import { If, AppToaster } from '@/components';
import { NormalCell, BalanceCell, BankBalanceCell } from './components';
import { transformTableStateToQuery, isBlank } from '@/utils';
export const DeleteAccountTypeError = {
AccountPredefined: 'account_predefined',
AccountHasAssociatedTransactions: 'account_has_associated_transactions',
};
/**
* Account name accessor.
*/
@@ -26,13 +31,13 @@ export const accountNameAccessor = (account) => {
* Handle delete errors in bulk and singular.
*/
export const handleDeleteErrors = (errors) => {
if (errors.find((e) => e.type === 'ACCOUNT.PREDEFINED')) {
if (errors.find((e) => e.type === DeleteAccountTypeError.AccountPredefined)) {
AppToaster.show({
message: intl.get('cannot_delete_predefined_accounts'),
intent: Intent.DANGER,
});
}
if (errors.find((e) => e.type === 'ACCOUNT.HAS.ASSOCIATED.TRANSACTIONS')) {
if (errors.find((e) => e.type === DeleteAccountTypeError.AccountHasAssociatedTransactions)) {
AppToaster.show({
message: intl.get('cannot_delete_account_has_associated_transactions'),
intent: Intent.DANGER,

View File

@@ -2,7 +2,7 @@
import React, { useState } from 'react';
import intl from 'react-intl-universal';
import { Intent, Alert } from '@blueprintjs/core';
import { queryCache } from 'react-query';
import { useQueryClient } from 'react-query';
import { FormattedMessage as T, AppToaster } from '@/components';
import { withAlertStoreConnect } from '@/containers/Alert/withAlertStoreConnect';
@@ -22,6 +22,7 @@ function AccountBulkActivateAlert({
requestBulkActivateAccounts,
}) {
const [isLoading, setLoading] = useState(false);
const queryClient = useQueryClient();
const selectedRowsCount = 0;
// Handle alert cancel.
@@ -38,9 +39,9 @@ function AccountBulkActivateAlert({
message: intl.get('the_accounts_has_been_successfully_activated'),
intent: Intent.SUCCESS,
});
queryCache.invalidateQueries('accounts-table');
queryClient.invalidateQueries('accounts-table');
})
.catch((errors) => { })
.catch((errors) => {})
.finally(() => {
setLoading(false);
closeAlert(name);

View File

@@ -3,7 +3,7 @@ import React, { useState } from 'react';
import { FormattedMessage as T } from '@/components';
import intl from 'react-intl-universal';
import { Intent, Alert } from '@blueprintjs/core';
import { queryCache } from 'react-query';
import { useQueryClient } from 'react-query';
import { AppToaster } from '@/components';
// import { withAccountsActions } from '@/containers/Accounts/withAccountsTableActions';
@@ -22,8 +22,8 @@ function AccountBulkInactivateAlert({
closeAlert,
}) {
const [isLoading, setLoading] = useState(false);
const queryClient = useQueryClient();
const selectedRowsCount = 0;
// Handle alert cancel.
@@ -39,9 +39,9 @@ function AccountBulkInactivateAlert({
message: intl.get('the_accounts_have_been_successfully_inactivated'),
intent: Intent.SUCCESS,
});
queryCache.invalidateQueries('accounts-table');
queryClient.invalidateQueries('accounts-table');
})
.catch((errors) => { })
.catch((errors) => {})
.finally(() => {
setLoading(false);
closeAlert(name);

View File

@@ -3,7 +3,7 @@ import React, { useCallback } from 'react';
import intl from 'react-intl-universal';
import { AppToaster, FormattedMessage as T } from '@/components';
import { Intent, Alert } from '@blueprintjs/core';
import { queryCache } from 'react-query';
import { useQueryClient } from 'react-query';
import { useApproveEstimate } from '@/hooks/query';
@@ -25,6 +25,7 @@ function EstimateApproveAlert({
// #withAlertActions
closeAlert,
}) {
const queryClient = useQueryClient();
const { mutateAsync: deliverEstimateMutate, isLoading } =
useApproveEstimate();
@@ -40,7 +41,7 @@ function EstimateApproveAlert({
message: intl.get('the_estimate_has_been_approved_successfully'),
intent: Intent.SUCCESS,
});
queryCache.invalidateQueries('estimates-table');
queryClient.invalidateQueries('estimates-table');
})
.catch((error) => {})
.finally(() => {

View File

@@ -16,7 +16,7 @@ import { FormattedMessage as T, If, FFormGroup, FSelect, FRadioGroup, FInputGrou
import { handleStringChange } from '@/utils';
import { FieldRequiredHint } from '@/components';
import { CLASSES } from '@/constants/classes';
import allocateLandedCostType from '@/constants/allocateLandedCostType';
import { AllocateLandedCostType } from '@/constants/allocateLandedCostType';
import AllocateLandedCostFormBody from './AllocateLandedCostFormBody';
import {
@@ -81,7 +81,7 @@ export default function AllocateLandedCostFormFields() {
>
<FSelect
name={'transaction_type'}
items={allocateLandedCostType}
items={AllocateLandedCostType}
onItemChange={handleTransactionTypeChange}
filterable={false}
valueAccessor={'value'}

View File

@@ -1,7 +1,7 @@
// @ts-nocheck
import React from 'react';
import { DialogContent } from '@/components';
import { useQuery, queryCache } from 'react-query';
import { useQuery, useQueryClient } from 'react-query';
import ReferenceNumberForm from '@/containers/JournalNumber/ReferenceNumberForm';
@@ -31,6 +31,7 @@ function BillNumberDialogContent({
// #withBillsActions
setBillNumberChanged,
}) {
const queryClient = useQueryClient();
const fetchSettings = useQuery(['settings'], () => requestFetchOptions({}));
const handleSubmitForm = (values, { setSubmitting }) => {
@@ -45,7 +46,7 @@ function BillNumberDialogContent({
setBillNumberChanged(true);
setTimeout(() => {
queryCache.invalidateQueries('settings');
queryClient.invalidateQueries('settings');
}, 250);
})
.catch(() => {

View File

@@ -9,7 +9,7 @@ import { FormattedMessage as T } from '@/components';
import { useHistory } from 'react-router-dom';
import { useContactDuplicateFromContext } from './ContactDuplicateProvider';
import Contacts from '@/constants/contactsOptions';
import { ContactsOptions } from '@/constants/contactsOptions';
import { withDialogActions } from '@/containers/Dialog/withDialogActions';
import { compose } from '@/utils';
@@ -66,7 +66,7 @@ function ContactDuplicateForm({
>
<FSelect
name={'contact_type'}
items={Contacts}
items={ContactsOptions}
placeholder={<T id={'select_contact'} />}
textAccessor={'name'}
valueAccessor={'path'}

View File

@@ -17,7 +17,7 @@ import {
FeatureCan,
InputPrependText,
} from '@/components';
import { FMoneyInputGroup, FFormGroup } from '@/components/Forms';
import { FMoneyInputGroup, FFormGroup, FDateInput } from '@/components/Forms';
import { useCustomerOpeningBalanceContext } from './CustomerOpeningBalanceFormProvider';
import { useSetPrimaryBranchToForm } from './utils';
@@ -59,28 +59,24 @@ function CustomerOpeningBalanceFields({
</FFormGroup>
{/*------------ Opening balance at -----------*/}
<FastField name={'opening_balance_at'}>
{({ form, field: { value } }) => (
<FormGroup
label={
<T id={'customer_opening_balance.label.opening_balance_at'} />
}
className={Classes.FILL}
>
<DateInput
{...momentFormatter('YYYY/MM/DD')}
onChange={handleDateChange((formattedDate) => {
form.setFieldValue('opening_balance_at', formattedDate);
})}
value={tansformDateValue(value)}
popoverProps={{ position: Position.BOTTOM, minimal: true }}
inputProps={{
leftIcon: <Icon icon={'date-range'} />,
}}
/>
</FormGroup>
)}
</FastField>
<FFormGroup
name={'opening_balance_at'}
label={<T id={'customer_opening_balance.label.opening_balance_at'} />}
fill
fastField
>
<FDateInput
name={'opening_balance_at'}
formatDate={(date) => date.toLocaleDateString()}
parseDate={(str) => new Date(str)}
popoverProps={{ position: Position.BOTTOM, minimal: true }}
inputProps={{
leftIcon: <Icon icon={'date-range'} />,
}}
fill
fastField
/>
</FFormGroup>
<If condition={!isEqual(base_currency, customer.currency_code)}>
{/*------------ Opening balance exchange rate -----------*/}
@@ -99,12 +95,15 @@ function CustomerOpeningBalanceFields({
<FFormGroup
label={<T id={'branch'} />}
name={'opening_balance_branch_id'}
className={classNames('form-group--select-list', Classes.FILL)}
fill
fastField
>
<BranchSelect
name={'opening_balance_branch_id'}
branches={branches}
popoverProps={{ minimal: true }}
fastField
fill
/>
</FFormGroup>
</FeatureCan>

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