mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-20 06:40:31 +00:00
refactor: sale estimates to nestjs
This commit is contained in:
@@ -34,9 +34,8 @@
|
|||||||
"@nestjs/throttler": "^6.2.1",
|
"@nestjs/throttler": "^6.2.1",
|
||||||
"@types/passport-local": "^1.0.38",
|
"@types/passport-local": "^1.0.38",
|
||||||
"@types/ramda": "^0.30.2",
|
"@types/ramda": "^0.30.2",
|
||||||
"js-money": "^0.6.3",
|
|
||||||
"accounting": "^0.4.1",
|
"accounting": "^0.4.1",
|
||||||
"object-hash": "^2.0.3",
|
"async": "^3.2.0",
|
||||||
"bull": "^4.16.3",
|
"bull": "^4.16.3",
|
||||||
"bullmq": "^5.21.1",
|
"bullmq": "^5.21.1",
|
||||||
"cache-manager": "^6.1.1",
|
"cache-manager": "^6.1.1",
|
||||||
@@ -45,6 +44,7 @@
|
|||||||
"class-validator": "^0.14.1",
|
"class-validator": "^0.14.1",
|
||||||
"express-validator": "^7.2.0",
|
"express-validator": "^7.2.0",
|
||||||
"fp-ts": "^2.16.9",
|
"fp-ts": "^2.16.9",
|
||||||
|
"js-money": "^0.6.3",
|
||||||
"knex": "^3.1.0",
|
"knex": "^3.1.0",
|
||||||
"lamda": "^0.4.1",
|
"lamda": "^0.4.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
@@ -53,6 +53,7 @@
|
|||||||
"mysql2": "^3.11.3",
|
"mysql2": "^3.11.3",
|
||||||
"nestjs-cls": "^4.4.1",
|
"nestjs-cls": "^4.4.1",
|
||||||
"nestjs-i18n": "^10.4.9",
|
"nestjs-i18n": "^10.4.9",
|
||||||
|
"object-hash": "^2.0.3",
|
||||||
"objection": "^3.1.5",
|
"objection": "^3.1.5",
|
||||||
"passport": "^0.7.0",
|
"passport": "^0.7.0",
|
||||||
"passport-jwt": "^4.0.1",
|
"passport-jwt": "^4.0.1",
|
||||||
@@ -61,6 +62,8 @@
|
|||||||
"redis": "^4.7.0",
|
"redis": "^4.7.0",
|
||||||
"reflect-metadata": "^0.2.0",
|
"reflect-metadata": "^0.2.0",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
|
"serialize-interceptor": "^1.1.7",
|
||||||
|
"strategy": "^1.1.1",
|
||||||
"zod": "^3.23.8"
|
"zod": "^3.23.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -213,6 +213,8 @@ export const events = {
|
|||||||
onPreMailSend: 'onSaleEstimatePreMailSend',
|
onPreMailSend: 'onSaleEstimatePreMailSend',
|
||||||
onMailSend: 'onSaleEstimateMailSend',
|
onMailSend: 'onSaleEstimateMailSend',
|
||||||
onMailSent: 'onSaleEstimateMailSend',
|
onMailSent: 'onSaleEstimateMailSend',
|
||||||
|
|
||||||
|
onViewed: 'onSaleEstimateViewed',
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import {
|
||||||
|
ExceptionFilter,
|
||||||
|
Catch,
|
||||||
|
ArgumentsHost,
|
||||||
|
HttpStatus,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { Response } from 'express';
|
||||||
|
import { ServiceError } from '@/modules/Items/ServiceError';
|
||||||
|
|
||||||
|
@Catch(ServiceError)
|
||||||
|
export class ServiceErrorFilter implements ExceptionFilter {
|
||||||
|
catch(exception: ServiceError, host: ArgumentsHost) {
|
||||||
|
const ctx = host.switchToHttp();
|
||||||
|
const response = ctx.getResponse<Response>();
|
||||||
|
const status = exception.getStatus();
|
||||||
|
|
||||||
|
response.status(status).json({
|
||||||
|
statusCode: status,
|
||||||
|
errorType: exception.errorType,
|
||||||
|
message: exception.message,
|
||||||
|
payload: exception.payload,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import {
|
||||||
|
type ExecutionContext,
|
||||||
|
Injectable,
|
||||||
|
type NestInterceptor,
|
||||||
|
type CallHandler,
|
||||||
|
Optional,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { type Observable } from 'rxjs';
|
||||||
|
import { map } from 'rxjs/operators';
|
||||||
|
|
||||||
|
export function camelToSnake<T = any>(value: T) {
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.map(camelToSnake);
|
||||||
|
}
|
||||||
|
if (typeof value === 'object' && !(value instanceof Date)) {
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(value).map(([key, value]) => [
|
||||||
|
key
|
||||||
|
.split(/(?=[A-Z])/)
|
||||||
|
.join('_')
|
||||||
|
.toLowerCase(),
|
||||||
|
camelToSnake(value),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function snakeToCamel<T = any>(value: T) {
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.map(snakeToCamel);
|
||||||
|
}
|
||||||
|
|
||||||
|
const impl = (str: string) => {
|
||||||
|
const converted = str.replace(/([-_]\w)/g, (group) =>
|
||||||
|
group[1].toUpperCase(),
|
||||||
|
);
|
||||||
|
return converted[0].toLowerCase() + converted.slice(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (typeof value === 'object' && !(value instanceof Date)) {
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(value).map(([key, value]) => [
|
||||||
|
impl(key),
|
||||||
|
snakeToCamel(value),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_STRATEGY = {
|
||||||
|
in: snakeToCamel,
|
||||||
|
out: camelToSnake,
|
||||||
|
};
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SerializeInterceptor implements NestInterceptor<any, any> {
|
||||||
|
constructor(@Optional() readonly strategy = DEFAULT_STRATEGY) {}
|
||||||
|
|
||||||
|
intercept(
|
||||||
|
context: ExecutionContext,
|
||||||
|
next: CallHandler<any>,
|
||||||
|
): Observable<any> {
|
||||||
|
const request = context.switchToHttp().getRequest();
|
||||||
|
request.body = this.strategy.in(request.body);
|
||||||
|
|
||||||
|
// handle returns stream..
|
||||||
|
return next.handle().pipe(map(this.strategy.out));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
|||||||
import { ClsMiddleware } from 'nestjs-cls';
|
import { ClsMiddleware } from 'nestjs-cls';
|
||||||
import { AppModule } from './modules/App/App.module';
|
import { AppModule } from './modules/App/App.module';
|
||||||
import './utils/moment-mysql';
|
import './utils/moment-mysql';
|
||||||
|
import { ServiceErrorFilter } from './common/filters/service-error.filter';
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
const app = await NestFactory.create(AppModule);
|
const app = await NestFactory.create(AppModule);
|
||||||
@@ -21,6 +22,8 @@ async function bootstrap() {
|
|||||||
const documentFactory = () => SwaggerModule.createDocument(app, config);
|
const documentFactory = () => SwaggerModule.createDocument(app, config);
|
||||||
SwaggerModule.setup('swagger', app, documentFactory);
|
SwaggerModule.setup('swagger', app, documentFactory);
|
||||||
|
|
||||||
|
app.useGlobalFilters(new ServiceErrorFilter());
|
||||||
|
|
||||||
await app.listen(process.env.PORT ?? 3000);
|
await app.listen(process.env.PORT ?? 3000);
|
||||||
}
|
}
|
||||||
bootstrap();
|
bootstrap();
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ import { TaxRatesModule } from '../TaxRates/TaxRate.module';
|
|||||||
import { PdfTemplatesModule } from '../PdfTemplate/PdfTemplates.module';
|
import { PdfTemplatesModule } from '../PdfTemplate/PdfTemplates.module';
|
||||||
import { BranchesModule } from '../Branches/Branches.module';
|
import { BranchesModule } from '../Branches/Branches.module';
|
||||||
import { WarehousesModule } from '../Warehouses/Warehouses.module';
|
import { WarehousesModule } from '../Warehouses/Warehouses.module';
|
||||||
|
import { SaleEstimatesModule } from '../SaleEstimates/SaleEstimates.module';
|
||||||
|
import { SerializeInterceptor } from '@/common/interceptors/serialize.interceptor';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -99,9 +101,14 @@ import { WarehousesModule } from '../Warehouses/Warehouses.module';
|
|||||||
PdfTemplatesModule,
|
PdfTemplatesModule,
|
||||||
BranchesModule,
|
BranchesModule,
|
||||||
WarehousesModule,
|
WarehousesModule,
|
||||||
|
SaleEstimatesModule,
|
||||||
],
|
],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [
|
providers: [
|
||||||
|
{
|
||||||
|
provide: APP_INTERCEPTOR,
|
||||||
|
useClass: SerializeInterceptor,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
provide: APP_GUARD,
|
provide: APP_GUARD,
|
||||||
useClass: JwtAuthGuard,
|
useClass: JwtAuthGuard,
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { Transformer } from "../Transformer/Transformer";
|
||||||
|
|
||||||
|
export class AttachmentTransformer extends Transformer {
|
||||||
|
/**
|
||||||
|
* Exclude attributes.
|
||||||
|
* @returns {string[]}
|
||||||
|
*/
|
||||||
|
public excludeAttributes = (): string[] => {
|
||||||
|
return ['id', 'createdAt'];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Includeded attributes.
|
||||||
|
* @returns {string[]}
|
||||||
|
*/
|
||||||
|
public includeAttributes = (): string[] => {
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export interface AttachmentLinkDTO {
|
||||||
|
key: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TransformerInjectable } from '../Transformer/TransformerInjectable.service';
|
||||||
|
import { TenancyDatabaseModule } from '../Tenancy/TenancyDB/TenancyDB.module';
|
||||||
|
import { AutoIncrementOrdersService } from './AutoIncrementOrders.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [TenancyDatabaseModule],
|
||||||
|
controllers: [],
|
||||||
|
providers: [AutoIncrementOrdersService, TransformerInjectable],
|
||||||
|
exports: [AutoIncrementOrdersService],
|
||||||
|
})
|
||||||
|
export class AutoIncrementOrdersModule {}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto increment orders service.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class AutoIncrementOrdersService {
|
||||||
|
/**
|
||||||
|
* Check if the auto increment is enabled for the given settings group.
|
||||||
|
* @param {string} settingsGroup - Settings group.
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
public autoIncrementEnabled = (settingsGroup: string): boolean => {
|
||||||
|
// const settings = this.tenancy.settings(tenantId);
|
||||||
|
// const group = settingsGroup;
|
||||||
|
|
||||||
|
// // Settings service transaction number and prefix.
|
||||||
|
// return settings.get({ group, key: 'auto_increment' }, false);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the next service transaction number.
|
||||||
|
* @param {string} settingsGroup
|
||||||
|
* @param {Function} getMaxTransactionNo
|
||||||
|
* @return {Promise<string>}
|
||||||
|
*/
|
||||||
|
getNextTransactionNumber(group: string): string {
|
||||||
|
// const settings = this.tenancy.settings(tenantId);
|
||||||
|
|
||||||
|
// // Settings service transaction number and prefix.
|
||||||
|
// const autoIncrement = this.autoIncrementEnabled(tenantId, group);
|
||||||
|
|
||||||
|
// const settingNo = settings.get({ group, key: 'next_number' }, '');
|
||||||
|
// const settingPrefix = settings.get({ group, key: 'number_prefix' }, '');
|
||||||
|
|
||||||
|
// return autoIncrement ? `${settingPrefix}${settingNo}` : '';
|
||||||
|
|
||||||
|
return '1';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increment setting next number.
|
||||||
|
* @param {string} orderGroup - Order group.
|
||||||
|
* @param {string} orderNumber -Order number.
|
||||||
|
*/
|
||||||
|
async incrementSettingsNextNumber(group: string) {
|
||||||
|
// const settings = this.tenancy.settings(tenantId);
|
||||||
|
// const settingNo = settings.get({ group, key: 'next_number' });
|
||||||
|
// const autoIncrement = settings.get({ group, key: 'auto_increment' });
|
||||||
|
// // Can't continue if the auto-increment of the service was disabled.
|
||||||
|
// if (!autoIncrement) {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// settings.set(
|
||||||
|
// { group, key: 'next_number' },
|
||||||
|
// transactionIncrement(settingNo)
|
||||||
|
// );
|
||||||
|
// await settings.save();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,35 +1,31 @@
|
|||||||
// import { Service, Inject } from 'typedi';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
// import { omit } from 'lodash';
|
import { omit } from 'lodash';
|
||||||
// import { BranchesSettings } from '../BranchesSettings';
|
import { BranchesSettingsService } from '../BranchesSettings';
|
||||||
|
|
||||||
// @Service()
|
@Injectable()
|
||||||
// export class BranchTransactionDTOTransform {
|
export class BranchTransactionDTOTransformer {
|
||||||
// @Inject()
|
constructor(private readonly branchesSettings: BranchesSettingsService) {}
|
||||||
// branchesSettings: BranchesSettings;
|
|
||||||
|
|
||||||
// /**
|
/**
|
||||||
// * Excludes DTO branch id when mutli-warehouses feature is inactive.
|
* Excludes DTO branch id when mutli-warehouses feature is inactive.
|
||||||
// * @param {number} tenantId
|
* @returns {any}
|
||||||
// * @returns {any}
|
*/
|
||||||
// */
|
private excludeDTOBranchIdWhenInactive = <T extends { branchId?: number }>(
|
||||||
// private excludeDTOBranchIdWhenInactive = <T extends { branchId?: number }>(
|
DTO: T,
|
||||||
// tenantId: number,
|
): Omit<T, 'branchId'> | T => {
|
||||||
// DTO: T
|
const isActive = this.branchesSettings.isMultiBranchesActive();
|
||||||
// ): Omit<T, 'branchId'> | T => {
|
|
||||||
// const isActive = this.branchesSettings.isMultiBranchesActive(tenantId);
|
|
||||||
|
|
||||||
// return !isActive ? omit(DTO, ['branchId']) : DTO;
|
return !isActive ? omit(DTO, ['branchId']) : DTO;
|
||||||
// };
|
};
|
||||||
|
|
||||||
// /**
|
/**
|
||||||
// * Transformes the input DTO for branches feature.
|
* Transformes the input DTO for branches feature.
|
||||||
// * @param {number} tenantId -
|
* @param {T} DTO -
|
||||||
// * @param {T} DTO -
|
* @returns {Omit<T, 'branchId'> | T}
|
||||||
// * @returns {Omit<T, 'branchId'> | T}
|
*/
|
||||||
// */
|
public transformDTO = <T extends { branchId?: number }>(
|
||||||
// public transformDTO =
|
DTO: T,
|
||||||
// <T extends { branchId?: number }>(tenantId: number) =>
|
): Omit<T, 'branchId'> | T => {
|
||||||
// (DTO: T): Omit<T, 'branchId'> | T => {
|
return this.excludeDTOBranchIdWhenInactive<T>(DTO);
|
||||||
// return this.excludeDTOBranchIdWhenInactive<T>(tenantId, DTO);
|
};
|
||||||
// };
|
}
|
||||||
// }
|
|
||||||
|
|||||||
229
packages/server-nest/src/modules/Contacts/models/Contact.ts
Normal file
229
packages/server-nest/src/modules/Contacts/models/Contact.ts
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
import { Model } from 'objection';
|
||||||
|
import { BaseModel } from '@/models/Model';
|
||||||
|
|
||||||
|
export class Contact extends BaseModel {
|
||||||
|
contactService: string;
|
||||||
|
contactType: string;
|
||||||
|
|
||||||
|
balance: number;
|
||||||
|
currencyCode: string;
|
||||||
|
|
||||||
|
openingBalance: number;
|
||||||
|
openingBalanceAt: Date;
|
||||||
|
|
||||||
|
salutation?: string;
|
||||||
|
firstName?: string;
|
||||||
|
lastName?: string;
|
||||||
|
companyName?: string;
|
||||||
|
|
||||||
|
displayName: string;
|
||||||
|
|
||||||
|
email?: string;
|
||||||
|
workPhone?: string;
|
||||||
|
personalPhone?: string;
|
||||||
|
website?: string;
|
||||||
|
|
||||||
|
billingAddress1?: string;
|
||||||
|
billingAddress2?: string;
|
||||||
|
billingAddressCity?: string;
|
||||||
|
billingAddressCountry?: string;
|
||||||
|
billingAddressEmail?: string;
|
||||||
|
billingAddressPostcode?: string;
|
||||||
|
billingAddressPhone?: string;
|
||||||
|
billingAddressState?: string;
|
||||||
|
|
||||||
|
shippingAddress1?: string;
|
||||||
|
shippingAddress2?: string;
|
||||||
|
shippingAddressCity?: string;
|
||||||
|
shippingAddressCountry?: string;
|
||||||
|
shippingAddressEmail?: string;
|
||||||
|
shippingAddressPostcode?: string;
|
||||||
|
shippingAddressPhone?: string;
|
||||||
|
shippingAddressState?: string;
|
||||||
|
|
||||||
|
note: string;
|
||||||
|
active: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Table name
|
||||||
|
*/
|
||||||
|
static get tableName() {
|
||||||
|
return 'contacts';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Model timestamps.
|
||||||
|
*/
|
||||||
|
get timestamps() {
|
||||||
|
return ['createdAt', 'updatedAt'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defined virtual attributes.
|
||||||
|
*/
|
||||||
|
static get virtualAttributes() {
|
||||||
|
return ['contactNormal', 'closingBalance', 'formattedContactService'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the contact normal by the given contact type.
|
||||||
|
*/
|
||||||
|
static getContactNormalByType(contactType) {
|
||||||
|
const types = {
|
||||||
|
vendor: 'credit',
|
||||||
|
customer: 'debit',
|
||||||
|
};
|
||||||
|
return types[contactType];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the contact normal by the given contact service.
|
||||||
|
* @param {string} contactService
|
||||||
|
*/
|
||||||
|
static getFormattedContactService(contactService) {
|
||||||
|
const types = {
|
||||||
|
customer: 'Customer',
|
||||||
|
vendor: 'Vendor',
|
||||||
|
};
|
||||||
|
return types[contactService];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the contact normal.
|
||||||
|
*/
|
||||||
|
get contactNormal() {
|
||||||
|
return Contact.getContactNormalByType(this.contactService);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve formatted contact service.
|
||||||
|
*/
|
||||||
|
get formattedContactService() {
|
||||||
|
return Contact.getFormattedContactService(this.contactService);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closing balance attribute.
|
||||||
|
*/
|
||||||
|
get closingBalance() {
|
||||||
|
return this.balance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Model modifiers.
|
||||||
|
*/
|
||||||
|
static get modifiers() {
|
||||||
|
return {
|
||||||
|
filterContactIds(query, customerIds) {
|
||||||
|
query.whereIn('id', customerIds);
|
||||||
|
},
|
||||||
|
|
||||||
|
customer(query) {
|
||||||
|
query.where('contact_service', 'customer');
|
||||||
|
},
|
||||||
|
|
||||||
|
vendor(query) {
|
||||||
|
query.where('contact_service', 'vendor');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Relationship mapping.
|
||||||
|
*/
|
||||||
|
static get relationMappings() {
|
||||||
|
const SaleEstimate = require('models/SaleEstimate');
|
||||||
|
const SaleReceipt = require('models/SaleReceipt');
|
||||||
|
const SaleInvoice = require('models/SaleInvoice');
|
||||||
|
const PaymentReceive = require('models/PaymentReceive');
|
||||||
|
const Bill = require('models/Bill');
|
||||||
|
const BillPayment = require('models/BillPayment');
|
||||||
|
const AccountTransaction = require('models/AccountTransaction');
|
||||||
|
|
||||||
|
return {
|
||||||
|
/**
|
||||||
|
* Contact may has many sales invoices.
|
||||||
|
*/
|
||||||
|
salesInvoices: {
|
||||||
|
relation: Model.HasManyRelation,
|
||||||
|
modelClass: SaleInvoice.default,
|
||||||
|
join: {
|
||||||
|
from: 'contacts.id',
|
||||||
|
to: 'sales_invoices.customerId',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contact may has many sales estimates.
|
||||||
|
*/
|
||||||
|
salesEstimates: {
|
||||||
|
relation: Model.HasManyRelation,
|
||||||
|
modelClass: SaleEstimate.default,
|
||||||
|
join: {
|
||||||
|
from: 'contacts.id',
|
||||||
|
to: 'sales_estimates.customerId',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contact may has many sales receipts.
|
||||||
|
*/
|
||||||
|
salesReceipts: {
|
||||||
|
relation: Model.HasManyRelation,
|
||||||
|
modelClass: SaleReceipt.default,
|
||||||
|
join: {
|
||||||
|
from: 'contacts.id',
|
||||||
|
to: 'sales_receipts.customerId',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contact may has many payments receives.
|
||||||
|
*/
|
||||||
|
paymentReceives: {
|
||||||
|
relation: Model.HasManyRelation,
|
||||||
|
modelClass: PaymentReceive.default,
|
||||||
|
join: {
|
||||||
|
from: 'contacts.id',
|
||||||
|
to: 'payment_receives.customerId',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contact may has many bills.
|
||||||
|
*/
|
||||||
|
bills: {
|
||||||
|
relation: Model.HasManyRelation,
|
||||||
|
modelClass: Bill.default,
|
||||||
|
join: {
|
||||||
|
from: 'contacts.id',
|
||||||
|
to: 'bills.vendorId',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contact may has many bills payments.
|
||||||
|
*/
|
||||||
|
billPayments: {
|
||||||
|
relation: Model.HasManyRelation,
|
||||||
|
modelClass: BillPayment.default,
|
||||||
|
join: {
|
||||||
|
from: 'contacts.id',
|
||||||
|
to: 'bills_payments.vendorId',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contact may has many accounts transactions.
|
||||||
|
*/
|
||||||
|
accountsTransactions: {
|
||||||
|
relation: Model.HasManyRelation,
|
||||||
|
modelClass: AccountTransaction.default,
|
||||||
|
join: {
|
||||||
|
from: 'contacts.id',
|
||||||
|
to: 'accounts_transactions.contactId',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
234
packages/server-nest/src/modules/Customers/models/Customer.ts
Normal file
234
packages/server-nest/src/modules/Customers/models/Customer.ts
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
import { Model, mixin } from 'objection';
|
||||||
|
import { BaseModel } from '@/models/Model';
|
||||||
|
// import TenantModel from 'models/TenantModel';
|
||||||
|
// import PaginationQueryBuilder from './Pagination';
|
||||||
|
// import ModelSetting from './ModelSetting';
|
||||||
|
// import CustomerSettings from './Customer.Settings';
|
||||||
|
// import CustomViewBaseModel from './CustomViewBaseModel';
|
||||||
|
// import { DEFAULT_VIEWS } from '@/services/Contacts/Customers/constants';
|
||||||
|
// import ModelSearchable from './ModelSearchable';
|
||||||
|
|
||||||
|
// class CustomerQueryBuilder extends PaginationQueryBuilder {
|
||||||
|
// constructor(...args) {
|
||||||
|
// super(...args);
|
||||||
|
|
||||||
|
// this.onBuild((builder) => {
|
||||||
|
// if (builder.isFind() || builder.isDelete() || builder.isUpdate()) {
|
||||||
|
// builder.where('contact_service', 'customer');
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
export class Customer extends BaseModel{
|
||||||
|
contactService: string;
|
||||||
|
contactType: string;
|
||||||
|
|
||||||
|
balance: number;
|
||||||
|
currencyCode: string;
|
||||||
|
|
||||||
|
openingBalance: number;
|
||||||
|
openingBalanceAt: Date;
|
||||||
|
openingBalanceExchangeRate: number;
|
||||||
|
|
||||||
|
salutation?: string;
|
||||||
|
firstName?: string;
|
||||||
|
lastName?: string;
|
||||||
|
companyName?: string;
|
||||||
|
|
||||||
|
displayName: string;
|
||||||
|
|
||||||
|
email?: string;
|
||||||
|
workPhone?: string;
|
||||||
|
personalPhone?: string;
|
||||||
|
website?: string;
|
||||||
|
|
||||||
|
billingAddress1?: string;
|
||||||
|
billingAddress2?: string;
|
||||||
|
billingAddressCity?: string;
|
||||||
|
billingAddressCountry?: string;
|
||||||
|
billingAddressEmail?: string;
|
||||||
|
billingAddressPostcode?: string;
|
||||||
|
billingAddressPhone?: string;
|
||||||
|
billingAddressState?: string;
|
||||||
|
|
||||||
|
shippingAddress1?: string;
|
||||||
|
shippingAddress2?: string;
|
||||||
|
shippingAddressCity?: string;
|
||||||
|
shippingAddressCountry?: string;
|
||||||
|
shippingAddressEmail?: string;
|
||||||
|
shippingAddressPostcode?: string;
|
||||||
|
shippingAddressPhone?: string;
|
||||||
|
shippingAddressState?: string;
|
||||||
|
|
||||||
|
note: string;
|
||||||
|
active: boolean;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query builder.
|
||||||
|
*/
|
||||||
|
// static get QueryBuilder() {
|
||||||
|
// return CustomerQueryBuilder;
|
||||||
|
// }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Table name
|
||||||
|
*/
|
||||||
|
static get tableName() {
|
||||||
|
return 'contacts';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Model timestamps.
|
||||||
|
*/
|
||||||
|
get timestamps() {
|
||||||
|
return ['createdAt', 'updatedAt'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defined virtual attributes.
|
||||||
|
*/
|
||||||
|
static get virtualAttributes() {
|
||||||
|
return ['localOpeningBalance', 'closingBalance', 'contactNormal'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closing balance attribute.
|
||||||
|
*/
|
||||||
|
get closingBalance() {
|
||||||
|
return this.balance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the local opening balance.
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
get localOpeningBalance() {
|
||||||
|
return this.openingBalance
|
||||||
|
? this.openingBalance * this.openingBalanceExchangeRate
|
||||||
|
: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the contact noraml;
|
||||||
|
*/
|
||||||
|
get contactNormal() {
|
||||||
|
return 'debit';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
get contactAddresses() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
mail: this.email,
|
||||||
|
label: this.displayName,
|
||||||
|
primary: true
|
||||||
|
},
|
||||||
|
].filter((c) => c.mail);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Model modifiers.
|
||||||
|
*/
|
||||||
|
static get modifiers() {
|
||||||
|
return {
|
||||||
|
/**
|
||||||
|
* Inactive/Active mode.
|
||||||
|
*/
|
||||||
|
inactiveMode(query, active = false) {
|
||||||
|
query.where('active', !active);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters the active customers.
|
||||||
|
*/
|
||||||
|
active(query) {
|
||||||
|
query.where('active', 1);
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Filters the inactive customers.
|
||||||
|
*/
|
||||||
|
inactive(query) {
|
||||||
|
query.where('active', 0);
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Filters the customers that have overdue invoices.
|
||||||
|
*/
|
||||||
|
overdue(query) {
|
||||||
|
query.select(
|
||||||
|
'*',
|
||||||
|
Customer.relatedQuery('overDueInvoices', query.knex())
|
||||||
|
.count()
|
||||||
|
.as('countOverdue')
|
||||||
|
);
|
||||||
|
query.having('countOverdue', '>', 0);
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Filters the unpaid customers.
|
||||||
|
*/
|
||||||
|
unpaid(query) {
|
||||||
|
query.whereRaw('`BALANCE` + `OPENING_BALANCE` <> 0');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Relationship mapping.
|
||||||
|
*/
|
||||||
|
// static get relationMappings() {
|
||||||
|
// const SaleInvoice = require('models/SaleInvoice');
|
||||||
|
|
||||||
|
// return {
|
||||||
|
// salesInvoices: {
|
||||||
|
// relation: Model.HasManyRelation,
|
||||||
|
// modelClass: SaleInvoice.default,
|
||||||
|
// join: {
|
||||||
|
// from: 'contacts.id',
|
||||||
|
// to: 'sales_invoices.customerId',
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
|
||||||
|
// overDueInvoices: {
|
||||||
|
// relation: Model.HasManyRelation,
|
||||||
|
// modelClass: SaleInvoice.default,
|
||||||
|
// join: {
|
||||||
|
// from: 'contacts.id',
|
||||||
|
// to: 'sales_invoices.customerId',
|
||||||
|
// },
|
||||||
|
// filter: (query) => {
|
||||||
|
// query.modify('overdue');
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
|
||||||
|
// static get meta() {
|
||||||
|
// return CustomerSettings;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Retrieve the default custom views, roles and columns.
|
||||||
|
// */
|
||||||
|
// static get defaultViews() {
|
||||||
|
// return DEFAULT_VIEWS;
|
||||||
|
// }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Model search attributes.
|
||||||
|
*/
|
||||||
|
static get searchRoles() {
|
||||||
|
return [
|
||||||
|
{ fieldKey: 'display_name', comparator: 'contains' },
|
||||||
|
{ condition: 'or', fieldKey: 'first_name', comparator: 'contains' },
|
||||||
|
{ condition: 'or', fieldKey: 'last_name', comparator: 'equals' },
|
||||||
|
{ condition: 'or', fieldKey: 'company_name', comparator: 'equals' },
|
||||||
|
{ condition: 'or', fieldKey: 'email', comparator: 'equals' },
|
||||||
|
{ condition: 'or', fieldKey: 'work_phone', comparator: 'equals' },
|
||||||
|
{ condition: 'or', fieldKey: 'personal_phone', comparator: 'equals' },
|
||||||
|
{ condition: 'or', fieldKey: 'website', comparator: 'equals' },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
208
packages/server-nest/src/modules/Expenses/Expenses.types.ts
Normal file
208
packages/server-nest/src/modules/Expenses/Expenses.types.ts
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
import { Knex } from 'knex';
|
||||||
|
import { Expense } from './models/Expense.model';
|
||||||
|
import { SystemUser } from '../System/models/SystemUser';
|
||||||
|
import { IFilterRole } from '../DynamicListing/DynamicFilter/DynamicFilter.types';
|
||||||
|
// import { ISystemUser } from './User';
|
||||||
|
// import { IFilterRole } from './DynamicFilter';
|
||||||
|
// import { IAccount } from './Account';
|
||||||
|
// import { AttachmentLinkDTO } from './Attachments';
|
||||||
|
|
||||||
|
export interface IPaginationMeta {
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
pageSize: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IExpensesFilter {
|
||||||
|
page: number;
|
||||||
|
pageSize: number;
|
||||||
|
filterRoles?: IFilterRole[];
|
||||||
|
columnSortBy: string;
|
||||||
|
sortOrder: string;
|
||||||
|
viewSlug?: string;
|
||||||
|
filterQuery?: (query: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// export interface IExpense {
|
||||||
|
// id: number;
|
||||||
|
// totalAmount: number;
|
||||||
|
// localAmount?: number;
|
||||||
|
// currencyCode: string;
|
||||||
|
// exchangeRate: number;
|
||||||
|
// description?: string;
|
||||||
|
// paymentAccountId: number;
|
||||||
|
// peyeeId?: number;
|
||||||
|
// referenceNo?: string;
|
||||||
|
// publishedAt: Date | null;
|
||||||
|
// userId: number;
|
||||||
|
// paymentDate: Date;
|
||||||
|
// payeeId: number;
|
||||||
|
// landedCostAmount: number;
|
||||||
|
// allocatedCostAmount: number;
|
||||||
|
// unallocatedCostAmount: number;
|
||||||
|
// categories?: IExpenseCategory[];
|
||||||
|
// isPublished: boolean;
|
||||||
|
|
||||||
|
// localLandedCostAmount?: number;
|
||||||
|
// localAllocatedCostAmount?: number;
|
||||||
|
// localUnallocatedCostAmount?: number;
|
||||||
|
|
||||||
|
// billableAmount: number;
|
||||||
|
// invoicedAmount: number;
|
||||||
|
|
||||||
|
// branchId?: number;
|
||||||
|
|
||||||
|
// createdAt?: Date;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// export interface IExpenseCategory {
|
||||||
|
// id?: number;
|
||||||
|
// expenseAccountId: number;
|
||||||
|
// index: number;
|
||||||
|
// description: string;
|
||||||
|
// expenseId: number;
|
||||||
|
// amount: number;
|
||||||
|
|
||||||
|
// projectId?: number;
|
||||||
|
|
||||||
|
// allocatedCostAmount: number;
|
||||||
|
// unallocatedCostAmount: number;
|
||||||
|
// landedCost: boolean;
|
||||||
|
|
||||||
|
// expenseAccount?: IAccount;
|
||||||
|
// }
|
||||||
|
|
||||||
|
export interface IExpenseCommonDTO {
|
||||||
|
currencyCode: string;
|
||||||
|
exchangeRate?: number;
|
||||||
|
description?: string;
|
||||||
|
paymentAccountId: number;
|
||||||
|
peyeeId?: number;
|
||||||
|
referenceNo?: string;
|
||||||
|
publish: boolean;
|
||||||
|
userId: number;
|
||||||
|
paymentDate: Date;
|
||||||
|
payeeId: number;
|
||||||
|
categories: IExpenseCategoryDTO[];
|
||||||
|
|
||||||
|
branchId?: number;
|
||||||
|
// attachments?: AttachmentLinkDTO[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IExpenseCreateDTO extends IExpenseCommonDTO {}
|
||||||
|
export interface IExpenseEditDTO extends IExpenseCommonDTO {}
|
||||||
|
|
||||||
|
export interface IExpenseCategoryDTO {
|
||||||
|
id?: number;
|
||||||
|
expenseAccountId: number;
|
||||||
|
index: number;
|
||||||
|
amount: number;
|
||||||
|
description?: string;
|
||||||
|
expenseId: number;
|
||||||
|
landedCost?: boolean;
|
||||||
|
projectId?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// export interface IExpensesService {
|
||||||
|
// newExpense(
|
||||||
|
// tenantid: number,
|
||||||
|
// expenseDTO: IExpenseDTO,
|
||||||
|
// authorizedUser: ISystemUser
|
||||||
|
// ): Promise<IExpense>;
|
||||||
|
|
||||||
|
// editExpense(
|
||||||
|
// tenantid: number,
|
||||||
|
// expenseId: number,
|
||||||
|
// expenseDTO: IExpenseDTO,
|
||||||
|
// authorizedUser: ISystemUser
|
||||||
|
// ): void;
|
||||||
|
|
||||||
|
// publishExpense(
|
||||||
|
// tenantId: number,
|
||||||
|
// expenseId: number,
|
||||||
|
// authorizedUser: ISystemUser
|
||||||
|
// ): Promise<void>;
|
||||||
|
|
||||||
|
// deleteExpense(
|
||||||
|
// tenantId: number,
|
||||||
|
// expenseId: number,
|
||||||
|
// authorizedUser: ISystemUser
|
||||||
|
// ): Promise<void>;
|
||||||
|
|
||||||
|
// getExpensesList(
|
||||||
|
// tenantId: number,
|
||||||
|
// expensesFilter: IExpensesFilter
|
||||||
|
// ): Promise<{
|
||||||
|
// expenses: IExpense[];
|
||||||
|
// pagination: IPaginationMeta;
|
||||||
|
// filterMeta: IFilterMeta;
|
||||||
|
// }>;
|
||||||
|
|
||||||
|
// getExpense(tenantId: number, expenseId: number): Promise<IExpense>;
|
||||||
|
// }
|
||||||
|
|
||||||
|
export interface IExpenseCreatingPayload {
|
||||||
|
trx: Knex.Transaction;
|
||||||
|
// tenantId: number;
|
||||||
|
expenseDTO: IExpenseCreateDTO;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IExpenseEventEditingPayload {
|
||||||
|
// tenantId: number;
|
||||||
|
oldExpense: Expense;
|
||||||
|
expenseDTO: IExpenseEditDTO;
|
||||||
|
trx: Knex.Transaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IExpenseCreatedPayload {
|
||||||
|
// tenantId: number;
|
||||||
|
expenseId: number;
|
||||||
|
// authorizedUser: ISystemUser;
|
||||||
|
expense: Expense;
|
||||||
|
expenseDTO: IExpenseCreateDTO;
|
||||||
|
trx?: Knex.Transaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IExpenseEventEditPayload {
|
||||||
|
// tenantId: number;
|
||||||
|
expenseId: number;
|
||||||
|
expense: Expense;
|
||||||
|
expenseDTO: IExpenseEditDTO;
|
||||||
|
authorizedUser: SystemUser;
|
||||||
|
oldExpense: Expense;
|
||||||
|
trx: Knex.Transaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IExpenseEventDeletePayload {
|
||||||
|
// tenantId: number;
|
||||||
|
expenseId: number;
|
||||||
|
authorizedUser: SystemUser;
|
||||||
|
oldExpense: Expense;
|
||||||
|
trx: Knex.Transaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IExpenseDeletingPayload {
|
||||||
|
trx: Knex.Transaction;
|
||||||
|
// tenantId: number;
|
||||||
|
oldExpense: Expense;
|
||||||
|
}
|
||||||
|
export interface IExpenseEventPublishedPayload {
|
||||||
|
// tenantId: number;
|
||||||
|
expenseId: number;
|
||||||
|
oldExpense: Expense;
|
||||||
|
expense: Expense;
|
||||||
|
authorizedUser: SystemUser;
|
||||||
|
trx: Knex.Transaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IExpensePublishingPayload {
|
||||||
|
trx: Knex.Transaction;
|
||||||
|
oldExpense: Expense;
|
||||||
|
// tenantId: number;
|
||||||
|
}
|
||||||
|
export enum ExpenseAction {
|
||||||
|
Create = 'Create',
|
||||||
|
Edit = 'Edit',
|
||||||
|
Delete = 'Delete',
|
||||||
|
View = 'View',
|
||||||
|
}
|
||||||
@@ -1,113 +1,113 @@
|
|||||||
import * as R from 'ramda';
|
// import * as R from 'ramda';
|
||||||
import {
|
// import {
|
||||||
AccountNormal,
|
// AccountNormal,
|
||||||
IExpenseCategory,
|
// IExpenseCategory,
|
||||||
ILedger,
|
// ILedger,
|
||||||
ILedgerEntry,
|
// ILedgerEntry,
|
||||||
} from '@/interfaces';
|
// } from '@/interfaces';
|
||||||
import Ledger from '../Accounting/Ledger';
|
// import Ledger from '../Accounting/Ledger';
|
||||||
|
|
||||||
export class ExpenseGL {
|
// export class ExpenseGL {
|
||||||
private expense: any;
|
// private expense: any;
|
||||||
|
|
||||||
/**
|
// /**
|
||||||
* Constructor method.
|
// * Constructor method.
|
||||||
*/
|
// */
|
||||||
constructor(expense: any) {
|
// constructor(expense: any) {
|
||||||
this.expense = expense;
|
// this.expense = expense;
|
||||||
}
|
// }
|
||||||
|
|
||||||
/**
|
// /**
|
||||||
* Retrieves the expense GL common entry.
|
// * Retrieves the expense GL common entry.
|
||||||
* @param {IExpense} expense
|
// * @param {IExpense} expense
|
||||||
* @returns {Partial<ILedgerEntry>}
|
// * @returns {Partial<ILedgerEntry>}
|
||||||
*/
|
// */
|
||||||
private getExpenseGLCommonEntry = (): Partial<ILedgerEntry> => {
|
// private getExpenseGLCommonEntry = (): Partial<ILedgerEntry> => {
|
||||||
return {
|
// return {
|
||||||
currencyCode: this.expense.currencyCode,
|
// currencyCode: this.expense.currencyCode,
|
||||||
exchangeRate: this.expense.exchangeRate,
|
// exchangeRate: this.expense.exchangeRate,
|
||||||
|
|
||||||
transactionType: 'Expense',
|
// transactionType: 'Expense',
|
||||||
transactionId: this.expense.id,
|
// transactionId: this.expense.id,
|
||||||
|
|
||||||
date: this.expense.paymentDate,
|
// date: this.expense.paymentDate,
|
||||||
userId: this.expense.userId,
|
// userId: this.expense.userId,
|
||||||
|
|
||||||
debit: 0,
|
// debit: 0,
|
||||||
credit: 0,
|
// credit: 0,
|
||||||
|
|
||||||
branchId: this.expense.branchId,
|
// branchId: this.expense.branchId,
|
||||||
};
|
// };
|
||||||
};
|
// };
|
||||||
|
|
||||||
/**
|
// /**
|
||||||
* Retrieves the expense GL payment entry.
|
// * Retrieves the expense GL payment entry.
|
||||||
* @param {IExpense} expense
|
// * @param {IExpense} expense
|
||||||
* @returns {ILedgerEntry}
|
// * @returns {ILedgerEntry}
|
||||||
*/
|
// */
|
||||||
private getExpenseGLPaymentEntry = (): ILedgerEntry => {
|
// private getExpenseGLPaymentEntry = (): ILedgerEntry => {
|
||||||
const commonEntry = this.getExpenseGLCommonEntry();
|
// const commonEntry = this.getExpenseGLCommonEntry();
|
||||||
|
|
||||||
return {
|
// return {
|
||||||
...commonEntry,
|
// ...commonEntry,
|
||||||
credit: this.expense.localAmount,
|
// credit: this.expense.localAmount,
|
||||||
accountId: this.expense.paymentAccountId,
|
// accountId: this.expense.paymentAccountId,
|
||||||
accountNormal:
|
// accountNormal:
|
||||||
this.expense?.paymentAccount?.accountNormal === 'debit'
|
// this.expense?.paymentAccount?.accountNormal === 'debit'
|
||||||
? AccountNormal.DEBIT
|
// ? AccountNormal.DEBIT
|
||||||
: AccountNormal.CREDIT,
|
// : AccountNormal.CREDIT,
|
||||||
index: 1,
|
// index: 1,
|
||||||
};
|
// };
|
||||||
};
|
// };
|
||||||
|
|
||||||
/**
|
// /**
|
||||||
* Retrieves the expense GL category entry.
|
// * Retrieves the expense GL category entry.
|
||||||
* @param {IExpense} expense -
|
// * @param {IExpense} expense -
|
||||||
* @param {IExpenseCategory} expenseCategory -
|
// * @param {IExpenseCategory} expenseCategory -
|
||||||
* @param {number} index
|
// * @param {number} index
|
||||||
* @returns {ILedgerEntry}
|
// * @returns {ILedgerEntry}
|
||||||
*/
|
// */
|
||||||
private getExpenseGLCategoryEntry = R.curry(
|
// private getExpenseGLCategoryEntry = R.curry(
|
||||||
(category: IExpenseCategory, index: number): ILedgerEntry => {
|
// (category: IExpenseCategory, index: number): ILedgerEntry => {
|
||||||
const commonEntry = this.getExpenseGLCommonEntry();
|
// const commonEntry = this.getExpenseGLCommonEntry();
|
||||||
const localAmount = category.amount * this.expense.exchangeRate;
|
// const localAmount = category.amount * this.expense.exchangeRate;
|
||||||
|
|
||||||
return {
|
// return {
|
||||||
...commonEntry,
|
// ...commonEntry,
|
||||||
accountId: category.expenseAccountId,
|
// accountId: category.expenseAccountId,
|
||||||
accountNormal: AccountNormal.DEBIT,
|
// accountNormal: AccountNormal.DEBIT,
|
||||||
debit: localAmount,
|
// debit: localAmount,
|
||||||
note: category.description,
|
// note: category.description,
|
||||||
index: index + 2,
|
// index: index + 2,
|
||||||
projectId: category.projectId,
|
// projectId: category.projectId,
|
||||||
};
|
// };
|
||||||
}
|
// }
|
||||||
);
|
// );
|
||||||
|
|
||||||
/**
|
// /**
|
||||||
* Retrieves the expense GL entries.
|
// * Retrieves the expense GL entries.
|
||||||
* @param {IExpense} expense
|
// * @param {IExpense} expense
|
||||||
* @returns {ILedgerEntry[]}
|
// * @returns {ILedgerEntry[]}
|
||||||
*/
|
// */
|
||||||
public getExpenseGLEntries = (): ILedgerEntry[] => {
|
// public getExpenseGLEntries = (): ILedgerEntry[] => {
|
||||||
const getCategoryEntry = this.getExpenseGLCategoryEntry();
|
// const getCategoryEntry = this.getExpenseGLCategoryEntry();
|
||||||
|
|
||||||
const paymentEntry = this.getExpenseGLPaymentEntry();
|
// const paymentEntry = this.getExpenseGLPaymentEntry();
|
||||||
const categoryEntries = this.expense.categories.map(getCategoryEntry);
|
// const categoryEntries = this.expense.categories.map(getCategoryEntry);
|
||||||
|
|
||||||
return [paymentEntry, ...categoryEntries];
|
// return [paymentEntry, ...categoryEntries];
|
||||||
};
|
// };
|
||||||
|
|
||||||
/**
|
// /**
|
||||||
* Retrieves the given expense ledger.
|
// * Retrieves the given expense ledger.
|
||||||
* @param {IExpense} expense
|
// * @param {IExpense} expense
|
||||||
* @returns {ILedger}
|
// * @returns {ILedger}
|
||||||
*/
|
// */
|
||||||
public getExpenseLedger = (): ILedger => {
|
// public getExpenseLedger = (): ILedger => {
|
||||||
const entries = this.getExpenseGLEntries();
|
// const entries = this.getExpenseGLEntries();
|
||||||
|
|
||||||
console.log(entries, 'entries');
|
// console.log(entries, 'entries');
|
||||||
|
|
||||||
return new Ledger(entries);
|
// return new Ledger(entries);
|
||||||
};
|
// };
|
||||||
}
|
// }
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
// import { Knex } from 'knex';
|
||||||
|
// import { ExpenseGL } from './ExpenseGL';
|
||||||
|
// import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
// import { Expense } from '../models/Expense.model';
|
||||||
|
|
||||||
|
// @Injectable()
|
||||||
|
// export class ExpenseGLEntries {
|
||||||
|
// constructor(
|
||||||
|
// @Inject(Expense.name)
|
||||||
|
// private readonly expense: typeof Expense,
|
||||||
|
// ) {}
|
||||||
|
// /**
|
||||||
|
// * Retrieves the expense G/L of the given id.
|
||||||
|
// * @param {number} expenseId
|
||||||
|
// * @param {Knex.Transaction} trx
|
||||||
|
// * @returns {Promise<ILedger>}
|
||||||
|
// */
|
||||||
|
// public getExpenseLedgerById = async (
|
||||||
|
// expenseId: number,
|
||||||
|
// trx?: Knex.Transaction,
|
||||||
|
// ): Promise<ILedger> => {
|
||||||
|
// const expense = await this.expense
|
||||||
|
// .query(trx)
|
||||||
|
// .findById(expenseId)
|
||||||
|
// .withGraphFetched('categories')
|
||||||
|
// .withGraphFetched('paymentAccount')
|
||||||
|
// .throwIfNotFound();
|
||||||
|
|
||||||
|
// return this.getExpenseLedger(expense);
|
||||||
|
// };
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Retrieves the given expense ledger.
|
||||||
|
// * @param {IExpense} expense
|
||||||
|
// * @returns {ILedger}
|
||||||
|
// */
|
||||||
|
// public getExpenseLedger = (expense: Expense): ILedger => {
|
||||||
|
// const expenseGL = new ExpenseGL(expense);
|
||||||
|
|
||||||
|
// return expenseGL.getExpenseLedger();
|
||||||
|
// };
|
||||||
|
// }
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
// import {
|
||||||
|
// IExpenseCreatedPayload,
|
||||||
|
// IExpenseEventDeletePayload,
|
||||||
|
// IExpenseEventEditPayload,
|
||||||
|
// IExpenseEventPublishedPayload,
|
||||||
|
// } from '../Expenses.types';
|
||||||
|
// import { ExpenseGLEntriesStorage } from './ExpenseGLEntriesStorage';
|
||||||
|
// import { Injectable } from '@nestjs/common';
|
||||||
|
// import { OnEvent } from '@nestjs/event-emitter';
|
||||||
|
// import { events } from '@/common/events/events';
|
||||||
|
|
||||||
|
// @Injectable()
|
||||||
|
// export class ExpensesWriteGLSubscriber {
|
||||||
|
// /**
|
||||||
|
// * @param {ExpenseGLEntriesStorage} expenseGLEntries -
|
||||||
|
// */
|
||||||
|
// constructor(private readonly expenseGLEntries: ExpenseGLEntriesStorage) {}
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Handles the writing journal entries once the expense created.
|
||||||
|
// * @param {IExpenseCreatedPayload} payload -
|
||||||
|
// */
|
||||||
|
// @OnEvent(events.expenses.onCreated)
|
||||||
|
// public async handleWriteGLEntriesOnceCreated({
|
||||||
|
// expense,
|
||||||
|
// trx,
|
||||||
|
// }: IExpenseCreatedPayload) {
|
||||||
|
// // In case expense published, write journal entries.
|
||||||
|
// if (!expense.publishedAt) return;
|
||||||
|
|
||||||
|
// await this.expenseGLEntries.writeExpenseGLEntries(expense.id, trx);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Handle writing expense journal entries once the expense edited.
|
||||||
|
// * @param {IExpenseEventEditPayload} payload -
|
||||||
|
// */
|
||||||
|
// @OnEvent(events.expenses.onEdited)
|
||||||
|
// public async handleRewriteGLEntriesOnceEdited({
|
||||||
|
// expenseId,
|
||||||
|
// expense,
|
||||||
|
// authorizedUser,
|
||||||
|
// trx,
|
||||||
|
// }: IExpenseEventEditPayload) {
|
||||||
|
// // Cannot continue if the expense is not published.
|
||||||
|
// if (!expense.publishedAt) return;
|
||||||
|
|
||||||
|
// await this.expenseGLEntries.rewriteExpenseGLEntries(expense.id, trx);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Reverts expense journal entries once the expense deleted.
|
||||||
|
// * @param {IExpenseEventDeletePayload} payload -
|
||||||
|
// */
|
||||||
|
// @OnEvent(events.expenses.onDeleted)
|
||||||
|
// public async handleRevertGLEntriesOnceDeleted({
|
||||||
|
// expenseId,
|
||||||
|
// trx,
|
||||||
|
// }: IExpenseEventDeletePayload) {
|
||||||
|
// await this.expenseGLEntries.revertExpenseGLEntries(expenseId, trx);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Handles writing expense journal once the expense publish.
|
||||||
|
// * @param {IExpenseEventPublishedPayload} payload -
|
||||||
|
// */
|
||||||
|
// @OnEvent(events.expenses.onPublished)
|
||||||
|
// public async handleWriteGLEntriesOncePublished({
|
||||||
|
// expense,
|
||||||
|
// trx,
|
||||||
|
// }: IExpenseEventPublishedPayload) {
|
||||||
|
// // In case expense published, write journal entries.
|
||||||
|
// if (!expense.publishedAt) return;
|
||||||
|
|
||||||
|
// await this.expenseGLEntries.rewriteExpenseGLEntries(expense.id, trx);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
import { Knex } from 'knex';
|
|
||||||
import { Inject, Service } from 'typedi';
|
|
||||||
import { IExpense, ILedger } from '@/interfaces';
|
|
||||||
import { ExpenseGL } from './ExpenseGL';
|
|
||||||
import HasTenancyService from '../Tenancy/TenancyService';
|
|
||||||
|
|
||||||
@Service()
|
|
||||||
export class ExpenseGLEntries {
|
|
||||||
@Inject()
|
|
||||||
private tenancy: HasTenancyService;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves the expense G/L of the given id.
|
|
||||||
* @param {number} tenantId
|
|
||||||
* @param {number} expenseId
|
|
||||||
* @param {Knex.Transaction} trx
|
|
||||||
* @returns {Promise<ILedger>}
|
|
||||||
*/
|
|
||||||
public getExpenseLedgerById = async (
|
|
||||||
tenantId: number,
|
|
||||||
expenseId: number,
|
|
||||||
trx?: Knex.Transaction
|
|
||||||
): Promise<ILedger> => {
|
|
||||||
const { Expense } = await this.tenancy.models(tenantId);
|
|
||||||
|
|
||||||
const expense = await Expense.query(trx)
|
|
||||||
.findById(expenseId)
|
|
||||||
.withGraphFetched('categories')
|
|
||||||
.withGraphFetched('paymentAccount')
|
|
||||||
.throwIfNotFound();
|
|
||||||
|
|
||||||
return this.getExpenseLedger(expense);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves the given expense ledger.
|
|
||||||
* @param {IExpense} expense
|
|
||||||
* @returns {ILedger}
|
|
||||||
*/
|
|
||||||
public getExpenseLedger = (expense: IExpense): ILedger => {
|
|
||||||
const expenseGL = new ExpenseGL(expense);
|
|
||||||
|
|
||||||
return expenseGL.getExpenseLedger();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,72 +1,67 @@
|
|||||||
import { Knex } from 'knex';
|
// import { Knex } from 'knex';
|
||||||
import { Service, Inject } from 'typedi';
|
// import { ExpenseGLEntries } from './ExpenseGLEntries.service';
|
||||||
import LedgerStorageService from '@/services/Accounting/LedgerStorageService';
|
// import { Injectable } from '@nestjs/common';
|
||||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
|
||||||
import { ExpenseGLEntries } from './ExpenseGLEntriesService';
|
|
||||||
|
|
||||||
@Service()
|
// @Injectable()
|
||||||
export class ExpenseGLEntriesStorage {
|
// export class ExpenseGLEntriesStorage {
|
||||||
@Inject()
|
// /**
|
||||||
private expenseGLEntries: ExpenseGLEntries;
|
// * @param {ExpenseGLEntries} expenseGLEntries
|
||||||
|
// * @param {LedgerStorageService} ledgerStorage
|
||||||
|
// */
|
||||||
|
// constructor(
|
||||||
|
// private readonly expenseGLEntries: ExpenseGLEntries,
|
||||||
|
// private readonly ledgerStorage: LedgerStorageService,
|
||||||
|
// ) {}
|
||||||
|
|
||||||
@Inject()
|
// /**
|
||||||
private ledgerStorage: LedgerStorageService;
|
// * Writes the expense GL entries.
|
||||||
|
// * @param {number} tenantId
|
||||||
|
// * @param {number} expenseId
|
||||||
|
// * @param {Knex.Transaction} trx
|
||||||
|
// */
|
||||||
|
// public writeExpenseGLEntries = async (
|
||||||
|
// expenseId: number,
|
||||||
|
// trx?: Knex.Transaction,
|
||||||
|
// ) => {
|
||||||
|
// // Retrieves the given expense ledger.
|
||||||
|
// const expenseLedger = await this.expenseGLEntries.getExpenseLedgerById(
|
||||||
|
// expenseId,
|
||||||
|
// trx,
|
||||||
|
// );
|
||||||
|
// // Commits the expense ledger entries.
|
||||||
|
// await this.ledgerStorage.commit(expenseLedger, trx);
|
||||||
|
// };
|
||||||
|
|
||||||
/**
|
// /**
|
||||||
* Writes the expense GL entries.
|
// * Reverts the given expense GL entries.
|
||||||
* @param {number} tenantId
|
// * @param {number} tenantId
|
||||||
* @param {number} expenseId
|
// * @param {number} expenseId
|
||||||
* @param {Knex.Transaction} trx
|
// * @param {Knex.Transaction} trx
|
||||||
*/
|
// */
|
||||||
public writeExpenseGLEntries = async (
|
// public revertExpenseGLEntries = async (
|
||||||
tenantId: number,
|
// expenseId: number,
|
||||||
expenseId: number,
|
// trx?: Knex.Transaction,
|
||||||
trx?: Knex.Transaction
|
// ) => {
|
||||||
) => {
|
// await this.ledgerStorage.deleteByReference(
|
||||||
// Retrieves the given expense ledger.
|
// expenseId,
|
||||||
const expenseLedger = await this.expenseGLEntries.getExpenseLedgerById(
|
// 'Expense',
|
||||||
tenantId,
|
// trx,
|
||||||
expenseId,
|
// );
|
||||||
trx
|
// };
|
||||||
);
|
|
||||||
// Commits the expense ledger entries.
|
|
||||||
await this.ledgerStorage.commit(tenantId, expenseLedger, trx);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
// /**
|
||||||
* Reverts the given expense GL entries.
|
// * Rewrites the expense GL entries.
|
||||||
* @param {number} tenantId
|
// * @param {number} expenseId
|
||||||
* @param {number} expenseId
|
// * @param {Knex.Transaction} trx
|
||||||
* @param {Knex.Transaction} trx
|
// */
|
||||||
*/
|
// public rewriteExpenseGLEntries = async (
|
||||||
public revertExpenseGLEntries = async (
|
// expenseId: number,
|
||||||
tenantId: number,
|
// trx?: Knex.Transaction,
|
||||||
expenseId: number,
|
// ) => {
|
||||||
trx?: Knex.Transaction
|
// // Reverts the expense GL entries.
|
||||||
) => {
|
// await this.revertExpenseGLEntries(expenseId, trx);
|
||||||
await this.ledgerStorage.deleteByReference(
|
|
||||||
tenantId,
|
|
||||||
expenseId,
|
|
||||||
'Expense',
|
|
||||||
trx
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
// // Writes the expense GL entries.
|
||||||
* Rewrites the expense GL entries.
|
// await this.writeExpenseGLEntries(expenseId, trx);
|
||||||
* @param {number} tenantId
|
// };
|
||||||
* @param {number} expenseId
|
// }
|
||||||
* @param {Knex.Transaction} trx
|
|
||||||
*/
|
|
||||||
public rewriteExpenseGLEntries = async (
|
|
||||||
tenantId: number,
|
|
||||||
expenseId: number,
|
|
||||||
trx?: Knex.Transaction
|
|
||||||
) => {
|
|
||||||
// Reverts the expense GL entries.
|
|
||||||
await this.revertExpenseGLEntries(tenantId, expenseId, trx);
|
|
||||||
|
|
||||||
// Writes the expense GL entries.
|
|
||||||
await this.writeExpenseGLEntries(tenantId, expenseId, trx);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,117 +0,0 @@
|
|||||||
import { Inject, Service } from 'typedi';
|
|
||||||
import events from '@/subscribers/events';
|
|
||||||
import TenancyService from '@/services/Tenancy/TenancyService';
|
|
||||||
import {
|
|
||||||
IExpenseCreatedPayload,
|
|
||||||
IExpenseEventDeletePayload,
|
|
||||||
IExpenseEventEditPayload,
|
|
||||||
IExpenseEventPublishedPayload,
|
|
||||||
} from '@/interfaces';
|
|
||||||
import { ExpenseGLEntriesStorage } from './ExpenseGLEntriesStorage';
|
|
||||||
|
|
||||||
@Service()
|
|
||||||
export class ExpensesWriteGLSubscriber {
|
|
||||||
@Inject()
|
|
||||||
private tenancy: TenancyService;
|
|
||||||
|
|
||||||
@Inject()
|
|
||||||
private expenseGLEntries: ExpenseGLEntriesStorage;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attaches events with handlers.
|
|
||||||
* @param bus
|
|
||||||
*/
|
|
||||||
public attach(bus) {
|
|
||||||
bus.subscribe(
|
|
||||||
events.expenses.onCreated,
|
|
||||||
this.handleWriteGLEntriesOnceCreated
|
|
||||||
);
|
|
||||||
bus.subscribe(
|
|
||||||
events.expenses.onEdited,
|
|
||||||
this.handleRewriteGLEntriesOnceEdited
|
|
||||||
);
|
|
||||||
bus.subscribe(
|
|
||||||
events.expenses.onDeleted,
|
|
||||||
this.handleRevertGLEntriesOnceDeleted
|
|
||||||
);
|
|
||||||
bus.subscribe(
|
|
||||||
events.expenses.onPublished,
|
|
||||||
this.handleWriteGLEntriesOncePublished
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles the writing journal entries once the expense created.
|
|
||||||
* @param {IExpenseCreatedPayload} payload -
|
|
||||||
*/
|
|
||||||
public handleWriteGLEntriesOnceCreated = async ({
|
|
||||||
expense,
|
|
||||||
tenantId,
|
|
||||||
trx,
|
|
||||||
}: IExpenseCreatedPayload) => {
|
|
||||||
// In case expense published, write journal entries.
|
|
||||||
if (!expense.publishedAt) return;
|
|
||||||
|
|
||||||
await this.expenseGLEntries.writeExpenseGLEntries(
|
|
||||||
tenantId,
|
|
||||||
expense.id,
|
|
||||||
trx
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle writing expense journal entries once the expense edited.
|
|
||||||
* @param {IExpenseEventEditPayload} payload -
|
|
||||||
*/
|
|
||||||
public handleRewriteGLEntriesOnceEdited = async ({
|
|
||||||
expenseId,
|
|
||||||
tenantId,
|
|
||||||
expense,
|
|
||||||
authorizedUser,
|
|
||||||
trx,
|
|
||||||
}: IExpenseEventEditPayload) => {
|
|
||||||
// Cannot continue if the expense is not published.
|
|
||||||
if (!expense.publishedAt) return;
|
|
||||||
|
|
||||||
await this.expenseGLEntries.rewriteExpenseGLEntries(
|
|
||||||
tenantId,
|
|
||||||
expense.id,
|
|
||||||
trx
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reverts expense journal entries once the expense deleted.
|
|
||||||
* @param {IExpenseEventDeletePayload} payload -
|
|
||||||
*/
|
|
||||||
public handleRevertGLEntriesOnceDeleted = async ({
|
|
||||||
expenseId,
|
|
||||||
tenantId,
|
|
||||||
trx,
|
|
||||||
}: IExpenseEventDeletePayload) => {
|
|
||||||
await this.expenseGLEntries.revertExpenseGLEntries(
|
|
||||||
tenantId,
|
|
||||||
expenseId,
|
|
||||||
trx
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles writing expense journal once the expense publish.
|
|
||||||
* @param {IExpenseEventPublishedPayload} payload -
|
|
||||||
*/
|
|
||||||
public handleWriteGLEntriesOncePublished = async ({
|
|
||||||
tenantId,
|
|
||||||
expense,
|
|
||||||
trx,
|
|
||||||
}: IExpenseEventPublishedPayload) => {
|
|
||||||
// In case expense published, write journal entries.
|
|
||||||
if (!expense.publishedAt) return;
|
|
||||||
|
|
||||||
await this.expenseGLEntries.rewriteExpenseGLEntries(
|
|
||||||
tenantId,
|
|
||||||
expense.id,
|
|
||||||
trx
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
252
packages/server-nest/src/modules/Items/ItemsEntries.service.ts
Normal file
252
packages/server-nest/src/modules/Items/ItemsEntries.service.ts
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
import { Knex } from 'knex';
|
||||||
|
import { sumBy, difference, map } from 'lodash';
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { Item } from './models/Item';
|
||||||
|
import { ItemEntry } from '../TransactionItemEntry/models/ItemEntry';
|
||||||
|
import { ServiceError } from './ServiceError';
|
||||||
|
import { IItemEntryDTO } from '../TransactionItemEntry/ItemEntry.types';
|
||||||
|
|
||||||
|
const ERRORS = {
|
||||||
|
ITEMS_NOT_FOUND: 'ITEMS_NOT_FOUND',
|
||||||
|
ENTRIES_IDS_NOT_FOUND: 'ENTRIES_IDS_NOT_FOUND',
|
||||||
|
NOT_PURCHASE_ABLE_ITEMS: 'NOT_PURCHASE_ABLE_ITEMS',
|
||||||
|
NOT_SELL_ABLE_ITEMS: 'NOT_SELL_ABLE_ITEMS',
|
||||||
|
};
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ItemsEntriesService {
|
||||||
|
/**
|
||||||
|
* @param {typeof Item} itemModel - Item model.
|
||||||
|
* @param {typeof ItemEntry} itemEntryModel - Item entry model.
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
@Inject(Item.name) private readonly itemModel: typeof Item,
|
||||||
|
@Inject(ItemEntry.name) private readonly itemEntryModel: typeof ItemEntry,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the inventory items entries of the reference id and type.
|
||||||
|
* @param {string} referenceType - Reference type.
|
||||||
|
* @param {number} referenceId - Reference id.
|
||||||
|
* @return {Promise<IItemEntry[]>}
|
||||||
|
*/
|
||||||
|
public async getInventoryEntries(
|
||||||
|
referenceType: string,
|
||||||
|
referenceId: number,
|
||||||
|
): Promise<ItemEntry[]> {
|
||||||
|
const itemsEntries = await this.itemEntryModel
|
||||||
|
.query()
|
||||||
|
.where('reference_type', referenceType)
|
||||||
|
.where('reference_id', referenceId);
|
||||||
|
|
||||||
|
const inventoryItems = await this.itemModel
|
||||||
|
.query()
|
||||||
|
.whereIn('id', map(itemsEntries, 'itemId'))
|
||||||
|
.where('type', 'inventory');
|
||||||
|
|
||||||
|
const inventoryItemsIds = map(inventoryItems, 'id');
|
||||||
|
|
||||||
|
return itemsEntries.filter(
|
||||||
|
(itemEntry) => inventoryItemsIds.indexOf(itemEntry.itemId) !== -1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter the given entries to inventory entries.
|
||||||
|
* @param {IItemEntry[]} entries - Items entries.
|
||||||
|
* @param {Knex.Transaction} trx - Knex transaction.
|
||||||
|
* @returns {Promise<IItemEntry[]>}
|
||||||
|
*/
|
||||||
|
public async filterInventoryEntries(
|
||||||
|
entries: ItemEntry[],
|
||||||
|
trx?: Knex.Transaction,
|
||||||
|
): Promise<ItemEntry[]> {
|
||||||
|
const entriesItemsIds = entries.map((e) => e.itemId);
|
||||||
|
|
||||||
|
const inventoryItems = await this.itemModel
|
||||||
|
.query(trx)
|
||||||
|
.whereIn('id', entriesItemsIds)
|
||||||
|
.where('type', 'inventory');
|
||||||
|
|
||||||
|
return entries.filter((entry) =>
|
||||||
|
inventoryItems.some((item) => item.id === entry.itemId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the entries items ids.
|
||||||
|
* @param {IItemEntryDTO[]} itemEntries - Items entries.
|
||||||
|
* @returns {Promise<Item[]>}
|
||||||
|
*/
|
||||||
|
public async validateItemsIdsExistance(itemEntries: IItemEntryDTO[]) {
|
||||||
|
const itemsIds = itemEntries.map((e) => e.itemId);
|
||||||
|
|
||||||
|
const foundItems = await this.itemModel.query().whereIn('id', itemsIds);
|
||||||
|
|
||||||
|
const foundItemsIds = foundItems.map((item: Item) => item.id);
|
||||||
|
const notFoundItemsIds = difference(itemsIds, foundItemsIds);
|
||||||
|
|
||||||
|
if (notFoundItemsIds.length > 0) {
|
||||||
|
throw new ServiceError(ERRORS.ITEMS_NOT_FOUND);
|
||||||
|
}
|
||||||
|
return foundItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the entries ids existance on the storage.
|
||||||
|
* @param {number} referenceId -
|
||||||
|
* @param {string} referenceType -
|
||||||
|
* @param {IItemEntryDTO[]} entries -
|
||||||
|
*/
|
||||||
|
public async validateEntriesIdsExistance(
|
||||||
|
referenceId: number,
|
||||||
|
referenceType: string,
|
||||||
|
billEntries: IItemEntryDTO[],
|
||||||
|
) {
|
||||||
|
const entriesIds = billEntries
|
||||||
|
.filter((e: ItemEntry) => e.id)
|
||||||
|
.map((e: ItemEntry) => e.id);
|
||||||
|
|
||||||
|
const storedEntries = await this.itemEntryModel
|
||||||
|
.query()
|
||||||
|
.whereIn('reference_id', [referenceId])
|
||||||
|
.whereIn('reference_type', [referenceType]);
|
||||||
|
|
||||||
|
const storedEntriesIds = storedEntries.map((entry) => entry.id);
|
||||||
|
const notFoundEntriesIds = difference(entriesIds, storedEntriesIds);
|
||||||
|
|
||||||
|
if (notFoundEntriesIds.length > 0) {
|
||||||
|
throw new ServiceError(ERRORS.ENTRIES_IDS_NOT_FOUND);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the entries items that not purchase-able.
|
||||||
|
* @param {IItemEntryDTO[]} itemEntries -
|
||||||
|
*/
|
||||||
|
public async validateNonPurchasableEntriesItems(
|
||||||
|
itemEntries: IItemEntryDTO[],
|
||||||
|
) {
|
||||||
|
const itemsIds = itemEntries.map((e: IItemEntryDTO) => e.itemId);
|
||||||
|
const purchasbleItems = await this.itemModel
|
||||||
|
.query()
|
||||||
|
.where('purchasable', true)
|
||||||
|
.whereIn('id', itemsIds);
|
||||||
|
|
||||||
|
const purchasbleItemsIds = purchasbleItems.map((item: Item) => item.id);
|
||||||
|
const notPurchasableItems = difference(itemsIds, purchasbleItemsIds);
|
||||||
|
|
||||||
|
if (notPurchasableItems.length > 0) {
|
||||||
|
throw new ServiceError(ERRORS.NOT_PURCHASE_ABLE_ITEMS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the entries items that not sell-able.
|
||||||
|
* @param {IItemEntryDTO[]} itemEntries -
|
||||||
|
*/
|
||||||
|
public async validateNonSellableEntriesItems(itemEntries: IItemEntryDTO[]) {
|
||||||
|
const itemsIds = itemEntries.map((e: IItemEntryDTO) => e.itemId);
|
||||||
|
|
||||||
|
const sellableItems = await this.itemModel
|
||||||
|
.query()
|
||||||
|
.where('sellable', true)
|
||||||
|
.whereIn('id', itemsIds);
|
||||||
|
|
||||||
|
const sellableItemsIds = sellableItems.map((item: Item) => item.id);
|
||||||
|
const nonSellableItems = difference(itemsIds, sellableItemsIds);
|
||||||
|
|
||||||
|
if (nonSellableItems.length > 0) {
|
||||||
|
throw new ServiceError(ERRORS.NOT_SELL_ABLE_ITEMS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changes items quantity from the given items entries the new and old onces.
|
||||||
|
* @param {IItemEntry[]} entries - Items entries.
|
||||||
|
* @param {IItemEntry[]} oldEntries - Old items entries.
|
||||||
|
*/
|
||||||
|
public async changeItemsQuantity(
|
||||||
|
entries: ItemEntry[],
|
||||||
|
oldEntries?: ItemEntry[],
|
||||||
|
): Promise<void> {
|
||||||
|
const opers = [];
|
||||||
|
|
||||||
|
// const diffEntries = entriesAmountDiff(
|
||||||
|
// entries,
|
||||||
|
// oldEntries,
|
||||||
|
// 'quantity',
|
||||||
|
// 'itemId',
|
||||||
|
// );
|
||||||
|
// diffEntries.forEach((entry: ItemEntry) => {
|
||||||
|
// const changeQuantityOper = this.itemRepository.changeNumber(
|
||||||
|
// { id: entry.itemId, type: 'inventory' },
|
||||||
|
// 'quantityOnHand',
|
||||||
|
// entry.quantity,
|
||||||
|
// );
|
||||||
|
// opers.push(changeQuantityOper);
|
||||||
|
// });
|
||||||
|
// await Promise.all(opers);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increment items quantity from the given items entries.
|
||||||
|
* @param {IItemEntry[]} entries - Items entries.
|
||||||
|
*/
|
||||||
|
public async incrementItemsEntries(entries: ItemEntry[]): Promise<void> {
|
||||||
|
return this.changeItemsQuantity(entries);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrement items quantity from the given items entries.
|
||||||
|
* @param {IItemEntry[]} entries - Items entries.
|
||||||
|
*/
|
||||||
|
public async decrementItemsQuantity(entries: ItemEntry[]): Promise<void> {
|
||||||
|
// return this.changeItemsQuantity(
|
||||||
|
// entries.map((entry) => ({
|
||||||
|
// ...entry,
|
||||||
|
// quantity: entry.quantity * -1,
|
||||||
|
// })),
|
||||||
|
// );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the cost/sell accounts to the invoice entries.
|
||||||
|
*/
|
||||||
|
public setItemsEntriesDefaultAccounts() {
|
||||||
|
return async (entries: ItemEntry[]) => {
|
||||||
|
const entriesItemsIds = entries.map((e) => e.itemId);
|
||||||
|
const items = await this.itemModel.query().whereIn('id', entriesItemsIds);
|
||||||
|
|
||||||
|
return entries.map((entry) => {
|
||||||
|
const item = items.find((i) => i.id === entry.itemId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...entry,
|
||||||
|
sellAccountId: entry.sellAccountId || item.sellAccountId,
|
||||||
|
...(item.type === 'inventory' && {
|
||||||
|
costAccountId: entry.costAccountId || item.costAccountId,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the total items entries.
|
||||||
|
* @param {ItemEntry[]} entries - Items entries.
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
public getTotalItemsEntries(entries: ItemEntry[]): number {
|
||||||
|
return sumBy(entries, (e) => ItemEntry.calcAmount(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the non-zero tax items entries.
|
||||||
|
* @param {ItemEntry[]} entries -
|
||||||
|
* @returns {ItemEntry[]}
|
||||||
|
*/
|
||||||
|
public getNonZeroEntries(entries: ItemEntry[]): ItemEntry[] {
|
||||||
|
return entries.filter((e) => e.taxRate > 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +1,19 @@
|
|||||||
|
import { HttpStatus } from '@nestjs/common';
|
||||||
|
|
||||||
export class ServiceError {
|
export class ServiceError extends Error {
|
||||||
errorType: string;
|
errorType: string;
|
||||||
message: string;
|
message: string;
|
||||||
payload: any;
|
payload: any;
|
||||||
|
|
||||||
constructor(errorType: string, message?: string, payload?: any) {
|
constructor(errorType: string, message?: string, payload?: any) {
|
||||||
this.errorType = errorType;
|
super(message);
|
||||||
this.message = message || null;
|
|
||||||
|
this.errorType = errorType;
|
||||||
this.payload = payload;
|
this.message = message || null;
|
||||||
}
|
this.payload = payload;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getStatus(): HttpStatus {
|
||||||
|
return HttpStatus.INTERNAL_SERVER_ERROR;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,6 +21,13 @@ export class Item extends BaseModel {
|
|||||||
public readonly costAccountId: number;
|
public readonly costAccountId: number;
|
||||||
public readonly inventoryAccountId: number;
|
public readonly inventoryAccountId: number;
|
||||||
public readonly categoryId: number;
|
public readonly categoryId: number;
|
||||||
|
public readonly pictureUri: string;
|
||||||
|
public readonly sellAccountId: number;
|
||||||
|
public readonly sellDescription: string;
|
||||||
|
public readonly purchaseDescription: string;
|
||||||
|
public readonly landedCost: boolean;
|
||||||
|
public readonly note: string;
|
||||||
|
public readonly userId: number;
|
||||||
|
|
||||||
public readonly warehouse!: Warehouse;
|
public readonly warehouse!: Warehouse;
|
||||||
|
|
||||||
|
|||||||
@@ -1,37 +1,40 @@
|
|||||||
// import { Inject, Service } from 'typedi';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
// import { isNil } from 'lodash';
|
import { isNil } from 'lodash';
|
||||||
// import HasTenancyService from '../Tenancy/TenancyService';
|
import { PdfTemplateModel } from './models/PdfTemplate';
|
||||||
|
|
||||||
// @Service()
|
@Injectable()
|
||||||
// export class BrandingTemplateDTOTransformer {
|
export class BrandingTemplateDTOTransformer {
|
||||||
// @Inject()
|
/**
|
||||||
// private tenancy: HasTenancyService;
|
* @param {PdfTemplateModel} - Pdf template model.
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
@Inject(PdfTemplateModel.name)
|
||||||
|
private readonly pdfTemplate: typeof PdfTemplateModel,
|
||||||
|
) {}
|
||||||
|
|
||||||
// /**
|
/**
|
||||||
// * Associates the default branding template id.
|
* Associates the default branding template id.
|
||||||
// * @param {number} tenantId
|
* @param {string} resource - Resource name.
|
||||||
// * @param {string} resource
|
* @param {Record<string, any>} object -
|
||||||
// * @param {Record<string, any>} object
|
* @param {string} attributeName
|
||||||
// * @param {string} attributeName
|
* @returns
|
||||||
// * @returns
|
*/
|
||||||
// */
|
public assocDefaultBrandingTemplate =
|
||||||
// public assocDefaultBrandingTemplate =
|
(resource: string) => async (object: Record<string, any>) => {
|
||||||
// (tenantId: number, resource: string) =>
|
const attributeName = 'pdfTemplateId';
|
||||||
// async (object: Record<string, any>) => {
|
|
||||||
// const { PdfTemplate } = this.tenancy.models(tenantId);
|
|
||||||
// const attributeName = 'pdfTemplateId';
|
|
||||||
|
|
||||||
// const defaultTemplate = await PdfTemplate.query()
|
const defaultTemplate = await this.pdfTemplate
|
||||||
// .modify('default')
|
.query()
|
||||||
// .findOne({ resource });
|
.modify('default')
|
||||||
|
.findOne({ resource });
|
||||||
|
|
||||||
// // If the default template is not found OR the given object has no defined template id.
|
// If the default template is not found OR the given object has no defined template id.
|
||||||
// if (!defaultTemplate || !isNil(object[attributeName])) {
|
if (!defaultTemplate || !isNil(object[attributeName])) {
|
||||||
// return object;
|
return object;
|
||||||
// }
|
}
|
||||||
// return {
|
return {
|
||||||
// ...object,
|
...object,
|
||||||
// [attributeName]: defaultTemplate.id,
|
[attributeName]: defaultTemplate.id,
|
||||||
// };
|
};
|
||||||
// };
|
};
|
||||||
// }
|
}
|
||||||
|
|||||||
@@ -0,0 +1,178 @@
|
|||||||
|
import { CreateSaleEstimate } from './commands/CreateSaleEstimate.service';
|
||||||
|
import {
|
||||||
|
// IFilterMeta,
|
||||||
|
// IPaginationMeta,
|
||||||
|
// IPaymentReceivedSmsDetails,
|
||||||
|
ISaleEstimateDTO,
|
||||||
|
// ISalesEstimatesFilter,
|
||||||
|
// SaleEstimateMailOptions,
|
||||||
|
// SaleEstimateMailOptionsDTO,
|
||||||
|
} from './types/SaleEstimates.types';
|
||||||
|
import { EditSaleEstimate } from './commands/EditSaleEstimate.service';
|
||||||
|
import { DeleteSaleEstimate } from './commands/DeleteSaleEstimate.service';
|
||||||
|
import { GetSaleEstimate } from './queries/GetSaleEstimate.service';
|
||||||
|
// import { GetSaleEstimates } from './queries/GetSaleEstimates';
|
||||||
|
import { DeliverSaleEstimateService } from './commands/DeliverSaleEstimate.service';
|
||||||
|
import { ApproveSaleEstimateService } from './commands/ApproveSaleEstimate.service';
|
||||||
|
import { RejectSaleEstimateService } from './commands/RejectSaleEstimate.service';
|
||||||
|
// import { SaleEstimateNotifyBySms } from './commands/SaleEstimateSmsNotify';
|
||||||
|
// import { SaleEstimatesPdf } from './queries/SaleEstimatesPdf';
|
||||||
|
// import { SendSaleEstimateMail } from './commands/SendSaleEstimateMail';
|
||||||
|
import { GetSaleEstimateState } from './queries/GetSaleEstimateState.service';
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { IFilterMeta, IPaginationMeta } from '@/interfaces/Model';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SaleEstimatesApplication {
|
||||||
|
constructor(
|
||||||
|
private readonly createSaleEstimateService: CreateSaleEstimate,
|
||||||
|
private readonly editSaleEstimateService: EditSaleEstimate,
|
||||||
|
private readonly deleteSaleEstimateService: DeleteSaleEstimate,
|
||||||
|
private readonly getSaleEstimateService: GetSaleEstimate,
|
||||||
|
// private readonly getSaleEstimatesService: GetSaleEstimates,
|
||||||
|
private readonly deliverSaleEstimateService: DeliverSaleEstimateService,
|
||||||
|
private readonly approveSaleEstimateService: ApproveSaleEstimateService,
|
||||||
|
private readonly rejectSaleEstimateService: RejectSaleEstimateService,
|
||||||
|
// private readonly saleEstimateNotifyBySmsService: SaleEstimateNotifyBySms,
|
||||||
|
// private readonly saleEstimatesPdfService: SaleEstimatesPdf,
|
||||||
|
// private readonly sendEstimateMailService: SendSaleEstimateMail,
|
||||||
|
private readonly getSaleEstimateStateService: GetSaleEstimateState,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a sale estimate.
|
||||||
|
* @param {EstimateDTO} estimate - Estimate DTO.
|
||||||
|
* @return {Promise<ISaleEstimate>}
|
||||||
|
*/
|
||||||
|
public createSaleEstimate(estimateDTO: ISaleEstimateDTO) {
|
||||||
|
return this.createSaleEstimateService.createEstimate(estimateDTO);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Edit the given sale estimate.
|
||||||
|
* @param {number} estimateId - Sale estimate ID.
|
||||||
|
* @param {EstimateDTO} estimate - Estimate DTO.
|
||||||
|
* @return {Promise<ISaleEstimate>}
|
||||||
|
*/
|
||||||
|
public editSaleEstimate(estimateId: number, estimateDTO: ISaleEstimateDTO) {
|
||||||
|
return this.editSaleEstimateService.editEstimate(estimateId, estimateDTO);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes the given sale estimate.
|
||||||
|
* @param {number} estimateId -
|
||||||
|
* @return {Promise<void>}
|
||||||
|
*/
|
||||||
|
public deleteSaleEstimate(estimateId: number) {
|
||||||
|
return this.deleteSaleEstimateService.deleteEstimate(estimateId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the given sale estimate.
|
||||||
|
* @param {number} estimateId - Sale estimate ID.
|
||||||
|
*/
|
||||||
|
public getSaleEstimate(estimateId: number) {
|
||||||
|
return this.getSaleEstimateService.getEstimate(estimateId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the sale estimate.
|
||||||
|
* @param {ISalesEstimatesFilter} filterDTO - Sales estimates filter DTO.
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
// public getSaleEstimates(filterDTO: ISalesEstimatesFilter) {
|
||||||
|
// return this.getSaleEstimatesService.getEstimates(filterDTO);
|
||||||
|
// }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deliver the given sale estimate.
|
||||||
|
* @param {number} saleEstimateId
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
public deliverSaleEstimate(saleEstimateId: number) {
|
||||||
|
return this.deliverSaleEstimateService.deliverSaleEstimate(saleEstimateId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Approve the given sale estimate.
|
||||||
|
* @param {number} saleEstimateId - Sale estimate ID.
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
public approveSaleEstimate(saleEstimateId: number) {
|
||||||
|
return this.approveSaleEstimateService.approveSaleEstimate(saleEstimateId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark the sale estimate as rejected from the customer.
|
||||||
|
* @param {number} saleEstimateId
|
||||||
|
*/
|
||||||
|
public async rejectSaleEstimate(saleEstimateId: number) {
|
||||||
|
return this.rejectSaleEstimateService.rejectSaleEstimate(saleEstimateId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notify the customer of the given sale estimate by SMS.
|
||||||
|
* @param {number} saleEstimateId - Sale estimate ID.
|
||||||
|
* @returns {Promise<ISaleEstimate>}
|
||||||
|
*/
|
||||||
|
public notifySaleEstimateBySms = async (saleEstimateId: number) => {
|
||||||
|
// return this.saleEstimateNotifyBySmsService.notifyBySms(
|
||||||
|
// saleEstimateId,
|
||||||
|
// );
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the SMS details of the given payment receive transaction.
|
||||||
|
* @param {number} saleEstimateId - Sale estimate ID.
|
||||||
|
* @returns {Promise<IPaymentReceivedSmsDetails>}
|
||||||
|
*/
|
||||||
|
public getSaleEstimateSmsDetails = (saleEstimateId: number) => {
|
||||||
|
// return this.saleEstimateNotifyBySmsService.smsDetails(
|
||||||
|
// saleEstimateId,
|
||||||
|
// );
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the PDF content of the given sale estimate.
|
||||||
|
* @param {number} saleEstimateId - Sale estimate ID.
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
public getSaleEstimatePdf(saleEstimateId: number) {
|
||||||
|
// return this.saleEstimatesPdfService.getSaleEstimatePdf(
|
||||||
|
// saleEstimateId,
|
||||||
|
// );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send the reminder mail of the given sale estimate.
|
||||||
|
* @param {number} saleEstimateId - Sale estimate ID.
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
public sendSaleEstimateMail() // saleEstimateId: number,
|
||||||
|
// saleEstimateMailOpts: SaleEstimateMailOptionsDTO,
|
||||||
|
{
|
||||||
|
// return this.sendEstimateMailService.triggerMail(
|
||||||
|
// saleEstimateId,
|
||||||
|
// saleEstimateMailOpts,
|
||||||
|
// );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the default mail options of the given sale estimate.
|
||||||
|
* @param {number} saleEstimateId
|
||||||
|
* @returns {Promise<SaleEstimateMailOptions>}
|
||||||
|
*/
|
||||||
|
public getSaleEstimateMail(saleEstimateId: number) {
|
||||||
|
// return this.sendEstimateMailService.getMailOptions(
|
||||||
|
// saleEstimateId,
|
||||||
|
// );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the current state of the sale estimate.
|
||||||
|
* @returns {Promise<ISaleEstimateState>} - A promise resolving to the sale estimate state.
|
||||||
|
*/
|
||||||
|
public getSaleEstimateState() {
|
||||||
|
return this.getSaleEstimateStateService.getSaleEstimateState();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
Delete,
|
||||||
|
Get,
|
||||||
|
Param,
|
||||||
|
ParseIntPipe,
|
||||||
|
Post,
|
||||||
|
Put,
|
||||||
|
Query,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { SaleEstimatesApplication } from './SaleEstimates.application';
|
||||||
|
import {
|
||||||
|
ISaleEstimateDTO,
|
||||||
|
// ISalesEstimatesFilter,
|
||||||
|
// SaleEstimateMailOptionsDTO,
|
||||||
|
} from './types/SaleEstimates.types';
|
||||||
|
import { SaleEstimate } from './models/SaleEstimate';
|
||||||
|
import { PublicRoute } from '../Auth/Jwt.guard';
|
||||||
|
|
||||||
|
@Controller('sales/estimates')
|
||||||
|
@PublicRoute()
|
||||||
|
export class SaleEstimatesController {
|
||||||
|
/**
|
||||||
|
* @param {SaleEstimatesApplication} saleEstimatesApplication - Sale estimates application.
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
private readonly saleEstimatesApplication: SaleEstimatesApplication,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
public createSaleEstimate(
|
||||||
|
@Body() estimateDTO: ISaleEstimateDTO,
|
||||||
|
): Promise<SaleEstimate> {
|
||||||
|
return this.saleEstimatesApplication.createSaleEstimate(estimateDTO);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Put(':id')
|
||||||
|
public editSaleEstimate(
|
||||||
|
@Param('id', ParseIntPipe) estimateId: number,
|
||||||
|
@Body() estimateDTO: ISaleEstimateDTO,
|
||||||
|
): Promise<SaleEstimate> {
|
||||||
|
return this.saleEstimatesApplication.editSaleEstimate(
|
||||||
|
estimateId,
|
||||||
|
estimateDTO,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(':id')
|
||||||
|
public deleteSaleEstimate(
|
||||||
|
@Param('id', ParseIntPipe) estimateId: number,
|
||||||
|
): Promise<void> {
|
||||||
|
return this.saleEstimatesApplication.deleteSaleEstimate(estimateId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':id')
|
||||||
|
public getSaleEstimate(@Param('id', ParseIntPipe) estimateId: number) {
|
||||||
|
return this.saleEstimatesApplication.getSaleEstimate(estimateId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Get()
|
||||||
|
// public getSaleEstimates(@Query() filterDTO: ISalesEstimatesFilter) {
|
||||||
|
// return this.saleEstimatesApplication.getSaleEstimates(filterDTO);
|
||||||
|
// }
|
||||||
|
|
||||||
|
@Post(':id/deliver')
|
||||||
|
public deliverSaleEstimate(
|
||||||
|
@Param('id', ParseIntPipe) saleEstimateId: number,
|
||||||
|
): Promise<void> {
|
||||||
|
return this.saleEstimatesApplication.deliverSaleEstimate(saleEstimateId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post(':id/approve')
|
||||||
|
public approveSaleEstimate(
|
||||||
|
@Param('id', ParseIntPipe) saleEstimateId: number,
|
||||||
|
): Promise<void> {
|
||||||
|
return this.saleEstimatesApplication.approveSaleEstimate(saleEstimateId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post(':id/reject')
|
||||||
|
public rejectSaleEstimate(
|
||||||
|
@Param('id', ParseIntPipe) saleEstimateId: number,
|
||||||
|
): Promise<void> {
|
||||||
|
return this.saleEstimatesApplication.rejectSaleEstimate(saleEstimateId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post(':id/notify-sms')
|
||||||
|
public notifySaleEstimateBySms(
|
||||||
|
@Param('id', ParseIntPipe) saleEstimateId: number,
|
||||||
|
) {
|
||||||
|
return this.saleEstimatesApplication.notifySaleEstimateBySms(
|
||||||
|
saleEstimateId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':id/sms-details')
|
||||||
|
public getSaleEstimateSmsDetails(
|
||||||
|
@Param('id', ParseIntPipe) saleEstimateId: number,
|
||||||
|
) {
|
||||||
|
return this.saleEstimatesApplication.getSaleEstimateSmsDetails(
|
||||||
|
saleEstimateId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':id/pdf')
|
||||||
|
public getSaleEstimatePdf(@Param('id', ParseIntPipe) saleEstimateId: number) {
|
||||||
|
return this.saleEstimatesApplication.getSaleEstimatePdf(saleEstimateId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Post(':id/mail')
|
||||||
|
// public sendSaleEstimateMail(
|
||||||
|
// @Param('id', ParseIntPipe) saleEstimateId: number,
|
||||||
|
// @Body() mailOptions: SaleEstimateMailOptionsDTO,
|
||||||
|
// ) {
|
||||||
|
// return this.saleEstimatesApplication.sendSaleEstimateMail(
|
||||||
|
// saleEstimateId,
|
||||||
|
// mailOptions,
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
@Get(':id/mail')
|
||||||
|
public getSaleEstimateMail(
|
||||||
|
@Param('id', ParseIntPipe) saleEstimateId: number,
|
||||||
|
) {
|
||||||
|
return this.saleEstimatesApplication.getSaleEstimateMail(saleEstimateId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('state')
|
||||||
|
public getSaleEstimateState() {
|
||||||
|
return this.saleEstimatesApplication.getSaleEstimateState();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TenancyContext } from '../Tenancy/TenancyContext.service';
|
||||||
|
import { TenancyDatabaseModule } from '../Tenancy/TenancyDB/TenancyDB.module';
|
||||||
|
import { TransformerInjectable } from '../Transformer/TransformerInjectable.service';
|
||||||
|
import { ApproveSaleEstimateService } from './commands/ApproveSaleEstimate.service';
|
||||||
|
import { ConvertSaleEstimate } from './commands/ConvetSaleEstimate.service';
|
||||||
|
import { CreateSaleEstimate } from './commands/CreateSaleEstimate.service';
|
||||||
|
import { DeliverSaleEstimateService } from './commands/DeliverSaleEstimate.service';
|
||||||
|
import { EditSaleEstimate } from './commands/EditSaleEstimate.service';
|
||||||
|
import { RejectSaleEstimateService } from './commands/RejectSaleEstimate.service';
|
||||||
|
import { SaleEstimateValidators } from './commands/SaleEstimateValidators.service';
|
||||||
|
import { SaleEstimatesController } from './SaleEstimates.controller';
|
||||||
|
import { ItemsEntriesService } from '../Items/ItemsEntries.service';
|
||||||
|
import { SaleEstimateDTOTransformer } from './commands/SaleEstimateDTOTransformer.service';
|
||||||
|
import { BranchTransactionDTOTransformer } from '../Branches/integrations/BranchTransactionDTOTransform';
|
||||||
|
import { BranchesSettingsService } from '../Branches/BranchesSettings';
|
||||||
|
import { WarehouseTransactionDTOTransform } from '../Warehouses/Integrations/WarehouseTransactionDTOTransform';
|
||||||
|
import { WarehousesSettings } from '../Warehouses/WarehousesSettings';
|
||||||
|
import { SaleEstimateIncrement } from './commands/SaleEstimateIncrement.service';
|
||||||
|
import { AutoIncrementOrdersService } from '../AutoIncrementOrders/AutoIncrementOrders.service';
|
||||||
|
import { BrandingTemplateDTOTransformer } from '../PdfTemplate/BrandingTemplateDTOTransformer';
|
||||||
|
import { SaleEstimatesApplication } from './SaleEstimates.application';
|
||||||
|
import { DeleteSaleEstimate } from './commands/DeleteSaleEstimate.service';
|
||||||
|
import { GetSaleEstimate } from './queries/GetSaleEstimate.service';
|
||||||
|
import { GetSaleEstimateState } from './queries/GetSaleEstimateState.service';
|
||||||
|
// import { SaleEstimateNotifyBySms } from './commands/SaleEstimateSmsNotify';
|
||||||
|
// import { SendSaleEstimateMail } from './commands/SendSaleEstimateMail';
|
||||||
|
//
|
||||||
|
@Module({
|
||||||
|
imports: [TenancyDatabaseModule],
|
||||||
|
controllers: [SaleEstimatesController],
|
||||||
|
providers: [
|
||||||
|
AutoIncrementOrdersService,
|
||||||
|
BrandingTemplateDTOTransformer,
|
||||||
|
SaleEstimateIncrement,
|
||||||
|
CreateSaleEstimate,
|
||||||
|
ConvertSaleEstimate,
|
||||||
|
EditSaleEstimate,
|
||||||
|
DeleteSaleEstimate,
|
||||||
|
GetSaleEstimate,
|
||||||
|
GetSaleEstimateState,
|
||||||
|
ApproveSaleEstimateService,
|
||||||
|
DeliverSaleEstimateService,
|
||||||
|
RejectSaleEstimateService,
|
||||||
|
SaleEstimateValidators,
|
||||||
|
ItemsEntriesService,
|
||||||
|
BranchesSettingsService,
|
||||||
|
WarehousesSettings,
|
||||||
|
BranchTransactionDTOTransformer,
|
||||||
|
WarehouseTransactionDTOTransform,
|
||||||
|
SaleEstimateDTOTransformer,
|
||||||
|
TenancyContext,
|
||||||
|
TransformerInjectable,
|
||||||
|
SaleEstimatesApplication
|
||||||
|
// SaleEstimateNotifyBySms,
|
||||||
|
// SendSaleEstimateMail,p
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class SaleEstimatesModule {}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
// import { Inject, Service } from 'typedi';
|
||||||
|
// import { ISalesInvoicesFilter } from '@/interfaces';
|
||||||
|
// import { Exportable } from '@/services/Export/Exportable';
|
||||||
|
// import { SaleEstimatesApplication } from './SaleEstimates.application';
|
||||||
|
// import { EXPORT_SIZE_LIMIT } from '@/services/Export/constants';
|
||||||
|
|
||||||
|
// @Service()
|
||||||
|
// export class SaleEstimatesExportable extends Exportable {
|
||||||
|
// @Inject()
|
||||||
|
// private saleEstimatesApplication: SaleEstimatesApplication;
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Retrieves the accounts data to exportable sheet.
|
||||||
|
// * @param {number} tenantId
|
||||||
|
// * @returns
|
||||||
|
// */
|
||||||
|
// public exportable(tenantId: number, query: ISalesInvoicesFilter) {
|
||||||
|
// const filterQuery = (query) => {
|
||||||
|
// query.withGraphFetched('branch');
|
||||||
|
// query.withGraphFetched('warehouse');
|
||||||
|
// };
|
||||||
|
// const parsedQuery = {
|
||||||
|
// sortOrder: 'desc',
|
||||||
|
// columnSortBy: 'created_at',
|
||||||
|
// ...query,
|
||||||
|
// page: 1,
|
||||||
|
// pageSize: EXPORT_SIZE_LIMIT,
|
||||||
|
// filterQuery,
|
||||||
|
// } as ISalesInvoicesFilter;
|
||||||
|
|
||||||
|
// return this.saleEstimatesApplication
|
||||||
|
// .getSaleEstimates(tenantId, parsedQuery)
|
||||||
|
// .then((output) => output.salesEstimates);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
// import { Inject, Service } from 'typedi';
|
||||||
|
// import { Knex } from 'knex';
|
||||||
|
// import { ISaleEstimateDTO } from '@/interfaces';
|
||||||
|
// import { CreateSaleEstimate } from './commands/CreateSaleEstimate.service';
|
||||||
|
// import { Importable } from '@/services/Import/Importable';
|
||||||
|
// import { SaleEstimatesSampleData } from './constants';
|
||||||
|
|
||||||
|
// @Service()
|
||||||
|
// export class SaleEstimatesImportable extends Importable {
|
||||||
|
// @Inject()
|
||||||
|
// private createEstimateService: CreateSaleEstimate;
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Importing to account service.
|
||||||
|
// * @param {number} tenantId
|
||||||
|
// * @param {IAccountCreateDTO} createAccountDTO
|
||||||
|
// * @returns
|
||||||
|
// */
|
||||||
|
// public importable(
|
||||||
|
// tenantId: number,
|
||||||
|
// createEstimateDTO: ISaleEstimateDTO,
|
||||||
|
// trx?: Knex.Transaction
|
||||||
|
// ) {
|
||||||
|
// return this.createEstimateService.createEstimate(
|
||||||
|
// tenantId,
|
||||||
|
// createEstimateDTO,
|
||||||
|
// trx
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Concurrrency controlling of the importing process.
|
||||||
|
// * @returns {number}
|
||||||
|
// */
|
||||||
|
// public get concurrency() {
|
||||||
|
// return 1;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Retrieves the sample data that used to download accounts sample sheet.
|
||||||
|
// */
|
||||||
|
// public sampleData(): any[] {
|
||||||
|
// return SaleEstimatesSampleData;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import {
|
||||||
|
ISaleEstimateApprovedEvent,
|
||||||
|
ISaleEstimateApprovingEvent,
|
||||||
|
} from '../types/SaleEstimates.types';
|
||||||
|
import { ERRORS } from '../constants';
|
||||||
|
import { Knex } from 'knex';
|
||||||
|
import moment from 'moment';
|
||||||
|
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
|
||||||
|
import { SaleEstimate } from '../models/SaleEstimate';
|
||||||
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
|
import { events } from '@/common/events/events';
|
||||||
|
import { ServiceError } from '@/modules/Items/ServiceError';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ApproveSaleEstimateService {
|
||||||
|
constructor(
|
||||||
|
@Inject(SaleEstimate.name)
|
||||||
|
private saleEstimateModel: typeof SaleEstimate,
|
||||||
|
private uow: UnitOfWork,
|
||||||
|
private eventPublisher: EventEmitter2,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark the sale estimate as approved from the customer.
|
||||||
|
* @param {number} saleEstimateId
|
||||||
|
* @return {Promise<void>}
|
||||||
|
*/
|
||||||
|
public async approveSaleEstimate(saleEstimateId: number): Promise<void> {
|
||||||
|
// Retrieve details of the given sale estimate id.
|
||||||
|
const oldSaleEstimate = await this.saleEstimateModel
|
||||||
|
.query()
|
||||||
|
.findById(saleEstimateId)
|
||||||
|
.throwIfNotFound();
|
||||||
|
|
||||||
|
// Throws error in case the sale estimate still not delivered to customer.
|
||||||
|
if (!oldSaleEstimate.isDelivered) {
|
||||||
|
throw new ServiceError(ERRORS.SALE_ESTIMATE_NOT_DELIVERED);
|
||||||
|
}
|
||||||
|
// Throws error in case the sale estimate already approved.
|
||||||
|
if (oldSaleEstimate.isApproved) {
|
||||||
|
throw new ServiceError(ERRORS.SALE_ESTIMATE_ALREADY_APPROVED);
|
||||||
|
}
|
||||||
|
// Triggers `onSaleEstimateApproving` event.
|
||||||
|
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
|
||||||
|
// Triggers `onSaleEstimateApproving` event.
|
||||||
|
await this.eventPublisher.emitAsync(events.saleEstimate.onApproving, {
|
||||||
|
trx,
|
||||||
|
oldSaleEstimate,
|
||||||
|
} as ISaleEstimateApprovingEvent);
|
||||||
|
|
||||||
|
// Update estimate as approved.
|
||||||
|
const saleEstimate = await this.saleEstimateModel
|
||||||
|
.query(trx)
|
||||||
|
.where('id', saleEstimateId)
|
||||||
|
.patchAndFetch({
|
||||||
|
approvedAt: moment().toMySqlDateTime(),
|
||||||
|
rejectedAt: null,
|
||||||
|
});
|
||||||
|
// Triggers `onSaleEstimateApproved` event.
|
||||||
|
await this.eventPublisher.emitAsync(events.saleEstimate.onApproved, {
|
||||||
|
trx,
|
||||||
|
oldSaleEstimate,
|
||||||
|
saleEstimate,
|
||||||
|
} as ISaleEstimateApprovedEvent);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
|
import { Knex } from 'knex';
|
||||||
|
import moment from 'moment';
|
||||||
|
import { SaleEstimate } from '../models/SaleEstimate';
|
||||||
|
import { events } from '@/common/events/events';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ConvertSaleEstimate {
|
||||||
|
constructor(
|
||||||
|
private readonly eventPublisher: EventEmitter2,
|
||||||
|
|
||||||
|
@Inject(SaleEstimate.name)
|
||||||
|
private readonly saleEstimateModel: typeof SaleEstimate,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts estimate to invoice.
|
||||||
|
* @param {number} estimateId -
|
||||||
|
* @return {Promise<void>}
|
||||||
|
*/
|
||||||
|
public async convertEstimateToInvoice(
|
||||||
|
estimateId: number,
|
||||||
|
invoiceId: number,
|
||||||
|
trx?: Knex.Transaction
|
||||||
|
): Promise<void> {
|
||||||
|
// Retrieve details of the given sale estimate.
|
||||||
|
const saleEstimate = await this.saleEstimateModel.query()
|
||||||
|
.findById(estimateId)
|
||||||
|
.throwIfNotFound();
|
||||||
|
|
||||||
|
// Marks the estimate as converted from the givne invoice.
|
||||||
|
await this.saleEstimateModel.query(trx).where('id', estimateId).patch({
|
||||||
|
convertedToInvoiceId: invoiceId,
|
||||||
|
convertedToInvoiceAt: moment().toMySqlDateTime(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Triggers `onSaleEstimateConvertedToInvoice` event.
|
||||||
|
await this.eventPublisher.emitAsync(
|
||||||
|
events.saleEstimate.onConvertedToInvoice,
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { Knex } from 'knex';
|
||||||
|
import {
|
||||||
|
ISaleEstimateCreatedPayload,
|
||||||
|
ISaleEstimateCreatingPayload,
|
||||||
|
ISaleEstimateDTO,
|
||||||
|
} from '../types/SaleEstimates.types';
|
||||||
|
import { SaleEstimateDTOTransformer } from './SaleEstimateDTOTransformer.service';
|
||||||
|
import { SaleEstimateValidators } from './SaleEstimateValidators.service';
|
||||||
|
import { SaleEstimate } from '../models/SaleEstimate';
|
||||||
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
|
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
|
||||||
|
import { events } from '@/common/events/events';
|
||||||
|
import { ItemsEntriesService } from '@/modules/Items/ItemsEntries.service';
|
||||||
|
import { Customer } from '@/modules/Customers/models/Customer';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class CreateSaleEstimate {
|
||||||
|
constructor(
|
||||||
|
@Inject(SaleEstimate.name) private saleEstimateModel: typeof SaleEstimate,
|
||||||
|
@Inject(Customer.name) private customerModel: typeof Customer,
|
||||||
|
private itemsEntriesService: ItemsEntriesService,
|
||||||
|
private eventPublisher: EventEmitter2,
|
||||||
|
private uow: UnitOfWork,
|
||||||
|
private transformerDTO: SaleEstimateDTOTransformer,
|
||||||
|
private validators: SaleEstimateValidators,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new estimate with associated entries.
|
||||||
|
* @param {ISaleEstimateDTO} estimateDTO - Sale estimate DTO object.
|
||||||
|
* @return {Promise<ISaleEstimate>}
|
||||||
|
*/
|
||||||
|
public async createEstimate(
|
||||||
|
estimateDTO: ISaleEstimateDTO,
|
||||||
|
trx?: Knex.Transaction,
|
||||||
|
): Promise<SaleEstimate> {
|
||||||
|
// Retrieve the given customer or throw not found service error.
|
||||||
|
const customer = await this.customerModel
|
||||||
|
.query()
|
||||||
|
.findById(estimateDTO.customerId)
|
||||||
|
.throwIfNotFound();
|
||||||
|
|
||||||
|
// Transform DTO object to model object.
|
||||||
|
const estimateObj = await this.transformerDTO.transformDTOToModel(
|
||||||
|
estimateDTO,
|
||||||
|
customer,
|
||||||
|
);
|
||||||
|
// Validate estimate number uniquiness on the storage.
|
||||||
|
await this.validators.validateEstimateNumberExistance(
|
||||||
|
estimateObj.estimateNumber,
|
||||||
|
);
|
||||||
|
// Validate items IDs existance on the storage.
|
||||||
|
await this.itemsEntriesService.validateItemsIdsExistance(
|
||||||
|
estimateDTO.entries,
|
||||||
|
);
|
||||||
|
// Validate non-sellable items.
|
||||||
|
await this.itemsEntriesService.validateNonSellableEntriesItems(
|
||||||
|
estimateDTO.entries,
|
||||||
|
);
|
||||||
|
// Creates a sale estimate transaction with associated transactions as UOW.
|
||||||
|
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
|
||||||
|
// Triggers `onSaleEstimateCreating` event.
|
||||||
|
await this.eventPublisher.emitAsync(events.saleEstimate.onCreating, {
|
||||||
|
estimateDTO,
|
||||||
|
trx,
|
||||||
|
} as ISaleEstimateCreatingPayload);
|
||||||
|
|
||||||
|
// Upsert the sale estimate graph to the storage.
|
||||||
|
const saleEstimate = await this.saleEstimateModel
|
||||||
|
.query(trx)
|
||||||
|
.upsertGraphAndFetch({
|
||||||
|
...estimateObj,
|
||||||
|
});
|
||||||
|
// Triggers `onSaleEstimateCreated` event.
|
||||||
|
await this.eventPublisher.emitAsync(events.saleEstimate.onCreated, {
|
||||||
|
saleEstimate,
|
||||||
|
saleEstimateId: saleEstimate.id,
|
||||||
|
saleEstimateDTO: estimateDTO,
|
||||||
|
trx,
|
||||||
|
} as ISaleEstimateCreatedPayload);
|
||||||
|
|
||||||
|
return saleEstimate;
|
||||||
|
}, trx);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import {
|
||||||
|
ISaleEstimateDeletedPayload,
|
||||||
|
ISaleEstimateDeletingPayload,
|
||||||
|
} from '../types/SaleEstimates.types';
|
||||||
|
import { ERRORS } from '../constants';
|
||||||
|
import { Knex } from 'knex';
|
||||||
|
import { SaleEstimate } from '../models/SaleEstimate';
|
||||||
|
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
|
||||||
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
|
import { events } from '@/common/events/events';
|
||||||
|
import { ServiceError } from '@/modules/Items/ServiceError';
|
||||||
|
import { ItemEntry } from '@/modules/TransactionItemEntry/models/ItemEntry';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class DeleteSaleEstimate {
|
||||||
|
constructor(
|
||||||
|
@Inject(SaleEstimate.name)
|
||||||
|
private readonly saleEstimateModel: typeof SaleEstimate,
|
||||||
|
|
||||||
|
@Inject(ItemEntry.name)
|
||||||
|
private readonly itemEntryModel: typeof ItemEntry,
|
||||||
|
|
||||||
|
private readonly eventPublisher: EventEmitter2,
|
||||||
|
private readonly uow: UnitOfWork,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes the given estimate id with associated entries.
|
||||||
|
* @async
|
||||||
|
* @param {number} estimateId
|
||||||
|
* @return {Promise<void>}
|
||||||
|
*/
|
||||||
|
public async deleteEstimate(estimateId: number): Promise<void> {
|
||||||
|
// Retrieve sale estimate or throw not found service error.
|
||||||
|
const oldSaleEstimate = await this.saleEstimateModel
|
||||||
|
.query()
|
||||||
|
.findById(estimateId)
|
||||||
|
.throwIfNotFound();
|
||||||
|
|
||||||
|
// Throw error if the sale estimate converted to sale invoice.
|
||||||
|
if (oldSaleEstimate.convertedToInvoiceId) {
|
||||||
|
throw new ServiceError(ERRORS.SALE_ESTIMATE_CONVERTED_TO_INVOICE);
|
||||||
|
}
|
||||||
|
// Updates the estimate with associated transactions under UOW enivrement.
|
||||||
|
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
|
||||||
|
// Triggers `onSaleEstimatedDeleting` event.
|
||||||
|
await this.eventPublisher.emitAsync(events.saleEstimate.onDeleting, {
|
||||||
|
trx,
|
||||||
|
oldSaleEstimate,
|
||||||
|
} as ISaleEstimateDeletingPayload);
|
||||||
|
|
||||||
|
// Delete sale estimate entries.
|
||||||
|
await this.itemEntryModel
|
||||||
|
.query(trx)
|
||||||
|
.where('reference_id', estimateId)
|
||||||
|
.where('reference_type', 'SaleEstimate')
|
||||||
|
.delete();
|
||||||
|
|
||||||
|
// Delete sale estimate transaction.
|
||||||
|
await this.saleEstimateModel.query(trx).where('id', estimateId).delete();
|
||||||
|
|
||||||
|
// Triggers `onSaleEstimatedDeleted` event.
|
||||||
|
await this.eventPublisher.emitAsync(events.saleEstimate.onDeleted, {
|
||||||
|
saleEstimateId: estimateId,
|
||||||
|
oldSaleEstimate,
|
||||||
|
trx,
|
||||||
|
} as ISaleEstimateDeletedPayload);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { Knex } from 'knex';
|
||||||
|
import moment from 'moment';
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
|
import {
|
||||||
|
ISaleEstimateEventDeliveredPayload,
|
||||||
|
ISaleEstimateEventDeliveringPayload,
|
||||||
|
} from '../types/SaleEstimates.types';
|
||||||
|
import { ERRORS } from '../constants';
|
||||||
|
import { SaleEstimate } from '../models/SaleEstimate';
|
||||||
|
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
|
||||||
|
import { events } from '@/common/events/events';
|
||||||
|
import { ServiceError } from '@/modules/Items/ServiceError';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class DeliverSaleEstimateService {
|
||||||
|
constructor(
|
||||||
|
@Inject(SaleEstimate.name)
|
||||||
|
private readonly saleEstimateModel: typeof SaleEstimate,
|
||||||
|
private readonly eventPublisher: EventEmitter2,
|
||||||
|
private readonly uow: UnitOfWork,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark the sale estimate as delivered.
|
||||||
|
* @param {number} saleEstimateId - Sale estimate id.
|
||||||
|
*/
|
||||||
|
public async deliverSaleEstimate(saleEstimateId: number): Promise<void> {
|
||||||
|
// Retrieve details of the given sale estimate id.
|
||||||
|
const oldSaleEstimate = await this.saleEstimateModel
|
||||||
|
.query()
|
||||||
|
.findById(saleEstimateId)
|
||||||
|
.throwIfNotFound();
|
||||||
|
|
||||||
|
// Throws error in case the sale estimate already published.
|
||||||
|
if (oldSaleEstimate.isDelivered) {
|
||||||
|
throw new ServiceError(ERRORS.SALE_ESTIMATE_ALREADY_DELIVERED);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Updates the sale estimate transaction with assocaited transactions
|
||||||
|
// under UOW envirement.
|
||||||
|
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
|
||||||
|
// Triggers `onSaleEstimateDelivering` event.
|
||||||
|
await this.eventPublisher.emitAsync(events.saleEstimate.onDelivering, {
|
||||||
|
oldSaleEstimate,
|
||||||
|
trx,
|
||||||
|
} as ISaleEstimateEventDeliveringPayload);
|
||||||
|
|
||||||
|
// Record the delivered at on the storage.
|
||||||
|
const saleEstimate = await this.saleEstimateModel
|
||||||
|
.query(trx)
|
||||||
|
.patchAndFetchById(saleEstimateId, {
|
||||||
|
deliveredAt: moment().toMySqlDateTime(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Triggers `onSaleEstimateDelivered` event.
|
||||||
|
await this.eventPublisher.emitAsync(events.saleEstimate.onDelivered, {
|
||||||
|
saleEstimate,
|
||||||
|
trx,
|
||||||
|
} as ISaleEstimateEventDeliveredPayload);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
import { Knex } from 'knex';
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
|
import {
|
||||||
|
ISaleEstimateDTO,
|
||||||
|
ISaleEstimateEditedPayload,
|
||||||
|
ISaleEstimateEditingPayload,
|
||||||
|
} from '../types/SaleEstimates.types';
|
||||||
|
import { SaleEstimateValidators } from './SaleEstimateValidators.service';
|
||||||
|
import { SaleEstimateDTOTransformer } from './SaleEstimateDTOTransformer.service';
|
||||||
|
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
|
||||||
|
import { events } from '@/common/events/events';
|
||||||
|
import { SaleEstimate } from '../models/SaleEstimate';
|
||||||
|
import { ItemsEntriesService } from '@/modules/Items/ItemsEntries.service';
|
||||||
|
import { Customer } from '@/modules/Customers/models/Customer';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class EditSaleEstimate {
|
||||||
|
constructor(
|
||||||
|
private readonly validators: SaleEstimateValidators,
|
||||||
|
private readonly itemsEntriesService: ItemsEntriesService,
|
||||||
|
private readonly eventPublisher: EventEmitter2,
|
||||||
|
private readonly uow: UnitOfWork,
|
||||||
|
private readonly transformerDTO: SaleEstimateDTOTransformer,
|
||||||
|
|
||||||
|
@Inject(SaleEstimate.name)
|
||||||
|
private readonly saleEstimateModel: typeof SaleEstimate,
|
||||||
|
|
||||||
|
@Inject(Customer.name)
|
||||||
|
private readonly customerModel: typeof Customer,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Edit details of the given estimate with associated entries.
|
||||||
|
* @async
|
||||||
|
* @param {Integer} estimateId
|
||||||
|
* @param {EstimateDTO} estimate
|
||||||
|
* @return {Promise<ISaleEstimate>}
|
||||||
|
*/
|
||||||
|
public async editEstimate(
|
||||||
|
estimateId: number,
|
||||||
|
estimateDTO: ISaleEstimateDTO,
|
||||||
|
): Promise<SaleEstimate> {
|
||||||
|
// Retrieve details of the given sale estimate id.
|
||||||
|
const oldSaleEstimate = await this.saleEstimateModel
|
||||||
|
.query()
|
||||||
|
.findById(estimateId);
|
||||||
|
|
||||||
|
// Validates the given estimate existance.
|
||||||
|
this.validators.validateEstimateExistance(oldSaleEstimate);
|
||||||
|
|
||||||
|
// Retrieve the given customer or throw not found service error.
|
||||||
|
const customer = await this.customerModel
|
||||||
|
.query()
|
||||||
|
.findById(estimateDTO.customerId)
|
||||||
|
.throwIfNotFound();
|
||||||
|
|
||||||
|
// Transform DTO object to model object.
|
||||||
|
const estimateObj = await this.transformerDTO.transformDTOToModel(
|
||||||
|
estimateDTO,
|
||||||
|
customer,
|
||||||
|
oldSaleEstimate,
|
||||||
|
);
|
||||||
|
// Validate estimate number uniquiness on the storage.
|
||||||
|
if (estimateDTO.estimateNumber) {
|
||||||
|
await this.validators.validateEstimateNumberExistance(
|
||||||
|
estimateDTO.estimateNumber,
|
||||||
|
estimateId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Validate sale estimate entries existance.
|
||||||
|
await this.itemsEntriesService.validateEntriesIdsExistance(
|
||||||
|
estimateId,
|
||||||
|
'SaleEstimate',
|
||||||
|
estimateDTO.entries,
|
||||||
|
);
|
||||||
|
// Validate items IDs existance on the storage.
|
||||||
|
await this.itemsEntriesService.validateItemsIdsExistance(
|
||||||
|
estimateDTO.entries,
|
||||||
|
);
|
||||||
|
// Validate non-sellable items.
|
||||||
|
await this.itemsEntriesService.validateNonSellableEntriesItems(
|
||||||
|
estimateDTO.entries,
|
||||||
|
);
|
||||||
|
// Edits estimate transaction with associated transactions
|
||||||
|
// under unit-of-work envirement.
|
||||||
|
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
|
||||||
|
// Trigger `onSaleEstimateEditing` event.
|
||||||
|
await this.eventPublisher.emitAsync(events.saleEstimate.onEditing, {
|
||||||
|
oldSaleEstimate,
|
||||||
|
estimateDTO,
|
||||||
|
trx,
|
||||||
|
} as ISaleEstimateEditingPayload);
|
||||||
|
|
||||||
|
// Upsert the estimate graph to the storage.
|
||||||
|
const saleEstimate = await this.saleEstimateModel
|
||||||
|
.query(trx)
|
||||||
|
.upsertGraphAndFetch({
|
||||||
|
id: estimateId,
|
||||||
|
...estimateObj,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Trigger `onSaleEstimateEdited` event.
|
||||||
|
await this.eventPublisher.emitAsync(events.saleEstimate.onEdited, {
|
||||||
|
estimateId,
|
||||||
|
saleEstimate,
|
||||||
|
oldSaleEstimate,
|
||||||
|
estimateDTO,
|
||||||
|
trx,
|
||||||
|
} as ISaleEstimateEditedPayload);
|
||||||
|
|
||||||
|
return saleEstimate;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import moment from 'moment';
|
||||||
|
import { Knex } from 'knex';
|
||||||
|
import { SaleEstimate } from '../models/SaleEstimate';
|
||||||
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
|
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
|
||||||
|
import { ServiceError } from '@/modules/Items/ServiceError';
|
||||||
|
import { ERRORS } from '../constants';
|
||||||
|
import { events } from '@/common/events/events';
|
||||||
|
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class RejectSaleEstimateService {
|
||||||
|
constructor(
|
||||||
|
@Inject(SaleEstimate.name)
|
||||||
|
private readonly saleEstimateModel: typeof SaleEstimate,
|
||||||
|
private readonly eventPublisher: EventEmitter2,
|
||||||
|
private readonly uow: UnitOfWork,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark the sale estimate as rejected from the customer.
|
||||||
|
* @param {number} saleEstimateId
|
||||||
|
*/
|
||||||
|
public async rejectSaleEstimate(saleEstimateId: number): Promise<void> {
|
||||||
|
// Retrieve details of the given sale estimate id.
|
||||||
|
const saleEstimate = await this.saleEstimateModel.query()
|
||||||
|
.findById(saleEstimateId)
|
||||||
|
.throwIfNotFound();
|
||||||
|
|
||||||
|
// Throws error in case the sale estimate still not delivered to customer.
|
||||||
|
if (!saleEstimate.isDelivered) {
|
||||||
|
throw new ServiceError(ERRORS.SALE_ESTIMATE_NOT_DELIVERED);
|
||||||
|
}
|
||||||
|
// Throws error in case the sale estimate already rejected.
|
||||||
|
if (saleEstimate.isRejected) {
|
||||||
|
throw new ServiceError(ERRORS.SALE_ESTIMATE_ALREADY_REJECTED);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
|
||||||
|
// Mark the sale estimate as reject on the storage.
|
||||||
|
await this.saleEstimateModel.query(trx).where('id', saleEstimateId).patch({
|
||||||
|
rejectedAt: moment().toMySqlDateTime(),
|
||||||
|
approvedAt: null,
|
||||||
|
});
|
||||||
|
// Triggers `onSaleEstimateRejected` event.
|
||||||
|
await this.eventPublisher.emitAsync(events.saleEstimate.onRejected, {});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
import * as R from 'ramda';
|
||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { omit, sumBy } from 'lodash';
|
||||||
|
import * as composeAsync from 'async/compose';
|
||||||
|
// import { ICustomer, ISaleEstimate, ISaleEstimateDTO } from '../types/SaleEstimates.types';
|
||||||
|
import { SaleEstimateValidators } from './SaleEstimateValidators.service';
|
||||||
|
// import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform';
|
||||||
|
// import { WarehouseTransactionDTOTransform } from '@/services/Warehouses/Integrations/WarehouseTransactionDTOTransform';
|
||||||
|
import { formatDateFields } from '@/utils/format-date-fields';
|
||||||
|
import * as moment from 'moment';
|
||||||
|
import { SaleEstimateIncrement } from './SaleEstimateIncrement.service';
|
||||||
|
import { BranchTransactionDTOTransformer } from '@/modules/Branches/integrations/BranchTransactionDTOTransform';
|
||||||
|
import { WarehouseTransactionDTOTransform } from '@/modules/Warehouses/Integrations/WarehouseTransactionDTOTransform';
|
||||||
|
import { assocItemEntriesDefaultIndex } from '@/utils/associate-item-entries-index';
|
||||||
|
import { BrandingTemplateDTOTransformer } from '@/modules/PdfTemplate/BrandingTemplateDTOTransformer';
|
||||||
|
import { ItemEntry } from '@/modules/TransactionItemEntry/models/ItemEntry';
|
||||||
|
import { SaleEstimate } from '../models/SaleEstimate';
|
||||||
|
import { Customer } from '@/modules/Customers/models/Customer';
|
||||||
|
import { ISaleEstimateDTO } from '../types/SaleEstimates.types';
|
||||||
|
// import { assocItemEntriesDefaultIndex } from '@/services/Items/utils';
|
||||||
|
// import { BrandingTemplateDTOTransformer } from '@/services/PdfTemplate/BrandingTemplateDTOTransformer';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SaleEstimateDTOTransformer {
|
||||||
|
constructor(
|
||||||
|
@Inject(ItemEntry.name)
|
||||||
|
private itemEntryModel: typeof ItemEntry,
|
||||||
|
private readonly validators: SaleEstimateValidators,
|
||||||
|
private readonly branchDTOTransform: BranchTransactionDTOTransformer,
|
||||||
|
private readonly warehouseDTOTransform: WarehouseTransactionDTOTransform,
|
||||||
|
private readonly estimateIncrement: SaleEstimateIncrement,
|
||||||
|
private readonly brandingTemplatesTransformer: BrandingTemplateDTOTransformer,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform create DTO object ot model object.
|
||||||
|
* @param {ISaleEstimateDTO} saleEstimateDTO - Sale estimate DTO.
|
||||||
|
* @param {Customer} paymentCustomer - Payment customer.
|
||||||
|
* @param {SaleEstimate} oldSaleEstimate - Old sale estimate.
|
||||||
|
* @return {ISaleEstimate}
|
||||||
|
*/
|
||||||
|
async transformDTOToModel(
|
||||||
|
estimateDTO: ISaleEstimateDTO,
|
||||||
|
paymentCustomer: Customer,
|
||||||
|
oldSaleEstimate?: SaleEstimate
|
||||||
|
): Promise<SaleEstimate> {
|
||||||
|
const amount = sumBy(estimateDTO.entries, (e) =>
|
||||||
|
this.itemEntryModel.calcAmount(e)
|
||||||
|
);
|
||||||
|
// Retrieve the next invoice number.
|
||||||
|
const autoNextNumber = this.estimateIncrement.getNextEstimateNumber();
|
||||||
|
|
||||||
|
// Retrieve the next estimate number.
|
||||||
|
const estimateNumber =
|
||||||
|
estimateDTO.estimateNumber ||
|
||||||
|
oldSaleEstimate?.estimateNumber ||
|
||||||
|
autoNextNumber;
|
||||||
|
|
||||||
|
// Validate the sale estimate number require.
|
||||||
|
this.validators.validateEstimateNoRequire(estimateNumber);
|
||||||
|
|
||||||
|
const entries = R.compose(
|
||||||
|
// Associate the reference type to item entries.
|
||||||
|
R.map((entry) => R.assoc('reference_type', 'SaleEstimate', entry)),
|
||||||
|
|
||||||
|
// Associate default index to item entries.
|
||||||
|
assocItemEntriesDefaultIndex
|
||||||
|
)(estimateDTO.entries);
|
||||||
|
|
||||||
|
const initialDTO = {
|
||||||
|
amount,
|
||||||
|
...formatDateFields(
|
||||||
|
omit(estimateDTO, ['delivered', 'entries', 'attachments']),
|
||||||
|
['estimateDate', 'expirationDate']
|
||||||
|
),
|
||||||
|
currencyCode: paymentCustomer.currencyCode,
|
||||||
|
exchangeRate: estimateDTO.exchangeRate || 1,
|
||||||
|
...(estimateNumber ? { estimateNumber } : {}),
|
||||||
|
entries,
|
||||||
|
// Avoid rewrite the deliver date in edit mode when already published.
|
||||||
|
...(estimateDTO.delivered &&
|
||||||
|
!oldSaleEstimate?.deliveredAt && {
|
||||||
|
deliveredAt: moment().toMySqlDateTime(),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
const initialAsyncDTO = await composeAsync(
|
||||||
|
// Assigns the default branding template id to the invoice DTO.
|
||||||
|
this.brandingTemplatesTransformer.assocDefaultBrandingTemplate(
|
||||||
|
'SaleEstimate'
|
||||||
|
)
|
||||||
|
)(initialDTO);
|
||||||
|
|
||||||
|
return R.compose(
|
||||||
|
this.branchDTOTransform.transformDTO<SaleEstimate>,
|
||||||
|
this.warehouseDTOTransform.transformDTO<SaleEstimate>,
|
||||||
|
)(initialAsyncDTO);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve estimate number to object model.
|
||||||
|
* @param {ISaleEstimateDTO} saleEstimateDTO
|
||||||
|
* @param {ISaleEstimate} oldSaleEstimate
|
||||||
|
*/
|
||||||
|
public transformEstimateNumberToModel(
|
||||||
|
saleEstimateDTO: ISaleEstimateDTO,
|
||||||
|
oldSaleEstimate?: SaleEstimate
|
||||||
|
): string {
|
||||||
|
const autoNextNumber = this.estimateIncrement.getNextEstimateNumber();
|
||||||
|
|
||||||
|
if (saleEstimateDTO.estimateNumber) {
|
||||||
|
return saleEstimateDTO.estimateNumber;
|
||||||
|
}
|
||||||
|
return oldSaleEstimate ? oldSaleEstimate.estimateNumber : autoNextNumber;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { AutoIncrementOrdersService } from '@/modules/AutoIncrementOrders/AutoIncrementOrders.service';
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SaleEstimateIncrement {
|
||||||
|
constructor(
|
||||||
|
private readonly autoIncrementOrdersService: AutoIncrementOrdersService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the next unique estimate number.
|
||||||
|
* @return {string}
|
||||||
|
*/
|
||||||
|
public getNextEstimateNumber(): string {
|
||||||
|
return this.autoIncrementOrdersService.getNextTransactionNumber(
|
||||||
|
'sales_estimates',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increment the estimate next number.
|
||||||
|
*/
|
||||||
|
public incrementNextEstimateNumber() {
|
||||||
|
return this.autoIncrementOrdersService.incrementSettingsNextNumber(
|
||||||
|
'sales_estimates',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,217 @@
|
|||||||
|
// import { Service, Inject } from 'typedi';
|
||||||
|
// import moment from 'moment';
|
||||||
|
// import events from '@/subscribers/events';
|
||||||
|
// import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||||
|
// import SaleNotifyBySms from '../SaleNotifyBySms';
|
||||||
|
// import SmsNotificationsSettingsService from '@/services/Settings/SmsNotificationsSettings';
|
||||||
|
// import {
|
||||||
|
// ICustomer,
|
||||||
|
// IPaymentReceivedSmsDetails,
|
||||||
|
// ISaleEstimate,
|
||||||
|
// SMS_NOTIFICATION_KEY,
|
||||||
|
// } from '@/interfaces';
|
||||||
|
// import { Tenant, TenantMetadata } from '@/system/models';
|
||||||
|
// import { formatNumber, formatSmsMessage } from 'utils';
|
||||||
|
// import { ServiceError } from '@/exceptions';
|
||||||
|
// import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||||
|
|
||||||
|
// const ERRORS = {
|
||||||
|
// SALE_ESTIMATE_NOT_FOUND: 'SALE_ESTIMATE_NOT_FOUND',
|
||||||
|
// };
|
||||||
|
|
||||||
|
// @Service()
|
||||||
|
// export class SaleEstimateNotifyBySms {
|
||||||
|
// @Inject()
|
||||||
|
// private tenancy: HasTenancyService;
|
||||||
|
|
||||||
|
// @Inject()
|
||||||
|
// private saleSmsNotification: SaleNotifyBySms;
|
||||||
|
|
||||||
|
// @Inject()
|
||||||
|
// private eventPublisher: EventPublisher;
|
||||||
|
|
||||||
|
// @Inject()
|
||||||
|
// private smsNotificationsSettings: SmsNotificationsSettingsService;
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// *
|
||||||
|
// * @param {number} tenantId
|
||||||
|
// * @param {number} saleEstimateId
|
||||||
|
// * @returns {Promise<ISaleEstimate>}
|
||||||
|
// */
|
||||||
|
// public notifyBySms = async (
|
||||||
|
// tenantId: number,
|
||||||
|
// saleEstimateId: number
|
||||||
|
// ): Promise<ISaleEstimate> => {
|
||||||
|
// const { SaleEstimate } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
|
// // Retrieve the sale invoice or throw not found service error.
|
||||||
|
// const saleEstimate = await SaleEstimate.query()
|
||||||
|
// .findById(saleEstimateId)
|
||||||
|
// .withGraphFetched('customer');
|
||||||
|
|
||||||
|
// // Validates the estimate transaction existance.
|
||||||
|
// this.validateEstimateExistance(saleEstimate);
|
||||||
|
|
||||||
|
// // Validate the customer phone number existance and number validation.
|
||||||
|
// this.saleSmsNotification.validateCustomerPhoneNumber(
|
||||||
|
// saleEstimate.customer.personalPhone
|
||||||
|
// );
|
||||||
|
// // Triggers `onSaleEstimateNotifySms` event.
|
||||||
|
// await this.eventPublisher.emitAsync(events.saleEstimate.onNotifySms, {
|
||||||
|
// tenantId,
|
||||||
|
// saleEstimate,
|
||||||
|
// });
|
||||||
|
// await this.sendSmsNotification(tenantId, saleEstimate);
|
||||||
|
|
||||||
|
// // Triggers `onSaleEstimateNotifySms` event.
|
||||||
|
// await this.eventPublisher.emitAsync(events.saleEstimate.onNotifiedSms, {
|
||||||
|
// tenantId,
|
||||||
|
// saleEstimate,
|
||||||
|
// });
|
||||||
|
// return saleEstimate;
|
||||||
|
// };
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// *
|
||||||
|
// * @param {number} tenantId
|
||||||
|
// * @param {ISaleEstimate} saleEstimate
|
||||||
|
// * @returns
|
||||||
|
// */
|
||||||
|
// private sendSmsNotification = async (
|
||||||
|
// tenantId: number,
|
||||||
|
// saleEstimate: ISaleEstimate & { customer: ICustomer }
|
||||||
|
// ) => {
|
||||||
|
// const smsClient = this.tenancy.smsClient(tenantId);
|
||||||
|
// const tenantMetadata = await TenantMetadata.query().findOne({ tenantId });
|
||||||
|
|
||||||
|
// // Retrieve the formatted sms notification message for estimate details.
|
||||||
|
// const formattedSmsMessage = this.formattedEstimateDetailsMessage(
|
||||||
|
// tenantId,
|
||||||
|
// saleEstimate,
|
||||||
|
// tenantMetadata
|
||||||
|
// );
|
||||||
|
// const phoneNumber = saleEstimate.customer.personalPhone;
|
||||||
|
|
||||||
|
// // Runs the send message job.
|
||||||
|
// return smsClient.sendMessageJob(phoneNumber, formattedSmsMessage);
|
||||||
|
// };
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Notify via SMS message after estimate creation.
|
||||||
|
// * @param {number} tenantId
|
||||||
|
// * @param {number} saleEstimateId
|
||||||
|
// * @returns {Promise<void>}
|
||||||
|
// */
|
||||||
|
// public notifyViaSmsNotificationAfterCreation = async (
|
||||||
|
// tenantId: number,
|
||||||
|
// saleEstimateId: number
|
||||||
|
// ): Promise<void> => {
|
||||||
|
// const notification = this.smsNotificationsSettings.getSmsNotificationMeta(
|
||||||
|
// tenantId,
|
||||||
|
// SMS_NOTIFICATION_KEY.SALE_ESTIMATE_DETAILS
|
||||||
|
// );
|
||||||
|
// // Can't continue if the sms auto-notification is not enabled.
|
||||||
|
// if (!notification.isNotificationEnabled) return;
|
||||||
|
|
||||||
|
// await this.notifyBySms(tenantId, saleEstimateId);
|
||||||
|
// };
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// *
|
||||||
|
// * @param {number} tenantId
|
||||||
|
// * @param {ISaleEstimate} saleEstimate
|
||||||
|
// * @param {TenantMetadata} tenantMetadata
|
||||||
|
// * @returns {string}
|
||||||
|
// */
|
||||||
|
// private formattedEstimateDetailsMessage = (
|
||||||
|
// tenantId: number,
|
||||||
|
// saleEstimate: ISaleEstimate,
|
||||||
|
// tenantMetadata: TenantMetadata
|
||||||
|
// ): string => {
|
||||||
|
// const notification = this.smsNotificationsSettings.getSmsNotificationMeta(
|
||||||
|
// tenantId,
|
||||||
|
// SMS_NOTIFICATION_KEY.SALE_ESTIMATE_DETAILS
|
||||||
|
// );
|
||||||
|
// return this.formateEstimateDetailsMessage(
|
||||||
|
// notification.smsMessage,
|
||||||
|
// saleEstimate,
|
||||||
|
// tenantMetadata
|
||||||
|
// );
|
||||||
|
// };
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Formattes the estimate sms notification details message.
|
||||||
|
// * @param {string} smsMessage
|
||||||
|
// * @param {ISaleEstimate} saleEstimate
|
||||||
|
// * @param {TenantMetadata} tenantMetadata
|
||||||
|
// * @returns {string}
|
||||||
|
// */
|
||||||
|
// private formateEstimateDetailsMessage = (
|
||||||
|
// smsMessage: string,
|
||||||
|
// saleEstimate: ISaleEstimate & { customer: ICustomer },
|
||||||
|
// tenantMetadata: TenantMetadata
|
||||||
|
// ) => {
|
||||||
|
// const formattedAmount = formatNumber(saleEstimate.amount, {
|
||||||
|
// currencyCode: saleEstimate.currencyCode,
|
||||||
|
// });
|
||||||
|
|
||||||
|
// return formatSmsMessage(smsMessage, {
|
||||||
|
// EstimateNumber: saleEstimate.estimateNumber,
|
||||||
|
// ReferenceNumber: saleEstimate.reference,
|
||||||
|
// EstimateDate: moment(saleEstimate.estimateDate).format('YYYY/MM/DD'),
|
||||||
|
// ExpirationDate: saleEstimate.expirationDate
|
||||||
|
// ? moment(saleEstimate.expirationDate).format('YYYY/MM/DD')
|
||||||
|
// : '',
|
||||||
|
// CustomerName: saleEstimate.customer.displayName,
|
||||||
|
// Amount: formattedAmount,
|
||||||
|
// CompanyName: tenantMetadata.name,
|
||||||
|
// });
|
||||||
|
// };
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Retrieve the SMS details of the given payment receive transaction.
|
||||||
|
// * @param {number} tenantId
|
||||||
|
// * @param {number} saleEstimateId
|
||||||
|
// * @returns {Promise<IPaymentReceivedSmsDetails>}
|
||||||
|
// */
|
||||||
|
// public smsDetails = async (
|
||||||
|
// tenantId: number,
|
||||||
|
// saleEstimateId: number
|
||||||
|
// ): Promise<IPaymentReceivedSmsDetails> => {
|
||||||
|
// const { SaleEstimate } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
|
// // Retrieve the sale invoice or throw not found service error.
|
||||||
|
// const saleEstimate = await SaleEstimate.query()
|
||||||
|
// .findById(saleEstimateId)
|
||||||
|
// .withGraphFetched('customer');
|
||||||
|
|
||||||
|
// // Validates the estimate existance.
|
||||||
|
// this.validateEstimateExistance(saleEstimate);
|
||||||
|
|
||||||
|
// // Retrieve the current tenant metadata.
|
||||||
|
// const tenantMetadata = await TenantMetadata.query().findOne({ tenantId });
|
||||||
|
|
||||||
|
// // Retrieve the formatted sms message from the given estimate model.
|
||||||
|
// const formattedSmsMessage = this.formattedEstimateDetailsMessage(
|
||||||
|
// tenantId,
|
||||||
|
// saleEstimate,
|
||||||
|
// tenantMetadata
|
||||||
|
// );
|
||||||
|
// return {
|
||||||
|
// customerName: saleEstimate.customer.displayName,
|
||||||
|
// customerPhoneNumber: saleEstimate.customer.personalPhone,
|
||||||
|
// smsMessage: formattedSmsMessage,
|
||||||
|
// };
|
||||||
|
// };
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Validates the sale estimate existance.
|
||||||
|
// * @param {ISaleEstimate} saleEstimate -
|
||||||
|
// */
|
||||||
|
// private validateEstimateExistance(saleEstimate: ISaleEstimate) {
|
||||||
|
// if (!saleEstimate) {
|
||||||
|
// throw new ServiceError(ERRORS.SALE_ESTIMATE_NOT_FOUND);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { ERRORS } from '../constants';
|
||||||
|
import { SaleEstimate } from '../models/SaleEstimate';
|
||||||
|
import { ServiceError } from '@/modules/Items/ServiceError';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SaleEstimateValidators {
|
||||||
|
constructor(
|
||||||
|
@Inject(SaleEstimate.name)
|
||||||
|
private readonly saleEstimateModel: typeof SaleEstimate,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the given estimate existance.
|
||||||
|
* @param {SaleEstimate | undefined | null} estimate - The sale estimate.
|
||||||
|
*/
|
||||||
|
public validateEstimateExistance(estimate: SaleEstimate | undefined | null) {
|
||||||
|
if (!estimate) {
|
||||||
|
throw new ServiceError(ERRORS.SALE_ESTIMATE_NOT_FOUND);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the estimate number unique on the storage.
|
||||||
|
* @param {string} estimateNumber - The estimate number.
|
||||||
|
* @param {number} notEstimateId - The estimate id to exclude from the search.
|
||||||
|
*/
|
||||||
|
public async validateEstimateNumberExistance(
|
||||||
|
estimateNumber: string,
|
||||||
|
notEstimateId?: number,
|
||||||
|
) {
|
||||||
|
const foundSaleEstimate = await this.saleEstimateModel
|
||||||
|
.query()
|
||||||
|
.findOne('estimate_number', estimateNumber)
|
||||||
|
.onBuild((builder) => {
|
||||||
|
if (notEstimateId) {
|
||||||
|
builder.whereNot('id', notEstimateId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (foundSaleEstimate) {
|
||||||
|
throw new ServiceError(
|
||||||
|
ERRORS.SALE_ESTIMATE_NUMBER_EXISTANCE,
|
||||||
|
'The given sale estimate is not unique.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the given sale estimate not already converted to invoice.
|
||||||
|
* @param {SaleEstimate} saleEstimate -
|
||||||
|
*/
|
||||||
|
public validateEstimateNotConverted(saleEstimate: SaleEstimate) {
|
||||||
|
if (saleEstimate.isConvertedToInvoice) {
|
||||||
|
throw new ServiceError(ERRORS.SALE_ESTIMATE_CONVERTED_TO_INVOICE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the sale estimate number require.
|
||||||
|
* @param {string} estimateNumber
|
||||||
|
*/
|
||||||
|
public validateEstimateNoRequire(estimateNumber: string) {
|
||||||
|
if (!estimateNumber) {
|
||||||
|
throw new ServiceError(ERRORS.SALE_ESTIMATE_NO_IS_REQUIRED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate the given customer has no sales estimates.
|
||||||
|
* @param {number} customerId - The customer id.
|
||||||
|
*/
|
||||||
|
public async validateCustomerHasNoEstimates(customerId: number) {
|
||||||
|
const estimates = await this.saleEstimateModel
|
||||||
|
.query()
|
||||||
|
.where('customer_id', customerId);
|
||||||
|
|
||||||
|
if (estimates.length > 0) {
|
||||||
|
throw new ServiceError(ERRORS.CUSTOMER_HAS_SALES_ESTIMATES);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,205 @@
|
|||||||
|
// import { Inject, Service } from 'typedi';
|
||||||
|
// import Mail from '@/lib/Mail';
|
||||||
|
// import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||||
|
// import {
|
||||||
|
// DEFAULT_ESTIMATE_REMINDER_MAIL_CONTENT,
|
||||||
|
// DEFAULT_ESTIMATE_REMINDER_MAIL_SUBJECT,
|
||||||
|
// } from '../constants';
|
||||||
|
// import { SaleEstimatesPdf } from '../queries/SaleEstimatesPdf';
|
||||||
|
// import { GetSaleEstimate } from '../queries/GetSaleEstimate.service';
|
||||||
|
// import {
|
||||||
|
// ISaleEstimateMailPresendEvent,
|
||||||
|
// SaleEstimateMailOptions,
|
||||||
|
// SaleEstimateMailOptionsDTO,
|
||||||
|
// } from '@/interfaces';
|
||||||
|
// import { ContactMailNotification } from '@/services/MailNotification/ContactMailNotification';
|
||||||
|
// import { mergeAndValidateMailOptions } from '@/services/MailNotification/utils';
|
||||||
|
// import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||||
|
// import events from '@/subscribers/events';
|
||||||
|
// import { transformEstimateToMailDataArgs } from '../utils';
|
||||||
|
|
||||||
|
// @Service()
|
||||||
|
// export class SendSaleEstimateMail {
|
||||||
|
// @Inject()
|
||||||
|
// private tenancy: HasTenancyService;
|
||||||
|
|
||||||
|
// @Inject()
|
||||||
|
// private estimatePdf: SaleEstimatesPdf;
|
||||||
|
|
||||||
|
// @Inject()
|
||||||
|
// private getSaleEstimateService: GetSaleEstimate;
|
||||||
|
|
||||||
|
// @Inject()
|
||||||
|
// private contactMailNotification: ContactMailNotification;
|
||||||
|
|
||||||
|
// @Inject('agenda')
|
||||||
|
// private agenda: any;
|
||||||
|
|
||||||
|
// @Inject()
|
||||||
|
// private eventPublisher: EventPublisher;
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Triggers the reminder mail of the given sale estimate.
|
||||||
|
// * @param {number} tenantId -
|
||||||
|
// * @param {number} saleEstimateId -
|
||||||
|
// * @param {SaleEstimateMailOptionsDTO} messageOptions -
|
||||||
|
// * @returns {Promise<void>}
|
||||||
|
// */
|
||||||
|
// public async triggerMail(
|
||||||
|
// tenantId: number,
|
||||||
|
// saleEstimateId: number,
|
||||||
|
// messageOptions: SaleEstimateMailOptionsDTO
|
||||||
|
// ): Promise<void> {
|
||||||
|
// const payload = {
|
||||||
|
// tenantId,
|
||||||
|
// saleEstimateId,
|
||||||
|
// messageOptions,
|
||||||
|
// };
|
||||||
|
// await this.agenda.now('sale-estimate-mail-send', payload);
|
||||||
|
|
||||||
|
// // Triggers `onSaleEstimatePreMailSend` event.
|
||||||
|
// await this.eventPublisher.emitAsync(events.saleEstimate.onPreMailSend, {
|
||||||
|
// tenantId,
|
||||||
|
// saleEstimateId,
|
||||||
|
// messageOptions,
|
||||||
|
// } as ISaleEstimateMailPresendEvent);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Formate the text of the mail.
|
||||||
|
// * @param {number} tenantId - Tenant id.
|
||||||
|
// * @param {number} estimateId - Estimate id.
|
||||||
|
// * @returns {Promise<Record<string, any>>}
|
||||||
|
// */
|
||||||
|
// public formatterArgs = async (tenantId: number, estimateId: number) => {
|
||||||
|
// const estimate = await this.getSaleEstimateService.getEstimate(
|
||||||
|
// tenantId,
|
||||||
|
// estimateId
|
||||||
|
// );
|
||||||
|
// return transformEstimateToMailDataArgs(estimate);
|
||||||
|
// };
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Retrieves the mail options.
|
||||||
|
// * @param {number} tenantId
|
||||||
|
// * @param {number} saleEstimateId
|
||||||
|
// * @returns {Promise<SaleEstimateMailOptions>}
|
||||||
|
// */
|
||||||
|
// public getMailOptions = async (
|
||||||
|
// tenantId: number,
|
||||||
|
// saleEstimateId: number,
|
||||||
|
// defaultSubject: string = DEFAULT_ESTIMATE_REMINDER_MAIL_SUBJECT,
|
||||||
|
// defaultMessage: string = DEFAULT_ESTIMATE_REMINDER_MAIL_CONTENT
|
||||||
|
// ): Promise<SaleEstimateMailOptions> => {
|
||||||
|
// const { SaleEstimate } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
|
// const saleEstimate = await SaleEstimate.query()
|
||||||
|
// .findById(saleEstimateId)
|
||||||
|
// .throwIfNotFound();
|
||||||
|
|
||||||
|
// const formatArgs = await this.formatterArgs(tenantId, saleEstimateId);
|
||||||
|
|
||||||
|
// const mailOptions =
|
||||||
|
// await this.contactMailNotification.getDefaultMailOptions(
|
||||||
|
// tenantId,
|
||||||
|
// saleEstimate.customerId
|
||||||
|
// );
|
||||||
|
// return {
|
||||||
|
// ...mailOptions,
|
||||||
|
// message: defaultMessage,
|
||||||
|
// subject: defaultSubject,
|
||||||
|
// attachEstimate: true,
|
||||||
|
// formatArgs,
|
||||||
|
// };
|
||||||
|
// };
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Formats the given mail options.
|
||||||
|
// * @param {number} tenantId
|
||||||
|
// * @param {number} saleEstimateId
|
||||||
|
// * @param {SaleEstimateMailOptions} mailOptions
|
||||||
|
// * @returns {Promise<SaleEstimateMailOptions>}
|
||||||
|
// */
|
||||||
|
// public formatMailOptions = async (
|
||||||
|
// tenantId: number,
|
||||||
|
// saleEstimateId: number,
|
||||||
|
// mailOptions: SaleEstimateMailOptions
|
||||||
|
// ): Promise<SaleEstimateMailOptions> => {
|
||||||
|
// const formatterArgs = await this.formatterArgs(tenantId, saleEstimateId);
|
||||||
|
// const formattedOptions =
|
||||||
|
// await this.contactMailNotification.formatMailOptions(
|
||||||
|
// tenantId,
|
||||||
|
// mailOptions,
|
||||||
|
// formatterArgs
|
||||||
|
// );
|
||||||
|
// return { ...formattedOptions };
|
||||||
|
// };
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Sends the mail notification of the given sale estimate.
|
||||||
|
// * @param {number} tenantId
|
||||||
|
// * @param {number} saleEstimateId
|
||||||
|
// * @param {SaleEstimateMailOptions} messageOptions
|
||||||
|
// * @returns {Promise<void>}
|
||||||
|
// */
|
||||||
|
// public async sendMail(
|
||||||
|
// tenantId: number,
|
||||||
|
// saleEstimateId: number,
|
||||||
|
// messageOptions: SaleEstimateMailOptionsDTO
|
||||||
|
// ): Promise<void> {
|
||||||
|
// const localMessageOpts = await this.getMailOptions(
|
||||||
|
// tenantId,
|
||||||
|
// saleEstimateId
|
||||||
|
// );
|
||||||
|
// // Overrides and validates the given mail options.
|
||||||
|
// const parsedMessageOptions = mergeAndValidateMailOptions(
|
||||||
|
// localMessageOpts,
|
||||||
|
// messageOptions
|
||||||
|
// ) as SaleEstimateMailOptions;
|
||||||
|
|
||||||
|
// const formattedOptions = await this.formatMailOptions(
|
||||||
|
// tenantId,
|
||||||
|
// saleEstimateId,
|
||||||
|
// parsedMessageOptions
|
||||||
|
// );
|
||||||
|
// const mail = new Mail()
|
||||||
|
// .setSubject(formattedOptions.subject)
|
||||||
|
// .setTo(formattedOptions.to)
|
||||||
|
// .setCC(formattedOptions.cc)
|
||||||
|
// .setBCC(formattedOptions.bcc)
|
||||||
|
// .setContent(formattedOptions.message);
|
||||||
|
|
||||||
|
// // Attaches the estimate pdf to the mail.
|
||||||
|
// if (formattedOptions.attachEstimate) {
|
||||||
|
// // Retrieves the estimate pdf and attaches it to the mail.
|
||||||
|
// const [estimatePdfBuffer, estimateFilename] =
|
||||||
|
// await this.estimatePdf.getSaleEstimatePdf(tenantId, saleEstimateId);
|
||||||
|
|
||||||
|
// mail.setAttachments([
|
||||||
|
// {
|
||||||
|
// filename: `${estimateFilename}.pdf`,
|
||||||
|
// content: estimatePdfBuffer,
|
||||||
|
// },
|
||||||
|
// ]);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const eventPayload = {
|
||||||
|
// tenantId,
|
||||||
|
// saleEstimateId,
|
||||||
|
// messageOptions,
|
||||||
|
// formattedOptions,
|
||||||
|
// };
|
||||||
|
// // Triggers `onSaleEstimateMailSend` event.
|
||||||
|
// await this.eventPublisher.emitAsync(
|
||||||
|
// events.saleEstimate.onMailSend,
|
||||||
|
// eventPayload as ISaleEstimateMailPresendEvent
|
||||||
|
// );
|
||||||
|
// await mail.send();
|
||||||
|
|
||||||
|
// // Triggers `onSaleEstimateMailSent` event.
|
||||||
|
// await this.eventPublisher.emitAsync(
|
||||||
|
// events.saleEstimate.onMailSent,
|
||||||
|
// eventPayload as ISaleEstimateMailPresendEvent
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// }
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
// import Container, { Service } from 'typedi';
|
||||||
|
// import { SendSaleEstimateMail } from './SendSaleEstimateMail';
|
||||||
|
|
||||||
|
// @Service()
|
||||||
|
// export class SendSaleEstimateMailJob {
|
||||||
|
// /**
|
||||||
|
// * Constructor method.
|
||||||
|
// */
|
||||||
|
// constructor(agenda) {
|
||||||
|
// agenda.define(
|
||||||
|
// 'sale-estimate-mail-send',
|
||||||
|
// { priority: 'high', concurrency: 2 },
|
||||||
|
// this.handler
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Triggers sending invoice mail.
|
||||||
|
// */
|
||||||
|
// private handler = async (job, done: Function) => {
|
||||||
|
// const { tenantId, saleEstimateId, messageOptions } = job.attrs.data;
|
||||||
|
// const sendSaleEstimateMail = Container.get(SendSaleEstimateMail);
|
||||||
|
|
||||||
|
// try {
|
||||||
|
// await sendSaleEstimateMail.sendMail(
|
||||||
|
// tenantId,
|
||||||
|
// saleEstimateId,
|
||||||
|
// messageOptions
|
||||||
|
// );
|
||||||
|
// done();
|
||||||
|
// } catch (error) {
|
||||||
|
// console.log(error);
|
||||||
|
// done(error);
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
// }
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { Knex } from 'knex';
|
||||||
|
import { SaleEstimate } from '../models/SaleEstimate';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class UnlinkConvertedSaleEstimate {
|
||||||
|
constructor(
|
||||||
|
@Inject(SaleEstimate.name)
|
||||||
|
private readonly saleEstimateModel: typeof SaleEstimate,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unlink the converted sale estimates from the given sale invoice.
|
||||||
|
* @param {number} invoiceId -
|
||||||
|
* @return {Promise<void>}
|
||||||
|
*/
|
||||||
|
public async unlinkConvertedEstimateFromInvoice(
|
||||||
|
invoiceId: number,
|
||||||
|
trx?: Knex.Transaction
|
||||||
|
): Promise<void> {
|
||||||
|
await this.saleEstimateModel.query(trx)
|
||||||
|
.where({
|
||||||
|
convertedToInvoiceId: invoiceId,
|
||||||
|
})
|
||||||
|
.patch({
|
||||||
|
convertedToInvoiceId: null,
|
||||||
|
convertedToInvoiceAt: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
287
packages/server-nest/src/modules/SaleEstimates/constants.ts
Normal file
287
packages/server-nest/src/modules/SaleEstimates/constants.ts
Normal file
@@ -0,0 +1,287 @@
|
|||||||
|
export const DEFAULT_ESTIMATE_REMINDER_MAIL_SUBJECT =
|
||||||
|
'Estimate {Estimate Number} is awaiting your approval';
|
||||||
|
export const DEFAULT_ESTIMATE_REMINDER_MAIL_CONTENT = `<p>Dear {Customer Name}</p>
|
||||||
|
<p>Thank you for your business, You can view or print your estimate from attachements.</p>
|
||||||
|
<p>
|
||||||
|
Estimate <strong>#{Estimate Number}</strong><br />
|
||||||
|
Expiration Date : <strong>{Estimate Expiration Date}</strong><br />
|
||||||
|
Amount : <strong>{Estimate Amount}</strong></br />
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<i>Regards</i><br />
|
||||||
|
<i>{Company Name}</i>
|
||||||
|
</p>
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const ERRORS = {
|
||||||
|
SALE_ESTIMATE_NOT_FOUND: 'SALE_ESTIMATE_NOT_FOUND',
|
||||||
|
SALE_ESTIMATE_NUMBER_EXISTANCE: 'SALE_ESTIMATE_NUMBER_EXISTANCE',
|
||||||
|
SALE_ESTIMATE_CONVERTED_TO_INVOICE: 'SALE_ESTIMATE_CONVERTED_TO_INVOICE',
|
||||||
|
SALE_ESTIMATE_NOT_DELIVERED: 'SALE_ESTIMATE_NOT_DELIVERED',
|
||||||
|
SALE_ESTIMATE_ALREADY_REJECTED: 'SALE_ESTIMATE_ALREADY_REJECTED',
|
||||||
|
CUSTOMER_HAS_SALES_ESTIMATES: 'CUSTOMER_HAS_SALES_ESTIMATES',
|
||||||
|
SALE_ESTIMATE_NO_IS_REQUIRED: 'SALE_ESTIMATE_NO_IS_REQUIRED',
|
||||||
|
SALE_ESTIMATE_ALREADY_DELIVERED: 'SALE_ESTIMATE_ALREADY_DELIVERED',
|
||||||
|
SALE_ESTIMATE_ALREADY_APPROVED: 'SALE_ESTIMATE_ALREADY_APPROVED',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DEFAULT_VIEW_COLUMNS = [];
|
||||||
|
export const DEFAULT_VIEWS = [
|
||||||
|
{
|
||||||
|
name: 'Draft',
|
||||||
|
slug: 'draft',
|
||||||
|
rolesLogicExpression: '1',
|
||||||
|
roles: [
|
||||||
|
{ index: 1, fieldKey: 'status', comparator: 'equals', value: 'draft' },
|
||||||
|
],
|
||||||
|
columns: DEFAULT_VIEW_COLUMNS,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Delivered',
|
||||||
|
slug: 'delivered',
|
||||||
|
rolesLogicExpression: '1',
|
||||||
|
roles: [
|
||||||
|
{
|
||||||
|
index: 1,
|
||||||
|
fieldKey: 'status',
|
||||||
|
comparator: 'equals',
|
||||||
|
value: 'delivered',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
columns: DEFAULT_VIEW_COLUMNS,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Approved',
|
||||||
|
slug: 'approved',
|
||||||
|
rolesLogicExpression: '1',
|
||||||
|
roles: [
|
||||||
|
{
|
||||||
|
index: 1,
|
||||||
|
fieldKey: 'status',
|
||||||
|
comparator: 'equals',
|
||||||
|
value: 'approved',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
columns: DEFAULT_VIEW_COLUMNS,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Rejected',
|
||||||
|
slug: 'rejected',
|
||||||
|
rolesLogicExpression: '1',
|
||||||
|
roles: [
|
||||||
|
{
|
||||||
|
index: 1,
|
||||||
|
fieldKey: 'status',
|
||||||
|
comparator: 'equals',
|
||||||
|
value: 'rejected',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
columns: DEFAULT_VIEW_COLUMNS,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Invoiced',
|
||||||
|
slug: 'invoiced',
|
||||||
|
rolesLogicExpression: '1',
|
||||||
|
roles: [
|
||||||
|
{
|
||||||
|
index: 1,
|
||||||
|
fieldKey: 'status',
|
||||||
|
comparator: 'equals',
|
||||||
|
value: 'invoiced',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
columns: DEFAULT_VIEW_COLUMNS,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Expired',
|
||||||
|
slug: 'expired',
|
||||||
|
rolesLogicExpression: '1',
|
||||||
|
roles: [
|
||||||
|
{
|
||||||
|
index: 1,
|
||||||
|
fieldKey: 'status',
|
||||||
|
comparator: 'equals',
|
||||||
|
value: 'expired',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
columns: DEFAULT_VIEW_COLUMNS,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Closed',
|
||||||
|
slug: 'closed',
|
||||||
|
rolesLogicExpression: '1',
|
||||||
|
roles: [
|
||||||
|
{
|
||||||
|
index: 1,
|
||||||
|
fieldKey: 'status',
|
||||||
|
comparator: 'equals',
|
||||||
|
value: 'closed',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
columns: DEFAULT_VIEW_COLUMNS,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const SaleEstimatesSampleData = [
|
||||||
|
{
|
||||||
|
Customer: 'Ambrose Olson',
|
||||||
|
'Estimate Date': '2024-01-01',
|
||||||
|
'Expiration Date': '2025-01-01',
|
||||||
|
'Estimate No.': 'EST-0001',
|
||||||
|
'Reference No.': 'REF-0001',
|
||||||
|
Currency: '',
|
||||||
|
'Exchange Rate': '',
|
||||||
|
Note: 'Vel autem quis aut ab.',
|
||||||
|
'Terms & Conditions': 'Provident illo architecto sit iste in.',
|
||||||
|
Delivered: 'T',
|
||||||
|
'Item Name': 'Hettinger, Schumm and Bartoletti',
|
||||||
|
Quantity: 1000,
|
||||||
|
Rate: 20,
|
||||||
|
'Line Description': 'Rem esse doloremque praesentium harum maiores.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Customer: 'Ambrose Olson',
|
||||||
|
'Estimate Date': '2024-01-02',
|
||||||
|
'Expiration Date': '2025-01-02',
|
||||||
|
'Estimate No.': 'EST-0002',
|
||||||
|
'Reference No.': 'REF-0002',
|
||||||
|
Currency: '',
|
||||||
|
'Exchange Rate': '',
|
||||||
|
Note: 'Tempora voluptas odio deleniti rerum vitae consequatur nihil quis sunt.',
|
||||||
|
'Terms & Conditions': 'Ut eum incidunt quibusdam rerum vero.',
|
||||||
|
Delivered: 'T',
|
||||||
|
'Item Name': 'Hettinger, Schumm and Bartoletti',
|
||||||
|
Quantity: 1000,
|
||||||
|
Rate: 20,
|
||||||
|
'Line Description': 'Qui voluptate aliquam maxime aliquam.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Customer: 'Ambrose Olson',
|
||||||
|
'Estimate Date': '2024-01-03',
|
||||||
|
'Expiration Date': '2025-01-03',
|
||||||
|
'Estimate No.': 'EST-0003',
|
||||||
|
'Reference No.': 'REF-0003',
|
||||||
|
Currency: '',
|
||||||
|
'Exchange Rate': '',
|
||||||
|
Note: 'Quia voluptatem delectus doloremque.',
|
||||||
|
'Terms & Conditions': 'Facilis porro vitae ratione.',
|
||||||
|
Delivered: 'T',
|
||||||
|
'Item Name': 'Hettinger, Schumm and Bartoletti',
|
||||||
|
Quantity: 1000,
|
||||||
|
Rate: 20,
|
||||||
|
'Line Description': 'Qui suscipit ducimus qui qui.',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const defaultEstimatePdfBrandingAttributes = {
|
||||||
|
primaryColor: '#000',
|
||||||
|
secondaryColor: '#000',
|
||||||
|
|
||||||
|
// # Company logo
|
||||||
|
showCompanyLogo: true,
|
||||||
|
companyLogoUri: '',
|
||||||
|
companyLogoKey: '',
|
||||||
|
|
||||||
|
companyName: '',
|
||||||
|
|
||||||
|
customerAddress: '',
|
||||||
|
companyAddress: '',
|
||||||
|
showCustomerAddress: true,
|
||||||
|
showCompanyAddress: true,
|
||||||
|
billedToLabel: 'Billed To',
|
||||||
|
|
||||||
|
total: '$1000.00',
|
||||||
|
totalLabel: 'Total',
|
||||||
|
showTotal: true,
|
||||||
|
|
||||||
|
subtotal: '1000/00',
|
||||||
|
subtotalLabel: 'Subtotal',
|
||||||
|
showSubtotal: true,
|
||||||
|
|
||||||
|
showCustomerNote: true,
|
||||||
|
customerNote:
|
||||||
|
'It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.',
|
||||||
|
customerNoteLabel: 'Customer Note',
|
||||||
|
|
||||||
|
showTermsConditions: true,
|
||||||
|
termsConditions:
|
||||||
|
'It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.',
|
||||||
|
termsConditionsLabel: 'Terms & Conditions',
|
||||||
|
|
||||||
|
lines: [
|
||||||
|
{
|
||||||
|
item: 'Simply dummy text',
|
||||||
|
description: 'Simply dummy text of the printing and typesetting',
|
||||||
|
rate: '1',
|
||||||
|
quantity: '1000',
|
||||||
|
total: '$1000.00',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
showEstimateNumber: true,
|
||||||
|
estimateNumberLabel: 'Estimate Number',
|
||||||
|
estimateNumebr: '346D3D40-0001',
|
||||||
|
|
||||||
|
estimateDate: 'September 3, 2024',
|
||||||
|
showEstimateDate: true,
|
||||||
|
estimateDateLabel: 'Estimate Date',
|
||||||
|
|
||||||
|
expirationDateLabel: 'Expiration Date',
|
||||||
|
showExpirationDate: true,
|
||||||
|
expirationDate: 'September 3, 2024',
|
||||||
|
};
|
||||||
|
|
||||||
|
interface EstimatePdfBrandingLineItem {
|
||||||
|
item: string;
|
||||||
|
description: string;
|
||||||
|
rate: string;
|
||||||
|
quantity: string;
|
||||||
|
total: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EstimatePdfBrandingAttributes {
|
||||||
|
primaryColor: string;
|
||||||
|
secondaryColor: string;
|
||||||
|
showCompanyLogo: boolean;
|
||||||
|
companyLogo: string;
|
||||||
|
companyName: string;
|
||||||
|
|
||||||
|
// Customer Address
|
||||||
|
showCustomerAddress: boolean;
|
||||||
|
customerAddress: string;
|
||||||
|
|
||||||
|
// Company Address
|
||||||
|
showCompanyAddress: boolean;
|
||||||
|
companyAddress: string;
|
||||||
|
billedToLabel: string;
|
||||||
|
|
||||||
|
total: string;
|
||||||
|
totalLabel: string;
|
||||||
|
showTotal: boolean;
|
||||||
|
|
||||||
|
subtotal: string;
|
||||||
|
subtotalLabel: string;
|
||||||
|
showSubtotal: boolean;
|
||||||
|
|
||||||
|
showCustomerNote: boolean;
|
||||||
|
customerNote: string;
|
||||||
|
customerNoteLabel: string;
|
||||||
|
|
||||||
|
showTermsConditions: boolean;
|
||||||
|
termsConditions: string;
|
||||||
|
termsConditionsLabel: string;
|
||||||
|
|
||||||
|
lines: EstimatePdfBrandingLineItem[];
|
||||||
|
|
||||||
|
showEstimateNumber: boolean;
|
||||||
|
estimateNumberLabel: string;
|
||||||
|
estimateNumebr: string;
|
||||||
|
|
||||||
|
estimateDate: string;
|
||||||
|
showEstimateDate: boolean;
|
||||||
|
estimateDateLabel: string;
|
||||||
|
|
||||||
|
expirationDateLabel: string;
|
||||||
|
showExpirationDate: boolean;
|
||||||
|
expirationDate: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,318 @@
|
|||||||
|
import { BaseModel } from '@/models/Model';
|
||||||
|
import moment from 'moment';
|
||||||
|
import { Model } from 'objection';
|
||||||
|
// import TenantModel from 'models/TenantModel';
|
||||||
|
// import { defaultToTransform } from 'utils';
|
||||||
|
// import SaleEstimateSettings from './SaleEstimate.Settings';
|
||||||
|
// import ModelSetting from './ModelSetting';
|
||||||
|
// import CustomViewBaseModel from './CustomViewBaseModel';
|
||||||
|
// import { DEFAULT_VIEWS } from '@/services/Sales/Estimates/constants';
|
||||||
|
// import ModelSearchable from './ModelSearchable';
|
||||||
|
|
||||||
|
export class SaleEstimate extends BaseModel {
|
||||||
|
exchangeRate!: number;
|
||||||
|
amount!: number;
|
||||||
|
|
||||||
|
currencyCode!: string;
|
||||||
|
|
||||||
|
customerId!: number;
|
||||||
|
estimateDate!: Date | string;
|
||||||
|
expirationDate!: Date | string;
|
||||||
|
reference!: string;
|
||||||
|
estimateNumber!: string;
|
||||||
|
note!: string;
|
||||||
|
termsConditions!: string;
|
||||||
|
sendToEmail!: string;
|
||||||
|
|
||||||
|
deliveredAt!: Date | string;
|
||||||
|
approvedAt!: Date | string;
|
||||||
|
rejectedAt!: Date | string;
|
||||||
|
|
||||||
|
userId!: number;
|
||||||
|
|
||||||
|
convertedToInvoiceId!: number;
|
||||||
|
convertedToInvoiceAt!: Date | string;
|
||||||
|
|
||||||
|
createdAt?: Date;
|
||||||
|
updatedAt?: Date | null;
|
||||||
|
|
||||||
|
branchId?: number;
|
||||||
|
warehouseId?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Table name
|
||||||
|
*/
|
||||||
|
static get tableName() {
|
||||||
|
return 'sales_estimates';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timestamps columns.
|
||||||
|
*/
|
||||||
|
get timestamps() {
|
||||||
|
return ['createdAt', 'updatedAt'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Virtual attributes.
|
||||||
|
*/
|
||||||
|
static get virtualAttributes() {
|
||||||
|
return [
|
||||||
|
'localAmount',
|
||||||
|
'isDelivered',
|
||||||
|
'isExpired',
|
||||||
|
'isConvertedToInvoice',
|
||||||
|
'isApproved',
|
||||||
|
'isRejected',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Estimate amount in local currency.
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
get localAmount() {
|
||||||
|
return this.amount * this.exchangeRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detarmines whether the sale estimate converted to sale invoice.
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
get isConvertedToInvoice() {
|
||||||
|
return !!(this.convertedToInvoiceId && this.convertedToInvoiceAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detarmines whether the estimate is delivered.
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
get isDelivered() {
|
||||||
|
return !!this.deliveredAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detarmines whether the estimate is expired.
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
get isExpired() {
|
||||||
|
// return defaultToTransform(
|
||||||
|
// this.expirationDate,
|
||||||
|
// moment().isAfter(this.expirationDate, 'day'),
|
||||||
|
// false
|
||||||
|
// );i
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detarmines whether the estimate is approved.
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
get isApproved() {
|
||||||
|
return !!this.approvedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detarmines whether the estimate is reject.
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
get isRejected() {
|
||||||
|
return !!this.rejectedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows to mark model as resourceable to viewable and filterable.
|
||||||
|
*/
|
||||||
|
static get resourceable() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Model modifiers.
|
||||||
|
*/
|
||||||
|
static get modifiers() {
|
||||||
|
return {
|
||||||
|
/**
|
||||||
|
* Filters the drafted estimates transactions.
|
||||||
|
*/
|
||||||
|
draft(query) {
|
||||||
|
query.where('delivered_at', null);
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Filters the delivered estimates transactions.
|
||||||
|
*/
|
||||||
|
delivered(query) {
|
||||||
|
query.whereNot('delivered_at', null);
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Filters the expired estimates transactions.
|
||||||
|
*/
|
||||||
|
expired(query) {
|
||||||
|
query.where('expiration_date', '<', moment().format('YYYY-MM-DD'));
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Filters the rejected estimates transactions.
|
||||||
|
*/
|
||||||
|
rejected(query) {
|
||||||
|
query.whereNot('rejected_at', null);
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Filters the invoiced estimates transactions.
|
||||||
|
*/
|
||||||
|
invoiced(query) {
|
||||||
|
query.whereNot('converted_to_invoice_at', null);
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Filters the approved estimates transactions.
|
||||||
|
*/
|
||||||
|
approved(query) {
|
||||||
|
query.whereNot('approved_at', null);
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Sorting the estimates orders by delivery status.
|
||||||
|
*/
|
||||||
|
orderByStatus(query, order) {
|
||||||
|
query.orderByRaw(`delivered_at is null ${order}`);
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Filtering the estimates oreders by status field.
|
||||||
|
*/
|
||||||
|
filterByStatus(query, filterType) {
|
||||||
|
switch (filterType) {
|
||||||
|
case 'draft':
|
||||||
|
query.modify('draft');
|
||||||
|
break;
|
||||||
|
case 'delivered':
|
||||||
|
query.modify('delivered');
|
||||||
|
break;
|
||||||
|
case 'approved':
|
||||||
|
query.modify('approved');
|
||||||
|
break;
|
||||||
|
case 'rejected':
|
||||||
|
query.modify('rejected');
|
||||||
|
break;
|
||||||
|
case 'invoiced':
|
||||||
|
query.modify('invoiced');
|
||||||
|
break;
|
||||||
|
case 'expired':
|
||||||
|
query.modify('expired');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Relationship mapping.
|
||||||
|
*/
|
||||||
|
static get relationMappings() {
|
||||||
|
const { ItemEntry } = require('../../Items/models/ItemEntry');
|
||||||
|
// const Customer = require('models/Customer');
|
||||||
|
// const Branch = require('models/Branch');
|
||||||
|
// const Warehouse = require('models/Warehouse');
|
||||||
|
// const Document = require('models/Document');
|
||||||
|
|
||||||
|
return {
|
||||||
|
// customer: {
|
||||||
|
// relation: Model.BelongsToOneRelation,
|
||||||
|
// modelClass: Customer.default,
|
||||||
|
// join: {
|
||||||
|
// from: 'sales_estimates.customerId',
|
||||||
|
// to: 'contacts.id',
|
||||||
|
// },
|
||||||
|
// filter(query) {
|
||||||
|
// query.where('contact_service', 'customer');
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
entries: {
|
||||||
|
relation: Model.HasManyRelation,
|
||||||
|
modelClass: ItemEntry,
|
||||||
|
|
||||||
|
join: {
|
||||||
|
from: 'sales_estimates.id',
|
||||||
|
to: 'items_entries.referenceId',
|
||||||
|
},
|
||||||
|
filter(builder) {
|
||||||
|
builder.where('reference_type', 'SaleEstimate');
|
||||||
|
builder.orderBy('index', 'ASC');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Sale estimate may belongs to branch.
|
||||||
|
// */
|
||||||
|
// branch: {
|
||||||
|
// relation: Model.BelongsToOneRelation,
|
||||||
|
// modelClass: Branch.default,
|
||||||
|
// join: {
|
||||||
|
// from: 'sales_estimates.branchId',
|
||||||
|
// to: 'branches.id',
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Sale estimate may has associated warehouse.
|
||||||
|
// */
|
||||||
|
// warehouse: {
|
||||||
|
// relation: Model.BelongsToOneRelation,
|
||||||
|
// modelClass: Warehouse.default,
|
||||||
|
// join: {
|
||||||
|
// from: 'sales_estimates.warehouseId',
|
||||||
|
// to: 'warehouses.id',
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Sale estimate transaction may has many attached attachments.
|
||||||
|
// */
|
||||||
|
// attachments: {
|
||||||
|
// relation: Model.ManyToManyRelation,
|
||||||
|
// modelClass: Document.default,
|
||||||
|
// join: {
|
||||||
|
// from: 'sales_estimates.id',
|
||||||
|
// through: {
|
||||||
|
// from: 'document_links.modelId',
|
||||||
|
// to: 'document_links.documentId',
|
||||||
|
// },
|
||||||
|
// to: 'documents.id',
|
||||||
|
// },
|
||||||
|
// filter(query) {
|
||||||
|
// query.where('model_ref', 'SaleEstimate');
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Model settings.
|
||||||
|
*/
|
||||||
|
// static get meta() {
|
||||||
|
// return SaleEstimateSettings;
|
||||||
|
// }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the default custom views, roles and columns.
|
||||||
|
*/
|
||||||
|
// static get defaultViews() {
|
||||||
|
// return DEFAULT_VIEWS;
|
||||||
|
// }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Model search roles.
|
||||||
|
*/
|
||||||
|
static get searchRoles() {
|
||||||
|
return [
|
||||||
|
{ fieldKey: 'amount', comparator: 'equals' },
|
||||||
|
{ condition: 'or', fieldKey: 'estimate_number', comparator: 'contains' },
|
||||||
|
{ condition: 'or', fieldKey: 'reference_no', comparator: 'contains' },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prevents mutate base currency since the model is not empty.
|
||||||
|
*/
|
||||||
|
static get preventMutateBaseCurrency() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { SaleEstimateTransfromer } from './SaleEstimate.transformer';
|
||||||
|
import { SaleEstimateValidators } from '../commands/SaleEstimateValidators.service';
|
||||||
|
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
|
||||||
|
import { SaleEstimate } from '../models/SaleEstimate';
|
||||||
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
|
import { events } from '@/common/events/events';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class GetSaleEstimate {
|
||||||
|
constructor(
|
||||||
|
@Inject(SaleEstimate.name)
|
||||||
|
private readonly saleEstimateModel: typeof SaleEstimate,
|
||||||
|
private readonly transformer: TransformerInjectable,
|
||||||
|
private readonly validators: SaleEstimateValidators,
|
||||||
|
private readonly eventPublisher: EventEmitter2,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the estimate details with associated entries.
|
||||||
|
* @async
|
||||||
|
* @param {Integer} estimateId
|
||||||
|
*/
|
||||||
|
public async getEstimate(estimateId: number) {
|
||||||
|
const estimate = await this.saleEstimateModel.query()
|
||||||
|
.findById(estimateId)
|
||||||
|
.withGraphFetched('entries.item')
|
||||||
|
.withGraphFetched('customer')
|
||||||
|
.withGraphFetched('branch')
|
||||||
|
.withGraphFetched('attachments');
|
||||||
|
|
||||||
|
// Validates the estimate existance.
|
||||||
|
this.validators.validateEstimateExistance(estimate);
|
||||||
|
|
||||||
|
// Transformes sale estimate model to POJO.
|
||||||
|
const transformed = await this.transformer.transform(
|
||||||
|
estimate,
|
||||||
|
new SaleEstimateTransfromer()
|
||||||
|
);
|
||||||
|
const eventPayload = { saleEstimateId: estimateId };
|
||||||
|
|
||||||
|
// Triggers `onSaleEstimateViewed` event.
|
||||||
|
await this.eventPublisher.emitAsync(
|
||||||
|
events.saleEstimate.onViewed,
|
||||||
|
eventPayload
|
||||||
|
);
|
||||||
|
return transformed;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { PdfTemplateModel } from '@/modules/PdfTemplate/models/PdfTemplate';
|
||||||
|
import { ISaleEstimateState } from '../types/SaleEstimates.types';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class GetSaleEstimateState {
|
||||||
|
constructor(
|
||||||
|
@Inject(PdfTemplateModel.name)
|
||||||
|
private pdfTemplateModel: typeof PdfTemplateModel,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the create/edit sale estimate state.
|
||||||
|
* @return {Promise<ISaleEstimateState>}
|
||||||
|
*/
|
||||||
|
public async getSaleEstimateState(): Promise<ISaleEstimateState> {
|
||||||
|
const defaultPdfTemplate = await this.pdfTemplateModel
|
||||||
|
.query()
|
||||||
|
.findOne({ resource: 'SaleEstimate' })
|
||||||
|
.modify('default');
|
||||||
|
|
||||||
|
return {
|
||||||
|
defaultTemplateId: defaultPdfTemplate?.id,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
// import * as R from 'ramda';
|
||||||
|
// import { Inject, Service } from 'typedi';
|
||||||
|
// import {
|
||||||
|
// IFilterMeta,
|
||||||
|
// IPaginationMeta,
|
||||||
|
// ISaleEstimate,
|
||||||
|
// ISalesEstimatesFilter,
|
||||||
|
// } from '@/interfaces';
|
||||||
|
// import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||||
|
// import DynamicListingService from '@/services/DynamicListing/DynamicListService';
|
||||||
|
// import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
||||||
|
// import { SaleEstimateDTOTransformer } from '../commands/SaleEstimateDTOTransformer';
|
||||||
|
// import { SaleEstimateTransfromer } from './SaleEstimate.transformer';
|
||||||
|
|
||||||
|
// @Service()
|
||||||
|
// export class GetSaleEstimates {
|
||||||
|
// @Inject()
|
||||||
|
// private tenancy: HasTenancyService;
|
||||||
|
|
||||||
|
// @Inject()
|
||||||
|
// private dynamicListService: DynamicListingService;
|
||||||
|
|
||||||
|
// @Inject()
|
||||||
|
// private transformer: TransformerInjectable;
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Retrieves estimates filterable and paginated list.
|
||||||
|
// * @param {number} tenantId -
|
||||||
|
// * @param {IEstimatesFilter} estimatesFilter -
|
||||||
|
// */
|
||||||
|
// public async getEstimates(
|
||||||
|
// tenantId: number,
|
||||||
|
// filterDTO: ISalesEstimatesFilter
|
||||||
|
// ): Promise<{
|
||||||
|
// salesEstimates: ISaleEstimate[];
|
||||||
|
// pagination: IPaginationMeta;
|
||||||
|
// filterMeta: IFilterMeta;
|
||||||
|
// }> {
|
||||||
|
// const { SaleEstimate } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
|
// // Parses filter DTO.
|
||||||
|
// const filter = this.parseListFilterDTO(filterDTO);
|
||||||
|
|
||||||
|
// // Dynamic list service.
|
||||||
|
// const dynamicFilter = await this.dynamicListService.dynamicList(
|
||||||
|
// tenantId,
|
||||||
|
// SaleEstimate,
|
||||||
|
// filter
|
||||||
|
// );
|
||||||
|
// const { results, pagination } = await SaleEstimate.query()
|
||||||
|
// .onBuild((builder) => {
|
||||||
|
// builder.withGraphFetched('customer');
|
||||||
|
// builder.withGraphFetched('entries');
|
||||||
|
// builder.withGraphFetched('entries.item');
|
||||||
|
// dynamicFilter.buildQuery()(builder);
|
||||||
|
// filterDTO?.filterQuery && filterDTO?.filterQuery(builder);
|
||||||
|
// })
|
||||||
|
// .pagination(filter.page - 1, filter.pageSize);
|
||||||
|
|
||||||
|
// const transformedEstimates = await this.transformer.transform(
|
||||||
|
// tenantId,
|
||||||
|
// results,
|
||||||
|
// new SaleEstimateTransfromer()
|
||||||
|
// );
|
||||||
|
// return {
|
||||||
|
// salesEstimates: transformedEstimates,
|
||||||
|
// pagination,
|
||||||
|
// filterMeta: dynamicFilter.getResponseMeta(),
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Parses the sale receipts list filter DTO.
|
||||||
|
// * @param filterDTO
|
||||||
|
// */
|
||||||
|
// private parseListFilterDTO(filterDTO) {
|
||||||
|
// return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
// import { Transformer } from '@/lib/Transformer/Transformer';
|
||||||
|
// import { ItemEntryTransformer } from '../Invoices/ItemEntryTransformer';
|
||||||
|
// import { AttachmentTransformer } from '@/services/Attachments/AttachmentTransformer';
|
||||||
|
|
||||||
|
import { Transformer } from '@/modules/Transformer/Transformer';
|
||||||
|
import { SaleEstimate } from '../models/SaleEstimate';
|
||||||
|
import { ItemEntryTransformer } from '@/modules/TransactionItemEntry/ItemEntry.transformer';
|
||||||
|
|
||||||
|
export class SaleEstimateTransfromer extends Transformer {
|
||||||
|
/**
|
||||||
|
* Include these attributes to sale invoice object.
|
||||||
|
* @returns {Array}
|
||||||
|
*/
|
||||||
|
public includeAttributes = (): string[] => {
|
||||||
|
return [
|
||||||
|
'formattedSubtotal',
|
||||||
|
'formattedAmount',
|
||||||
|
'formattedEstimateDate',
|
||||||
|
'formattedExpirationDate',
|
||||||
|
'formattedDeliveredAtDate',
|
||||||
|
'formattedApprovedAtDate',
|
||||||
|
'formattedRejectedAtDate',
|
||||||
|
'formattedCreatedAt',
|
||||||
|
'entries',
|
||||||
|
'attachments',
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve formatted estimate date.
|
||||||
|
* @param {ISaleEstimate} invoice
|
||||||
|
* @returns {String}
|
||||||
|
*/
|
||||||
|
protected formattedEstimateDate = (estimate: SaleEstimate): string => {
|
||||||
|
return this.formatDate(estimate.estimateDate);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve formatted estimate date.
|
||||||
|
* @param {ISaleEstimate} invoice
|
||||||
|
* @returns {String}
|
||||||
|
*/
|
||||||
|
protected formattedExpirationDate = (estimate: SaleEstimate): string => {
|
||||||
|
return this.formatDate(estimate.expirationDate);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the formatted estimate created at.
|
||||||
|
* @param {ISaleEstimate} estimate -
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
protected formattedCreatedAt = (estimate: SaleEstimate): string => {
|
||||||
|
return this.formatDate(estimate.createdAt);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve formatted estimate date.
|
||||||
|
* @param {ISaleEstimate} invoice
|
||||||
|
* @returns {String}
|
||||||
|
*/
|
||||||
|
protected formattedDeliveredAtDate = (estimate: SaleEstimate): string => {
|
||||||
|
return this.formatDate(estimate.deliveredAt);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve formatted estimate date.
|
||||||
|
* @param {ISaleEstimate} invoice
|
||||||
|
* @returns {String}
|
||||||
|
*/
|
||||||
|
protected formattedApprovedAtDate = (estimate: SaleEstimate): string => {
|
||||||
|
return this.formatDate(estimate.approvedAt);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve formatted estimate date.
|
||||||
|
* @param {ISaleEstimate} invoice
|
||||||
|
* @returns {String}
|
||||||
|
*/
|
||||||
|
protected formattedRejectedAtDate = (estimate: SaleEstimate): string => {
|
||||||
|
return this.formatDate(estimate.rejectedAt);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve formatted invoice amount.
|
||||||
|
* @param {ISaleEstimate} estimate
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
protected formattedAmount = (estimate: SaleEstimate): string => {
|
||||||
|
return this.formatNumber(estimate.amount, {
|
||||||
|
currencyCode: estimate.currencyCode,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the formatted invoice subtotal.
|
||||||
|
* @param {ISaleEstimate} estimate
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
protected formattedSubtotal = (estimate: SaleEstimate): string => {
|
||||||
|
return this.formatNumber(estimate.amount, { money: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the entries of the sale estimate.
|
||||||
|
* @param {ISaleEstimate} estimate
|
||||||
|
* @returns {}
|
||||||
|
*/
|
||||||
|
protected entries = (estimate: SaleEstimate) => {
|
||||||
|
// return this.item(estimate.entries, new ItemEntryTransformer(), {
|
||||||
|
// currencyCode: estimate.currencyCode,
|
||||||
|
// });
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the sale estimate attachments.
|
||||||
|
* @param {ISaleInvoice} invoice
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
protected attachments = (estimate: SaleEstimate) => {
|
||||||
|
// return this.item(estimate.attachments, new AttachmentTransformer());
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
// import { Inject, Service } from 'typedi';
|
||||||
|
// import { ChromiumlyTenancy } from '@/services/ChromiumlyTenancy/ChromiumlyTenancy';
|
||||||
|
// import { TemplateInjectable } from '@/services/TemplateInjectable/TemplateInjectable';
|
||||||
|
// import { GetSaleEstimate } from './GetSaleEstimate.service';
|
||||||
|
// import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||||
|
// import { SaleEstimatePdfTemplate } from '../Invoices/SaleEstimatePdfTemplate';
|
||||||
|
// import { transformEstimateToPdfTemplate } from '../utils';
|
||||||
|
// import { EstimatePdfBrandingAttributes } from '../constants';
|
||||||
|
// import events from '@/subscribers/events';
|
||||||
|
// import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||||
|
|
||||||
|
// @Service()
|
||||||
|
// export class SaleEstimatesPdf {
|
||||||
|
// @Inject()
|
||||||
|
// private tenancy: HasTenancyService;
|
||||||
|
|
||||||
|
// @Inject()
|
||||||
|
// private chromiumlyTenancy: ChromiumlyTenancy;
|
||||||
|
|
||||||
|
// @Inject()
|
||||||
|
// private templateInjectable: TemplateInjectable;
|
||||||
|
|
||||||
|
// @Inject()
|
||||||
|
// private getSaleEstimate: GetSaleEstimate;
|
||||||
|
|
||||||
|
// @Inject()
|
||||||
|
// private estimatePdfTemplate: SaleEstimatePdfTemplate;
|
||||||
|
|
||||||
|
// @Inject()
|
||||||
|
// private eventPublisher: EventPublisher;
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Retrieve sale invoice pdf content.
|
||||||
|
// * @param {number} tenantId -
|
||||||
|
// * @param {ISaleInvoice} saleInvoice -
|
||||||
|
// */
|
||||||
|
// public async getSaleEstimatePdf(
|
||||||
|
// tenantId: number,
|
||||||
|
// saleEstimateId: number
|
||||||
|
// ): Promise<[Buffer, string]> {
|
||||||
|
// const filename = await this.getSaleEstimateFilename(
|
||||||
|
// tenantId,
|
||||||
|
// saleEstimateId
|
||||||
|
// );
|
||||||
|
// const brandingAttributes = await this.getEstimateBrandingAttributes(
|
||||||
|
// tenantId,
|
||||||
|
// saleEstimateId
|
||||||
|
// );
|
||||||
|
// const htmlContent = await this.templateInjectable.render(
|
||||||
|
// tenantId,
|
||||||
|
// 'modules/estimate-regular',
|
||||||
|
// brandingAttributes
|
||||||
|
// );
|
||||||
|
// const content = await this.chromiumlyTenancy.convertHtmlContent(
|
||||||
|
// tenantId,
|
||||||
|
// htmlContent
|
||||||
|
// );
|
||||||
|
// const eventPayload = { tenantId, saleEstimateId };
|
||||||
|
|
||||||
|
// // Triggers the `onSaleEstimatePdfViewed` event.
|
||||||
|
// await this.eventPublisher.emitAsync(
|
||||||
|
// events.saleEstimate.onPdfViewed,
|
||||||
|
// eventPayload
|
||||||
|
// );
|
||||||
|
// return [content, filename];
|
||||||
|
// }
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Retrieves the filename file document of the given estimate.
|
||||||
|
// * @param {number} tenantId
|
||||||
|
// * @param {number} estimateId
|
||||||
|
// * @returns {Promise<string>}
|
||||||
|
// */
|
||||||
|
// private async getSaleEstimateFilename(tenantId: number, estimateId: number) {
|
||||||
|
// const { SaleEstimate } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
|
// const estimate = await SaleEstimate.query().findById(estimateId);
|
||||||
|
|
||||||
|
// return `Estimate-${estimate.estimateNumber}`;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Retrieves the given estimate branding attributes.
|
||||||
|
// * @param {number} tenantId - Tenant id.
|
||||||
|
// * @param {number} estimateId - Estimate id.
|
||||||
|
// * @returns {Promise<EstimatePdfBrandingAttributes>}
|
||||||
|
// */
|
||||||
|
// async getEstimateBrandingAttributes(
|
||||||
|
// tenantId: number,
|
||||||
|
// estimateId: number
|
||||||
|
// ): Promise<EstimatePdfBrandingAttributes> {
|
||||||
|
// const { PdfTemplate } = this.tenancy.models(tenantId);
|
||||||
|
// const saleEstimate = await this.getSaleEstimate.getEstimate(
|
||||||
|
// tenantId,
|
||||||
|
// estimateId
|
||||||
|
// );
|
||||||
|
// // Retrieve the invoice template id of not found get the default template id.
|
||||||
|
// const templateId =
|
||||||
|
// saleEstimate.pdfTemplateId ??
|
||||||
|
// (
|
||||||
|
// await PdfTemplate.query().findOne({
|
||||||
|
// resource: 'SaleEstimate',
|
||||||
|
// default: true,
|
||||||
|
// })
|
||||||
|
// )?.id;
|
||||||
|
// const brandingTemplate =
|
||||||
|
// await this.estimatePdfTemplate.getEstimatePdfTemplate(
|
||||||
|
// tenantId,
|
||||||
|
// templateId
|
||||||
|
// );
|
||||||
|
// return {
|
||||||
|
// ...brandingTemplate.attributes,
|
||||||
|
// ...transformEstimateToPdfTemplate(saleEstimate),
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
// }
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
// import { ERRORS } from '../constants';
|
||||||
|
// import { OnEvent } from '@nestjs/event-emitter';
|
||||||
|
// import { Injectable } from '@nestjs/common';
|
||||||
|
// import { DeliverSaleEstimateService } from '../commands/DeliverSaleEstimate.service';
|
||||||
|
// import { events } from '@/common/events/events';
|
||||||
|
// import { ISaleEstimateMailPresendEvent } from '../types/SaleEstimates.types';
|
||||||
|
// import { ServiceError } from '@/modules/Items/ServiceError';
|
||||||
|
|
||||||
|
// @Injectable()
|
||||||
|
// export class SaleEstimateMarkApprovedOnMailSentSubscriber {
|
||||||
|
// constructor(
|
||||||
|
// private readonly deliverEstimateService: DeliverSaleEstimateService,
|
||||||
|
// ) {}
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Marks the given estimate approved on submitting mail.
|
||||||
|
// * @param {ISaleEstimateMailPresendEvent}
|
||||||
|
// */
|
||||||
|
// @OnEvent(events.saleEstimate.onPreMailSend)
|
||||||
|
// public async markEstimateApproved({
|
||||||
|
// saleEstimateId,
|
||||||
|
// }: ISaleEstimateMailPresendEvent) {
|
||||||
|
// try {
|
||||||
|
// await this.deliverEstimateService.deliverSaleEstimate(saleEstimateId);
|
||||||
|
// } catch (error) {
|
||||||
|
// if (
|
||||||
|
// error instanceof ServiceError &&
|
||||||
|
// error.errorType === ERRORS.SALE_ESTIMATE_ALREADY_DELIVERED
|
||||||
|
// ) {
|
||||||
|
// } else {
|
||||||
|
// throw error;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
import { Knex } from 'knex';
|
||||||
|
// import { CommonMailOptions, CommonMailOptionsDTO } from './Mailable';
|
||||||
|
// import { AttachmentLinkDTO } from './Attachments';
|
||||||
|
import { SaleEstimate } from '../models/SaleEstimate';
|
||||||
|
import { IItemEntryDTO } from '@/modules/TransactionItemEntry/ItemEntry.types';
|
||||||
|
import { AttachmentLinkDTO } from '@/modules/Attachments/Attachments.types';
|
||||||
|
|
||||||
|
export interface ISaleEstimateDTO {
|
||||||
|
customerId: number;
|
||||||
|
exchangeRate?: number;
|
||||||
|
estimateDate?: Date;
|
||||||
|
reference?: string;
|
||||||
|
estimateNumber?: string;
|
||||||
|
entries: IItemEntryDTO[];
|
||||||
|
note: string;
|
||||||
|
termsConditions: string;
|
||||||
|
sendToEmail: string;
|
||||||
|
delivered: boolean;
|
||||||
|
|
||||||
|
branchId?: number;
|
||||||
|
warehouseId?: number;
|
||||||
|
attachments?: AttachmentLinkDTO[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// export interface ISalesEstimatesFilter extends IDynamicListFilterDTO {
|
||||||
|
// stringifiedFilterRoles?: string;
|
||||||
|
// filterQuery?: (q: any) => void;
|
||||||
|
// }
|
||||||
|
|
||||||
|
export interface ISaleEstimateCreatedPayload {
|
||||||
|
// tenantId: number;
|
||||||
|
saleEstimate: SaleEstimate;
|
||||||
|
saleEstimateId: number;
|
||||||
|
saleEstimateDTO: ISaleEstimateDTO;
|
||||||
|
trx?: Knex.Transaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISaleEstimateCreatingPayload {
|
||||||
|
estimateDTO: ISaleEstimateDTO;
|
||||||
|
tenantId: number;
|
||||||
|
trx: Knex.Transaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISaleEstimateEditedPayload {
|
||||||
|
// tenantId: number;
|
||||||
|
estimateId: number;
|
||||||
|
saleEstimate: SaleEstimate;
|
||||||
|
oldSaleEstimate: SaleEstimate;
|
||||||
|
estimateDTO: ISaleEstimateDTO;
|
||||||
|
trx?: Knex.Transaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISaleEstimateEditingPayload {
|
||||||
|
// tenantId: number;
|
||||||
|
oldSaleEstimate: SaleEstimate;
|
||||||
|
estimateDTO: ISaleEstimateDTO;
|
||||||
|
trx: Knex.Transaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISaleEstimateDeletedPayload {
|
||||||
|
// tenantId: number;
|
||||||
|
saleEstimateId: number;
|
||||||
|
oldSaleEstimate: SaleEstimate;
|
||||||
|
trx: Knex.Transaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISaleEstimateDeletingPayload {
|
||||||
|
// tenantId: number;
|
||||||
|
oldSaleEstimate: SaleEstimate;
|
||||||
|
trx: Knex.Transaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISaleEstimateEventDeliveredPayload {
|
||||||
|
// tenantId: number;
|
||||||
|
saleEstimate: SaleEstimate;
|
||||||
|
trx: Knex.Transaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISaleEstimateEventDeliveringPayload {
|
||||||
|
// tenantId: number;
|
||||||
|
oldSaleEstimate: SaleEstimate;
|
||||||
|
trx: Knex.Transaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum SaleEstimateAction {
|
||||||
|
Create = 'Create',
|
||||||
|
Edit = 'Edit',
|
||||||
|
Delete = 'Delete',
|
||||||
|
View = 'View',
|
||||||
|
NotifyBySms = 'NotifyBySms',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISaleEstimateApprovingEvent {
|
||||||
|
// tenantId: number;
|
||||||
|
oldSaleEstimate: SaleEstimate;
|
||||||
|
trx: Knex.Transaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ISaleEstimateApprovedEvent {
|
||||||
|
// tenantId: number;
|
||||||
|
oldSaleEstimate: SaleEstimate;
|
||||||
|
saleEstimate: SaleEstimate;
|
||||||
|
trx: Knex.Transaction;
|
||||||
|
}
|
||||||
|
|
||||||
|
// export interface SaleEstimateMailOptions extends CommonMailOptions {
|
||||||
|
// attachEstimate?: boolean;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// export interface SaleEstimateMailOptionsDTO extends CommonMailOptionsDTO {
|
||||||
|
// attachEstimate?: boolean;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// export interface ISaleEstimateMailPresendEvent {
|
||||||
|
// // tenantId: number;
|
||||||
|
// saleEstimateId: number;
|
||||||
|
// messageOptions: SaleEstimateMailOptionsDTO;
|
||||||
|
// }
|
||||||
|
|
||||||
|
export interface ISaleEstimateState {
|
||||||
|
defaultTemplateId: number;
|
||||||
|
}
|
||||||
34
packages/server-nest/src/modules/SaleEstimates/utils.ts
Normal file
34
packages/server-nest/src/modules/SaleEstimates/utils.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { contactAddressTextFormat } from '@/utils/address-text-format';
|
||||||
|
import { EstimatePdfBrandingAttributes } from './constants';
|
||||||
|
|
||||||
|
export const transformEstimateToPdfTemplate = (
|
||||||
|
estimate
|
||||||
|
): Partial<EstimatePdfBrandingAttributes> => {
|
||||||
|
return {
|
||||||
|
expirationDate: estimate.formattedExpirationDate,
|
||||||
|
estimateNumebr: estimate.estimateNumber,
|
||||||
|
estimateDate: estimate.formattedEstimateDate,
|
||||||
|
lines: estimate.entries.map((entry) => ({
|
||||||
|
item: entry.item.name,
|
||||||
|
description: entry.description,
|
||||||
|
rate: entry.rateFormatted,
|
||||||
|
quantity: entry.quantityFormatted,
|
||||||
|
total: entry.totalFormatted,
|
||||||
|
})),
|
||||||
|
total: estimate.formattedSubtotal,
|
||||||
|
subtotal: estimate.formattedSubtotal,
|
||||||
|
customerNote: estimate.note,
|
||||||
|
termsConditions: estimate.termsConditions,
|
||||||
|
customerAddress: contactAddressTextFormat(estimate.customer),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const transformEstimateToMailDataArgs = (estimate: any) => {
|
||||||
|
return {
|
||||||
|
'Customer Name': estimate.customer.displayName,
|
||||||
|
'Estimate Number': estimate.estimateNumber,
|
||||||
|
'Estimate Date': estimate.formattedEstimateDate,
|
||||||
|
'Estimate Amount': estimate.formattedAmount,
|
||||||
|
'Estimate Expiration Date': estimate.formattedExpirationDate,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -11,7 +11,6 @@ import { TenantMetadata } from '../models/TenantMetadataModel';
|
|||||||
const models = [SystemUser, PlanSubscription, TenantModel, TenantMetadata];
|
const models = [SystemUser, PlanSubscription, TenantModel, TenantMetadata];
|
||||||
|
|
||||||
const modelProviders = models.map((model) => {
|
const modelProviders = models.map((model) => {
|
||||||
console.log(model.name, model, 'model.name');
|
|
||||||
return {
|
return {
|
||||||
provide: model.name,
|
provide: model.name,
|
||||||
useValue: model,
|
useValue: model,
|
||||||
|
|||||||
19
packages/server-nest/src/modules/TaxRates/utils.ts
Normal file
19
packages/server-nest/src/modules/TaxRates/utils.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
/**
|
||||||
|
* Get inclusive tax amount.
|
||||||
|
* @param {number} amount
|
||||||
|
* @param {number} taxRate
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
export const getInclusiveTaxAmount = (amount: number, taxRate: number) => {
|
||||||
|
return (amount * taxRate) / (100 + taxRate);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get exclusive tax amount.
|
||||||
|
* @param {number} amount
|
||||||
|
* @param {number} taxRate
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
export const getExlusiveTaxAmount = (amount: number, taxRate: number) => {
|
||||||
|
return (amount * taxRate) / 100;
|
||||||
|
};
|
||||||
@@ -14,6 +14,9 @@ import { PdfTemplateModel } from '@/modules/PdfTemplate/models/PdfTemplate';
|
|||||||
import { Warehouse } from '@/modules/Warehouses/models/Warehouse.model';
|
import { Warehouse } from '@/modules/Warehouses/models/Warehouse.model';
|
||||||
import { ItemWarehouseQuantity } from '@/modules/Warehouses/models/ItemWarehouseQuantity';
|
import { ItemWarehouseQuantity } from '@/modules/Warehouses/models/ItemWarehouseQuantity';
|
||||||
import { Branch } from '@/modules/Branches/models/Branch.model';
|
import { Branch } from '@/modules/Branches/models/Branch.model';
|
||||||
|
import { SaleEstimate } from '@/modules/SaleEstimates/models/SaleEstimate';
|
||||||
|
import { Customer } from '@/modules/Customers/models/Customer';
|
||||||
|
import { Contact } from '@/modules/Contacts/models/Contact';
|
||||||
|
|
||||||
const models = [
|
const models = [
|
||||||
Item,
|
Item,
|
||||||
@@ -28,6 +31,9 @@ const models = [
|
|||||||
Warehouse,
|
Warehouse,
|
||||||
ItemWarehouseQuantity,
|
ItemWarehouseQuantity,
|
||||||
Branch,
|
Branch,
|
||||||
|
SaleEstimate,
|
||||||
|
Customer,
|
||||||
|
Contact
|
||||||
];
|
];
|
||||||
|
|
||||||
const modelProviders = models.map((model) => {
|
const modelProviders = models.map((model) => {
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { Transformer } from '../Transformer/Transformer';
|
||||||
|
import { ItemEntry } from './models/ItemEntry';
|
||||||
|
|
||||||
|
interface ItemEntryTransformerContext{
|
||||||
|
currencyCode: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ItemEntryTransformer extends Transformer<{}, ItemEntryTransformerContext> {
|
||||||
|
/**
|
||||||
|
* Include these attributes to item entry object.
|
||||||
|
* @returns {Array}
|
||||||
|
*/
|
||||||
|
public includeAttributes = (): string[] => {
|
||||||
|
return ['quantityFormatted', 'rateFormatted', 'totalFormatted'];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the formatted quantitty of item entry.
|
||||||
|
* @param {IItemEntry} entry
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
protected quantityFormatted = (entry: ItemEntry): string => {
|
||||||
|
return this.formatNumber(entry.quantity, { money: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the formatted rate of item entry.
|
||||||
|
* @param {IItemEntry} itemEntry -
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
protected rateFormatted = (entry: ItemEntry): string => {
|
||||||
|
return this.formatNumber(entry.rate, {
|
||||||
|
currencyCode: this.context.currencyCode,
|
||||||
|
money: false,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the formatted total of item entry.
|
||||||
|
* @param {IItemEntry} entry
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
protected totalFormatted = (entry: ItemEntry): string => {
|
||||||
|
return this.formatNumber(entry.total, {
|
||||||
|
currencyCode: this.context.currencyCode,
|
||||||
|
money: false,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
export type IItemEntryTransactionType = 'SaleInvoice' | 'Bill' | 'SaleReceipt';
|
||||||
|
|
||||||
|
export interface IItemEntryDTO {
|
||||||
|
id?: number;
|
||||||
|
index?: number;
|
||||||
|
itemId: number;
|
||||||
|
landedCost?: boolean;
|
||||||
|
warehouseId?: number;
|
||||||
|
|
||||||
|
projectRefId?: number;
|
||||||
|
projectRefType?: ProjectLinkRefType;
|
||||||
|
projectRefInvoicedAmount?: number;
|
||||||
|
|
||||||
|
taxRateId?: number;
|
||||||
|
taxCode?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ProjectLinkRefType {
|
||||||
|
Task = 'TASK',
|
||||||
|
Bill = 'BILL',
|
||||||
|
Expense = 'EXPENSE',
|
||||||
|
}
|
||||||
@@ -0,0 +1,244 @@
|
|||||||
|
import { Model } from 'objection';
|
||||||
|
import { BaseModel } from '@/models/Model';
|
||||||
|
import {
|
||||||
|
getExlusiveTaxAmount,
|
||||||
|
getInclusiveTaxAmount,
|
||||||
|
} from '@/modules/TaxRates/utils';
|
||||||
|
|
||||||
|
export class ItemEntry extends BaseModel {
|
||||||
|
public referenceType: string;
|
||||||
|
public referenceId: string;
|
||||||
|
|
||||||
|
public index: number;
|
||||||
|
public itemId: number;
|
||||||
|
public description: string;
|
||||||
|
|
||||||
|
public sellAccountId: number;
|
||||||
|
public costAccountId: number;
|
||||||
|
|
||||||
|
public landedCost: boolean;
|
||||||
|
public allocatedCostAmount: number;
|
||||||
|
public taxRate: number;
|
||||||
|
public discount: number;
|
||||||
|
public quantity: number;
|
||||||
|
public rate: number;
|
||||||
|
public isInclusiveTax: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Table name.
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
static get tableName() {
|
||||||
|
return 'items_entries';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timestamps columns.
|
||||||
|
* @returns {string[]}
|
||||||
|
*/
|
||||||
|
get timestamps() {
|
||||||
|
return ['created_at', 'updated_at'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Virtual attributes.
|
||||||
|
* @returns {string[]}
|
||||||
|
*/
|
||||||
|
static get virtualAttributes() {
|
||||||
|
return [
|
||||||
|
'amount',
|
||||||
|
'taxAmount',
|
||||||
|
'amountExludingTax',
|
||||||
|
'amountInclusingTax',
|
||||||
|
'total',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Item entry total.
|
||||||
|
* Amount of item entry includes tax and subtracted discount amount.
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
get total() {
|
||||||
|
return this.amountInclusingTax;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Item entry amount.
|
||||||
|
* Amount of item entry that may include or exclude tax.
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
get amount() {
|
||||||
|
return this.quantity * this.rate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Item entry amount including tax.
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
get amountInclusingTax() {
|
||||||
|
return this.isInclusiveTax ? this.amount : this.amount + this.taxAmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Item entry amount excluding tax.
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
get amountExludingTax() {
|
||||||
|
return this.isInclusiveTax ? this.amount - this.taxAmount : this.amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Discount amount.
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
get discountAmount() {
|
||||||
|
return this.amount * (this.discount / 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tag rate fraction.
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
get tagRateFraction() {
|
||||||
|
return this.taxRate / 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tax amount withheld.
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
get taxAmount() {
|
||||||
|
return this.isInclusiveTax
|
||||||
|
? getInclusiveTaxAmount(this.amount, this.taxRate)
|
||||||
|
: getExlusiveTaxAmount(this.amount, this.taxRate);
|
||||||
|
}
|
||||||
|
|
||||||
|
static calcAmount(itemEntry) {
|
||||||
|
const { discount, quantity, rate } = itemEntry;
|
||||||
|
const total = quantity * rate;
|
||||||
|
|
||||||
|
return discount ? total - total * discount * 0.01 : total;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Item entry relations.
|
||||||
|
*/
|
||||||
|
static get relationMappings() {
|
||||||
|
const Item = require('models/Item');
|
||||||
|
const BillLandedCostEntry = require('models/BillLandedCostEntry');
|
||||||
|
const SaleInvoice = require('models/SaleInvoice');
|
||||||
|
const Bill = require('models/Bill');
|
||||||
|
const SaleReceipt = require('models/SaleReceipt');
|
||||||
|
const SaleEstimate = require('models/SaleEstimate');
|
||||||
|
const ProjectTask = require('models/Task');
|
||||||
|
const Expense = require('models/Expense');
|
||||||
|
const TaxRate = require('models/TaxRate');
|
||||||
|
|
||||||
|
return {
|
||||||
|
item: {
|
||||||
|
relation: Model.BelongsToOneRelation,
|
||||||
|
modelClass: Item.default,
|
||||||
|
join: {
|
||||||
|
from: 'items_entries.itemId',
|
||||||
|
to: 'items.id',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
allocatedCostEntries: {
|
||||||
|
relation: Model.HasManyRelation,
|
||||||
|
modelClass: BillLandedCostEntry.default,
|
||||||
|
join: {
|
||||||
|
from: 'items_entries.referenceId',
|
||||||
|
to: 'bill_located_cost_entries.entryId',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
invoice: {
|
||||||
|
relation: Model.BelongsToOneRelation,
|
||||||
|
modelClass: SaleInvoice.default,
|
||||||
|
join: {
|
||||||
|
from: 'items_entries.referenceId',
|
||||||
|
to: 'sales_invoices.id',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
bill: {
|
||||||
|
relation: Model.BelongsToOneRelation,
|
||||||
|
modelClass: Bill.default,
|
||||||
|
join: {
|
||||||
|
from: 'items_entries.referenceId',
|
||||||
|
to: 'bills.id',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
estimate: {
|
||||||
|
relation: Model.BelongsToOneRelation,
|
||||||
|
modelClass: SaleEstimate.default,
|
||||||
|
join: {
|
||||||
|
from: 'items_entries.referenceId',
|
||||||
|
to: 'sales_estimates.id',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sale receipt reference.
|
||||||
|
*/
|
||||||
|
receipt: {
|
||||||
|
relation: Model.BelongsToOneRelation,
|
||||||
|
modelClass: SaleReceipt.default,
|
||||||
|
join: {
|
||||||
|
from: 'items_entries.referenceId',
|
||||||
|
to: 'sales_receipts.id',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Project task reference.
|
||||||
|
*/
|
||||||
|
projectTaskRef: {
|
||||||
|
relation: Model.HasManyRelation,
|
||||||
|
modelClass: ProjectTask.default,
|
||||||
|
join: {
|
||||||
|
from: 'items_entries.projectRefId',
|
||||||
|
to: 'tasks.id',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Project expense reference.
|
||||||
|
*/
|
||||||
|
projectExpenseRef: {
|
||||||
|
relation: Model.HasManyRelation,
|
||||||
|
modelClass: Expense.default,
|
||||||
|
join: {
|
||||||
|
from: 'items_entries.projectRefId',
|
||||||
|
to: 'expenses_transactions.id',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Project bill reference.
|
||||||
|
*/
|
||||||
|
projectBillRef: {
|
||||||
|
relation: Model.HasManyRelation,
|
||||||
|
modelClass: Bill.default,
|
||||||
|
join: {
|
||||||
|
from: 'items_entries.projectRefId',
|
||||||
|
to: 'bills.id',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tax rate reference.
|
||||||
|
*/
|
||||||
|
tax: {
|
||||||
|
relation: Model.HasOneRelation,
|
||||||
|
modelClass: TaxRate.default,
|
||||||
|
join: {
|
||||||
|
from: 'items_entries.taxRateId',
|
||||||
|
to: 'tax_rates.id',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,8 +7,8 @@ import { TransformerContext } from './Transformer.types';
|
|||||||
|
|
||||||
const EXPORT_DTE_FORMAT = 'YYYY-MM-DD';
|
const EXPORT_DTE_FORMAT = 'YYYY-MM-DD';
|
||||||
|
|
||||||
export class Transformer {
|
export class Transformer<T = {}, ExtraContext = {}> {
|
||||||
public context: TransformerContext;
|
public context: ExtraContext & TransformerContext;
|
||||||
public options: Record<string, any>;
|
public options: Record<string, any>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -88,7 +88,7 @@ export class Transformer {
|
|||||||
// sortObjectKeysAlphabetically,
|
// sortObjectKeysAlphabetically,
|
||||||
this.transform,
|
this.transform,
|
||||||
R.when(this.hasExcludeAttributes, this.excludeAttributesTransformed),
|
R.when(this.hasExcludeAttributes, this.excludeAttributesTransformed),
|
||||||
this.includeAttributesTransformed
|
this.includeAttributesTransformed,
|
||||||
)(normlizedItem);
|
)(normlizedItem);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -119,7 +119,7 @@ export class Transformer {
|
|||||||
return attributes
|
return attributes
|
||||||
.filter(
|
.filter(
|
||||||
(attribute) =>
|
(attribute) =>
|
||||||
isFunction(this[attribute]) || !isUndefined(item[attribute])
|
isFunction(this[attribute]) || !isUndefined(item[attribute]),
|
||||||
)
|
)
|
||||||
.reduce((acc, attribute: string) => {
|
.reduce((acc, attribute: string) => {
|
||||||
acc[attribute] = isFunction(this[attribute])
|
acc[attribute] = isFunction(this[attribute])
|
||||||
@@ -216,7 +216,7 @@ export class Transformer {
|
|||||||
public item(
|
public item(
|
||||||
obj: Record<string, any>,
|
obj: Record<string, any>,
|
||||||
transformer: Transformer,
|
transformer: Transformer,
|
||||||
options?: any
|
options?: any,
|
||||||
) {
|
) {
|
||||||
transformer.setOptions(options);
|
transformer.setOptions(options);
|
||||||
transformer.setContext(this.context);
|
transformer.setContext(this.context);
|
||||||
|
|||||||
@@ -1,38 +1,37 @@
|
|||||||
// import { Service, Inject } from 'typedi';
|
import { omit } from 'lodash';
|
||||||
// import { omit } from 'lodash';
|
import { Injectable } from '@nestjs/common';
|
||||||
// import * as R from 'ramda';
|
import { WarehousesSettings } from '../WarehousesSettings';
|
||||||
// import { WarehousesSettings } from '../WarehousesSettings';
|
|
||||||
|
|
||||||
// @Service()
|
@Injectable()
|
||||||
// export class WarehouseTransactionDTOTransform {
|
export class WarehouseTransactionDTOTransform {
|
||||||
// @Inject()
|
constructor(
|
||||||
// private warehousesSettings: WarehousesSettings;
|
private readonly warehousesSettings: WarehousesSettings,
|
||||||
|
) {}
|
||||||
|
|
||||||
// /**
|
/**
|
||||||
// * Excludes DTO warehouse id when mutli-warehouses feature is inactive.
|
* Excludes DTO warehouse id when mutli-warehouses feature is inactive.
|
||||||
// * @param {number} tenantId
|
* @param {number} tenantId
|
||||||
// * @returns {Promise<Omit<T, 'warehouseId'> | T>}
|
* @returns {Promise<Omit<T, 'warehouseId'> | T>}
|
||||||
// */
|
*/
|
||||||
// private excludeDTOWarehouseIdWhenInactive = <
|
private excludeDTOWarehouseIdWhenInactive = <
|
||||||
// T extends { warehouseId?: number }
|
T extends { warehouseId?: number }
|
||||||
// >(
|
>(
|
||||||
// tenantId: number,
|
DTO: T
|
||||||
// DTO: T
|
): Omit<T, 'warehouseId'> | T => {
|
||||||
// ): Omit<T, 'warehouseId'> | T => {
|
const isActive = this.warehousesSettings.isMultiWarehousesActive();
|
||||||
// const isActive = this.warehousesSettings.isMultiWarehousesActive(tenantId);
|
|
||||||
|
|
||||||
// return !isActive ? omit(DTO, ['warehouseId']) : DTO;
|
return !isActive ? omit(DTO, ['warehouseId']) : DTO;
|
||||||
// };
|
};
|
||||||
|
|
||||||
// /**
|
/**
|
||||||
// *
|
*
|
||||||
// * @param {number} tenantId
|
* @param {number} tenantId
|
||||||
// * @param {T} DTO -
|
* @param {T} DTO -
|
||||||
// * @returns {Omit<T, 'warehouseId'> | T}
|
* @returns {Omit<T, 'warehouseId'> | T}
|
||||||
// */
|
*/
|
||||||
// public transformDTO =
|
public transformDTO =
|
||||||
// <T extends { warehouseId?: number }>(tenantId: number) =>
|
<T extends { warehouseId?: number }>(DTO: T): Omit<T, 'warehouseId'> | T => {
|
||||||
// (DTO: T): Omit<T, 'warehouseId'> | T => {
|
return this.excludeDTOWarehouseIdWhenInactive<T>(DTO);
|
||||||
// return this.excludeDTOWarehouseIdWhenInactive<T>(tenantId, DTO);
|
};
|
||||||
// };
|
}
|
||||||
// }
|
|
||||||
113
packages/server-nest/src/utils/address-text-format.ts
Normal file
113
packages/server-nest/src/utils/address-text-format.ts
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import { Contact } from "@/modules/Contacts/models/Contact";
|
||||||
|
|
||||||
|
interface OrganizationAddressFormatArgs {
|
||||||
|
organizationName?: string;
|
||||||
|
address1?: string;
|
||||||
|
address2?: string;
|
||||||
|
state?: string;
|
||||||
|
city?: string;
|
||||||
|
country?: string;
|
||||||
|
postalCode?: string;
|
||||||
|
phone?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultOrganizationAddressFormat = `
|
||||||
|
<strong>{ORGANIZATION_NAME}</strong>
|
||||||
|
{ADDRESS_1}
|
||||||
|
{ADDRESS_2}
|
||||||
|
{CITY} {STATE} {POSTAL_CODE}
|
||||||
|
{COUNTRY}
|
||||||
|
{PHONE}
|
||||||
|
`;
|
||||||
|
/**
|
||||||
|
* Formats the address text based on the provided message and arguments.
|
||||||
|
* This function replaces placeholders in the message with actual values
|
||||||
|
* from the OrganizationAddressFormatArgs. It ensures that the final
|
||||||
|
* formatted message is clean and free of excessive newlines.
|
||||||
|
*
|
||||||
|
* @param {string} message - The message template containing placeholders.
|
||||||
|
* @param {Record<string, string>} args - The arguments containing the values to replace in the message.
|
||||||
|
* @returns {string} - The formatted address text.
|
||||||
|
*/
|
||||||
|
const formatText = (message: string, replacements: Record<string, string>) => {
|
||||||
|
let formattedMessage = Object.entries(replacements).reduce(
|
||||||
|
(msg, [key, value]) => {
|
||||||
|
return msg.split(`{${key}}`).join(value || '');
|
||||||
|
},
|
||||||
|
message
|
||||||
|
);
|
||||||
|
// Removes any empty lines.
|
||||||
|
formattedMessage = formattedMessage.replace(/^\s*[\r\n]/gm, '');
|
||||||
|
formattedMessage = formattedMessage.replace(/\n{2,}/g, '\n');
|
||||||
|
formattedMessage = formattedMessage.replace(/\n/g, '<br />');
|
||||||
|
formattedMessage = formattedMessage.trim();
|
||||||
|
|
||||||
|
return formattedMessage;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const organizationAddressTextFormat = (
|
||||||
|
message: string,
|
||||||
|
args: OrganizationAddressFormatArgs
|
||||||
|
) => {
|
||||||
|
const replacements: Record<string, string> = {
|
||||||
|
ORGANIZATION_NAME: args.organizationName || '',
|
||||||
|
ADDRESS_1: args.address1 || '',
|
||||||
|
ADDRESS_2: args.address2 || '',
|
||||||
|
CITY: args.city || '',
|
||||||
|
STATE: args.state || '',
|
||||||
|
POSTAL_CODE: args.postalCode || '',
|
||||||
|
COUNTRY: args.country || '',
|
||||||
|
PHONE: args.phone || '',
|
||||||
|
};
|
||||||
|
return formatText(message, replacements);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ContactAddressTextFormatArgs {
|
||||||
|
displayName?: string;
|
||||||
|
state?: string;
|
||||||
|
postalCode?: string;
|
||||||
|
email?: string;
|
||||||
|
country?: string;
|
||||||
|
city?: string;
|
||||||
|
address2?: string;
|
||||||
|
address1?: string;
|
||||||
|
phone?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultContactAddressFormat = `{CONTACT_NAME}
|
||||||
|
{ADDRESS_1}
|
||||||
|
{ADDRESS_2}
|
||||||
|
{CITY} {STATE} {POSTAL_CODE}
|
||||||
|
{COUNTRY}
|
||||||
|
{PHONE}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const contactAddressTextFormat = (
|
||||||
|
contact: Contact,
|
||||||
|
message: string = defaultContactAddressFormat
|
||||||
|
) => {
|
||||||
|
const args = {
|
||||||
|
displayName: contact.displayName,
|
||||||
|
address1: contact.billingAddress1,
|
||||||
|
address2: contact.billingAddress2,
|
||||||
|
state: contact.billingAddressState,
|
||||||
|
country: contact.billingAddressCountry,
|
||||||
|
postalCode: contact?.billingAddressPostcode,
|
||||||
|
city: contact?.billingAddressCity,
|
||||||
|
email: contact?.email,
|
||||||
|
phone: contact?.billingAddressPhone,
|
||||||
|
} as ContactAddressTextFormatArgs;
|
||||||
|
|
||||||
|
const replacements: Record<string, string> = {
|
||||||
|
CONTACT_NAME: args.displayName || '',
|
||||||
|
ADDRESS_1: args.address1 || '',
|
||||||
|
ADDRESS_2: args.address2 || '',
|
||||||
|
CITY: args.city || '',
|
||||||
|
STATE: args.state || '',
|
||||||
|
POSTAL_CODE: args.postalCode || '',
|
||||||
|
COUNTRY: args.country || '',
|
||||||
|
EMAIL: args?.email || '',
|
||||||
|
PHONE: args?.phone || '',
|
||||||
|
};
|
||||||
|
return formatText(message, replacements);
|
||||||
|
};
|
||||||
23
packages/server-nest/src/utils/format-date-fields.ts
Normal file
23
packages/server-nest/src/utils/format-date-fields.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import * as moment from 'moment';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats the given date fields.
|
||||||
|
* @param {any} inputDTO - Input data.
|
||||||
|
* @param {Array<string>} fields - Fields to format.
|
||||||
|
* @param {string} format - Format string.
|
||||||
|
* @returns {any}
|
||||||
|
*/
|
||||||
|
export const formatDateFields = (
|
||||||
|
inputDTO: any,
|
||||||
|
fields: Array<string>,
|
||||||
|
format = 'YYYY-MM-DD',
|
||||||
|
) => {
|
||||||
|
const _inputDTO = { ...inputDTO };
|
||||||
|
|
||||||
|
fields.forEach((field) => {
|
||||||
|
if (_inputDTO[field]) {
|
||||||
|
_inputDTO[field] = moment(_inputDTO[field]).format(format);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return _inputDTO;
|
||||||
|
};
|
||||||
61
packages/server-nest/test/sale-estimates.e2e-spec.ts
Normal file
61
packages/server-nest/test/sale-estimates.e2e-spec.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import * as request from 'supertest';
|
||||||
|
import { faker } from '@faker-js/faker';
|
||||||
|
import { app } from './init-app-test';
|
||||||
|
|
||||||
|
describe('Sale Estimates (e2e)', () => {
|
||||||
|
it('/sales/estimates (POST)', async () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.post('/sales/estimates')
|
||||||
|
.set('organization-id', '4064541lv40nhca')
|
||||||
|
.send({
|
||||||
|
customerId: 2,
|
||||||
|
estimateDate: '2022-02-02',
|
||||||
|
expirationDate: '2020-03-02',
|
||||||
|
delivered: false,
|
||||||
|
estimateNumber: faker.string.uuid(),
|
||||||
|
discount: 100,
|
||||||
|
discountType: 'amount',
|
||||||
|
entries: [
|
||||||
|
{
|
||||||
|
index: 1,
|
||||||
|
itemId: 1001,
|
||||||
|
quantity: 3,
|
||||||
|
rate: 1000,
|
||||||
|
description: "It's description here.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
.expect(201);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('/sales/estimates (DELETE)', async () => {
|
||||||
|
const response = await request(app.getHttpServer())
|
||||||
|
.post('/sales/estimates')
|
||||||
|
.set('organization-id', '4064541lv40nhca')
|
||||||
|
.send({
|
||||||
|
customerId: 2,
|
||||||
|
estimateDate: '2022-02-02',
|
||||||
|
expirationDate: '2020-03-02',
|
||||||
|
delivered: false,
|
||||||
|
estimateNumber: faker.string.uuid(),
|
||||||
|
discount: 100,
|
||||||
|
discountType: 'amount',
|
||||||
|
entries: [
|
||||||
|
{
|
||||||
|
index: 1,
|
||||||
|
itemId: 1001,
|
||||||
|
quantity: 3,
|
||||||
|
rate: 1000,
|
||||||
|
description: "It's description here.",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const estimateId = response.body.id;
|
||||||
|
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.delete(`/sales/estimates/${estimateId}`)
|
||||||
|
.set('organization-id', '4064541lv40nhca')
|
||||||
|
.expect(200);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
|
import { Injectable, Inject } from '@nestjs/common';
|
||||||
|
import { Knex } from 'knex';
|
||||||
import { sumBy, difference, map } from 'lodash';
|
import { sumBy, difference, map } from 'lodash';
|
||||||
import { Inject, Service } from 'typedi';
|
|
||||||
import { IItemEntry, IItemEntryDTO, IItem } from '@/interfaces';
|
import { IItemEntry, IItemEntryDTO, IItem } from '@/interfaces';
|
||||||
import { ServiceError } from '@/exceptions';
|
import { ServiceError } from '@/exceptions';
|
||||||
import TenancyService from '@/services/Tenancy/TenancyService';
|
import { Item, ItemEntry } from '@/models';
|
||||||
import { ItemEntry } from '@/models';
|
|
||||||
import { entriesAmountDiff } from 'utils';
|
import { entriesAmountDiff } from 'utils';
|
||||||
import { Knex } from 'knex';
|
|
||||||
|
|
||||||
const ERRORS = {
|
const ERRORS = {
|
||||||
ITEMS_NOT_FOUND: 'ITEMS_NOT_FOUND',
|
ITEMS_NOT_FOUND: 'ITEMS_NOT_FOUND',
|
||||||
@@ -14,38 +13,36 @@ const ERRORS = {
|
|||||||
NOT_SELL_ABLE_ITEMS: 'NOT_SELL_ABLE_ITEMS',
|
NOT_SELL_ABLE_ITEMS: 'NOT_SELL_ABLE_ITEMS',
|
||||||
};
|
};
|
||||||
|
|
||||||
@Service()
|
@Injectable()
|
||||||
export default class ItemsEntriesService {
|
export default class ItemsEntriesService {
|
||||||
@Inject()
|
constructor(
|
||||||
private tenancy: TenancyService;
|
@Inject(Item.name)
|
||||||
|
private readonly itemModel: typeof Item,
|
||||||
|
@Inject(ItemEntry.name)
|
||||||
|
private readonly itemEntryModel: typeof ItemEntry,
|
||||||
|
private readonly itemRepository: any, // Replace 'any' with proper repository type
|
||||||
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve the inventory items entries of the reference id and type.
|
* Retrieve the inventory items entries of the reference id and type.
|
||||||
* @param {number} tenantId
|
|
||||||
* @param {string} referenceType
|
* @param {string} referenceType
|
||||||
* @param {string} referenceId
|
* @param {string} referenceId
|
||||||
* @return {Promise<IItemEntry[]>}
|
* @return {Promise<IItemEntry[]>}
|
||||||
*/
|
*/
|
||||||
public async getInventoryEntries(
|
public async getInventoryEntries(
|
||||||
tenantId: number,
|
|
||||||
referenceType: string,
|
referenceType: string,
|
||||||
referenceId: number
|
referenceId: number
|
||||||
): Promise<IItemEntry[]> {
|
): Promise<IItemEntry[]> {
|
||||||
const { Item, ItemEntry } = this.tenancy.models(tenantId);
|
const itemsEntries = await this.itemEntryModel.query()
|
||||||
|
|
||||||
const itemsEntries = await ItemEntry.query()
|
|
||||||
.where('reference_type', referenceType)
|
.where('reference_type', referenceType)
|
||||||
.where('reference_id', referenceId);
|
.where('reference_id', referenceId);
|
||||||
|
|
||||||
// Inventory items.
|
const inventoryItems = await this.itemModel.query()
|
||||||
const inventoryItems = await Item.query()
|
|
||||||
.whereIn('id', map(itemsEntries, 'itemId'))
|
.whereIn('id', map(itemsEntries, 'itemId'))
|
||||||
.where('type', 'inventory');
|
.where('type', 'inventory');
|
||||||
|
|
||||||
// Inventory items ids.
|
|
||||||
const inventoryItemsIds = map(inventoryItems, 'id');
|
const inventoryItemsIds = map(inventoryItems, 'id');
|
||||||
|
|
||||||
// Filtering the inventory items entries.
|
|
||||||
const inventoryItemsEntries = itemsEntries.filter(
|
const inventoryItemsEntries = itemsEntries.filter(
|
||||||
(itemEntry) => inventoryItemsIds.indexOf(itemEntry.itemId) !== -1
|
(itemEntry) => inventoryItemsIds.indexOf(itemEntry.itemId) !== -1
|
||||||
);
|
);
|
||||||
@@ -58,15 +55,12 @@ export default class ItemsEntriesService {
|
|||||||
* @returns {IItemEntry[]}
|
* @returns {IItemEntry[]}
|
||||||
*/
|
*/
|
||||||
public async filterInventoryEntries(
|
public async filterInventoryEntries(
|
||||||
tenantId: number,
|
|
||||||
entries: IItemEntry[],
|
entries: IItemEntry[],
|
||||||
trx?: Knex.Transaction
|
trx?: Knex.Transaction
|
||||||
): Promise<IItemEntry[]> {
|
): Promise<IItemEntry[]> {
|
||||||
const { Item } = this.tenancy.models(tenantId);
|
|
||||||
const entriesItemsIds = entries.map((e) => e.itemId);
|
const entriesItemsIds = entries.map((e) => e.itemId);
|
||||||
|
|
||||||
// Retrieve entries inventory items.
|
const inventoryItems = await this.itemModel.query(trx)
|
||||||
const inventoryItems = await Item.query(trx)
|
|
||||||
.whereIn('id', entriesItemsIds)
|
.whereIn('id', entriesItemsIds)
|
||||||
.where('type', 'inventory');
|
.where('type', 'inventory');
|
||||||
|
|
||||||
@@ -79,17 +73,14 @@ export default class ItemsEntriesService {
|
|||||||
/**
|
/**
|
||||||
* Validates the entries items ids.
|
* Validates the entries items ids.
|
||||||
* @async
|
* @async
|
||||||
* @param {number} tenantId -
|
* @param {IItemEntryDTO[]} itemEntries -
|
||||||
* @param {IItemEntryDTO} itemEntries -
|
|
||||||
*/
|
*/
|
||||||
public async validateItemsIdsExistance(
|
public async validateItemsIdsExistance(
|
||||||
tenantId: number,
|
|
||||||
itemEntries: IItemEntryDTO[]
|
itemEntries: IItemEntryDTO[]
|
||||||
) {
|
) {
|
||||||
const { Item } = this.tenancy.models(tenantId);
|
|
||||||
const itemsIds = itemEntries.map((e) => e.itemId);
|
const itemsIds = itemEntries.map((e) => e.itemId);
|
||||||
|
|
||||||
const foundItems = await Item.query().whereIn('id', itemsIds);
|
const foundItems = await this.itemModel.query().whereIn('id', itemsIds);
|
||||||
|
|
||||||
const foundItemsIds = foundItems.map((item: IItem) => item.id);
|
const foundItemsIds = foundItems.map((item: IItem) => item.id);
|
||||||
const notFoundItemsIds = difference(itemsIds, foundItemsIds);
|
const notFoundItemsIds = difference(itemsIds, foundItemsIds);
|
||||||
@@ -102,22 +93,19 @@ export default class ItemsEntriesService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates the entries ids existance on the storage.
|
* Validates the entries ids existance on the storage.
|
||||||
* @param {number} tenantId -
|
|
||||||
* @param {number} billId -
|
* @param {number} billId -
|
||||||
* @param {IItemEntry[]} billEntries -
|
* @param {IItemEntry[]} billEntries -
|
||||||
*/
|
*/
|
||||||
public async validateEntriesIdsExistance(
|
public async validateEntriesIdsExistance(
|
||||||
tenantId: number,
|
|
||||||
referenceId: number,
|
referenceId: number,
|
||||||
referenceType: string,
|
referenceType: string,
|
||||||
billEntries: IItemEntryDTO[]
|
billEntries: IItemEntryDTO[]
|
||||||
) {
|
) {
|
||||||
const { ItemEntry } = this.tenancy.models(tenantId);
|
|
||||||
const entriesIds = billEntries
|
const entriesIds = billEntries
|
||||||
.filter((e: IItemEntry) => e.id)
|
.filter((e: IItemEntry) => e.id)
|
||||||
.map((e: IItemEntry) => e.id);
|
.map((e: IItemEntry) => e.id);
|
||||||
|
|
||||||
const storedEntries = await ItemEntry.query()
|
const storedEntries = await this.itemEntryModel.query()
|
||||||
.whereIn('reference_id', [referenceId])
|
.whereIn('reference_id', [referenceId])
|
||||||
.whereIn('reference_type', [referenceType]);
|
.whereIn('reference_type', [referenceType]);
|
||||||
|
|
||||||
@@ -133,13 +121,11 @@ export default class ItemsEntriesService {
|
|||||||
* Validate the entries items that not purchase-able.
|
* Validate the entries items that not purchase-able.
|
||||||
*/
|
*/
|
||||||
public async validateNonPurchasableEntriesItems(
|
public async validateNonPurchasableEntriesItems(
|
||||||
tenantId: number,
|
|
||||||
itemEntries: IItemEntryDTO[]
|
itemEntries: IItemEntryDTO[]
|
||||||
) {
|
) {
|
||||||
const { Item } = this.tenancy.models(tenantId);
|
|
||||||
const itemsIds = itemEntries.map((e: IItemEntryDTO) => e.itemId);
|
const itemsIds = itemEntries.map((e: IItemEntryDTO) => e.itemId);
|
||||||
|
|
||||||
const purchasbleItems = await Item.query()
|
const purchasbleItems = await this.itemModel.query()
|
||||||
.where('purchasable', true)
|
.where('purchasable', true)
|
||||||
.whereIn('id', itemsIds);
|
.whereIn('id', itemsIds);
|
||||||
|
|
||||||
@@ -155,13 +141,11 @@ export default class ItemsEntriesService {
|
|||||||
* Validate the entries items that not sell-able.
|
* Validate the entries items that not sell-able.
|
||||||
*/
|
*/
|
||||||
public async validateNonSellableEntriesItems(
|
public async validateNonSellableEntriesItems(
|
||||||
tenantId: number,
|
|
||||||
itemEntries: IItemEntryDTO[]
|
itemEntries: IItemEntryDTO[]
|
||||||
) {
|
) {
|
||||||
const { Item } = this.tenancy.models(tenantId);
|
|
||||||
const itemsIds = itemEntries.map((e: IItemEntryDTO) => e.itemId);
|
const itemsIds = itemEntries.map((e: IItemEntryDTO) => e.itemId);
|
||||||
|
|
||||||
const sellableItems = await Item.query()
|
const sellableItems = await this.itemModel.query()
|
||||||
.where('sellable', true)
|
.where('sellable', true)
|
||||||
.whereIn('id', itemsIds);
|
.whereIn('id', itemsIds);
|
||||||
|
|
||||||
@@ -175,16 +159,13 @@ export default class ItemsEntriesService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Changes items quantity from the given items entries the new and old onces.
|
* Changes items quantity from the given items entries the new and old onces.
|
||||||
* @param {number} tenantId
|
* @param {IItemEntry[]} entries - Items entries.
|
||||||
* @param {IItemEntry} entries - Items entries.
|
* @param {IItemEntry[]} oldEntries - Old items entries.
|
||||||
* @param {IItemEntry} oldEntries - Old items entries.
|
|
||||||
*/
|
*/
|
||||||
public async changeItemsQuantity(
|
public async changeItemsQuantity(
|
||||||
tenantId: number,
|
|
||||||
entries: IItemEntry[],
|
entries: IItemEntry[],
|
||||||
oldEntries?: IItemEntry[]
|
oldEntries?: IItemEntry[]
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const { itemRepository } = this.tenancy.repositories(tenantId);
|
|
||||||
const opers = [];
|
const opers = [];
|
||||||
|
|
||||||
const diffEntries = entriesAmountDiff(
|
const diffEntries = entriesAmountDiff(
|
||||||
@@ -194,7 +175,7 @@ export default class ItemsEntriesService {
|
|||||||
'itemId'
|
'itemId'
|
||||||
);
|
);
|
||||||
diffEntries.forEach((entry: IItemEntry) => {
|
diffEntries.forEach((entry: IItemEntry) => {
|
||||||
const changeQuantityOper = itemRepository.changeNumber(
|
const changeQuantityOper = this.itemRepository.changeNumber(
|
||||||
{ id: entry.itemId, type: 'inventory' },
|
{ id: entry.itemId, type: 'inventory' },
|
||||||
'quantityOnHand',
|
'quantityOnHand',
|
||||||
entry.quantity
|
entry.quantity
|
||||||
@@ -206,27 +187,22 @@ export default class ItemsEntriesService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Increment items quantity from the given items entries.
|
* Increment items quantity from the given items entries.
|
||||||
* @param {number} tenantId - Tenant id.
|
* @param {IItemEntry[]} entries - Items entries.
|
||||||
* @param {IItemEntry} entries - Items entries.
|
|
||||||
*/
|
*/
|
||||||
public async incrementItemsEntries(
|
public async incrementItemsEntries(
|
||||||
tenantId: number,
|
|
||||||
entries: IItemEntry[]
|
entries: IItemEntry[]
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
return this.changeItemsQuantity(tenantId, entries);
|
return this.changeItemsQuantity(entries);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decrement items quantity from the given items entries.
|
* Decrement items quantity from the given items entries.
|
||||||
* @param {number} tenantId - Tenant id.
|
* @param {IItemEntry[]} entries - Items entries.
|
||||||
* @param {IItemEntry} entries - Items entries.
|
|
||||||
*/
|
*/
|
||||||
public async decrementItemsQuantity(
|
public async decrementItemsQuantity(
|
||||||
tenantId: number,
|
|
||||||
entries: IItemEntry[]
|
entries: IItemEntry[]
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
return this.changeItemsQuantity(
|
return this.changeItemsQuantity(
|
||||||
tenantId,
|
|
||||||
entries.map((entry) => ({
|
entries.map((entry) => ({
|
||||||
...entry,
|
...entry,
|
||||||
quantity: entry.quantity * -1,
|
quantity: entry.quantity * -1,
|
||||||
@@ -237,12 +213,10 @@ export default class ItemsEntriesService {
|
|||||||
/**
|
/**
|
||||||
* Sets the cost/sell accounts to the invoice entries.
|
* Sets the cost/sell accounts to the invoice entries.
|
||||||
*/
|
*/
|
||||||
public setItemsEntriesDefaultAccounts(tenantId: number) {
|
public setItemsEntriesDefaultAccounts() {
|
||||||
return async (entries: IItemEntry[]) => {
|
return async (entries: IItemEntry[]) => {
|
||||||
const { Item } = this.tenancy.models(tenantId);
|
|
||||||
|
|
||||||
const entriesItemsIds = entries.map((e) => e.itemId);
|
const entriesItemsIds = entries.map((e) => e.itemId);
|
||||||
const items = await Item.query().whereIn('id', entriesItemsIds);
|
const items = await this.itemModel.query().whereIn('id', entriesItemsIds);
|
||||||
|
|
||||||
return entries.map((entry) => {
|
return entries.map((entry) => {
|
||||||
const item = items.find((i) => i.id === entry.itemId);
|
const item = items.find((i) => i.id === entry.itemId);
|
||||||
|
|||||||
14
src/errors/service-error.ts
Normal file
14
src/errors/service-error.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { HttpStatus } from '@nestjs/common';
|
||||||
|
|
||||||
|
export class ServiceError extends Error {
|
||||||
|
constructor(
|
||||||
|
public message: string,
|
||||||
|
private status: HttpStatus = HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
getStatus(): HttpStatus {
|
||||||
|
return this.status;
|
||||||
|
}
|
||||||
|
}
|
||||||
19
src/filters/service-error.filter.ts
Normal file
19
src/filters/service-error.filter.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { ExceptionFilter, Catch, ArgumentsHost, HttpStatus } from '@nestjs/common';
|
||||||
|
import { Response } from 'express';
|
||||||
|
import { ServiceError } from '../errors/service-error';
|
||||||
|
|
||||||
|
@Catch(ServiceError)
|
||||||
|
export class ServiceErrorFilter implements ExceptionFilter {
|
||||||
|
catch(exception: ServiceError, host: ArgumentsHost) {
|
||||||
|
const ctx = host.switchToHttp();
|
||||||
|
const response = ctx.getResponse<Response>();
|
||||||
|
const status = exception.getStatus();
|
||||||
|
|
||||||
|
response
|
||||||
|
.status(status)
|
||||||
|
.json({
|
||||||
|
statusCode: status,
|
||||||
|
message: exception.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
13
src/main.ts
Normal file
13
src/main.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { NestFactory } from '@nestjs/core';
|
||||||
|
import { AppModule } from './app.module';
|
||||||
|
import { ServiceErrorFilter } from './filters/service-error.filter';
|
||||||
|
|
||||||
|
async function bootstrap() {
|
||||||
|
const app = await NestFactory.create(AppModule);
|
||||||
|
|
||||||
|
// Register the ServiceErrorFilter globally
|
||||||
|
app.useGlobalFilters(new ServiceErrorFilter());
|
||||||
|
|
||||||
|
await app.listen(3000);
|
||||||
|
}
|
||||||
|
bootstrap();
|
||||||
Reference in New Issue
Block a user