invoice item total and subtotal

This commit is contained in:
2026-05-16 15:59:40 +03:30
parent 8017484f54
commit 7133b47c95
7 changed files with 58 additions and 17 deletions
+11
View File
@@ -0,0 +1,11 @@
node_modules
dist
.git
.gitignore
*.md
.env*
coverage
.nyc_output
logs
*.log
npm-debug.log*
+3 -4
View File
@@ -1,9 +1,8 @@
# Stage 1: Base # Stage 1: Base
FROM node:22-alpine AS base FROM node:22-alpine AS base
# Install timezone support # Set timezone (Node.js respects TZ env variable natively)
RUN apk add --no-cache tzdata ENV TZ=Asia/Tehran
RUN cp /usr/share/zoneinfo/Asia/Tehran /etc/localtime && echo "Asia/Tehran" > /etc/timezone
# Set workdir for clarity # Set workdir for clarity
WORKDIR /app WORKDIR /app
@@ -12,7 +11,7 @@ WORKDIR /app
FROM base AS deps FROM base AS deps
WORKDIR /temp-deps WORKDIR /temp-deps
COPY package*.json ./ COPY package*.json ./
RUN npm ci --ignore-scripts RUN npm ci --ignore-scripts --no-fund --no-audit
# Stage 3: Build # Stage 3: Build
FROM base AS builder FROM base AS builder
@@ -2831,6 +2831,16 @@
"nullable": false, "nullable": false,
"mappedType": "integer" "mappedType": "integer"
}, },
"sub_total": {
"name": "sub_total",
"type": "int",
"generated": "(COALESCE(unit_price, 0) * quantity) STORED",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "integer"
},
"discount": { "discount": {
"name": "discount", "name": "discount",
"type": "int", "type": "int",
@@ -2840,6 +2850,16 @@
"nullable": true, "nullable": true,
"mappedType": "integer" "mappedType": "integer"
}, },
"total": {
"name": "total",
"type": "int",
"generated": "((COALESCE(unit_price, 0) * quantity) - COALESCE(discount, 0)) STORED",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"mappedType": "integer"
},
"description": { "description": {
"name": "description", "name": "description",
"type": "text", "type": "text",
@@ -0,0 +1,9 @@
import { Migration } from '@mikro-orm/migrations';
export class Migration20260516121012 extends Migration {
override async up(): Promise<void> {
this.addSql(`alter table "invoice_items" add column "sub_total" int generated always as (COALESCE(unit_price, 0) * quantity) STORED not null, add column "total" int generated always as ((COALESCE(unit_price, 0) * quantity) - COALESCE(discount, 0)) STORED not null;`);
}
}
@@ -88,7 +88,8 @@ export class CreateInvoiceDto {
description?: string; description?: string;
@IsArray() @IsArray()
@IsOptional()
@Type(() => IAttachmentDto) @Type(() => IAttachmentDto)
@ApiProperty({ type: [IAttachmentDto] }) @ApiProperty({ type: [IAttachmentDto] })
attachments: IAttachmentDto[]; attachments?: IAttachmentDto[];
} }
@@ -29,7 +29,6 @@ export class InvoiceItem extends BaseEntity {
@Property({ @Property({
type: 'int', type: 'int',
columnType: 'int GENERATED ALWAYS AS (COALESCE(unit_price, 0) * quantity) STORED', columnType: 'int GENERATED ALWAYS AS (COALESCE(unit_price, 0) * quantity) STORED',
persist: false,
}) })
subTotal!: number & Opt; subTotal!: number & Opt;
@@ -42,7 +41,6 @@ export class InvoiceItem extends BaseEntity {
@Property({ @Property({
type: 'int', type: 'int',
columnType: 'int GENERATED ALWAYS AS ((COALESCE(unit_price, 0) * quantity) - COALESCE(discount, 0)) STORED', columnType: 'int GENERATED ALWAYS AS ((COALESCE(unit_price, 0) * quantity) - COALESCE(discount, 0)) STORED',
persist: false
}) })
total!: number & Opt; total!: number & Opt;
+13 -10
View File
@@ -22,7 +22,7 @@ export class InvoiceService {
private readonly productService: ProductService, private readonly productService: ProductService,
private readonly invoiceRepository: InvoiceRepository, private readonly invoiceRepository: InvoiceRepository,
private readonly eventEmitter: EventEmitter2, private readonly eventEmitter: EventEmitter2,
) {} ) { }
async create(dto: CreateInvoiceDto) { async create(dto: CreateInvoiceDto) {
@@ -59,13 +59,16 @@ export class InvoiceService {
...(request && { request }), ...(request && { request }),
enableTax, enableTax,
approvalDeadline, approvalDeadline,
// discount, discount: 0,
paidAmount: 0, paidAmount: 0,
// balance: 0, balance: 0,
attachments: [], attachments: dto.attachments ?? [],
paymentMethod: dto.paymentMethod, paymentMethod: dto.paymentMethod,
invoiceNumber: 20, // TODO : invoice number invoiceNumber: 20, // TODO : invoice number
description: dto.description description: dto.description,
subTotal: 0,
taxAmount: 0,
total: 0,
}); });
em.persist(invoice); em.persist(invoice);
@@ -95,7 +98,7 @@ export class InvoiceService {
// const total = Math.max(0, subTotal - discount + taxAmount); // const total = Math.max(0, subTotal - discount + taxAmount);
// invoice.subTotal = subTotal; // invoice.subTotal = subTotal;
invoice.attachments = dto.attachments; // invoice.attachments = dto.attachments;
// invoice.taxAmount = taxAmount; // invoice.taxAmount = taxAmount;
// invoice.total = total; // invoice.total = total;
// invoice.balance = total - invoice.paidAmount; // invoice.balance = total - invoice.paidAmount;
@@ -114,7 +117,7 @@ export class InvoiceService {
await em.flush(); await em.flush();
return invoice; return invoice;
}); });
// TODO : maybe it is needed to get a fresh instance. // TODO : maybe it is needed to get a fresh instance.
this.eventEmitter.emit( this.eventEmitter.emit(
InvoiceCreatedEvent.name, InvoiceCreatedEvent.name,
new InvoiceCreatedEvent( new InvoiceCreatedEvent(
@@ -184,9 +187,9 @@ export class InvoiceService {
if (dto.approvalDeadline !== undefined) { if (dto.approvalDeadline !== undefined) {
invoice.approvalDeadline = new Date(dto.approvalDeadline); invoice.approvalDeadline = new Date(dto.approvalDeadline);
} }
if(dto.paymentMethod !== undefined) invoice.paymentMethod = dto.paymentMethod; if (dto.paymentMethod !== undefined) invoice.paymentMethod = dto.paymentMethod;
if(dto.description !== undefined) invoice.description = dto.description; if (dto.description !== undefined) invoice.description = dto.description;
if(dto.attachments !== undefined) invoice.attachments = dto.attachments; if (dto.attachments !== undefined) invoice.attachments = dto.attachments;
if (dto.items !== undefined) { if (dto.items !== undefined) {
// Items in dto.items represent the full list: delete any existing items not in the list // Items in dto.items represent the full list: delete any existing items not in the list