mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-15 04:10:32 +00:00
feat(nestjs): migrate to NestJS
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { CreateCreditNoteService } from './commands/CreateCreditNote.service';
|
||||
import { DeleteCreditNoteService } from './commands/DeleteCreditNote.service';
|
||||
import { EditCreditNoteService } from './commands/EditCreditNote.service';
|
||||
import { OpenCreditNoteService } from './commands/OpenCreditNote.service';
|
||||
import { GetCreditNotePdf } from './queries/GetCreditNotePdf.serivce';
|
||||
import { ICreditNotesQueryDTO } from './types/CreditNotes.types';
|
||||
import { GetCreditNotesService } from './queries/GetCreditNotes.service';
|
||||
import { CreateCreditNoteDto, EditCreditNoteDto } from './dtos/CreditNote.dto';
|
||||
|
||||
@Injectable()
|
||||
export class CreditNoteApplication {
|
||||
constructor(
|
||||
private readonly createCreditNoteService: CreateCreditNoteService,
|
||||
private readonly editCreditNoteService: EditCreditNoteService,
|
||||
private readonly openCreditNoteService: OpenCreditNoteService,
|
||||
private readonly deleteCreditNoteService: DeleteCreditNoteService,
|
||||
private readonly getCreditNotePdfService: GetCreditNotePdf,
|
||||
private readonly getCreditNotesService: GetCreditNotesService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Creates a new credit note.
|
||||
* @param {CreateCreditNoteDto} creditNoteDTO
|
||||
* @returns {Promise<CreditNote>}
|
||||
*/
|
||||
createCreditNote(creditNoteDTO: CreateCreditNoteDto) {
|
||||
return this.createCreditNoteService.creditCreditNote(creditNoteDTO);
|
||||
}
|
||||
|
||||
/**
|
||||
* Edits a credit note.
|
||||
* @param {number} creditNoteId
|
||||
* @param {EditCreditNoteDto} creditNoteDTO
|
||||
* @returns {Promise<CreditNote>}
|
||||
*/
|
||||
editCreditNote(creditNoteId: number, creditNoteDTO: EditCreditNoteDto) {
|
||||
return this.editCreditNoteService.editCreditNote(
|
||||
creditNoteId,
|
||||
creditNoteDTO,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a credit note.
|
||||
* @param {number} creditNoteId
|
||||
* @returns {Promise<CreditNote>}
|
||||
*/
|
||||
openCreditNote(creditNoteId: number) {
|
||||
return this.openCreditNoteService.openCreditNote(creditNoteId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a credit note.
|
||||
* @param {number} creditNoteId
|
||||
* @returns {Promise<CreditNote>}
|
||||
*/
|
||||
deleteCreditNote(creditNoteId: number) {
|
||||
return this.deleteCreditNoteService.deleteCreditNote(creditNoteId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the PDF for a credit note.
|
||||
* @param {number} creditNoteId
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
getCreditNotePdf(creditNoteId: number) {
|
||||
return this.getCreditNotePdfService.getCreditNotePdf(creditNoteId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the credit notes list.
|
||||
* @param {ICreditNotesQueryDTO} creditNotesQuery
|
||||
* @returns {Promise<GetCreditNotesResponse>}
|
||||
*/
|
||||
getCreditNotes(creditNotesQuery: ICreditNotesQueryDTO) {
|
||||
return this.getCreditNotesService.getCreditNotesList(creditNotesQuery);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Param,
|
||||
Post,
|
||||
Put,
|
||||
Query,
|
||||
} from '@nestjs/common';
|
||||
import { CreditNoteApplication } from './CreditNoteApplication.service';
|
||||
import { ICreditNotesQueryDTO } from './types/CreditNotes.types';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { CreateCreditNoteDto, EditCreditNoteDto } from './dtos/CreditNote.dto';
|
||||
|
||||
@Controller('credit-notes')
|
||||
@ApiTags('credit-notes')
|
||||
export class CreditNotesController {
|
||||
/**
|
||||
* @param {CreditNoteApplication} creditNoteApplication - The credit note application service.
|
||||
*/
|
||||
constructor(private creditNoteApplication: CreditNoteApplication) {}
|
||||
|
||||
@Post()
|
||||
createCreditNote(@Body() creditNoteDTO: CreateCreditNoteDto) {
|
||||
return this.creditNoteApplication.createCreditNote(creditNoteDTO);
|
||||
}
|
||||
|
||||
@Get()
|
||||
getCreditNotes(@Query() creditNotesQuery: ICreditNotesQueryDTO) {
|
||||
return this.creditNoteApplication.getCreditNotes(creditNotesQuery);
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
editCreditNote(
|
||||
@Param('id') creditNoteId: number,
|
||||
@Body() creditNoteDTO: EditCreditNoteDto,
|
||||
) {
|
||||
return this.creditNoteApplication.editCreditNote(
|
||||
creditNoteId,
|
||||
creditNoteDTO,
|
||||
);
|
||||
}
|
||||
|
||||
@Put(':id/open')
|
||||
openCreditNote(@Param('id') creditNoteId: number) {
|
||||
return this.creditNoteApplication.openCreditNote(creditNoteId);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
deleteCreditNote(@Param('id') creditNoteId: number) {
|
||||
return this.creditNoteApplication.deleteCreditNote(creditNoteId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { CreateCreditNoteService } from './commands/CreateCreditNote.service';
|
||||
import { CommandCreditNoteDTOTransform } from './commands/CommandCreditNoteDTOTransform.service';
|
||||
import { EditCreditNoteService } from './commands/EditCreditNote.service';
|
||||
import { OpenCreditNoteService } from './commands/OpenCreditNote.service';
|
||||
import { DeleteCreditNoteService } from './commands/DeleteCreditNote.service';
|
||||
import { CreditNoteAutoIncrementService } from './commands/CreditNoteAutoIncrement.service';
|
||||
import { CreditNoteApplication } from './CreditNoteApplication.service';
|
||||
import { CreditNotesController } from './CreditNotes.controller';
|
||||
import { GetCreditNoteState } from './queries/GetCreditNoteState.service';
|
||||
import { GetCreditNotePdf } from './queries/GetCreditNotePdf.serivce';
|
||||
import { ItemsModule } from '../Items/items.module';
|
||||
import { BranchesModule } from '../Branches/Branches.module';
|
||||
import { WarehousesModule } from '../Warehouses/Warehouses.module';
|
||||
import { PdfTemplatesModule } from '../PdfTemplate/PdfTemplates.module';
|
||||
import { ChromiumlyTenancyModule } from '../ChromiumlyTenancy/ChromiumlyTenancy.module';
|
||||
import { TemplateInjectableModule } from '../TemplateInjectable/TemplateInjectable.module';
|
||||
import { GetCreditNote } from './queries/GetCreditNote.service';
|
||||
import { CreditNoteBrandingTemplate } from './queries/CreditNoteBrandingTemplate.service';
|
||||
import { AutoIncrementOrdersModule } from '../AutoIncrementOrders/AutoIncrementOrders.module';
|
||||
import { CreditNoteGLEntries } from './commands/CreditNoteGLEntries';
|
||||
import { CreditNoteGLEntriesSubscriber } from './subscribers/CreditNoteGLEntriesSubscriber';
|
||||
import { LedgerModule } from '../Ledger/Ledger.module';
|
||||
import { AccountsModule } from '../Accounts/Accounts.module';
|
||||
import { GetCreditNotesService } from './queries/GetCreditNotes.service';
|
||||
import { DynamicListModule } from '../DynamicListing/DynamicList.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ItemsModule,
|
||||
BranchesModule,
|
||||
WarehousesModule,
|
||||
PdfTemplatesModule,
|
||||
ChromiumlyTenancyModule,
|
||||
TemplateInjectableModule,
|
||||
AutoIncrementOrdersModule,
|
||||
LedgerModule,
|
||||
AccountsModule,
|
||||
DynamicListModule
|
||||
],
|
||||
providers: [
|
||||
CreateCreditNoteService,
|
||||
GetCreditNote,
|
||||
CommandCreditNoteDTOTransform,
|
||||
EditCreditNoteService,
|
||||
OpenCreditNoteService,
|
||||
DeleteCreditNoteService,
|
||||
GetCreditNotePdf,
|
||||
GetCreditNotesService,
|
||||
CreditNoteAutoIncrementService,
|
||||
GetCreditNoteState,
|
||||
CreditNoteApplication,
|
||||
CreditNoteBrandingTemplate,
|
||||
CreditNoteGLEntries,
|
||||
CreditNoteGLEntriesSubscriber,
|
||||
],
|
||||
exports: [
|
||||
CreateCreditNoteService,
|
||||
GetCreditNote,
|
||||
CommandCreditNoteDTOTransform,
|
||||
EditCreditNoteService,
|
||||
OpenCreditNoteService,
|
||||
DeleteCreditNoteService,
|
||||
GetCreditNotePdf,
|
||||
CreditNoteAutoIncrementService,
|
||||
GetCreditNoteState,
|
||||
CreditNoteApplication,
|
||||
CreditNoteBrandingTemplate,
|
||||
],
|
||||
controllers: [CreditNotesController],
|
||||
})
|
||||
export class CreditNotesModule {}
|
||||
@@ -0,0 +1,112 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { omit } from 'lodash';
|
||||
import * as moment from 'moment';
|
||||
import * as composeAsync from 'async/compose';
|
||||
import * as R from 'ramda';
|
||||
import { ERRORS } from '../constants';
|
||||
import { ServiceError } from '@/modules/Items/ServiceError';
|
||||
import { ItemsEntriesService } from '@/modules/Items/ItemsEntries.service';
|
||||
import { BranchTransactionDTOTransformer } from '@/modules/Branches/integrations/BranchTransactionDTOTransform';
|
||||
import { WarehouseTransactionDTOTransform } from '@/modules/Warehouses/Integrations/WarehouseTransactionDTOTransform';
|
||||
import { BrandingTemplateDTOTransformer } from '../../PdfTemplate/BrandingTemplateDTOTransformer';
|
||||
import { assocItemEntriesDefaultIndex } from '@/utils/associate-item-entries-index';
|
||||
import { CreditNoteAutoIncrementService } from './CreditNoteAutoIncrement.service';
|
||||
import { CreditNote } from '../models/CreditNote';
|
||||
import {
|
||||
CreateCreditNoteDto,
|
||||
CreditNoteEntryDto,
|
||||
EditCreditNoteDto,
|
||||
} from '../dtos/CreditNote.dto';
|
||||
|
||||
@Injectable()
|
||||
export class CommandCreditNoteDTOTransform {
|
||||
/**
|
||||
* @param {ItemsEntriesService} itemsEntriesService - The items entries service.
|
||||
* @param {BranchTransactionDTOTransformer} branchDTOTransform - The branch transaction DTO transformer.
|
||||
* @param {WarehouseTransactionDTOTransform} warehouseDTOTransform - The warehouse transaction DTO transformer.
|
||||
* @param {BrandingTemplateDTOTransformer} brandingTemplatesTransformer - The branding template DTO transformer.
|
||||
* @param {CreditNoteAutoIncrementService} creditNoteAutoIncrement - The credit note auto increment service.
|
||||
*/
|
||||
constructor(
|
||||
private readonly itemsEntriesService: ItemsEntriesService,
|
||||
private readonly branchDTOTransform: BranchTransactionDTOTransformer,
|
||||
private readonly warehouseDTOTransform: WarehouseTransactionDTOTransform,
|
||||
private readonly brandingTemplatesTransformer: BrandingTemplateDTOTransformer,
|
||||
private readonly creditNoteAutoIncrement: CreditNoteAutoIncrementService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Transforms the credit/edit DTO to model.
|
||||
* @param {ICreditNoteNewDTO | ICreditNoteEditDTO} creditNoteDTO
|
||||
* @param {string} customerCurrencyCode -
|
||||
*/
|
||||
public transformCreateEditDTOToModel = async (
|
||||
creditNoteDTO: CreateCreditNoteDto | EditCreditNoteDto,
|
||||
customerCurrencyCode: string,
|
||||
oldCreditNote?: CreditNote,
|
||||
): Promise<CreditNote> => {
|
||||
// Retrieve the total amount of the given items entries.
|
||||
const amount = this.itemsEntriesService.getTotalItemsEntries(
|
||||
creditNoteDTO.entries,
|
||||
);
|
||||
const entries = R.compose(
|
||||
// Associate the default index to each item entry.
|
||||
assocItemEntriesDefaultIndex,
|
||||
|
||||
// Associate the reference type to credit note entries.
|
||||
R.map((entry: CreditNoteEntryDto) => ({
|
||||
...entry,
|
||||
referenceType: 'CreditNote',
|
||||
})),
|
||||
)(creditNoteDTO.entries);
|
||||
|
||||
// Retrieves the next credit note number.
|
||||
const autoNextNumber = this.creditNoteAutoIncrement.getNextCreditNumber();
|
||||
|
||||
// Determines the credit note number.
|
||||
const creditNoteNumber =
|
||||
creditNoteDTO.creditNoteNumber ||
|
||||
oldCreditNote?.creditNoteNumber ||
|
||||
autoNextNumber;
|
||||
|
||||
const initialDTO = {
|
||||
...omit(creditNoteDTO, ['open', 'attachments']),
|
||||
creditNoteNumber,
|
||||
amount,
|
||||
currencyCode: customerCurrencyCode,
|
||||
exchangeRate: creditNoteDTO.exchangeRate || 1,
|
||||
entries,
|
||||
...(creditNoteDTO.open &&
|
||||
!oldCreditNote?.openedAt && {
|
||||
openedAt: moment().toMySqlDateTime(),
|
||||
}),
|
||||
refundedAmount: 0,
|
||||
invoicesAmount: 0,
|
||||
};
|
||||
const asyncDto = (await composeAsync(
|
||||
this.branchDTOTransform.transformDTO<CreditNote>,
|
||||
this.warehouseDTOTransform.transformDTO<CreditNote>,
|
||||
|
||||
// Assigns the default branding template id to the invoice DTO.
|
||||
this.brandingTemplatesTransformer.assocDefaultBrandingTemplate(
|
||||
'CreditNote',
|
||||
),
|
||||
)(initialDTO)) as CreditNote;
|
||||
|
||||
return asyncDto;
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate the credit note remaining amount.
|
||||
* @param {ICreditNote} creditNote
|
||||
* @param {number} amount
|
||||
*/
|
||||
public validateCreditRemainingAmount = (
|
||||
creditNote: CreditNote,
|
||||
amount: number,
|
||||
) => {
|
||||
if (creditNote.creditsRemaining < amount) {
|
||||
throw new ServiceError(ERRORS.CREDIT_NOTE_HAS_NO_REMAINING_AMOUNT);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import { Knex } from 'knex';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import {
|
||||
ICreditNoteCreatedPayload,
|
||||
ICreditNoteCreatingPayload,
|
||||
} from '../types/CreditNotes.types';
|
||||
import { CreditNote } from '../models/CreditNote';
|
||||
import { Contact } from '../../Contacts/models/Contact';
|
||||
import { CommandCreditNoteDTOTransform } from './CommandCreditNoteDTOTransform.service';
|
||||
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
|
||||
import { ItemsEntriesService } from '@/modules/Items/ItemsEntries.service';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { events } from '@/common/events/events';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
import { CreateCreditNoteDto } from '../dtos/CreditNote.dto';
|
||||
|
||||
@Injectable()
|
||||
export class CreateCreditNoteService {
|
||||
/**
|
||||
* @param {UnitOfWork} uow - Unit of work.
|
||||
* @param {ItemsEntriesService} itemsEntriesService - Items entries service.
|
||||
* @param {EventEmitter2} eventPublisher - Event emitter.
|
||||
* @param {typeof CreditNote} creditNoteModel - Credit note model.
|
||||
* @param {typeof Contact} contactModel - Contact model.
|
||||
* @param {CommandCreditNoteDTOTransform} commandCreditNoteDTOTransform - Command credit note DTO transform service.
|
||||
*/
|
||||
constructor(
|
||||
private readonly uow: UnitOfWork,
|
||||
private readonly itemsEntriesService: ItemsEntriesService,
|
||||
private readonly eventPublisher: EventEmitter2,
|
||||
private readonly commandCreditNoteDTOTransform: CommandCreditNoteDTOTransform,
|
||||
|
||||
@Inject(CreditNote.name)
|
||||
private readonly creditNoteModel: TenantModelProxy<typeof CreditNote>,
|
||||
|
||||
@Inject(Contact.name)
|
||||
private readonly contactModel: TenantModelProxy<typeof Contact>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Creates a new credit note.
|
||||
* @param creditNoteDTO
|
||||
*/
|
||||
public creditCreditNote = async (
|
||||
creditNoteDTO: CreateCreditNoteDto,
|
||||
trx?: Knex.Transaction,
|
||||
) => {
|
||||
// Triggers `onCreditNoteCreate` event.
|
||||
await this.eventPublisher.emitAsync(events.creditNote.onCreate, {
|
||||
creditNoteDTO,
|
||||
});
|
||||
// Validate customer existance.
|
||||
const customer = await this.contactModel()
|
||||
.query()
|
||||
.modify('customer')
|
||||
.findById(creditNoteDTO.customerId)
|
||||
.throwIfNotFound();
|
||||
|
||||
// Validate items ids existance.
|
||||
await this.itemsEntriesService.validateItemsIdsExistance(
|
||||
creditNoteDTO.entries,
|
||||
);
|
||||
// Validate items should be sellable items.
|
||||
await this.itemsEntriesService.validateNonSellableEntriesItems(
|
||||
creditNoteDTO.entries,
|
||||
);
|
||||
// Transformes the given DTO to storage layer data.
|
||||
const creditNoteModel =
|
||||
await this.commandCreditNoteDTOTransform.transformCreateEditDTOToModel(
|
||||
creditNoteDTO,
|
||||
customer.currencyCode,
|
||||
);
|
||||
// Creates a new credit card transactions under unit-of-work envirement.
|
||||
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
|
||||
// Triggers `onCreditNoteCreating` event.
|
||||
await this.eventPublisher.emitAsync(events.creditNote.onCreating, {
|
||||
creditNoteDTO,
|
||||
trx,
|
||||
} as ICreditNoteCreatingPayload);
|
||||
|
||||
// Upsert the credit note graph.
|
||||
const creditNote = await this.creditNoteModel()
|
||||
.query(trx)
|
||||
.upsertGraph({
|
||||
...creditNoteModel,
|
||||
});
|
||||
// Triggers `onCreditNoteCreated` event.
|
||||
await this.eventPublisher.emitAsync(events.creditNote.onCreated, {
|
||||
creditNoteDTO,
|
||||
creditNote,
|
||||
creditNoteId: creditNote.id,
|
||||
trx,
|
||||
} as ICreditNoteCreatedPayload);
|
||||
|
||||
return creditNote;
|
||||
}, trx);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { AutoIncrementOrdersService } from '@/modules/AutoIncrementOrders/AutoIncrementOrders.service';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class CreditNoteAutoIncrementService {
|
||||
constructor(
|
||||
private readonly autoIncrementOrdersService: AutoIncrementOrdersService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Retrieve the next unique credit number.
|
||||
* @return {string}
|
||||
*/
|
||||
public getNextCreditNumber(): Promise<string> {
|
||||
return this.autoIncrementOrdersService.getNextTransactionNumber(
|
||||
'credit_note',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment the credit note serial next number.
|
||||
*/
|
||||
public incrementSerialNumber() {
|
||||
return this.autoIncrementOrdersService.incrementSettingsNextNumber(
|
||||
'credit_note',
|
||||
);
|
||||
}
|
||||
}
|
||||
185
packages/server/src/modules/CreditNotes/commands/CreditNoteGL.ts
Normal file
185
packages/server/src/modules/CreditNotes/commands/CreditNoteGL.ts
Normal file
@@ -0,0 +1,185 @@
|
||||
import { ILedgerEntry } from '@/modules/Ledger/types/Ledger.types';
|
||||
import { CreditNote } from '../models/CreditNote';
|
||||
import { AccountNormal } from '@/interfaces/Account';
|
||||
import { Ledger } from '@/modules/Ledger/Ledger';
|
||||
import { ItemEntry } from '@/modules/TransactionItemEntry/models/ItemEntry';
|
||||
|
||||
export class CreditNoteGL {
|
||||
creditNoteModel: CreditNote;
|
||||
ARAccountId: number;
|
||||
discountAccountId: number;
|
||||
adjustmentAccountId: number;
|
||||
|
||||
/**
|
||||
* @param {CreditNote} creditNoteModel - Credit note model.
|
||||
*/
|
||||
constructor(creditNoteModel: CreditNote) {
|
||||
this.creditNoteModel = creditNoteModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the A/R account id.
|
||||
* @param {number} ARAccountId - A/R account id.
|
||||
*/
|
||||
public setARAccountId(ARAccountId: number) {
|
||||
this.ARAccountId = ARAccountId;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the discount account id.
|
||||
* @param {number} discountAccountId - Discount account id.
|
||||
*/
|
||||
public setDiscountAccountId(discountAccountId: number) {
|
||||
this.discountAccountId = discountAccountId;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the adjustment account id.
|
||||
* @param {number} adjustmentAccountId - Adjustment account id.
|
||||
*/
|
||||
public setAdjustmentAccountId(adjustmentAccountId: number) {
|
||||
this.adjustmentAccountId = adjustmentAccountId;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the credit note common entry.
|
||||
* @returns {ICreditNoteGLCommonEntry}
|
||||
*/
|
||||
private get creditNoteCommonEntry() {
|
||||
return {
|
||||
date: this.creditNoteModel.creditNoteDate,
|
||||
userId: this.creditNoteModel.userId,
|
||||
currencyCode: this.creditNoteModel.currencyCode,
|
||||
exchangeRate: this.creditNoteModel.exchangeRate,
|
||||
|
||||
transactionType: 'CreditNote',
|
||||
transactionId: this.creditNoteModel.id,
|
||||
|
||||
transactionNumber: this.creditNoteModel.creditNoteNumber,
|
||||
referenceNumber: this.creditNoteModel.referenceNo,
|
||||
|
||||
createdAt: this.creditNoteModel.createdAt,
|
||||
indexGroup: 10,
|
||||
|
||||
credit: 0,
|
||||
debit: 0,
|
||||
|
||||
branchId: this.creditNoteModel.branchId,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the creidt note A/R entry.
|
||||
* @param {ICreditNote} creditNote -
|
||||
* @param {number} ARAccountId -
|
||||
* @returns {ILedgerEntry}
|
||||
*/
|
||||
private get creditNoteAREntry() {
|
||||
const commonEntry = this.creditNoteCommonEntry;
|
||||
|
||||
return {
|
||||
...commonEntry,
|
||||
credit: this.creditNoteModel.totalLocal,
|
||||
accountId: this.ARAccountId,
|
||||
contactId: this.creditNoteModel.customerId,
|
||||
index: 1,
|
||||
accountNormal: AccountNormal.DEBIT,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the credit note item entry.
|
||||
* @param {ItemEntry} entry
|
||||
* @param {number} index
|
||||
* @returns {ILedgerEntry}
|
||||
*/
|
||||
private getCreditNoteItemEntry(
|
||||
entry: ItemEntry,
|
||||
index: number,
|
||||
): ILedgerEntry {
|
||||
const commonEntry = this.creditNoteCommonEntry;
|
||||
const totalLocal =
|
||||
entry.totalExcludingTax * this.creditNoteModel.exchangeRate;
|
||||
|
||||
return {
|
||||
...commonEntry,
|
||||
debit: totalLocal,
|
||||
accountId: entry.sellAccountId || entry.item.sellAccountId,
|
||||
note: entry.description,
|
||||
index: index + 2,
|
||||
itemId: entry.itemId,
|
||||
accountNormal: AccountNormal.CREDIT,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the credit note discount entry.
|
||||
* @param {ICreditNote} creditNote
|
||||
* @param {number} discountAccountId
|
||||
* @returns {ILedgerEntry}
|
||||
*/
|
||||
private get discountEntry(): ILedgerEntry {
|
||||
const commonEntry = this.creditNoteCommonEntry;
|
||||
|
||||
return {
|
||||
...commonEntry,
|
||||
credit: this.creditNoteModel.discountAmountLocal,
|
||||
accountId: this.discountAccountId,
|
||||
accountNormal: AccountNormal.CREDIT,
|
||||
index: 1,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the credit note adjustment entry.
|
||||
* @param {ICreditNote} creditNote
|
||||
* @param {number} adjustmentAccountId
|
||||
* @returns {ILedgerEntry}
|
||||
*/
|
||||
private get adjustmentEntry(): ILedgerEntry {
|
||||
const commonEntry = this.creditNoteCommonEntry;
|
||||
const adjustmentAmount = Math.abs(this.creditNoteModel.adjustmentLocal);
|
||||
|
||||
return {
|
||||
...commonEntry,
|
||||
credit: this.creditNoteModel.adjustmentLocal < 0 ? adjustmentAmount : 0,
|
||||
debit: this.creditNoteModel.adjustmentLocal > 0 ? adjustmentAmount : 0,
|
||||
accountId: this.adjustmentAccountId,
|
||||
accountNormal: AccountNormal.CREDIT,
|
||||
index: 1,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the credit note GL entries.
|
||||
* @param {ICreditNote} creditNote - Credit note.
|
||||
* @param {IAccount} receivableAccount - Receviable account.
|
||||
* @returns {ILedgerEntry[]} - Ledger entries.
|
||||
*/
|
||||
public getCreditNoteGLEntries(): ILedgerEntry[] {
|
||||
const AREntry = this.creditNoteAREntry;
|
||||
|
||||
const itemsEntries = this.creditNoteModel.entries.map((entry, index) =>
|
||||
this.getCreditNoteItemEntry(entry, index),
|
||||
);
|
||||
const discountEntry = this.discountEntry;
|
||||
const adjustmentEntry = this.adjustmentEntry;
|
||||
|
||||
return [AREntry, discountEntry, adjustmentEntry, ...itemsEntries];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the credit note GL.
|
||||
* @param {ICreditNote} creditNote
|
||||
* @param {number} receivableAccount
|
||||
* @returns {Ledger}
|
||||
*/
|
||||
public getCreditNoteLedger(): Ledger {
|
||||
const ledgerEntries = this.getCreditNoteGLEntries();
|
||||
|
||||
return new Ledger(ledgerEntries);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import { Knex } from 'knex';
|
||||
import { CreditNoteGL } from './CreditNoteGL';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { LedgerStorageService } from '@/modules/Ledger/LedgerStorage.service';
|
||||
import { CreditNote } from '../models/CreditNote';
|
||||
import { AccountRepository } from '@/modules/Accounts/repositories/Account.repository';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
|
||||
@Injectable()
|
||||
export class CreditNoteGLEntries {
|
||||
constructor(
|
||||
private readonly ledgerStorage: LedgerStorageService,
|
||||
private readonly accountRepository: AccountRepository,
|
||||
|
||||
@Inject(CreditNote.name)
|
||||
private readonly creditNoteModel: TenantModelProxy<typeof CreditNote>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Reverts the credit note associated GL entries.
|
||||
* @param {number} creditNoteId - Credit note id.
|
||||
* @param {Knex.Transaction} trx
|
||||
*/
|
||||
public revertVendorCreditGLEntries = async (
|
||||
creditNoteId: number,
|
||||
trx?: Knex.Transaction,
|
||||
): Promise<void> => {
|
||||
await this.ledgerStorage.deleteByReference(creditNoteId, 'CreditNote', trx);
|
||||
};
|
||||
|
||||
/**
|
||||
* Writes vendor credit associated GL entries.
|
||||
* @param {number} creditNoteId - Credit note id.
|
||||
* @param {Knex.Transaction} trx - Knex transactions.
|
||||
*/
|
||||
public createVendorCreditGLEntries = async (
|
||||
creditNoteId: number,
|
||||
trx?: Knex.Transaction,
|
||||
): Promise<void> => {
|
||||
// Retrieve the credit note with associated entries and items.
|
||||
const creditNoteWithItems = await CreditNote.query(trx)
|
||||
.findById(creditNoteId)
|
||||
.withGraphFetched('entries.item');
|
||||
|
||||
// Retreive the the `accounts receivable` account based on the given currency.
|
||||
const ARAccount =
|
||||
await this.accountRepository.findOrCreateAccountReceivable(
|
||||
creditNoteWithItems.currencyCode,
|
||||
);
|
||||
const discountAccount =
|
||||
await this.accountRepository.findOrCreateDiscountAccount({});
|
||||
|
||||
const adjustmentAccount =
|
||||
await this.accountRepository.findOrCreateOtherChargesAccount({});
|
||||
|
||||
const creditNoteLedger = new CreditNoteGL(creditNoteWithItems)
|
||||
.setARAccountId(ARAccount.id)
|
||||
.setDiscountAccountId(discountAccount.id)
|
||||
.setAdjustmentAccountId(adjustmentAccount.id)
|
||||
.getCreditNoteLedger();
|
||||
|
||||
// Saves the credit note GL entries.
|
||||
await this.ledgerStorage.commit(creditNoteLedger, trx);
|
||||
};
|
||||
|
||||
/**
|
||||
* Edits vendor credit associated GL entries.
|
||||
* @param {number} creditNoteId - Credit note id.
|
||||
* @param {Knex.Transaction} trx
|
||||
*/
|
||||
public editVendorCreditGLEntries = async (
|
||||
creditNoteId: number,
|
||||
trx?: Knex.Transaction,
|
||||
): Promise<void> => {
|
||||
// Reverts vendor credit GL entries.
|
||||
await this.revertVendorCreditGLEntries(creditNoteId, trx);
|
||||
|
||||
// Creates vendor credit Gl entries.
|
||||
await this.createVendorCreditGLEntries(creditNoteId, trx);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// import { Inject, Service } from 'typedi';
|
||||
// import { ICreditNotesQueryDTO } from '@/interfaces';
|
||||
// import { Exportable } from '@/services/Export/Exportable';
|
||||
// import ListCreditNotes from '../ListCreditNotes';
|
||||
|
||||
// @Service()
|
||||
// export class CreditNotesExportable extends Exportable {
|
||||
// @Inject()
|
||||
// private getCreditNotes: ListCreditNotes;
|
||||
|
||||
// /**
|
||||
// * Retrieves the accounts data to exportable sheet.
|
||||
// * @param {number} tenantId -
|
||||
// * @param {IVendorCreditsQueryDTO} query -
|
||||
// * @returns {}
|
||||
// */
|
||||
// public exportable(tenantId: number, query: ICreditNotesQueryDTO) {
|
||||
// const filterQuery = (query) => {
|
||||
// query.withGraphFetched('branch');
|
||||
// query.withGraphFetched('warehouse');
|
||||
// };
|
||||
// const parsedQuery = {
|
||||
// sortOrder: 'desc',
|
||||
// columnSortBy: 'created_at',
|
||||
// ...query,
|
||||
// page: 1,
|
||||
// pageSize: 12000,
|
||||
// filterQuery,
|
||||
// } as ICreditNotesQueryDTO;
|
||||
|
||||
// return this.getCreditNotes
|
||||
// .getCreditNotesList(tenantId, parsedQuery)
|
||||
// .then((output) => output.creditNotes);
|
||||
// }
|
||||
// }
|
||||
@@ -0,0 +1,44 @@
|
||||
// import { Inject, Service } from 'typedi';
|
||||
// import { Knex } from 'knex';
|
||||
// import { ICreditNoteNewDTO } from '@/interfaces';
|
||||
// import { Importable } from '../Import/Importable';
|
||||
// import CreateCreditNote from './commands/CreateCreditNote.service';
|
||||
|
||||
// @Service()
|
||||
// export class CreditNotesImportable extends Importable {
|
||||
// @Inject()
|
||||
// private createCreditNoteImportable: CreateCreditNote;
|
||||
|
||||
// /**
|
||||
// * Importing to account service.
|
||||
// * @param {number} tenantId
|
||||
// * @param {IAccountCreateDTO} createAccountDTO
|
||||
// * @returns
|
||||
// */
|
||||
// public importable(
|
||||
// tenantId: number,
|
||||
// createAccountDTO: ICreditNoteNewDTO,
|
||||
// trx?: Knex.Transaction
|
||||
// ) {
|
||||
// return this.createCreditNoteImportable.newCreditNote(
|
||||
// tenantId,
|
||||
// createAccountDTO,
|
||||
// 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 [];
|
||||
// }
|
||||
// }
|
||||
@@ -0,0 +1,82 @@
|
||||
// @ts-nocheck
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InventoryTransactionsService } from '@/modules/InventoryCost/commands/InventoryTransactions.service';
|
||||
import { ItemsEntriesService } from '@/modules/Items/ItemsEntries.service';
|
||||
import { CreditNote } from '../models/CreditNote';
|
||||
import { Knex } from 'knex';
|
||||
@Injectable()
|
||||
export class CreditNoteInventoryTransactions {
|
||||
constructor(
|
||||
private readonly inventoryService: InventoryTransactionsService,
|
||||
private readonly itemsEntriesService: ItemsEntriesService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Creates credit note inventory transactions.
|
||||
* @param {number} tenantId
|
||||
* @param {ICreditNote} creditNote
|
||||
*/
|
||||
public createInventoryTransactions = async (
|
||||
creditNote: CreditNote,
|
||||
trx?: Knex.Transaction,
|
||||
): Promise<void> => {
|
||||
// Loads the inventory items entries of the given sale invoice.
|
||||
const inventoryEntries =
|
||||
await this.itemsEntriesService.filterInventoryEntries(creditNote.entries);
|
||||
|
||||
const transaction = {
|
||||
transactionId: creditNote.id,
|
||||
transactionType: 'CreditNote',
|
||||
transactionNumber: creditNote.creditNoteNumber,
|
||||
exchangeRate: creditNote.exchangeRate,
|
||||
date: creditNote.creditNoteDate,
|
||||
direction: 'IN',
|
||||
entries: inventoryEntries,
|
||||
createdAt: creditNote.createdAt,
|
||||
warehouseId: creditNote.warehouseId,
|
||||
};
|
||||
// Writes inventory tranactions.
|
||||
await this.inventoryService.recordInventoryTransactionsFromItemsEntries(
|
||||
transaction,
|
||||
false,
|
||||
trx,
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Edits vendor credit associated inventory transactions.
|
||||
* @param {number} tenantId
|
||||
* @param {number} creditNoteId
|
||||
* @param {ICreditNote} creditNote
|
||||
* @param {Knex.Transactions} trx
|
||||
*/
|
||||
public editInventoryTransactions = async (
|
||||
creditNoteId: number,
|
||||
creditNote: CreditNote,
|
||||
trx?: Knex.Transaction,
|
||||
): Promise<void> => {
|
||||
// Deletes inventory transactions.
|
||||
await this.deleteInventoryTransactions(creditNoteId, trx);
|
||||
|
||||
// Re-write inventory transactions.
|
||||
await this.createInventoryTransactions(creditNote, trx);
|
||||
};
|
||||
|
||||
/**
|
||||
* Deletes credit note associated inventory transactions.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {number} creditNoteId - Credit note id.
|
||||
* @param {Knex.Transaction} trx -
|
||||
*/
|
||||
public deleteInventoryTransactions = async (
|
||||
creditNoteId: number,
|
||||
trx?: Knex.Transaction,
|
||||
): Promise<void> => {
|
||||
// Deletes the inventory transactions by the given reference id and type.
|
||||
await this.inventoryService.deleteInventoryTransactions(
|
||||
creditNoteId,
|
||||
'CreditNote',
|
||||
trx,
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Knex } from 'knex';
|
||||
import {
|
||||
ICreditNoteDeletedPayload,
|
||||
ICreditNoteDeletingPayload,
|
||||
} from '../types/CreditNotes.types';
|
||||
import { ERRORS } from '../constants';
|
||||
import { CreditNote } from '../models/CreditNote';
|
||||
import { CreditNoteAppliedInvoice } from '../../CreditNotesApplyInvoice/models/CreditNoteAppliedInvoice';
|
||||
import { RefundCreditNote as RefundCreditNoteModel } from '../../CreditNoteRefunds/models/RefundCreditNote';
|
||||
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { ItemEntry } from '@/modules/TransactionItemEntry/models/ItemEntry';
|
||||
import { ServiceError } from '@/modules/Items/ServiceError';
|
||||
import { events } from '@/common/events/events';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
|
||||
@Injectable()
|
||||
export class DeleteCreditNoteService {
|
||||
/**
|
||||
* @param {UnitOfWork} uow - Unit of work.
|
||||
* @param {EventEmitter2} eventPublisher - Event emitter.
|
||||
* @param {typeof CreditNote} creditNoteModel - Credit note model.
|
||||
* @param {typeof ItemEntry} itemEntryModel - Item entry model.
|
||||
* @param {typeof CreditNoteAppliedInvoice} creditNoteAppliedInvoiceModel - Credit note applied invoice model.
|
||||
* @param {typeof RefundCreditNote} refundCreditNoteModel - Refund credit note model.
|
||||
*/
|
||||
constructor(
|
||||
private readonly uow: UnitOfWork,
|
||||
private readonly eventPublisher: EventEmitter2,
|
||||
|
||||
@Inject(CreditNote.name)
|
||||
private readonly creditNoteModel: TenantModelProxy<typeof CreditNote>,
|
||||
|
||||
@Inject(ItemEntry.name)
|
||||
private readonly itemEntryModel: TenantModelProxy<typeof ItemEntry>,
|
||||
|
||||
@Inject(CreditNoteAppliedInvoice.name)
|
||||
private readonly creditNoteAppliedInvoiceModel: TenantModelProxy<
|
||||
typeof CreditNoteAppliedInvoice
|
||||
>,
|
||||
|
||||
@Inject(RefundCreditNoteModel.name)
|
||||
private readonly refundCreditNoteModel: TenantModelProxy<
|
||||
typeof RefundCreditNoteModel
|
||||
>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Deletes the given credit note transactions.
|
||||
* @param {number} creditNoteId
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public async deleteCreditNote(creditNoteId: number): Promise<void> {
|
||||
// Retrieve the credit note or throw not found service error.
|
||||
const oldCreditNote = await this.creditNoteModel()
|
||||
.query()
|
||||
.findById(creditNoteId)
|
||||
.throwIfNotFound();
|
||||
|
||||
// Validate credit note has no refund transactions.
|
||||
await this.validateCreditNoteHasNoRefundTransactions(creditNoteId);
|
||||
|
||||
// Validate credit note has no applied invoices transactions.
|
||||
await this.validateCreditNoteHasNoApplyInvoiceTransactions(creditNoteId);
|
||||
|
||||
// Deletes the credit note transactions under unit-of-work transaction.
|
||||
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
|
||||
// Triggers `onCreditNoteDeleting` event.
|
||||
await this.eventPublisher.emitAsync(events.creditNote.onDeleting, {
|
||||
trx,
|
||||
oldCreditNote,
|
||||
} as ICreditNoteDeletingPayload);
|
||||
|
||||
// Deletes the associated credit note entries.
|
||||
await this.itemEntryModel()
|
||||
.query(trx)
|
||||
.where('reference_id', creditNoteId)
|
||||
.where('reference_type', 'CreditNote')
|
||||
.delete();
|
||||
|
||||
// Deletes the credit note transaction.
|
||||
await this.creditNoteModel().query(trx).findById(creditNoteId).delete();
|
||||
|
||||
// Triggers `onCreditNoteDeleted` event.
|
||||
await this.eventPublisher.emitAsync(events.creditNote.onDeleted, {
|
||||
oldCreditNote,
|
||||
creditNoteId,
|
||||
trx,
|
||||
} as ICreditNoteDeletedPayload);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates credit note has no associated refund transactions.
|
||||
* @param {number} creditNoteId
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
private async validateCreditNoteHasNoRefundTransactions(
|
||||
creditNoteId: number,
|
||||
): Promise<void> {
|
||||
const refundTransactions = await this.refundCreditNoteModel()
|
||||
.query()
|
||||
.where('creditNoteId', creditNoteId);
|
||||
|
||||
if (refundTransactions.length > 0) {
|
||||
throw new ServiceError(ERRORS.CREDIT_NOTE_HAS_REFUNDS_TRANSACTIONS);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate credit note has no associated applied invoices transactions.
|
||||
* @param {number} creditNoteId - Credit note id.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
private async validateCreditNoteHasNoApplyInvoiceTransactions(
|
||||
creditNoteId: number,
|
||||
): Promise<void> {
|
||||
const appliedTransactions = await this.creditNoteAppliedInvoiceModel()
|
||||
.query()
|
||||
.where('creditNoteId', creditNoteId);
|
||||
|
||||
if (appliedTransactions.length > 0) {
|
||||
throw new ServiceError(ERRORS.CREDIT_NOTE_HAS_APPLIED_INVOICES);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import {
|
||||
ICreditNoteEditedPayload,
|
||||
ICreditNoteEditingPayload,
|
||||
} from '../types/CreditNotes.types';
|
||||
import { Knex } from 'knex';
|
||||
import { CreditNote } from '../models/CreditNote';
|
||||
import { Contact } from '../../Contacts/models/Contact';
|
||||
import { ItemsEntriesService } from '../../Items/ItemsEntries.service';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { UnitOfWork } from '../../Tenancy/TenancyDB/UnitOfWork.service';
|
||||
import { events } from '@/common/events/events';
|
||||
import { CommandCreditNoteDTOTransform } from './CommandCreditNoteDTOTransform.service';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
import { EditCreditNoteDto } from '../dtos/CreditNote.dto';
|
||||
|
||||
@Injectable()
|
||||
export class EditCreditNoteService {
|
||||
/**
|
||||
* @param {typeof CreditNote} creditNoteModel - The credit note model.
|
||||
* @param {typeof Contact} contactModel - The contact model.
|
||||
* @param {CommandCreditNoteDTOTransform} commandCreditNoteDTOTransform - The command credit note DTO transform service.
|
||||
* @param {ItemsEntriesService} itemsEntriesService - The items entries service.
|
||||
* @param {EventEmitter2} eventPublisher - The event publisher.
|
||||
* @param {UnitOfWork} uow - The unit of work.
|
||||
*/
|
||||
constructor(
|
||||
@Inject(CreditNote.name)
|
||||
private creditNoteModel: TenantModelProxy<typeof CreditNote>,
|
||||
|
||||
@Inject(Contact.name)
|
||||
private contactModel: TenantModelProxy<typeof Contact>,
|
||||
|
||||
private commandCreditNoteDTOTransform: CommandCreditNoteDTOTransform,
|
||||
private itemsEntriesService: ItemsEntriesService,
|
||||
private eventPublisher: EventEmitter2,
|
||||
private uow: UnitOfWork,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Edits the given credit note.
|
||||
* @param {ICreditNoteEditDTO} creditNoteEditDTO -
|
||||
*/
|
||||
public async editCreditNote(
|
||||
creditNoteId: number,
|
||||
creditNoteEditDTO: EditCreditNoteDto,
|
||||
) {
|
||||
// Retrieve the sale invoice or throw not found service error.
|
||||
const oldCreditNote = await this.creditNoteModel()
|
||||
.query()
|
||||
.findById(creditNoteId)
|
||||
.throwIfNotFound();
|
||||
|
||||
// Validate customer existance.
|
||||
const customer = await this.contactModel()
|
||||
.query()
|
||||
.findById(creditNoteEditDTO.customerId);
|
||||
|
||||
// Validate items ids existance.
|
||||
await this.itemsEntriesService.validateItemsIdsExistance(
|
||||
creditNoteEditDTO.entries,
|
||||
);
|
||||
// Validate non-sellable entries items.
|
||||
await this.itemsEntriesService.validateNonSellableEntriesItems(
|
||||
creditNoteEditDTO.entries,
|
||||
);
|
||||
// Validate the items entries existance.
|
||||
await this.itemsEntriesService.validateEntriesIdsExistance(
|
||||
creditNoteId,
|
||||
'CreditNote',
|
||||
creditNoteEditDTO.entries,
|
||||
);
|
||||
// Transformes the given DTO to storage layer data.
|
||||
const creditNoteModel =
|
||||
await this.commandCreditNoteDTOTransform.transformCreateEditDTOToModel(
|
||||
creditNoteEditDTO,
|
||||
customer.currencyCode,
|
||||
oldCreditNote,
|
||||
);
|
||||
// Sales the credit note transactions with associated entries.
|
||||
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
|
||||
// Triggers `onCreditNoteEditing` event.
|
||||
await this.eventPublisher.emitAsync(events.creditNote.onEditing, {
|
||||
creditNoteEditDTO,
|
||||
oldCreditNote,
|
||||
trx,
|
||||
} as ICreditNoteEditingPayload);
|
||||
|
||||
// Saves the credit note graph to the storage.
|
||||
const creditNote = await this.creditNoteModel()
|
||||
.query(trx)
|
||||
.upsertGraph({
|
||||
id: creditNoteId,
|
||||
...creditNoteModel,
|
||||
});
|
||||
// Triggers `onCreditNoteEdited` event.
|
||||
await this.eventPublisher.emitAsync(events.creditNote.onEdited, {
|
||||
trx,
|
||||
oldCreditNote,
|
||||
creditNoteId,
|
||||
creditNote,
|
||||
creditNoteEditDTO,
|
||||
} as ICreditNoteEditedPayload);
|
||||
|
||||
return creditNote;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import {
|
||||
ICreditNoteOpenedPayload,
|
||||
ICreditNoteOpeningPayload,
|
||||
} from '../types/CreditNotes.types';
|
||||
import { Knex } from 'knex';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { ERRORS } from '../constants';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
|
||||
import { CreditNote } from '../models/CreditNote';
|
||||
import { events } from '@/common/events/events';
|
||||
import { ServiceError } from '@/modules/Items/ServiceError';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
|
||||
@Injectable()
|
||||
export class OpenCreditNoteService {
|
||||
/**
|
||||
* @param {EventEmitter2} eventPublisher - The event publisher.
|
||||
* @param {UnitOfWork} uow - The unit of work.
|
||||
* @param {typeof CreditNote} creditNoteModel - The credit note model.
|
||||
*/
|
||||
constructor(
|
||||
private readonly eventPublisher: EventEmitter2,
|
||||
private readonly uow: UnitOfWork,
|
||||
|
||||
@Inject(CreditNote.name)
|
||||
private readonly creditNoteModel: TenantModelProxy<typeof CreditNote>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Opens the given credit note.
|
||||
* @param {number} creditNoteId -
|
||||
* @returns {Promise<CreditNote>}
|
||||
*/
|
||||
public openCreditNote = async (creditNoteId: number): Promise<CreditNote> => {
|
||||
// Retrieve the sale invoice or throw not found service error.
|
||||
const oldCreditNote = await this.creditNoteModel()
|
||||
.query()
|
||||
.findById(creditNoteId)
|
||||
.throwIfNotFound();
|
||||
|
||||
// Throw service error if the credit note is already open.
|
||||
this.throwErrorIfAlreadyOpen(oldCreditNote);
|
||||
|
||||
// Triggers `onCreditNoteOpen` event.
|
||||
this.eventPublisher.emitAsync(events.creditNote.onOpen, {
|
||||
creditNoteId,
|
||||
oldCreditNote,
|
||||
});
|
||||
// Sales the credit note transactions with associated entries.
|
||||
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
|
||||
const eventPayload = {
|
||||
oldCreditNote,
|
||||
trx,
|
||||
} as ICreditNoteOpeningPayload;
|
||||
|
||||
// Triggers `onCreditNoteOpening` event.
|
||||
await this.eventPublisher.emitAsync(
|
||||
events.creditNote.onOpening,
|
||||
eventPayload,
|
||||
);
|
||||
// Saves the credit note graph to the storage.
|
||||
const creditNote = await this.creditNoteModel()
|
||||
.query(trx)
|
||||
.updateAndFetchById(creditNoteId, {
|
||||
openedAt: new Date(),
|
||||
});
|
||||
// Triggers `onCreditNoteOpened` event.
|
||||
await this.eventPublisher.emitAsync(events.creditNote.onOpened, {
|
||||
...eventPayload,
|
||||
creditNote,
|
||||
} as ICreditNoteOpenedPayload);
|
||||
|
||||
return creditNote;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Throws an error if the given credit note is already open.
|
||||
* @param {CreditNote} creditNote -
|
||||
*/
|
||||
public throwErrorIfAlreadyOpen = (creditNote: CreditNote) => {
|
||||
if (creditNote.openedAt) {
|
||||
throw new ServiceError(ERRORS.CREDIT_NOTE_ALREADY_OPENED);
|
||||
}
|
||||
};
|
||||
}
|
||||
132
packages/server/src/modules/CreditNotes/constants.ts
Normal file
132
packages/server/src/modules/CreditNotes/constants.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
export const ERRORS = {
|
||||
CREDIT_NOTE_NOT_FOUND: 'CREDIT_NOTE_NOT_FOUND',
|
||||
REFUND_CREDIT_NOTE_NOT_FOUND: 'REFUND_CREDIT_NOTE_NOT_FOUND',
|
||||
CREDIT_NOTE_ALREADY_OPENED: 'CREDIT_NOTE_ALREADY_OPENED',
|
||||
ACCOUNT_INVALID_TYPE: 'ACCOUNT_INVALID_TYPE',
|
||||
CREDIT_NOTE_HAS_NO_REMAINING_AMOUNT: 'CREDIT_NOTE_HAS_NO_REMAINING_AMOUNT',
|
||||
INVOICES_HAS_NO_REMAINING_AMOUNT: 'INVOICES_HAS_NO_REMAINING_AMOUNT',
|
||||
CREDIT_NOTE_APPLY_TO_INVOICES_NOT_FOUND:
|
||||
'CREDIT_NOTE_APPLY_TO_INVOICES_NOT_FOUND',
|
||||
CREDIT_NOTE_HAS_REFUNDS_TRANSACTIONS: 'CREDIT_NOTE_HAS_REFUNDS_TRANSACTIONS',
|
||||
CREDIT_NOTE_HAS_APPLIED_INVOICES: 'CREDIT_NOTE_HAS_APPLIED_INVOICES',
|
||||
CUSTOMER_HAS_LINKED_CREDIT_NOTES: 'CUSTOMER_HAS_LINKED_CREDIT_NOTES',
|
||||
};
|
||||
|
||||
export const DEFAULT_VIEW_COLUMNS = [];
|
||||
export const DEFAULT_VIEWS = [
|
||||
{
|
||||
name: 'credit_note.view.draft',
|
||||
slug: 'draft',
|
||||
rolesLogicExpression: '1',
|
||||
roles: [
|
||||
{ index: 1, fieldKey: 'status', comparator: 'equals', value: 'draft' },
|
||||
],
|
||||
columns: DEFAULT_VIEW_COLUMNS,
|
||||
},
|
||||
{
|
||||
name: 'credit_note.view.published',
|
||||
slug: 'published',
|
||||
rolesLogicExpression: '1',
|
||||
roles: [
|
||||
{
|
||||
index: 1,
|
||||
fieldKey: 'status',
|
||||
comparator: 'equals',
|
||||
value: 'published',
|
||||
},
|
||||
],
|
||||
columns: DEFAULT_VIEW_COLUMNS,
|
||||
},
|
||||
{
|
||||
name: 'credit_note.view.open',
|
||||
slug: 'open',
|
||||
rolesLogicExpression: '1',
|
||||
roles: [
|
||||
{
|
||||
index: 1,
|
||||
fieldKey: 'status',
|
||||
comparator: 'equals',
|
||||
value: 'open',
|
||||
},
|
||||
],
|
||||
columns: DEFAULT_VIEW_COLUMNS,
|
||||
},
|
||||
{
|
||||
name: 'credit_note.view.closed',
|
||||
slug: 'closed',
|
||||
rolesLogicExpression: '1',
|
||||
roles: [
|
||||
{
|
||||
index: 1,
|
||||
fieldKey: 'status',
|
||||
comparator: 'equals',
|
||||
value: 'closed',
|
||||
},
|
||||
],
|
||||
columns: DEFAULT_VIEW_COLUMNS,
|
||||
},
|
||||
];
|
||||
|
||||
export const defaultCreditNoteBrandingAttributes = {
|
||||
// # Colors
|
||||
primaryColor: '',
|
||||
secondaryColor: '',
|
||||
|
||||
// # Company logo
|
||||
showCompanyLogo: true,
|
||||
companyLogoKey: '',
|
||||
companyLogoUri: '',
|
||||
|
||||
// # Company name
|
||||
companyName: 'Bigcapital Technology, Inc.',
|
||||
|
||||
// # Customer address
|
||||
showCustomerAddress: true,
|
||||
customerAddress: '',
|
||||
|
||||
// # Company address
|
||||
showCompanyAddress: true,
|
||||
companyAddress: '',
|
||||
billedToLabel: 'Billed To',
|
||||
|
||||
// Total
|
||||
total: '$1000.00',
|
||||
totalLabel: 'Total',
|
||||
showTotal: true,
|
||||
|
||||
// Subtotal
|
||||
subtotal: '1000/00',
|
||||
subtotalLabel: 'Subtotal',
|
||||
showSubtotal: true,
|
||||
|
||||
// Customer note
|
||||
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',
|
||||
|
||||
// Terms & conditions
|
||||
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',
|
||||
},
|
||||
],
|
||||
// Credit note number.
|
||||
showCreditNoteNumber: true,
|
||||
creditNoteNumberLabel: 'Credit Note Number',
|
||||
creditNoteNumebr: '346D3D40-0001',
|
||||
|
||||
// Credit note date.
|
||||
creditNoteDate: 'September 3, 2024',
|
||||
showCreditNoteDate: true,
|
||||
creditNoteDateLabel: 'Credit Note Date',
|
||||
};
|
||||
143
packages/server/src/modules/CreditNotes/dtos/CreditNote.dto.ts
Normal file
143
packages/server/src/modules/CreditNotes/dtos/CreditNote.dto.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
import { ItemEntryDto } from '@/modules/TransactionItemEntry/dto/ItemEntry.dto';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
import {
|
||||
ArrayMinSize,
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
IsDate,
|
||||
IsEnum,
|
||||
IsInt,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsPositive,
|
||||
IsString,
|
||||
Min,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
|
||||
enum DiscountType {
|
||||
Percentage = 'percentage',
|
||||
Amount = 'amount',
|
||||
}
|
||||
|
||||
export class CreditNoteEntryDto extends ItemEntryDto {}
|
||||
|
||||
class AttachmentDto {
|
||||
@IsString()
|
||||
key: string;
|
||||
}
|
||||
|
||||
export class CommandCreditNoteDto {
|
||||
@IsInt()
|
||||
@ApiProperty({ example: 1, description: 'The customer ID' })
|
||||
customerId: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsPositive()
|
||||
@ApiProperty({ example: 3.43, description: 'The exchange rate' })
|
||||
exchangeRate?: number;
|
||||
|
||||
@IsDate()
|
||||
@Type(() => Date)
|
||||
@ApiProperty({ example: '2021-09-01', description: 'The credit note date' })
|
||||
creditNoteDate: Date;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiProperty({ example: '123', description: 'The reference number' })
|
||||
referenceNo?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiProperty({ example: '123', description: 'The credit note number' })
|
||||
creditNoteNumber?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiProperty({ example: '123', description: 'The note' })
|
||||
note?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiProperty({ example: '123', description: 'The terms and conditions' })
|
||||
termsConditions?: string;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiProperty({
|
||||
example: false,
|
||||
description: 'The credit note is open',
|
||||
})
|
||||
open: boolean = false;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@ApiProperty({
|
||||
example: 1,
|
||||
description: 'The warehouse ID',
|
||||
})
|
||||
warehouseId?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@ApiProperty({
|
||||
example: 1,
|
||||
description: 'The branch ID',
|
||||
})
|
||||
branchId?: number;
|
||||
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => CreditNoteEntryDto)
|
||||
@ArrayMinSize(1)
|
||||
@ApiProperty({
|
||||
example: [
|
||||
{
|
||||
itemId: 1,
|
||||
quantity: 1,
|
||||
rate: 10,
|
||||
taxRateId: 1,
|
||||
},
|
||||
],
|
||||
description: 'The credit note entries',
|
||||
})
|
||||
entries: CreditNoteEntryDto[];
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => AttachmentDto)
|
||||
attachments?: AttachmentDto[];
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@ApiProperty({
|
||||
example: 1,
|
||||
description: 'The pdf template ID',
|
||||
})
|
||||
pdfTemplateId?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@ApiProperty({
|
||||
example: 10,
|
||||
description: 'The discount amount',
|
||||
})
|
||||
discount?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(DiscountType)
|
||||
@ApiProperty({
|
||||
example: 'percentage',
|
||||
description: 'The discount type',
|
||||
enum: DiscountType,
|
||||
})
|
||||
discountType?: DiscountType;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
adjustment?: number;
|
||||
}
|
||||
|
||||
export class CreateCreditNoteDto extends CommandCreditNoteDto {}
|
||||
export class EditCreditNoteDto extends CommandCreditNoteDto {}
|
||||
@@ -0,0 +1,12 @@
|
||||
import {
|
||||
IsDate,
|
||||
IsInt,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsPositive,
|
||||
IsString,
|
||||
} from 'class-validator';
|
||||
|
||||
export class RefundCreditNoteDto {
|
||||
|
||||
}
|
||||
437
packages/server/src/modules/CreditNotes/models/CreditNote.ts
Normal file
437
packages/server/src/modules/CreditNotes/models/CreditNote.ts
Normal file
@@ -0,0 +1,437 @@
|
||||
import { DiscountType } from '@/common/types/Discount';
|
||||
import { BaseModel } from '@/models/Model';
|
||||
import { Branch } from '@/modules/Branches/models/Branch.model';
|
||||
import { Customer } from '@/modules/Customers/models/Customer';
|
||||
import { TenantBaseModel } from '@/modules/System/models/TenantBaseModel';
|
||||
import { ItemEntry } from '@/modules/TransactionItemEntry/models/ItemEntry';
|
||||
import { Warehouse } from '@/modules/Warehouses/models/Warehouse.model';
|
||||
import { mixin, Model, raw } from 'objection';
|
||||
|
||||
export class CreditNote extends TenantBaseModel {
|
||||
public amount: number;
|
||||
public exchangeRate: number;
|
||||
public openedAt: Date;
|
||||
public discount: number;
|
||||
public discountType: DiscountType;
|
||||
public adjustment: number;
|
||||
public refundedAmount: number;
|
||||
public invoicesAmount: number;
|
||||
public creditNoteDate: Date;
|
||||
public creditNoteNumber: string;
|
||||
public referenceNo: string;
|
||||
public currencyCode: string;
|
||||
public customerId: number;
|
||||
|
||||
public userId: number;
|
||||
|
||||
public branchId: number;
|
||||
public warehouseId: number;
|
||||
|
||||
public customer!: Customer;
|
||||
public entries!: ItemEntry[];
|
||||
|
||||
public branch!: Branch;
|
||||
public warehouse!: Warehouse;
|
||||
|
||||
public createdAt!: Date | string;
|
||||
public updatedAt!: Date | string;
|
||||
|
||||
/**
|
||||
* Table name
|
||||
*/
|
||||
static get tableName() {
|
||||
return 'credit_notes';
|
||||
}
|
||||
|
||||
/**
|
||||
* Timestamps columns.
|
||||
*/
|
||||
get timestamps() {
|
||||
return ['created_at', 'updated_at'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Virtual attributes.
|
||||
*/
|
||||
static get virtualAttributes() {
|
||||
return [
|
||||
'localAmount',
|
||||
'isDraft',
|
||||
'isPublished',
|
||||
'isOpen',
|
||||
'isClosed',
|
||||
|
||||
'creditsRemaining',
|
||||
'creditsUsed',
|
||||
|
||||
'subtotal',
|
||||
'subtotalLocal',
|
||||
|
||||
'discountAmount',
|
||||
'discountAmountLocal',
|
||||
'discountPercentage',
|
||||
|
||||
'total',
|
||||
'totalLocal',
|
||||
|
||||
'adjustmentLocal',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Credit note amount in local currency.
|
||||
* @returns {number}
|
||||
*/
|
||||
get localAmount() {
|
||||
return this.amount * this.exchangeRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Credit note subtotal.
|
||||
* @returns {number}
|
||||
*/
|
||||
get subtotal() {
|
||||
return this.amount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Credit note subtotal in local currency.
|
||||
* @returns {number}
|
||||
*/
|
||||
get subtotalLocal() {
|
||||
return this.subtotal * this.exchangeRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Discount amount.
|
||||
* @returns {number}
|
||||
*/
|
||||
get discountAmount() {
|
||||
return this.discountType === DiscountType.Amount
|
||||
? this.discount
|
||||
: this.subtotal * (this.discount / 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Discount amount in local currency.
|
||||
* @returns {number}
|
||||
*/
|
||||
get discountAmountLocal() {
|
||||
return this.discountAmount ? this.discountAmount * this.exchangeRate : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Discount percentage.
|
||||
* @returns {number | null}
|
||||
*/
|
||||
get discountPercentage(): number | null {
|
||||
return this.discountType === DiscountType.Percentage ? this.discount : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjustment amount in local currency.
|
||||
* @returns {number}
|
||||
*/
|
||||
get adjustmentLocal() {
|
||||
return this.adjustment ? this.adjustment * this.exchangeRate : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Credit note total.
|
||||
* @returns {number}
|
||||
*/
|
||||
get total() {
|
||||
return this.subtotal - this.discountAmount + this.adjustment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Credit note total in local currency.
|
||||
* @returns {number}
|
||||
*/
|
||||
get totalLocal() {
|
||||
return this.total * this.exchangeRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detarmines whether the credit note is draft.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
get isDraft() {
|
||||
return !this.openedAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detarmines whether vendor credit is published.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
get isPublished() {
|
||||
return !!this.openedAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detarmines whether the credit note is open.
|
||||
* @return {boolean}
|
||||
*/
|
||||
get isOpen() {
|
||||
return !!this.openedAt && this.creditsRemaining > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detarmines whether the credit note is closed.
|
||||
* @return {boolean}
|
||||
*/
|
||||
get isClosed() {
|
||||
return this.openedAt && this.creditsRemaining === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the credits remaining.
|
||||
*/
|
||||
get creditsRemaining() {
|
||||
return Math.max(this.amount - this.refundedAmount - this.invoicesAmount, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the credits used.
|
||||
*/
|
||||
get creditsUsed() {
|
||||
return this.refundedAmount + this.invoicesAmount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Model modifiers.
|
||||
*/
|
||||
static get modifiers() {
|
||||
return {
|
||||
/**
|
||||
* Filters the credit notes in draft status.
|
||||
*/
|
||||
draft(query) {
|
||||
query.where('opened_at', null);
|
||||
},
|
||||
|
||||
/**
|
||||
* Filters the.
|
||||
*/
|
||||
published(query) {
|
||||
query.whereNot('opened_at', null);
|
||||
},
|
||||
|
||||
/**
|
||||
* Filters the open credit notes.
|
||||
*/
|
||||
open(query) {
|
||||
query
|
||||
.where(
|
||||
raw(`COALESCE(REFUNDED_AMOUNT) + COALESCE(INVOICES_AMOUNT) <
|
||||
COALESCE(AMOUNT)`),
|
||||
)
|
||||
.modify('published');
|
||||
},
|
||||
|
||||
/**
|
||||
* Filters the closed credit notes.
|
||||
*/
|
||||
closed(query) {
|
||||
query
|
||||
.where(
|
||||
raw(`COALESCE(REFUNDED_AMOUNT) + COALESCE(INVOICES_AMOUNT) =
|
||||
COALESCE(AMOUNT)`),
|
||||
)
|
||||
.modify('published');
|
||||
},
|
||||
|
||||
/**
|
||||
* Status filter.
|
||||
*/
|
||||
filterByStatus(query, filterType) {
|
||||
switch (filterType) {
|
||||
case 'draft':
|
||||
query.modify('draft');
|
||||
break;
|
||||
case 'published':
|
||||
query.modify('published');
|
||||
break;
|
||||
case 'open':
|
||||
default:
|
||||
query.modify('open');
|
||||
break;
|
||||
case 'closed':
|
||||
query.modify('closed');
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
sortByStatus(query, order) {
|
||||
query.orderByRaw(
|
||||
`COALESCE(REFUNDED_AMOUNT) + COALESCE(INVOICES_AMOUNT) = COALESCE(AMOUNT) ${order}`,
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Relationship mapping.
|
||||
*/
|
||||
static get relationMappings() {
|
||||
const {
|
||||
AccountTransaction,
|
||||
} = require('../../Accounts/models/AccountTransaction.model');
|
||||
const {
|
||||
ItemEntry,
|
||||
} = require('../../TransactionItemEntry/models/ItemEntry');
|
||||
const { Customer } = require('../../Customers/models/Customer');
|
||||
const { Branch } = require('../../Branches/models/Branch.model');
|
||||
const { Document } = require('../../ChromiumlyTenancy/models/Document');
|
||||
const { Warehouse } = require('../../Warehouses/models/Warehouse.model');
|
||||
const {
|
||||
PdfTemplateModel,
|
||||
} = require('../../PdfTemplate/models/PdfTemplate');
|
||||
|
||||
return {
|
||||
/**
|
||||
* Credit note associated entries.
|
||||
*/
|
||||
entries: {
|
||||
relation: Model.HasManyRelation,
|
||||
modelClass: ItemEntry,
|
||||
join: {
|
||||
from: 'credit_notes.id',
|
||||
to: 'items_entries.referenceId',
|
||||
},
|
||||
filter(builder) {
|
||||
builder.where('reference_type', 'CreditNote');
|
||||
builder.orderBy('index', 'ASC');
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Belongs to customer model.
|
||||
*/
|
||||
customer: {
|
||||
relation: Model.BelongsToOneRelation,
|
||||
modelClass: Customer,
|
||||
join: {
|
||||
from: 'credit_notes.customerId',
|
||||
to: 'contacts.id',
|
||||
},
|
||||
filter(query) {
|
||||
query.where('contact_service', 'Customer');
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Credit note associated GL entries.
|
||||
*/
|
||||
transactions: {
|
||||
relation: Model.HasManyRelation,
|
||||
modelClass: AccountTransaction,
|
||||
join: {
|
||||
from: 'credit_notes.id',
|
||||
to: 'accounts_transactions.referenceId',
|
||||
},
|
||||
filter(builder) {
|
||||
builder.where('reference_type', 'CreditNote');
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Credit note may belongs to branch.
|
||||
*/
|
||||
branch: {
|
||||
relation: Model.BelongsToOneRelation,
|
||||
modelClass: Branch,
|
||||
join: {
|
||||
from: 'credit_notes.branchId',
|
||||
to: 'branches.id',
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Credit note may has associated warehouse.
|
||||
*/
|
||||
warehouse: {
|
||||
relation: Model.BelongsToOneRelation,
|
||||
modelClass: Warehouse,
|
||||
join: {
|
||||
from: 'credit_notes.warehouseId',
|
||||
to: 'warehouses.id',
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Credit note may has many attached attachments.
|
||||
*/
|
||||
attachments: {
|
||||
relation: Model.ManyToManyRelation,
|
||||
modelClass: Document,
|
||||
join: {
|
||||
from: 'credit_notes.id',
|
||||
through: {
|
||||
from: 'document_links.modelId',
|
||||
to: 'document_links.documentId',
|
||||
},
|
||||
to: 'documents.id',
|
||||
},
|
||||
filter(query) {
|
||||
query.where('model_ref', 'CreditNote');
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Credit note may belongs to pdf branding template.
|
||||
*/
|
||||
pdfTemplate: {
|
||||
relation: Model.BelongsToOneRelation,
|
||||
modelClass: PdfTemplateModel,
|
||||
join: {
|
||||
from: 'credit_notes.pdfTemplateId',
|
||||
to: 'pdf_templates.id',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// /**
|
||||
// * Sale invoice meta.
|
||||
// */
|
||||
// static get meta() {
|
||||
// return CreditNoteMeta;
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * Retrieve the default custom views, roles and columns.
|
||||
// */
|
||||
// static get defaultViews() {
|
||||
// return DEFAULT_VIEWS;
|
||||
// }
|
||||
|
||||
/**
|
||||
* Model searchable.
|
||||
*/
|
||||
static get searchable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Model search attributes.
|
||||
*/
|
||||
static get searchRoles() {
|
||||
return [
|
||||
{ fieldKey: 'credit_number', comparator: 'contains' },
|
||||
{ condition: 'or', fieldKey: 'reference_no', comparator: 'contains' },
|
||||
{ condition: 'or', fieldKey: 'amount', comparator: 'equals' },
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevents mutate base currency since the model is not empty.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
static get preventMutateBaseCurrency() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { defaultCreditNoteBrandingAttributes } from '../constants';
|
||||
import { GetPdfTemplateService } from '../../PdfTemplate/queries/GetPdfTemplate.service';
|
||||
import { GetOrganizationBrandingAttributesService } from '../../PdfTemplate/queries/GetOrganizationBrandingAttributes.service';
|
||||
import { mergePdfTemplateWithDefaultAttributes } from '../../SaleInvoices/utils';
|
||||
|
||||
@Injectable()
|
||||
export class CreditNoteBrandingTemplate {
|
||||
constructor(
|
||||
private getPdfTemplateService: GetPdfTemplateService,
|
||||
private getOrgBrandingAttributes: GetOrganizationBrandingAttributesService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Retrieves the credit note branding template.
|
||||
* @param {number} templateId
|
||||
* @returns {}
|
||||
*/
|
||||
public async getCreditNoteBrandingTemplate(templateId: number) {
|
||||
const template =
|
||||
await this.getPdfTemplateService.getPdfTemplate(templateId);
|
||||
|
||||
// Retrieves the organization branding attributes.
|
||||
const commonOrgBrandingAttrs =
|
||||
await this.getOrgBrandingAttributes.getOrganizationBrandingAttributes();
|
||||
|
||||
// Merges the default branding attributes with common organization branding attrs.
|
||||
const organizationBrandingAttrs = {
|
||||
...defaultCreditNoteBrandingAttributes,
|
||||
...commonOrgBrandingAttrs,
|
||||
};
|
||||
const brandingTemplateAttrs = {
|
||||
...template.attributes,
|
||||
companyLogoUri: template.companyLogoUri,
|
||||
};
|
||||
const attributes = mergePdfTemplateWithDefaultAttributes(
|
||||
brandingTemplateAttrs,
|
||||
organizationBrandingAttrs,
|
||||
);
|
||||
return {
|
||||
...template,
|
||||
attributes,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
import { AttachmentTransformer } from "@/modules/Attachments/Attachment.transformer";
|
||||
import { ItemEntryTransformer } from "@/modules/TransactionItemEntry/ItemEntry.transformer";
|
||||
import { Transformer } from "@/modules/Transformer/Transformer";
|
||||
|
||||
export class CreditNoteTransformer extends Transformer {
|
||||
/**
|
||||
* Include these attributes to sale credit note object.
|
||||
* @returns {Array}
|
||||
*/
|
||||
public includeAttributes = (): string[] => {
|
||||
return [
|
||||
'formattedCreditsRemaining',
|
||||
'formattedCreditNoteDate',
|
||||
'formattedCreatedAt',
|
||||
'formattedCreatedAt',
|
||||
'formattedAmount',
|
||||
'formattedCreditsUsed',
|
||||
'formattedSubtotal',
|
||||
|
||||
'discountAmountFormatted',
|
||||
'discountAmountLocalFormatted',
|
||||
|
||||
'discountPercentageFormatted',
|
||||
|
||||
'adjustmentFormatted',
|
||||
'adjustmentLocalFormatted',
|
||||
|
||||
'totalFormatted',
|
||||
'totalLocalFormatted',
|
||||
|
||||
'entries',
|
||||
'attachments',
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve formatted credit note date.
|
||||
* @param {ICreditNote} credit
|
||||
* @returns {String}
|
||||
*/
|
||||
protected formattedCreditNoteDate = (credit): string => {
|
||||
return this.formatDate(credit.creditNoteDate);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve formatted created at date.
|
||||
* @param credit
|
||||
* @returns {string}
|
||||
*/
|
||||
protected formattedCreatedAt = (credit): string => {
|
||||
return this.formatDate(credit.createdAt);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve formatted invoice amount.
|
||||
* @param {ICreditNote} credit
|
||||
* @returns {string}
|
||||
*/
|
||||
protected formattedAmount = (credit): string => {
|
||||
return this.formatNumber(credit.amount, {
|
||||
currencyCode: credit.currencyCode,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve formatted credits remaining.
|
||||
* @param {ICreditNote} credit
|
||||
* @returns {string}
|
||||
*/
|
||||
protected formattedCreditsRemaining = (credit) => {
|
||||
return this.formatNumber(credit.creditsRemaining, {
|
||||
currencyCode: credit.currencyCode,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve formatted credits used.
|
||||
* @param {ICreditNote} credit
|
||||
* @returns {string}
|
||||
*/
|
||||
protected formattedCreditsUsed = (credit) => {
|
||||
return this.formatNumber(credit.creditsUsed, {
|
||||
currencyCode: credit.currencyCode,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the formatted subtotal.
|
||||
* @param {ICreditNote} credit
|
||||
* @returns {string}
|
||||
*/
|
||||
protected formattedSubtotal = (credit): string => {
|
||||
return this.formatNumber(credit.amount, { money: false });
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves formatted discount amount.
|
||||
* @param credit
|
||||
* @returns {string}
|
||||
*/
|
||||
protected discountAmountFormatted = (credit): string => {
|
||||
return this.formatNumber(credit.discountAmount, {
|
||||
currencyCode: credit.currencyCode,
|
||||
excerptZero: true,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the formatted discount amount in local currency.
|
||||
* @param {ICreditNote} credit
|
||||
* @returns {string}
|
||||
*/
|
||||
protected discountAmountLocalFormatted = (credit): string => {
|
||||
return this.formatNumber(credit.discountAmountLocal, {
|
||||
currencyCode: credit.currencyCode,
|
||||
excerptZero: true,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves formatted discount percentage.
|
||||
* @param credit
|
||||
* @returns {string}
|
||||
*/
|
||||
protected discountPercentageFormatted = (credit): string => {
|
||||
return credit.discountPercentage ? `${credit.discountPercentage}%` : '';
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves formatted adjustment amount.
|
||||
* @param credit
|
||||
* @returns {string}
|
||||
*/
|
||||
protected adjustmentFormatted = (credit): string => {
|
||||
return this.formatMoney(credit.adjustment, {
|
||||
currencyCode: credit.currencyCode,
|
||||
excerptZero: true,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the formatted adjustment amount in local currency.
|
||||
* @param {ICreditNote} credit
|
||||
* @returns {string}
|
||||
*/
|
||||
protected adjustmentLocalFormatted = (credit): string => {
|
||||
return this.formatNumber(credit.adjustmentLocal, {
|
||||
currencyCode: this.context.organization.baseCurrency,
|
||||
excerptZero: true,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the formatted total.
|
||||
* @param credit
|
||||
* @returns {string}
|
||||
*/
|
||||
protected totalFormatted = (credit): string => {
|
||||
return this.formatNumber(credit.total, {
|
||||
currencyCode: credit.currencyCode,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the formatted total in local currency.
|
||||
* @param credit
|
||||
* @returns {string}
|
||||
*/
|
||||
protected totalLocalFormatted = (credit): string => {
|
||||
return this.formatNumber(credit.totalLocal, {
|
||||
currencyCode: credit.currencyCode,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the entries of the credit note.
|
||||
* @param {ICreditNote} credit
|
||||
* @returns {}
|
||||
*/
|
||||
protected entries = (credit) => {
|
||||
return this.item(credit.entries, new ItemEntryTransformer(), {
|
||||
currencyCode: credit.currencyCode,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the credit note attachments.
|
||||
* @param {ISaleInvoice} invoice
|
||||
* @returns
|
||||
*/
|
||||
protected attachments = (creditNote) => {
|
||||
return this.item(creditNote.attachments, new AttachmentTransformer());
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
|
||||
import { ERRORS } from '../constants';
|
||||
import { CreditNoteTransformer } from './CreditNoteTransformer';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { CreditNote } from '../models/CreditNote';
|
||||
import { ServiceError } from '@/modules/Items/ServiceError';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
|
||||
@Injectable()
|
||||
export class GetCreditNote {
|
||||
constructor(
|
||||
private readonly transformer: TransformerInjectable,
|
||||
|
||||
@Inject(CreditNote.name)
|
||||
private readonly creditNoteModel: TenantModelProxy<typeof CreditNote>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Retrieve the credit note graph.
|
||||
* @param {number} creditNoteId
|
||||
*/
|
||||
public async getCreditNote(creditNoteId: number) {
|
||||
// Retrieve the vendor credit model graph.
|
||||
const creditNote = await this.creditNoteModel()
|
||||
.query()
|
||||
.findById(creditNoteId)
|
||||
.withGraphFetched('entries.item')
|
||||
.withGraphFetched('customer')
|
||||
.withGraphFetched('branch')
|
||||
.withGraphFetched('attachments');
|
||||
|
||||
if (!creditNote) {
|
||||
throw new ServiceError(ERRORS.CREDIT_NOTE_NOT_FOUND);
|
||||
}
|
||||
// Transforms the credit note model to POJO.
|
||||
return this.transformer.transform(creditNote, new CreditNoteTransformer());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { GetCreditNote } from './GetCreditNote.service';
|
||||
import { CreditNoteBrandingTemplate } from './CreditNoteBrandingTemplate.service';
|
||||
import { transformCreditNoteToPdfTemplate } from '../utils';
|
||||
import { CreditNote } from '../models/CreditNote';
|
||||
import { ChromiumlyTenancy } from '@/modules/ChromiumlyTenancy/ChromiumlyTenancy.service';
|
||||
import { TemplateInjectable } from '@/modules/TemplateInjectable/TemplateInjectable.service';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { PdfTemplateModel } from '@/modules/PdfTemplate/models/PdfTemplate';
|
||||
import { CreditNotePdfTemplateAttributes } from '../types/CreditNotes.types';
|
||||
import { events } from '@/common/events/events';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
|
||||
@Injectable()
|
||||
export class GetCreditNotePdf {
|
||||
/**
|
||||
* @param {ChromiumlyTenancy} chromiumlyTenancy - Chromiumly tenancy service.
|
||||
* @param {TemplateInjectable} templateInjectable - Template injectable service.
|
||||
* @param {GetCreditNote} getCreditNoteService - Get credit note service.
|
||||
* @param {CreditNoteBrandingTemplate} creditNoteBrandingTemplate - Credit note branding template service.
|
||||
* @param {EventEmitter2} eventPublisher - Event publisher service.
|
||||
* @param {typeof CreditNote} creditNoteModel - Credit note model.
|
||||
* @param {typeof PdfTemplateModel} pdfTemplateModel - Pdf template model.
|
||||
*/
|
||||
constructor(
|
||||
private readonly chromiumlyTenancy: ChromiumlyTenancy,
|
||||
private readonly templateInjectable: TemplateInjectable,
|
||||
private readonly getCreditNoteService: GetCreditNote,
|
||||
private readonly creditNoteBrandingTemplate: CreditNoteBrandingTemplate,
|
||||
private readonly eventPublisher: EventEmitter2,
|
||||
|
||||
@Inject(CreditNote.name)
|
||||
private readonly creditNoteModel: TenantModelProxy<typeof CreditNote>,
|
||||
|
||||
@Inject(PdfTemplateModel.name)
|
||||
private readonly pdfTemplateModel: TenantModelProxy<
|
||||
typeof PdfTemplateModel
|
||||
>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Retrieves sale invoice pdf content.
|
||||
* @param {number} creditNoteId - Credit note id.
|
||||
* @returns {Promise<[Buffer, string]>}
|
||||
*/
|
||||
public async getCreditNotePdf(
|
||||
creditNoteId: number,
|
||||
): Promise<[Buffer, string]> {
|
||||
const brandingAttributes =
|
||||
await this.getCreditNoteBrandingAttributes(creditNoteId);
|
||||
const htmlContent = await this.templateInjectable.render(
|
||||
'modules/credit-note-standard',
|
||||
brandingAttributes,
|
||||
);
|
||||
const filename = await this.getCreditNoteFilename(creditNoteId);
|
||||
|
||||
const document =
|
||||
await this.chromiumlyTenancy.convertHtmlContent(htmlContent);
|
||||
const eventPayload = { creditNoteId };
|
||||
|
||||
// Triggers the `onCreditNotePdfViewed` event.
|
||||
await this.eventPublisher.emitAsync(
|
||||
events.creditNote.onPdfViewed,
|
||||
eventPayload,
|
||||
);
|
||||
return [document, filename];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the filename pdf document of the given credit note.
|
||||
* @param {number} creditNoteId
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
public async getCreditNoteFilename(creditNoteId: number): Promise<string> {
|
||||
const creditNote = await this.creditNoteModel()
|
||||
.query()
|
||||
.findById(creditNoteId);
|
||||
return `Credit-${creditNote.creditNoteNumber}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves credit note branding attributes.
|
||||
* @param {number} creditNoteId - The ID of the credit note.
|
||||
* @returns {Promise<CreditNotePdfTemplateAttributes>} The credit note branding attributes.
|
||||
*/
|
||||
public async getCreditNoteBrandingAttributes(
|
||||
creditNoteId: number,
|
||||
): Promise<CreditNotePdfTemplateAttributes> {
|
||||
const creditNote =
|
||||
await this.getCreditNoteService.getCreditNote(creditNoteId);
|
||||
|
||||
// Retrieve the invoice template id of not found get the default template id.
|
||||
const templateId =
|
||||
creditNote.pdfTemplateId ??
|
||||
(
|
||||
await this.pdfTemplateModel().query().findOne({
|
||||
resource: 'CreditNote',
|
||||
default: true,
|
||||
})
|
||||
)?.id;
|
||||
// Retrieves the credit note branding template.
|
||||
const brandingTemplate =
|
||||
await this.creditNoteBrandingTemplate.getCreditNoteBrandingTemplate(
|
||||
templateId,
|
||||
);
|
||||
return {
|
||||
...brandingTemplate.attributes,
|
||||
...transformCreditNoteToPdfTemplate(creditNote),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { ICreditNoteState } from '../types/CreditNotes.types';
|
||||
import { PdfTemplateModel } from '@/modules/PdfTemplate/models/PdfTemplate';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
|
||||
@Injectable()
|
||||
export class GetCreditNoteState {
|
||||
constructor(
|
||||
@Inject(PdfTemplateModel.name)
|
||||
private pdfTemplateModel: TenantModelProxy<typeof PdfTemplateModel>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Retrieves the create/edit initial state of the payment received.
|
||||
* @return {Promise<ICreditNoteState>}
|
||||
*/
|
||||
public async getCreditNoteState(): Promise<ICreditNoteState> {
|
||||
const defaultPdfTemplate = await this.pdfTemplateModel()
|
||||
.query()
|
||||
.findOne({ resource: 'CreditNote' })
|
||||
.modify('default');
|
||||
|
||||
return {
|
||||
defaultTemplateId: defaultPdfTemplate?.id,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import * as R from 'ramda';
|
||||
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
|
||||
import { DynamicListService } from '@/modules/DynamicListing/DynamicList.service';
|
||||
import {
|
||||
GetCreditNotesResponse,
|
||||
ICreditNotesQueryDTO,
|
||||
} from '../types/CreditNotes.types';
|
||||
import { CreditNote } from '../models/CreditNote';
|
||||
import { CreditNoteTransformer } from './CreditNoteTransformer';
|
||||
import { Inject } from '@nestjs/common';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
|
||||
|
||||
@Injectable()
|
||||
export class GetCreditNotesService {
|
||||
constructor(
|
||||
private readonly dynamicListService: DynamicListService,
|
||||
private readonly transformer: TransformerInjectable,
|
||||
|
||||
@Inject(CreditNote.name)
|
||||
private readonly creditNoteModel: TenantModelProxy<typeof CreditNote>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Parses the sale invoice list filter DTO.
|
||||
* @param filterDTO
|
||||
* @returns
|
||||
*/
|
||||
private parseListFilterDTO = (filterDTO) => {
|
||||
return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO);
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the paginated and filterable credit notes list.
|
||||
* @param {number} tenantId -
|
||||
* @param {ICreditNotesQueryDTO} creditNotesQuery -
|
||||
*/
|
||||
public async getCreditNotesList(
|
||||
creditNotesQuery: ICreditNotesQueryDTO,
|
||||
): Promise<GetCreditNotesResponse> {
|
||||
// Parses stringified filter roles.
|
||||
const filter = this.parseListFilterDTO(creditNotesQuery);
|
||||
|
||||
// Dynamic list service.
|
||||
const dynamicFilter = await this.dynamicListService.dynamicList(
|
||||
this.creditNoteModel(),
|
||||
filter,
|
||||
);
|
||||
const { results, pagination } = await this.creditNoteModel()
|
||||
.query()
|
||||
.onBuild((builder) => {
|
||||
builder.withGraphFetched('entries.item');
|
||||
builder.withGraphFetched('customer');
|
||||
|
||||
dynamicFilter.buildQuery()(builder);
|
||||
creditNotesQuery?.filterQuery?.(builder as any);
|
||||
})
|
||||
.pagination(filter.page - 1, filter.pageSize);
|
||||
|
||||
// Transforomes the credit notes to POJO.
|
||||
const creditNotes = await this.transformer.transform(
|
||||
results,
|
||||
new CreditNoteTransformer(),
|
||||
);
|
||||
|
||||
return {
|
||||
creditNotes,
|
||||
pagination,
|
||||
filterMeta: dynamicFilter.getResponseMeta(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Transformer } from "@/modules/Transformer/Transformer";
|
||||
|
||||
export class RefundCreditNoteTransformer extends Transformer{
|
||||
/**
|
||||
* Includeded attributes.
|
||||
* @returns {string[]}
|
||||
*/
|
||||
public includeAttributes = (): string[] => {
|
||||
return ['formttedAmount', 'formattedDate'];
|
||||
};
|
||||
|
||||
/**
|
||||
* Formatted amount.
|
||||
* @returns {string}
|
||||
*/
|
||||
protected formttedAmount = (item) => {
|
||||
return this.formatNumber(item.amount, {
|
||||
currencyCode: item.currencyCode,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Formatted date.
|
||||
* @returns {string}
|
||||
*/
|
||||
protected formattedDate = (item) => {
|
||||
return this.formatDate(item.date);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// import { Service, Inject } from 'typedi';
|
||||
// import events from '@/subscribers/events';
|
||||
// import BaseCreditNotes from '../commands/CommandCreditNoteDTOTransform.service';
|
||||
// import { ICreditNoteCreatedPayload } from '@/interfaces';
|
||||
|
||||
// @Service()
|
||||
// export default class CreditNoteAutoSerialSubscriber {
|
||||
// @Inject()
|
||||
// creditNotesService: BaseCreditNotes;
|
||||
|
||||
// /**
|
||||
// * Attaches events with handlers.
|
||||
// */
|
||||
// public attach(bus) {
|
||||
// bus.subscribe(
|
||||
// events.creditNote.onCreated,
|
||||
// this.autoSerialIncrementOnceCreated
|
||||
// );
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * Auto serial increment once credit note created.
|
||||
// * @param {ICreditNoteCreatedPayload} payload -
|
||||
// */
|
||||
// private autoSerialIncrementOnceCreated = async ({
|
||||
// tenantId,
|
||||
// }: ICreditNoteCreatedPayload) => {
|
||||
// await this.creditNotesService.incrementSerialNumber(tenantId);
|
||||
// };
|
||||
// }
|
||||
@@ -0,0 +1,78 @@
|
||||
import {
|
||||
ICreditNoteCreatedPayload,
|
||||
ICreditNoteDeletedPayload,
|
||||
ICreditNoteEditedPayload,
|
||||
ICreditNoteOpenedPayload,
|
||||
} from '../types/CreditNotes.types';
|
||||
import { CreditNoteGLEntries } from '../commands/CreditNoteGLEntries';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { events } from '@/common/events/events';
|
||||
|
||||
@Injectable()
|
||||
export class CreditNoteGLEntriesSubscriber {
|
||||
constructor(private readonly creditNoteGLEntries: CreditNoteGLEntries) {}
|
||||
|
||||
/**
|
||||
* Writes the GL entries once the credit note transaction created or open.
|
||||
* @param {ICreditNoteCreatedPayload|ICreditNoteOpenedPayload} payload -
|
||||
*/
|
||||
@OnEvent(events.creditNote.onCreated)
|
||||
public async writeGlEntriesOnceCreditNoteCreated({
|
||||
creditNote,
|
||||
trx,
|
||||
}: ICreditNoteCreatedPayload | ICreditNoteOpenedPayload) {
|
||||
// Can't continue if the credit note is not published yet.
|
||||
if (!creditNote.isPublished) return;
|
||||
|
||||
await this.creditNoteGLEntries.createVendorCreditGLEntries(
|
||||
creditNote.id,
|
||||
trx,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the GL entries once the vendor credit transaction opened.
|
||||
* @param {ICreditNoteOpenedPayload} payload
|
||||
*/
|
||||
@OnEvent(events.creditNote.onOpened)
|
||||
public async writeGLEntriesOnceCreditNoteOpened({
|
||||
creditNote,
|
||||
trx,
|
||||
}: ICreditNoteOpenedPayload) {
|
||||
await this.creditNoteGLEntries.createVendorCreditGLEntries(
|
||||
creditNote.id,
|
||||
trx,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverts GL entries once credit note deleted.
|
||||
*/
|
||||
@OnEvent(events.creditNote.onDeleted)
|
||||
public async revertGLEntriesOnceCreditNoteDeleted({
|
||||
oldCreditNote,
|
||||
creditNoteId,
|
||||
trx,
|
||||
}: ICreditNoteDeletedPayload) {
|
||||
// Can't continue if the credit note is not published yet.
|
||||
if (!oldCreditNote.isPublished) return;
|
||||
|
||||
await this.creditNoteGLEntries.revertVendorCreditGLEntries(creditNoteId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Edits vendor credit associated GL entries once the transaction edited.
|
||||
* @param {ICreditNoteEditedPayload} payload -
|
||||
*/
|
||||
@OnEvent(events.creditNote.onEdited)
|
||||
public async editVendorCreditGLEntriesOnceEdited({
|
||||
creditNote,
|
||||
trx,
|
||||
}: ICreditNoteEditedPayload) {
|
||||
// Can't continue if the credit note is not published yet.
|
||||
if (!creditNote.isPublished) return;
|
||||
|
||||
await this.creditNoteGLEntries.editVendorCreditGLEntries(creditNote.id, trx);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import {
|
||||
ICreditNoteCreatedPayload,
|
||||
ICreditNoteDeletedPayload,
|
||||
ICreditNoteEditedPayload,
|
||||
} from '../types/CreditNotes.types';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
import { events } from '@/common/events/events';
|
||||
import { CreditNoteInventoryTransactions } from '../commands/CreditNotesInventoryTransactions';
|
||||
|
||||
@Injectable()
|
||||
export class CreditNoteInventoryTransactionsSubscriber {
|
||||
constructor(
|
||||
private readonly inventoryTransactions: CreditNoteInventoryTransactions,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Writes inventory transactions once credit note created.
|
||||
* @param {ICreditNoteCreatedPayload} payload -
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
@OnEvent(events.creditNote.onCreated)
|
||||
@OnEvent(events.creditNote.onOpened)
|
||||
public async writeInventoryTranscationsOnceCreated({
|
||||
creditNote,
|
||||
trx,
|
||||
}: ICreditNoteCreatedPayload) {
|
||||
// Can't continue if the credit note is open yet.
|
||||
if (!creditNote.isOpen) return;
|
||||
|
||||
await this.inventoryTransactions.createInventoryTransactions(
|
||||
creditNote,
|
||||
trx,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewrites inventory transactions once credit note edited.
|
||||
* @param {ICreditNoteEditedPayload} payload -
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
@OnEvent(events.creditNote.onEdited)
|
||||
public async rewriteInventoryTransactionsOnceEdited({
|
||||
creditNote,
|
||||
trx,
|
||||
}: ICreditNoteEditedPayload) {
|
||||
// Can't continue if the credit note is open yet.
|
||||
if (!creditNote.isOpen) return;
|
||||
|
||||
await this.inventoryTransactions.editInventoryTransactions(
|
||||
creditNote.id,
|
||||
creditNote,
|
||||
trx,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverts inventory transactions once credit note deleted.
|
||||
* @param {ICreditNoteDeletedPayload} payload -
|
||||
*/
|
||||
@OnEvent(events.creditNote.onDeleted)
|
||||
public async revertInventoryTransactionsOnceDeleted({
|
||||
oldCreditNote,
|
||||
trx,
|
||||
}: ICreditNoteDeletedPayload) {
|
||||
// Can't continue if the credit note is open yet.
|
||||
if (!oldCreditNote.isOpen) return;
|
||||
|
||||
await this.inventoryTransactions.deleteInventoryTransactions(
|
||||
oldCreditNote.id,
|
||||
trx,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// import { Inject, Service } from 'typedi';
|
||||
// import { ServiceError } from '@/exceptions';
|
||||
// import TenancyService from '@/services/Tenancy/TenancyService';
|
||||
// import events from '@/subscribers/events';
|
||||
// import { ICustomerDeletingPayload } from '@/interfaces';
|
||||
// import DeleteCustomerLinkedCreidtNote from '../commands/DeleteCustomerLinkedCreditNote.service';
|
||||
|
||||
// const ERRORS = {
|
||||
// CUSTOMER_HAS_TRANSACTIONS: 'CUSTOMER_HAS_TRANSACTIONS',
|
||||
// };
|
||||
|
||||
// @Service()
|
||||
// export default class DeleteCustomerLinkedCreditSubscriber {
|
||||
// @Inject()
|
||||
// tenancy: TenancyService;
|
||||
|
||||
// @Inject()
|
||||
// deleteCustomerLinkedCredit: DeleteCustomerLinkedCreidtNote;
|
||||
|
||||
// /**
|
||||
// * Attaches events with handlers.
|
||||
// * @param bus
|
||||
// */
|
||||
// public attach = (bus) => {
|
||||
// bus.subscribe(
|
||||
// events.customers.onDeleting,
|
||||
// this.validateCustomerHasNoLinkedCreditsOnDeleting
|
||||
// );
|
||||
// };
|
||||
|
||||
// /**
|
||||
// * Validate vendor has no associated credit transaction once the vendor deleting.
|
||||
// * @param {IVendorEventDeletingPayload} payload -
|
||||
// */
|
||||
// public validateCustomerHasNoLinkedCreditsOnDeleting = async ({
|
||||
// tenantId,
|
||||
// customerId,
|
||||
// }: ICustomerDeletingPayload) => {
|
||||
// try {
|
||||
// await this.deleteCustomerLinkedCredit.validateCustomerHasNoCreditTransaction(
|
||||
// tenantId,
|
||||
// customerId
|
||||
// );
|
||||
// } catch (error) {
|
||||
// throw new ServiceError(ERRORS.CUSTOMER_HAS_TRANSACTIONS);
|
||||
// }
|
||||
// };
|
||||
// }
|
||||
@@ -0,0 +1,61 @@
|
||||
// import { Service, Inject } from 'typedi';
|
||||
// import events from '@/subscribers/events';
|
||||
// import RefundCreditNoteGLEntries from '../commands/RefundCreditNoteGLEntries';
|
||||
// import {
|
||||
// IRefundCreditNoteCreatedPayload,
|
||||
// IRefundCreditNoteDeletedPayload,
|
||||
// } from '@/interfaces';
|
||||
|
||||
// @Service()
|
||||
// export default class RefundCreditNoteGLEntriesSubscriber {
|
||||
// @Inject()
|
||||
// refundCreditGLEntries: RefundCreditNoteGLEntries;
|
||||
|
||||
// /**
|
||||
// * Attaches events with handlers.
|
||||
// */
|
||||
// public attach = (bus) => {
|
||||
// bus.subscribe(
|
||||
// events.creditNote.onRefundCreated,
|
||||
// this.writeRefundCreditGLEntriesOnceCreated
|
||||
// );
|
||||
// bus.subscribe(
|
||||
// events.creditNote.onRefundDeleted,
|
||||
// this.revertRefundCreditGLEntriesOnceDeleted
|
||||
// );
|
||||
// };
|
||||
|
||||
// /**
|
||||
// * Writes refund credit note GL entries once the transaction created.
|
||||
// * @param {IRefundCreditNoteCreatedPayload} payload -
|
||||
// */
|
||||
// private writeRefundCreditGLEntriesOnceCreated = async ({
|
||||
// trx,
|
||||
// refundCreditNote,
|
||||
// creditNote,
|
||||
// tenantId,
|
||||
// }: IRefundCreditNoteCreatedPayload) => {
|
||||
// await this.refundCreditGLEntries.createRefundCreditGLEntries(
|
||||
// tenantId,
|
||||
// refundCreditNote.id,
|
||||
// trx
|
||||
// );
|
||||
// };
|
||||
|
||||
// /**
|
||||
// * Reverts refund credit note GL entries once the transaction deleted.
|
||||
// * @param {IRefundCreditNoteDeletedPayload} payload -
|
||||
// */
|
||||
// private revertRefundCreditGLEntriesOnceDeleted = async ({
|
||||
// trx,
|
||||
// refundCreditId,
|
||||
// oldRefundCredit,
|
||||
// tenantId,
|
||||
// }: IRefundCreditNoteDeletedPayload) => {
|
||||
// await this.refundCreditGLEntries.revertRefundCreditGLEntries(
|
||||
// tenantId,
|
||||
// refundCreditId,
|
||||
// trx
|
||||
// );
|
||||
// };
|
||||
// }
|
||||
@@ -0,0 +1,62 @@
|
||||
// import { Inject, Service } from 'typedi';
|
||||
// import {
|
||||
// IRefundCreditNoteCreatedPayload,
|
||||
// IRefundCreditNoteDeletedPayload,
|
||||
// } from '@/interfaces';
|
||||
// import events from '@/subscribers/events';
|
||||
// import RefundSyncCreditNoteBalance from '../commands/RefundSyncCreditNoteBalance';
|
||||
|
||||
// @Service()
|
||||
// export default class RefundSyncCreditNoteBalanceSubscriber {
|
||||
// @Inject()
|
||||
// refundSyncCreditBalance: RefundSyncCreditNoteBalance;
|
||||
|
||||
// /**
|
||||
// * Attaches events with handlers.
|
||||
// */
|
||||
// attach(bus) {
|
||||
// bus.subscribe(
|
||||
// events.creditNote.onRefundCreated,
|
||||
// this.incrementRefundedAmountOnceRefundCreated
|
||||
// );
|
||||
// bus.subscribe(
|
||||
// events.creditNote.onRefundDeleted,
|
||||
// this.decrementRefundedAmountOnceRefundDeleted
|
||||
// );
|
||||
// return bus;
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * Increment credit note refunded amount once associated refund transaction created.
|
||||
// * @param {IRefundCreditNoteCreatedPayload} payload -
|
||||
// */
|
||||
// private incrementRefundedAmountOnceRefundCreated = async ({
|
||||
// trx,
|
||||
// refundCreditNote,
|
||||
// tenantId,
|
||||
// }: IRefundCreditNoteCreatedPayload) => {
|
||||
// await this.refundSyncCreditBalance.incrementCreditNoteRefundAmount(
|
||||
// tenantId,
|
||||
// refundCreditNote.creditNoteId,
|
||||
// refundCreditNote.amount,
|
||||
// trx
|
||||
// );
|
||||
// };
|
||||
|
||||
// /**
|
||||
// * Decrement credit note refunded amount once associated refuned transaction deleted.
|
||||
// * @param {IRefundCreditNoteDeletedPayload} payload -
|
||||
// */
|
||||
// private decrementRefundedAmountOnceRefundDeleted = async ({
|
||||
// trx,
|
||||
// oldRefundCredit,
|
||||
// tenantId,
|
||||
// }: IRefundCreditNoteDeletedPayload) => {
|
||||
// await this.refundSyncCreditBalance.decrementCreditNoteRefundAmount(
|
||||
// tenantId,
|
||||
// oldRefundCredit.creditNoteId,
|
||||
// oldRefundCredit.amount,
|
||||
// trx
|
||||
// );
|
||||
// };
|
||||
// }
|
||||
@@ -0,0 +1,159 @@
|
||||
import { Knex } from 'knex';
|
||||
import { CreditNote } from '../models/CreditNote';
|
||||
import { IFilterMeta, IPaginationMeta } from '@/interfaces/Model';
|
||||
import { IDynamicListFilter } from '@/modules/DynamicListing/DynamicFilter/DynamicFilter.types';
|
||||
import { ILedgerEntry } from '@/modules/Ledger/types/Ledger.types';
|
||||
import { CreateCreditNoteDto, EditCreditNoteDto } from '../dtos/CreditNote.dto';
|
||||
|
||||
export enum CreditNoteAction {
|
||||
Create = 'Create',
|
||||
Edit = 'Edit',
|
||||
Delete = 'Delete',
|
||||
View = 'View',
|
||||
Refund = 'Refund',
|
||||
}
|
||||
|
||||
export interface ICreditNoteDeletingPayload {
|
||||
tenantId: number;
|
||||
oldCreditNote: CreditNote;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface ICreditNoteDeletedPayload {
|
||||
tenantId: number;
|
||||
oldCreditNote: CreditNote;
|
||||
creditNoteId: number;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface ICreditNoteEditingPayload {
|
||||
oldCreditNote: CreditNote;
|
||||
creditNoteEditDTO: EditCreditNoteDto;
|
||||
trx?: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface ICreditNoteEditedPayload {
|
||||
trx?: Knex.Transaction;
|
||||
oldCreditNote: CreditNote;
|
||||
creditNote: CreditNote;
|
||||
creditNoteEditDTO: EditCreditNoteDto;
|
||||
}
|
||||
|
||||
export interface ICreditNoteCreatedPayload {
|
||||
creditNoteDTO: CreateCreditNoteDto;
|
||||
creditNote: CreditNote;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface ICreditNoteCreatingPayload {
|
||||
creditNoteDTO: CreateCreditNoteDto;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface ICreditNoteOpeningPayload {
|
||||
oldCreditNote: CreditNote;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
|
||||
export interface ICreditNoteOpenedPayload {
|
||||
creditNote: CreditNote;
|
||||
oldCreditNote: CreditNote;
|
||||
trx: Knex.Transaction;
|
||||
}
|
||||
export interface ICreditNotesQueryDTO extends IDynamicListFilter {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
searchKeyword?: string;
|
||||
filterQuery?: (builder: Knex.QueryBuilder) => void;
|
||||
}
|
||||
|
||||
export interface ICreditNoteRefundDTO {
|
||||
fromAccountId: number;
|
||||
amount: number;
|
||||
exchangeRate?: number;
|
||||
referenceNo: string;
|
||||
description: string;
|
||||
date: Date;
|
||||
branchId?: number;
|
||||
}
|
||||
|
||||
export type ICreditNoteGLCommonEntry = Pick<
|
||||
ILedgerEntry,
|
||||
| 'date'
|
||||
| 'userId'
|
||||
| 'currencyCode'
|
||||
| 'exchangeRate'
|
||||
| 'transactionType'
|
||||
| 'transactionId'
|
||||
| 'transactionNumber'
|
||||
| 'referenceNumber'
|
||||
| 'createdAt'
|
||||
| 'indexGroup'
|
||||
| 'credit'
|
||||
| 'debit'
|
||||
| 'branchId'
|
||||
>;
|
||||
|
||||
export interface GetCreditNotesResponse {
|
||||
creditNotes: CreditNote[];
|
||||
pagination: IPaginationMeta;
|
||||
filterMeta: IFilterMeta;
|
||||
}
|
||||
|
||||
export interface CreditNotePdfTemplateAttributes {
|
||||
// # Primary color
|
||||
primaryColor: string;
|
||||
secondaryColor: string;
|
||||
|
||||
// # Company logo
|
||||
showCompanyLogo: boolean;
|
||||
companyLogo: string;
|
||||
|
||||
// # Company name
|
||||
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: Array<{
|
||||
item: string;
|
||||
description: string;
|
||||
rate: string;
|
||||
quantity: string;
|
||||
total: string;
|
||||
}>;
|
||||
|
||||
showCreditNoteNumber: boolean;
|
||||
creditNoteNumberLabel: string;
|
||||
creditNoteNumebr: string;
|
||||
|
||||
creditNoteDate: string;
|
||||
showCreditNoteDate: boolean;
|
||||
creditNoteDateLabel: string;
|
||||
}
|
||||
|
||||
export interface ICreditNoteState {
|
||||
defaultTemplateId: number;
|
||||
}
|
||||
26
packages/server/src/modules/CreditNotes/utils.ts
Normal file
26
packages/server/src/modules/CreditNotes/utils.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
// @ts-nocheck
|
||||
import { CreditNotePdfTemplateAttributes, ICreditNote } from '@/interfaces';
|
||||
import { contactAddressTextFormat } from '@/utils/address-text-format';
|
||||
|
||||
export const transformCreditNoteToPdfTemplate = (
|
||||
creditNote: ICreditNote
|
||||
): Partial<CreditNotePdfTemplateAttributes> => {
|
||||
return {
|
||||
creditNoteDate: creditNote.formattedCreditNoteDate,
|
||||
creditNoteNumebr: creditNote.creditNoteNumber,
|
||||
|
||||
total: creditNote.formattedAmount,
|
||||
subtotal: creditNote.formattedSubtotal,
|
||||
|
||||
lines: creditNote.entries?.map((entry) => ({
|
||||
item: entry.item.name,
|
||||
description: entry.description,
|
||||
rate: entry.rateFormatted,
|
||||
quantity: entry.quantityFormatted,
|
||||
total: entry.totalFormatted,
|
||||
})),
|
||||
customerNote: creditNote.note,
|
||||
termsConditions: creditNote.termsConditions,
|
||||
customerAddress: contactAddressTextFormat(creditNote.customer),
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user