feat: migrate manual journal to nestjs

This commit is contained in:
Ahmed Bouhuolia
2024-12-26 20:57:21 +02:00
parent cd84872a61
commit 736cedd63d
34 changed files with 2129 additions and 69 deletions

View File

@@ -45,6 +45,7 @@ import { SaleEstimatesModule } from '../SaleEstimates/SaleEstimates.module';
import { BillsModule } from '../Bills/Bills.module';
import { SaleInvoicesModule } from '../SaleInvoices/SaleInvoices.module';
import { SaleReceiptsModule } from '../SaleReceipts/SaleReceipts.module';
import { ManualJournalsModule } from '../ManualJournals/ManualJournals.module';
// import { BillPaymentsModule } from '../BillPayments/BillPayments.module';
@Module({
@@ -115,6 +116,7 @@ import { SaleReceiptsModule } from '../SaleReceipts/SaleReceipts.module';
SaleEstimatesModule,
SaleReceiptsModule,
BillsModule,
ManualJournalsModule
// BillPaymentsModule,
],
controllers: [AppController],

View File

@@ -14,6 +14,7 @@ import { BranchesApplication } from './BranchesApplication.service';
import { BranchesSettingsService } from './BranchesSettings';
import { BranchCommandValidator } from './commands/BranchCommandValidator.service';
import { BranchTransactionDTOTransformer } from './integrations/BranchTransactionDTOTransform';
import { ManualJournalBranchesDTOTransformer } from './integrations/ManualJournals/ManualJournalDTOTransformer.service';
@Module({
imports: [TenancyDatabaseModule],
@@ -31,8 +32,13 @@ import { BranchTransactionDTOTransformer } from './integrations/BranchTransactio
TenancyContext,
TransformerInjectable,
BranchCommandValidator,
BranchTransactionDTOTransformer
BranchTransactionDTOTransformer,
ManualJournalBranchesDTOTransformer,
],
exports: [
BranchesSettingsService,
BranchTransactionDTOTransformer,
ManualJournalBranchesDTOTransformer,
],
exports: [BranchTransactionDTOTransformer],
})
export class BranchesModule {}

View File

@@ -0,0 +1,37 @@
import { Inject, Injectable } from '@nestjs/common';
import { omit } from 'lodash';
import { BranchesSettingsService } from '../../BranchesSettings';
import { IManualJournalDTO } from '@/modules/ManualJournals/types/ManualJournals.types';
@Injectable()
export class ManualJournalBranchesDTOTransformer {
constructor(
@Inject()
private readonly branchesSettings: BranchesSettingsService,
) {}
/**
*
* @param DTO
* @returns
*/
private excludeDTOBranchIdWhenInactive = (
DTO: IManualJournalDTO,
): IManualJournalDTO => {
const isActive = this.branchesSettings.isMultiBranchesActive();
if (isActive) return DTO;
return {
...DTO,
entries: DTO.entries.map((e) => omit(e, ['branchId'])),
};
};
/**
*
*/
public transformDTO = (DTO: IManualJournalDTO): IManualJournalDTO => {
return this.excludeDTOBranchIdWhenInactive(DTO);
};
}

View File

@@ -1,32 +0,0 @@
// import { omit } from 'lodash';
// import { Inject, Service } from 'typedi';
// import { IManualJournal } from '@/interfaces';
// import { BranchesSettings } from '../../BranchesSettings';
// @Service()
// export class ManualJournalBranchesDTOTransformer {
// @Inject()
// branchesSettings: BranchesSettings;
// private excludeDTOBranchIdWhenInactive = (
// tenantId: number,
// DTO: IManualJournal
// ): IManualJournal => {
// const isActive = this.branchesSettings.isMultiBranchesActive(tenantId);
// if (isActive) return DTO;
// return {
// ...DTO,
// entries: DTO.entries.map((e) => omit(e, ['branchId'])),
// };
// };
// /**
// *
// */
// public transformDTO =
// (tenantId: number) =>
// (DTO: IManualJournal): IManualJournal => {
// return this.excludeDTOBranchIdWhenInactive(tenantId, DTO);
// };
// }

View File

@@ -132,13 +132,13 @@ export class Contact extends BaseModel {
* Relationship mapping.
*/
static get relationMappings() {
const SaleEstimate = require('models/SaleEstimate');
const SaleReceipt = require('models/SaleReceipt');
const SaleInvoice = require('models/SaleInvoice');
const PaymentReceive = require('models/PaymentReceive');
const Bill = require('models/Bill');
const BillPayment = require('models/BillPayment');
const AccountTransaction = require('models/AccountTransaction');
const { SaleEstimate } = require('../../SaleEstimates/models/SaleEstimate');
const { SaleReceipt } = require('../../SaleReceipts/models/SaleReceipt');
const { SaleInvoice } = require('../../SaleInvoices/models/SaleInvoice');
const { PaymentReceived } = require('../../PaymentReceived/models/PaymentReceived');
const { Bill } = require('../../Bills/models/Bill');
const { BillPayment } = require('../../BillPayments/models/BillPayment');
const { AccountTransaction } = require('../../Accounts/models/AccountTransaction.model');
return {
/**
@@ -146,7 +146,7 @@ export class Contact extends BaseModel {
*/
salesInvoices: {
relation: Model.HasManyRelation,
modelClass: SaleInvoice.default,
modelClass: SaleInvoice,
join: {
from: 'contacts.id',
to: 'sales_invoices.customerId',
@@ -158,7 +158,7 @@ export class Contact extends BaseModel {
*/
salesEstimates: {
relation: Model.HasManyRelation,
modelClass: SaleEstimate.default,
modelClass: SaleEstimate,
join: {
from: 'contacts.id',
to: 'sales_estimates.customerId',
@@ -170,7 +170,7 @@ export class Contact extends BaseModel {
*/
salesReceipts: {
relation: Model.HasManyRelation,
modelClass: SaleReceipt.default,
modelClass: SaleReceipt,
join: {
from: 'contacts.id',
to: 'sales_receipts.customerId',
@@ -182,7 +182,7 @@ export class Contact extends BaseModel {
*/
paymentReceives: {
relation: Model.HasManyRelation,
modelClass: PaymentReceive.default,
modelClass: PaymentReceived,
join: {
from: 'contacts.id',
to: 'payment_receives.customerId',
@@ -194,7 +194,7 @@ export class Contact extends BaseModel {
*/
bills: {
relation: Model.HasManyRelation,
modelClass: Bill.default,
modelClass: Bill,
join: {
from: 'contacts.id',
to: 'bills.vendorId',
@@ -206,7 +206,7 @@ export class Contact extends BaseModel {
*/
billPayments: {
relation: Model.HasManyRelation,
modelClass: BillPayment.default,
modelClass: BillPayment,
join: {
from: 'contacts.id',
to: 'bills_payments.vendorId',
@@ -218,7 +218,7 @@ export class Contact extends BaseModel {
*/
accountsTransactions: {
relation: Model.HasManyRelation,
modelClass: AccountTransaction.default,
modelClass: AccountTransaction,
join: {
from: 'contacts.id',
to: 'accounts_transactions.contactId',

View File

@@ -30,9 +30,9 @@ export class CommandExpenseValidator {
/**
* Retrieve expense accounts or throw error in case one of the given accounts
* not found not the storage.
* @param {number} tenantId
* @param {number} expenseAccountsIds
* @throws {ServiceError}
* @param {Array<Account>} tenantId
* @param {number} expenseAccountsIds
* @throws {ServiceError}
* @returns {Promise<IAccount[]>}
*/
public validateExpensesAccountsExistance(

View File

@@ -6,7 +6,7 @@ import { CustomersImportable } from '../Contacts/Customers/CustomersImportable';
import { VendorsImportable } from '../Contacts/Vendors/VendorsImportable';
import { ItemsImportable } from '../Items/ItemsImportable';
import { ItemCategoriesImportable } from '../ItemCategories/ItemCategoriesImportable';
import { ManualJournalImportable } from '../ManualJournals/ManualJournalsImport';
import { ManualJournalImportable } from '../ManualJournals/commands/ManualJournalsImport';
import { BillsImportable } from '../Purchases/Bills/BillsImportable';
import { ExpensesImportable } from '../Expenses/ExpensesImportable';
import { SaleInvoicesImportable } from '../Sales/Invoices/SaleInvoicesImportable';

View File

@@ -0,0 +1,49 @@
import {
Body,
Controller,
Delete,
Get,
Param,
Post,
Put,
} from '@nestjs/common';
import { ManualJournalsApplication } from './ManualJournalsApplication.service';
import { IManualJournalDTO } from './types/ManualJournals.types';
import { PublicRoute } from '../Auth/Jwt.guard';
@Controller('manual-journals')
@PublicRoute()
export class ManualJournalsController {
constructor(private manualJournalsApplication: ManualJournalsApplication) {}
@Post()
public createManualJournal(@Body() manualJournalDTO: IManualJournalDTO) {
return this.manualJournalsApplication.createManualJournal(manualJournalDTO);
}
@Put(':id')
public editManualJournal(
@Param('id') manualJournalId: number,
@Body() manualJournalDTO: IManualJournalDTO,
) {
return this.manualJournalsApplication.editManualJournal(
manualJournalId,
manualJournalDTO,
);
}
@Delete(':id')
public deleteManualJournal(@Param('id') manualJournalId: number) {
return this.manualJournalsApplication.deleteManualJournal(manualJournalId);
}
@Post(':id/publish')
public publishManualJournal(@Param('id') manualJournalId: number) {
return this.manualJournalsApplication.publishManualJournal(manualJournalId);
}
@Get(':id')
public getManualJournal(@Param('id') manualJournalId: number) {
return this.manualJournalsApplication.getManualJournal(manualJournalId);
}
}

View File

@@ -0,0 +1,34 @@
import { Module } from '@nestjs/common';
import { CreateManualJournalService } from './commands/CreateManualJournal.service';
import { EditManualJournal } from './commands/EditManualJournal.service';
import { DeleteManualJournalService } from './commands/DeleteManualJournal.service';
import { PublishManualJournal } from './commands/PublishManualJournal.service';
import { CommandManualJournalValidators } from './commands/CommandManualJournalValidators.service';
import { AutoIncrementManualJournal } from './commands/AutoIncrementManualJournal.service';
import { ManualJournalBranchesDTOTransformer } from '../Branches/integrations/ManualJournals/ManualJournalDTOTransformer.service';
import { TenancyContext } from '../Tenancy/TenancyContext.service';
import { AutoIncrementOrdersService } from '../AutoIncrementOrders/AutoIncrementOrders.service';
import { BranchesModule } from '../Branches/Branches.module';
import { ManualJournalsController } from './ManualJournals.controller';
import { ManualJournalsApplication } from './ManualJournalsApplication.service';
import { GetManualJournal } from './queries/GetManualJournal.service';
@Module({
imports: [BranchesModule],
controllers: [ManualJournalsController],
providers: [
TenancyContext,
CreateManualJournalService,
EditManualJournal,
DeleteManualJournalService,
PublishManualJournal,
CommandManualJournalValidators,
AutoIncrementManualJournal,
CommandManualJournalValidators,
ManualJournalBranchesDTOTransformer,
AutoIncrementOrdersService,
ManualJournalsApplication,
GetManualJournal
],
})
export class ManualJournalsModule {}

View File

@@ -0,0 +1,89 @@
import { Injectable } from '@nestjs/common';
import { CreateManualJournalService } from './commands/CreateManualJournal.service';
import { EditManualJournal } from './commands/EditManualJournal.service';
import { PublishManualJournal } from './commands/PublishManualJournal.service';
import { GetManualJournal } from './queries/GetManualJournal.service';
import { DeleteManualJournalService } from './commands/DeleteManualJournal.service';
import { IManualJournalDTO, } from './types/ManualJournals.types';
// import { GetManualJournals } from './queries/GetManualJournals';
@Injectable()
export class ManualJournalsApplication {
constructor(
private createManualJournalService: CreateManualJournalService,
private editManualJournalService: EditManualJournal,
private deleteManualJournalService: DeleteManualJournalService,
private publishManualJournalService: PublishManualJournal,
private getManualJournalService: GetManualJournal,
// private getManualJournalsService: GetManualJournals,
) {}
/**
* Make journal entries.
* @param {number} tenantId
* @param {IManualJournalDTO} manualJournalDTO
* @returns {Promise<IManualJournal>}
*/
public createManualJournal = (manualJournalDTO: IManualJournalDTO) => {
return this.createManualJournalService.makeJournalEntries(manualJournalDTO);
};
/**
* Edits jouranl entries.
* @param {number} manualJournalId
* @param {IMakeJournalDTO} manualJournalDTO
*/
public editManualJournal = (
manualJournalId: number,
manualJournalDTO: IManualJournalDTO,
) => {
return this.editManualJournalService.editJournalEntries(
manualJournalId,
manualJournalDTO,
);
};
/**
* Deletes the given manual journal
* @param {number} manualJournalId
* @return {Promise<void>}
*/
public deleteManualJournal = (manualJournalId: number) => {
return this.deleteManualJournalService.deleteManualJournal(
manualJournalId,
);
};
/**
* Publish the given manual journal.
* @param {number} manualJournalId - Manual journal id.
*/
public publishManualJournal = (manualJournalId: number) => {
return this.publishManualJournalService.publishManualJournal(
manualJournalId,
);
};
/**
* Retrieves the specific manual journal.
* @param {number} manualJournalId
* @returns
*/
public getManualJournal = (manualJournalId: number) => {
return this.getManualJournalService.getManualJournal(
manualJournalId,
);
};
/**
* Retrieves the paginated manual journals.
* @param {number} tenantId
* @param {IManualJournalsFilter} filterDTO
* @returns
*/
// public getManualJournals = (
// filterDTO: IManualJournalsFilter,
// ) => {
// // return this.getManualJournalsService.getManualJournals(filterDTO);
// };
}

View File

@@ -0,0 +1,42 @@
import { Injectable } from '@nestjs/common';
import { AutoIncrementOrdersService } from '@/modules/AutoIncrementOrders/AutoIncrementOrders.service';
@Injectable()
export class AutoIncrementManualJournal {
/**
*
* @param autoIncrementOrdersService
*/
constructor(
private readonly autoIncrementOrdersService: AutoIncrementOrdersService
) {}
/**
*
* @returns {boolean}
*/
public autoIncrementEnabled = () => {
return this.autoIncrementOrdersService.autoIncrementEnabled(
'manual_journals'
);
};
/**
* Retrieve the next journal number.
*/
public getNextJournalNumber = (): string => {
return this.autoIncrementOrdersService.getNextTransactionNumber(
'manual_journals'
);
};
/**
* Increment the manual journal number.
* @param {number} tenantId
*/
public incrementNextJournalNumber = () => {
return this.autoIncrementOrdersService.incrementSettingsNextNumber(
'manual_journals'
);
};
}

View File

@@ -0,0 +1,295 @@
import { Inject, Injectable } from '@nestjs/common';
import { difference, isEmpty, round, sumBy } from 'lodash';
import {
IManualJournalDTO,
IManualJournalEntryDTO,
} from '../types/ManualJournals.types';
import { ERRORS } from '../constants';
import { ServiceError } from '@/modules/Items/ServiceError';
import { Account } from '@/modules/Accounts/models/Account.model';
import { ManualJournal } from '../models/ManualJournal';
import { Contact } from '@/modules/Contacts/models/Contact';
@Injectable()
export class CommandManualJournalValidators {
constructor(
@Inject(Account.name)
private readonly accountModel: typeof Account,
@Inject(ManualJournal.name)
private readonly manualJournalModel: typeof ManualJournal,
@Inject(Contact.name)
private readonly contactModel: typeof Contact,
) {}
/**
* Validate manual journal credit and debit should be equal.
* @param {IManualJournalDTO} manualJournalDTO
*/
public valdiateCreditDebitTotalEquals(manualJournalDTO: IManualJournalDTO) {
const totalCredit = round(
sumBy(manualJournalDTO.entries, (entry) => entry.credit || 0),
2,
);
const totalDebit = round(
sumBy(manualJournalDTO.entries, (entry) => entry.debit || 0),
2,
);
if (totalCredit <= 0 || totalDebit <= 0) {
throw new ServiceError(ERRORS.CREDIT_DEBIT_NOT_EQUAL_ZERO);
}
if (totalCredit !== totalDebit) {
throw new ServiceError(ERRORS.CREDIT_DEBIT_NOT_EQUAL);
}
}
/**
* Validate manual entries accounts existance on the storage.
* @param {number} tenantId -
* @param {IManualJournalDTO} manualJournalDTO -
*/
public async validateAccountsExistance(manualJournalDTO: IManualJournalDTO) {
const manualAccountsIds = manualJournalDTO.entries.map((e) => e.accountId);
const accounts = await this.accountModel
.query()
.whereIn('id', manualAccountsIds);
const storedAccountsIds = accounts.map((account) => account.id);
if (difference(manualAccountsIds, storedAccountsIds).length > 0) {
throw new ServiceError(ERRORS.ACCOUNTS_IDS_NOT_FOUND);
}
}
/**
* Validate manual journal number unique.
* @param {number} tenantId
* @param {IManualJournalDTO} manualJournalDTO
*/
public async validateManualJournalNoUnique(
journalNumber: string,
notId?: number,
) {
const journals = await this.manualJournalModel
.query()
.where('journal_number', journalNumber)
.onBuild((builder) => {
if (notId) {
builder.whereNot('id', notId);
}
});
if (journals.length > 0) {
throw new ServiceError(
ERRORS.JOURNAL_NUMBER_EXISTS,
'The journal number is already exist.',
);
}
}
/**
* Validate accounts with contact type.
* @param {number} tenantId
* @param {IManualJournalDTO} manualJournalDTO
* @param {string} accountBySlug
* @param {string} contactType
*/
public async validateAccountWithContactType(
entriesDTO: IManualJournalEntryDTO[],
accountBySlug: string,
contactType: string,
): Promise<void | ServiceError> {
// Retrieve account meta by the given account slug.
const account = await this.accountModel
.query()
.findOne('slug', accountBySlug);
// Retrieve all stored contacts on the storage from contacts entries.
const storedContacts = await this.contactModel.query().whereIn(
'id',
entriesDTO
.filter((entry) => entry.contactId)
.map((entry) => entry.contactId),
);
// Converts the stored contacts to map with id as key and entry as value.
const storedContactsMap = new Map(
storedContacts.map((contact) => [contact.id, contact]),
);
// Filter all entries of the given account.
const accountEntries = entriesDTO.filter(
(entry) => entry.accountId === account.id,
);
// Can't continue if there is no entry that associate to the given account.
if (accountEntries.length === 0) {
return;
}
// Filter entries that have no contact type or not equal the valid type.
const entriesNoContact = accountEntries.filter((entry) => {
const contact = storedContactsMap.get(entry.contactId);
return !contact || contact.contactService !== contactType;
});
// Throw error in case one of entries that has invalid contact type.
if (entriesNoContact.length > 0) {
const indexes = entriesNoContact.map((e) => e.index);
return new ServiceError(ERRORS.ENTRIES_SHOULD_ASSIGN_WITH_CONTACT, '', {
accountSlug: accountBySlug,
contactType,
indexes,
});
}
}
/**
* Dynamic validates accounts with contacts.
* @param {number} tenantId
* @param {IManualJournalDTO} manualJournalDTO
*/
public async dynamicValidateAccountsWithContactType(
entriesDTO: IManualJournalEntryDTO[],
): Promise<any> {
return Promise.all([
this.validateAccountWithContactType(
entriesDTO,
'accounts-receivable',
'customer',
),
this.validateAccountWithContactType(
entriesDTO,
'accounts-payable',
'vendor',
),
]).then((results) => {
const metadataErrors = results
.filter((result) => result instanceof ServiceError)
.map((result: ServiceError) => result.payload);
if (metadataErrors.length > 0) {
throw new ServiceError(
ERRORS.ENTRIES_SHOULD_ASSIGN_WITH_CONTACT,
'',
metadataErrors,
);
}
return results;
});
}
/**
* Validate entries contacts existance.
* @param {number} tenantId -
* @param {IManualJournalDTO} manualJournalDTO
*/
public async validateContactsExistance(manualJournalDTO: IManualJournalDTO) {
// Filters the entries that have contact only.
const entriesContactPairs = manualJournalDTO.entries.filter(
(entry) => entry.contactId,
);
if (entriesContactPairs.length > 0) {
const entriesContactsIds = entriesContactPairs.map(
(entry) => entry.contactId,
);
// Retrieve all stored contacts on the storage from contacts entries.
const storedContacts = await this.contactModel
.query()
.whereIn('id', entriesContactsIds);
// Converts the stored contacts to map with id as key and entry as value.
const storedContactsMap = new Map(
storedContacts.map((contact) => [contact.id, contact]),
);
const notFoundContactsIds = [];
entriesContactPairs.forEach((contactEntry) => {
const storedContact = storedContactsMap.get(contactEntry.contactId);
// in case the contact id not found.
if (!storedContact) {
notFoundContactsIds.push(storedContact);
}
});
if (notFoundContactsIds.length > 0) {
throw new ServiceError(ERRORS.CONTACTS_NOT_FOUND, '', {
contactsIds: notFoundContactsIds,
});
}
}
}
/**
* Validates expenses is not already published before.
* @param {ManualJournal} manualJournal
*/
public validateManualJournalIsNotPublished(manualJournal: ManualJournal) {
if (manualJournal.publishedAt) {
throw new ServiceError(ERRORS.MANUAL_JOURNAL_ALREADY_PUBLISHED);
}
}
/**
* Validates the manual journal number require.
* @param {string} journalNumber
* @throws {ServiceError(ERRORS.MANUAL_JOURNAL_NO_REQUIRED)}
*/
public validateJournalNoRequireWhenAutoNotEnabled = (
journalNumber: string,
) => {
if (isEmpty(journalNumber)) {
throw new ServiceError(ERRORS.MANUAL_JOURNAL_NO_REQUIRED);
}
};
/**
* Filters the not published manual jorunals.
* @param {IManualJournal[]} manualJournal - Manual journal.
* @return {IManualJournal[]}
*/
public getNonePublishedManualJournals(
manualJournals: ManualJournal[],
): ManualJournal[] {
return manualJournals.filter((manualJournal) => !manualJournal.publishedAt);
}
/**
* Filters the published manual journals.
* @param {IManualJournal[]} manualJournal - Manual journal.
* @return {IManualJournal[]}
*/
public getPublishedManualJournals(
manualJournals: ManualJournal[],
): ManualJournal[] {
return manualJournals.filter((expense) => expense.publishedAt);
}
/**
*
* @param {number} tenantId
* @param {IManualJournalDTO} manualJournalDTO
*/
public validateJournalCurrencyWithAccountsCurrency = async (
manualJournalDTO: IManualJournalDTO,
baseCurrency: string,
) => {
const accountsIds = manualJournalDTO.entries.map((e) => e.accountId);
const accounts = await this.accountModel.query().whereIn('id', accountsIds);
// Filters the accounts that has no base currency or DTO currency.
const notSupportedCurrency = accounts.filter((account) => {
if (
account.currencyCode === baseCurrency ||
account.currencyCode === manualJournalDTO.currencyCode
) {
return false;
}
return true;
});
if (notSupportedCurrency.length > 0) {
throw new ServiceError(
ERRORS.COULD_NOT_ASSIGN_DIFFERENT_CURRENCY_TO_ACCOUNTS,
);
}
};
}

View File

@@ -0,0 +1,159 @@
import { sumBy, omit } from 'lodash';
import * as moment from 'moment';
import * as R from 'ramda';
import { Knex } from 'knex';
import { Inject, Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import {
IManualJournalDTO,
IManualJournalEventCreatedPayload,
IManualJournalCreatingPayload,
} from '../types/ManualJournals.types';
import { CommandManualJournalValidators } from './CommandManualJournalValidators.service';
import { AutoIncrementManualJournal } from './AutoIncrementManualJournal.service';
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
import { events } from '@/common/events/events';
import { ManualJournal } from '../models/ManualJournal';
import { assocItemEntriesDefaultIndex } from '@/utils/associate-item-entries-index';
import { TenancyContext } from '@/modules/Tenancy/TenancyContext.service';
import { ManualJournalBranchesDTOTransformer } from '@/modules/Branches/integrations/ManualJournals/ManualJournalDTOTransformer.service';
@Injectable()
export class CreateManualJournalService {
constructor(
private tenancyContext: TenancyContext,
private eventPublisher: EventEmitter2,
private uow: UnitOfWork,
private validator: CommandManualJournalValidators,
private autoIncrement: AutoIncrementManualJournal,
private branchesDTOTransformer: ManualJournalBranchesDTOTransformer,
@Inject(ManualJournal.name)
private manualJournalModel: typeof ManualJournal,
) {}
/**
* Transform the new manual journal DTO to upsert graph operation.
* @param {IManualJournalDTO} manualJournalDTO - Manual jorunal DTO.
* @returns {Promise<ManualJournal>}
*/
private async transformNewDTOToModel(
manualJournalDTO: IManualJournalDTO,
): Promise<ManualJournal> {
const amount = sumBy(manualJournalDTO.entries, 'credit') || 0;
const date = moment(manualJournalDTO.date).format('YYYY-MM-DD');
// Retrieve the next manual journal number.
const autoNextNumber = this.autoIncrement.getNextJournalNumber();
// The manual or auto-increment journal number.
const journalNumber = manualJournalDTO.journalNumber || autoNextNumber;
const tenant = await this.tenancyContext.getTenant(true);
const authorizedUser = await this.tenancyContext.getSystemUser();
const entries = R.compose(
// Associate the default index to each item entry.
assocItemEntriesDefaultIndex,
)(manualJournalDTO.entries);
const initialDTO = {
...omit(manualJournalDTO, ['publish', 'attachments']),
...(manualJournalDTO.publish
? { publishedAt: moment().toMySqlDateTime() }
: {}),
amount,
date,
currencyCode:
manualJournalDTO.currencyCode || tenant?.metadata?.baseCurrency,
exchangeRate: manualJournalDTO.exchangeRate || 1,
journalNumber,
entries,
userId: authorizedUser.id,
};
return R.compose(
// Omits the `branchId` from entries if multiply branches feature not active.
this.branchesDTOTransformer.transformDTO,
)(initialDTO) as ManualJournal;
}
/**
* Authorize the manual journal creating.
* @param {number} tenantId
* @param {IManualJournalDTO} manualJournalDTO
* @param {ISystemUser} authorizedUser
*/
private authorize = async (manualJournalDTO: IManualJournalDTO) => {
const tenant = await this.tenancyContext.getTenant(true);
// Validate the total credit should equals debit.
this.validator.valdiateCreditDebitTotalEquals(manualJournalDTO);
// Validate the contacts existance.
await this.validator.validateContactsExistance(manualJournalDTO);
// Validate entries accounts existance.
await this.validator.validateAccountsExistance(manualJournalDTO);
// Validate manual journal number require when auto-increment not enabled.
this.validator.validateJournalNoRequireWhenAutoNotEnabled(
manualJournalDTO.journalNumber,
);
// Validate manual journal uniquiness on the storage.
if (manualJournalDTO.journalNumber) {
await this.validator.validateManualJournalNoUnique(
manualJournalDTO.journalNumber,
);
}
// Validate accounts with contact type from the given config.
await this.validator.dynamicValidateAccountsWithContactType(
manualJournalDTO.entries,
)
// Validates the accounts currency with journal currency.
await this.validator.validateJournalCurrencyWithAccountsCurrency(
manualJournalDTO,
tenant.metadata.baseCurrency,
);
};
/**
* Make journal entries.
* @param {number} tenantId
* @param {IManualJournalDTO} manualJournalDTO
* @param {ISystemUser} authorizedUser
*/
public makeJournalEntries = async (
manualJournalDTO: IManualJournalDTO,
trx?: Knex.Transaction,
): Promise<ManualJournal> => {
// Authorize manual journal creating.
await this.authorize(manualJournalDTO);
// Transformes the next DTO to model.
const manualJournalObj = await this.transformNewDTOToModel(manualJournalDTO);
// Creates a manual journal transactions with associated transactions
// under unit-of-work envirement.
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
// Triggers `onManualJournalCreating` event.
await this.eventPublisher.emitAsync(events.manualJournals.onCreating, {
manualJournalDTO,
trx,
} as IManualJournalCreatingPayload);
// Upsert the manual journal object.
const manualJournal = await this.manualJournalModel
.query(trx)
.upsertGraph({
...manualJournalObj,
});
// Triggers `onManualJournalCreated` event.
await this.eventPublisher.emitAsync(events.manualJournals.onCreated, {
manualJournal,
manualJournalDTO,
trx,
} as IManualJournalEventCreatedPayload);
return manualJournal;
}, trx);
};
}

View File

@@ -0,0 +1,71 @@
import { Inject, Injectable } from '@nestjs/common';
import { Knex } from 'knex';
import { EventEmitter2 } from '@nestjs/event-emitter';
import {
IManualJournalEventDeletedPayload,
IManualJournalDeletingPayload,
} from '../types/ManualJournals.types';
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
import { ManualJournal } from '../models/ManualJournal';
import { ManualJournalEntry } from '../models/ManualJournalEntry';
import { events } from '@/common/events/events';
@Injectable()
export class DeleteManualJournalService {
constructor(
private readonly eventPublisher: EventEmitter2,
private readonly uow: UnitOfWork,
@Inject(ManualJournal.name)
private readonly manualJournalModel: typeof ManualJournal,
@Inject(ManualJournalEntry.name)
private readonly manualJournalEntryModel: typeof ManualJournalEntry,
) {}
/**
* Deletes the given manual journal
* @param {number} manualJournalId
* @return {Promise<void>}
*/
public deleteManualJournal = async (
manualJournalId: number,
): Promise<{
oldManualJournal: ManualJournal;
}> => {
// Validate the manual journal exists on the storage.
const oldManualJournal = await this.manualJournalModel.query()
.findById(manualJournalId)
.throwIfNotFound();
// Deletes the manual journal with associated transactions under unit-of-work envirement.
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
// Triggers `onManualJournalDeleting` event.
await this.eventPublisher.emitAsync(events.manualJournals.onDeleting, {
oldManualJournal,
trx,
} as IManualJournalDeletingPayload);
// Deletes the manual journal entries.
await this.manualJournalEntryModel
.query(trx)
.where('manualJournalId', manualJournalId)
.delete();
// Deletes the manual journal transaction.
await this.manualJournalModel
.query(trx)
.findById(manualJournalId)
.delete();
// Triggers `onManualJournalDeleted` event.
await this.eventPublisher.emitAsync(events.manualJournals.onDeleted, {
manualJournalId,
oldManualJournal,
trx,
} as IManualJournalEventDeletedPayload);
return { oldManualJournal };
});
};
}

View File

@@ -0,0 +1,139 @@
import { Knex } from 'knex';
import { omit, sumBy } from 'lodash';
import * as moment from 'moment';
import { Inject, Injectable } from '@nestjs/common';
import {
IManualJournalDTO,
IManualJournalEventEditedPayload,
IManualJournalEditingPayload,
} from '../types/ManualJournals.types';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
import { CommandManualJournalValidators } from './CommandManualJournalValidators.service';
import { events } from '@/common/events/events';
import { ManualJournal } from '../models/ManualJournal';
@Injectable()
export class EditManualJournal {
constructor(
private eventPublisher: EventEmitter2,
private uow: UnitOfWork,
private validator: CommandManualJournalValidators,
@Inject(ManualJournal.name)
private manualJournalModel: typeof ManualJournal,
) {}
/**
* Authorize the manual journal editing.
* @param {number} tenantId
* @param {number} manualJournalId
* @param {IManualJournalDTO} manualJournalDTO
*/
private authorize = async (
manualJournalId: number,
manualJournalDTO: IManualJournalDTO,
) => {
// Validates the total credit and debit to be equals.
this.validator.valdiateCreditDebitTotalEquals(manualJournalDTO);
// Validate the contacts existance.
await this.validator.validateContactsExistance(manualJournalDTO);
// Validates entries accounts existance.
await this.validator.validateAccountsExistance(manualJournalDTO);
// Validates the manual journal number uniquiness.
if (manualJournalDTO.journalNumber) {
await this.validator.validateManualJournalNoUnique(
manualJournalDTO.journalNumber,
manualJournalId,
);
}
// Validate accounts with contact type from the given config.
await this.validator.dynamicValidateAccountsWithContactType(
manualJournalDTO.entries,
);
};
/**
* Transform the edit manual journal DTO to upsert graph operation.
* @param {IManualJournalDTO} manualJournalDTO - Manual jorunal DTO.
* @param {IManualJournal} oldManualJournal
*/
private transformEditDTOToModel = (
manualJournalDTO: IManualJournalDTO,
oldManualJournal: ManualJournal,
) => {
const amount = sumBy(manualJournalDTO.entries, 'credit') || 0;
const date = moment(manualJournalDTO.date).format('YYYY-MM-DD');
return {
id: oldManualJournal.id,
...omit(manualJournalDTO, ['publish', 'attachments']),
...(manualJournalDTO.publish && !oldManualJournal.publishedAt
? { publishedAt: moment().toMySqlDateTime() }
: {}),
amount,
date,
};
};
/**
* Edits jouranl entries.
* @param {number} tenantId
* @param {number} manualJournalId
* @param {IMakeJournalDTO} manualJournalDTO
* @param {ISystemUser} authorizedUser
*/
public async editJournalEntries(
manualJournalId: number,
manualJournalDTO: IManualJournalDTO,
): Promise<{
manualJournal: ManualJournal;
oldManualJournal: ManualJournal;
}> {
// Validates the manual journal existance on the storage.
const oldManualJournal = await ManualJournal.query()
.findById(manualJournalId)
.throwIfNotFound();
// Authorize manual journal editing.
await this.authorize(manualJournalId, manualJournalDTO);
// Transform manual journal DTO to model.
const manualJournalObj = this.transformEditDTOToModel(
manualJournalDTO,
oldManualJournal,
);
// Edits the manual journal transactions with associated transactions
// under unit-of-work envirement.
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
// Triggers `onManualJournalEditing` event.
await this.eventPublisher.emitAsync(events.manualJournals.onEditing, {
manualJournalDTO,
oldManualJournal,
trx,
} as IManualJournalEditingPayload);
// Upserts the manual journal graph to the storage.
await this.manualJournalModel.query(trx).upsertGraph({
...manualJournalObj,
});
// Retrieve the given manual journal with associated entries after modifications.
const manualJournal = await this.manualJournalModel.query(trx)
.findById(manualJournalId)
.withGraphFetched('entries');
// Triggers `onManualJournalEdited` event.
await this.eventPublisher.emitAsync(events.manualJournals.onEdited, {
manualJournal,
oldManualJournal,
manualJournalDTO,
trx,
} as IManualJournalEventEditedPayload);
return { manualJournal, oldManualJournal };
});
}
}

View File

@@ -0,0 +1,30 @@
// import { Inject, Service } from 'typedi';
// import { IManualJournalsFilter } from '@/interfaces';
// import { Exportable } from '../../Export/Exportable';
// import { ManualJournalsApplication } from '../ManualJournalsApplication';
// import { EXPORT_SIZE_LIMIT } from '../../Export/constants';
// @Service()
// export class ManualJournalsExportable extends Exportable {
// @Inject()
// private manualJournalsApplication: ManualJournalsApplication;
// /**
// * Retrieves the manual journals data to exportable sheet.
// * @param {number} tenantId
// * @returns
// */
// public exportable(tenantId: number, query: IManualJournalsFilter) {
// const parsedQuery = {
// sortOrder: 'desc',
// columnSortBy: 'created_at',
// ...query,
// page: 1,
// pageSize: EXPORT_SIZE_LIMIT,
// } as IManualJournalsFilter;
// return this.manualJournalsApplication
// .getManualJournals(tenantId, parsedQuery)
// .then((output) => output.manualJournals);
// }
// }

View File

@@ -0,0 +1,161 @@
// import { Service, Inject } from 'typedi';
// import * as R from 'ramda';
// import {
// IManualJournal,
// IManualJournalEntry,
// ILedgerEntry,
// } from '@/interfaces';
// import { Knex } from 'knex';
// import Ledger from '@/services/Accounting/Ledger';
// import LedgerStorageService from '@/services/Accounting/LedgerStorageService';
// import HasTenancyService from '@/services/Tenancy/TenancyService';
// @Service()
// export class ManualJournalGLEntries {
// @Inject()
// private ledgerStorage: LedgerStorageService;
// @Inject()
// private tenancy: HasTenancyService;
// /**
// * Create manual journal GL entries.
// * @param {number} tenantId
// * @param {number} manualJournalId
// * @param {Knex.Transaction} trx
// */
// public createManualJournalGLEntries = async (
// tenantId: number,
// manualJournalId: number,
// trx?: Knex.Transaction
// ) => {
// const { ManualJournal } = this.tenancy.models(tenantId);
// // Retrieves the given manual journal with associated entries.
// const manualJournal = await ManualJournal.query(trx)
// .findById(manualJournalId)
// .withGraphFetched('entries.account');
// // Retrieves the ledger entries of the given manual journal.
// const ledger = this.getManualJournalGLedger(manualJournal);
// // Commits the given ledger on the storage.
// await this.ledgerStorage.commit(tenantId, ledger, trx);
// };
// /**
// * Edits manual journal GL entries.
// * @param {number} tenantId
// * @param {number} manualJournalId
// * @param {Knex.Transaction} trx
// */
// public editManualJournalGLEntries = async (
// tenantId: number,
// manualJournalId: number,
// trx?: Knex.Transaction
// ) => {
// // Reverts the manual journal GL entries.
// await this.revertManualJournalGLEntries(tenantId, manualJournalId, trx);
// // Write the manual journal GL entries.
// await this.createManualJournalGLEntries(tenantId, manualJournalId, trx);
// };
// /**
// * Deletes the manual journal GL entries.
// * @param {number} tenantId
// * @param {number} manualJournalId
// * @param {Knex.Transaction} trx
// */
// public revertManualJournalGLEntries = async (
// tenantId: number,
// manualJournalId: number,
// trx?: Knex.Transaction
// ): Promise<void> => {
// return this.ledgerStorage.deleteByReference(
// tenantId,
// manualJournalId,
// 'Journal',
// trx
// );
// };
// /**
// * Retrieves the ledger of the given manual journal.
// * @param {IManualJournal} manualJournal
// * @returns {Ledger}
// */
// private getManualJournalGLedger = (manualJournal: IManualJournal) => {
// const entries = this.getManualJournalGLEntries(manualJournal);
// return new Ledger(entries);
// };
// /**
// * Retrieves the common entry details of the manual journal
// * @param {IManualJournal} manualJournal
// * @returns {Partial<ILedgerEntry>}
// */
// private getManualJournalCommonEntry = (
// manualJournal: IManualJournal
// ): Partial<ILedgerEntry> => {
// return {
// transactionNumber: manualJournal.journalNumber,
// referenceNumber: manualJournal.reference,
// createdAt: manualJournal.createdAt,
// date: manualJournal.date,
// currencyCode: manualJournal.currencyCode,
// exchangeRate: manualJournal.exchangeRate,
// transactionType: 'Journal',
// transactionId: manualJournal.id,
// userId: manualJournal.userId,
// };
// };
// /**
// * Retrieves the ledger entry of the given manual journal and
// * its associated entry.
// * @param {IManualJournal} manualJournal -
// * @param {IManualJournalEntry} entry -
// * @returns {ILedgerEntry}
// */
// private getManualJournalEntry = R.curry(
// (
// manualJournal: IManualJournal,
// entry: IManualJournalEntry
// ): ILedgerEntry => {
// const commonEntry = this.getManualJournalCommonEntry(manualJournal);
// return {
// ...commonEntry,
// debit: entry.debit,
// credit: entry.credit,
// accountId: entry.accountId,
// contactId: entry.contactId,
// note: entry.note,
// index: entry.index,
// accountNormal: entry.account.accountNormal,
// branchId: entry.branchId,
// projectId: entry.projectId,
// };
// }
// );
// /**
// * Retrieves the ledger of the given manual journal.
// * @param {IManualJournal} manualJournal
// * @returns {ILedgerEntry[]}
// */
// private getManualJournalGLEntries = (
// manualJournal: IManualJournal
// ): ILedgerEntry[] => {
// const transformEntry = this.getManualJournalEntry(manualJournal);
// return manualJournal.entries.map(transformEntry).flat();
// };
// }

View File

@@ -0,0 +1,131 @@
// import { Inject } from 'typedi';
// import { EventSubscriber } from 'event-dispatch';
// import {
// IManualJournalEventCreatedPayload,
// IManualJournalEventEditedPayload,
// IManualJournalEventPublishedPayload,
// IManualJournalEventDeletedPayload,
// } from '@/interfaces';
// import events from '@/subscribers/events';
// import { ManualJournalGLEntries } from './ManualJournalGLEntries';
// import { AutoIncrementManualJournal } from './AutoIncrementManualJournal.service';
// @EventSubscriber()
// export class ManualJournalWriteGLSubscriber {
// @Inject()
// private manualJournalGLEntries: ManualJournalGLEntries;
// @Inject()
// private manualJournalAutoIncrement: AutoIncrementManualJournal;
// /**
// * Attaches events with handlers.
// * @param bus
// */
// public attach(bus) {
// bus.subscribe(
// events.manualJournals.onCreated,
// this.handleWriteJournalEntriesOnCreated
// );
// bus.subscribe(
// events.manualJournals.onCreated,
// this.handleJournalNumberIncrement
// );
// bus.subscribe(
// events.manualJournals.onEdited,
// this.handleRewriteJournalEntriesOnEdited
// );
// bus.subscribe(
// events.manualJournals.onPublished,
// this.handleWriteJournalEntriesOnPublished
// );
// bus.subscribe(
// events.manualJournals.onDeleted,
// this.handleRevertJournalEntries
// );
// }
// /**
// * Handle manual journal created event.
// * @param {IManualJournalEventCreatedPayload} payload -
// * @returns {Promise<void>}
// */
// private handleWriteJournalEntriesOnCreated = async ({
// tenantId,
// manualJournal,
// trx,
// }: IManualJournalEventCreatedPayload) => {
// // Ingore writing manual journal journal entries in case was not published.
// if (!manualJournal.publishedAt) return;
// await this.manualJournalGLEntries.createManualJournalGLEntries(
// tenantId,
// manualJournal.id,
// trx
// );
// };
// /**
// * Handles the manual journal next number increment once the journal be created.
// * @param {IManualJournalEventCreatedPayload} payload -
// * @return {Promise<void>}
// */
// private handleJournalNumberIncrement = async ({
// tenantId,
// }: IManualJournalEventCreatedPayload) => {
// await this.manualJournalAutoIncrement.incrementNextJournalNumber(tenantId);
// };
// /**
// * Handle manual journal edited event.
// * @param {IManualJournalEventEditedPayload}
// * @return {Promise<void>}
// */
// private handleRewriteJournalEntriesOnEdited = async ({
// tenantId,
// manualJournal,
// oldManualJournal,
// trx,
// }: IManualJournalEventEditedPayload) => {
// if (manualJournal.publishedAt) {
// await this.manualJournalGLEntries.editManualJournalGLEntries(
// tenantId,
// manualJournal.id,
// trx
// );
// }
// };
// /**
// * Handles writing journal entries once the manula journal publish.
// * @param {IManualJournalEventPublishedPayload} payload -
// * @return {Promise<void>}
// */
// private handleWriteJournalEntriesOnPublished = async ({
// tenantId,
// manualJournal,
// trx,
// }: IManualJournalEventPublishedPayload) => {
// await this.manualJournalGLEntries.createManualJournalGLEntries(
// tenantId,
// manualJournal.id,
// trx
// );
// };
// /**
// * Handle manual journal deleted event.
// * @param {IManualJournalEventDeletedPayload} payload -
// */
// private handleRevertJournalEntries = async ({
// tenantId,
// manualJournalId,
// trx,
// }: IManualJournalEventDeletedPayload) => {
// await this.manualJournalGLEntries.revertManualJournalGLEntries(
// tenantId,
// manualJournalId,
// trx
// );
// };
// }

View File

@@ -0,0 +1,60 @@
// import { Inject } from 'typedi';
// import { Knex } from 'knex';
// import * as Yup from 'yup';
// import { Importable } from '../../Import/Importable';
// import { CreateManualJournalService } from './CreateManualJournal.service';
// import { IManualJournalDTO } from '@/interfaces';
// import { ImportableContext } from '../../Import/interfaces';
// import { ManualJournalsSampleData } from '../constants';
// export class ManualJournalImportable extends Importable {
// @Inject()
// private createManualJournalService: CreateManualJournalService;
// /**
// * Importing to account service.
// * @param {number} tenantId
// * @param {IAccountCreateDTO} createAccountDTO
// * @returns
// */
// public importable(
// tenantId: number,
// createJournalDTO: IManualJournalDTO,
// trx?: Knex.Transaction
// ) {
// return this.createManualJournalService.makeJournalEntries(
// tenantId,
// createJournalDTO,
// {},
// trx
// );
// }
// /**
// * Transformes the DTO before passing it to importable and validation.
// * @param {Record<string, any>} createDTO
// * @param {ImportableContext} context
// * @returns {Record<string, any>}
// */
// public transform(createDTO: Record<string, any>, context: ImportableContext) {
// return createDTO;
// }
// /**
// * Params validation schema.
// * @returns {ValidationSchema[]}
// */
// public paramsValidationSchema() {
// return Yup.object().shape({
// autoIncrement: Yup.boolean(),
// });
// }
// /**
// * Retrieves the sample data of manual journals that used to download sample sheet.
// * @returns {Record<string, any>}
// */
// public sampleData(): Record<string, any>[] {
// return ManualJournalsSampleData;
// }
// }

View File

@@ -0,0 +1,73 @@
import * as moment from 'moment';
import { Knex } from 'knex';
import { Inject, Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
import {
IManualJournalEventPublishedPayload,
IManualJournalPublishingPayload,
} from '../types/ManualJournals.types';
import { CommandManualJournalValidators } from './CommandManualJournalValidators.service';
import { UnitOfWork } from '@/modules/Tenancy/TenancyDB/UnitOfWork.service';
import { ManualJournal } from '../models/ManualJournal';
import { events } from '@/common/events/events';
@Injectable()
export class PublishManualJournal {
constructor(
private eventPublisher: EventEmitter2,
private uow: UnitOfWork,
private validator: CommandManualJournalValidators,
@Inject(ManualJournal.name)
private manualJournalModel: typeof ManualJournal,
) {}
/**
* Authorize the manual journal publishing.
* @param {number} manualJournalId - Manual journal id.
*/
private authorize = (oldManualJournal: ManualJournal) => {
// Validate the manual journal is not published.
this.validator.validateManualJournalIsNotPublished(oldManualJournal);
};
/**
* Publish the given manual journal.
* @param {number} manualJournalId - Manual journal id.
*/
public async publishManualJournal(manualJournalId: number): Promise<void> {
// Find the old manual journal or throw not found error.
const oldManualJournal = await this.manualJournalModel
.query()
.findById(manualJournalId)
.throwIfNotFound();
// Authorize the manual journal publishing.
await this.authorize(oldManualJournal);
// Publishes the manual journal with associated transactions.
return this.uow.withTransaction(async (trx: Knex.Transaction) => {
// Triggers `onManualJournalPublishing` event.
await this.eventPublisher.emitAsync(events.manualJournals.onPublishing, {
oldManualJournal,
trx,
} as IManualJournalPublishingPayload);
// Mark the given manual journal as published.
await this.manualJournalModel.query(trx).findById(manualJournalId).patch({
publishedAt: moment().toMySqlDateTime(),
});
// Retrieve the manual journal with enrties after modification.
const manualJournal = await this.manualJournalModel.query()
.findById(manualJournalId)
.withGraphFetched('entries');
// Triggers `onManualJournalPublishedBulk` event.
await this.eventPublisher.emitAsync(events.manualJournals.onPublished, {
manualJournal,
oldManualJournal,
trx,
} as IManualJournalEventPublishedPayload);
});
}
}

View File

@@ -0,0 +1,64 @@
export const ERRORS = {
NOT_FOUND: 'manual_journal_not_found',
CREDIT_DEBIT_NOT_EQUAL_ZERO: 'credit_debit_not_equal_zero',
CREDIT_DEBIT_NOT_EQUAL: 'credit_debit_not_equal',
ACCOUNTS_IDS_NOT_FOUND: 'accounts_ids_not_found',
JOURNAL_NUMBER_EXISTS: 'journal_number_exists',
ENTRIES_SHOULD_ASSIGN_WITH_CONTACT: 'ENTRIES_SHOULD_ASSIGN_WITH_CONTACT',
CONTACTS_NOT_FOUND: 'contacts_not_found',
ENTRIES_CONTACTS_NOT_FOUND: 'ENTRIES_CONTACTS_NOT_FOUND',
MANUAL_JOURNAL_ALREADY_PUBLISHED: 'MANUAL_JOURNAL_ALREADY_PUBLISHED',
MANUAL_JOURNAL_NO_REQUIRED: 'MANUAL_JOURNAL_NO_REQUIRED',
COULD_NOT_ASSIGN_DIFFERENT_CURRENCY_TO_ACCOUNTS:
'COULD_NOT_ASSIGN_DIFFERENT_CURRENCY_TO_ACCOUNTS',
MANUAL_JOURNAL_ENTRIES_HAVE_NO_BRANCH_ID:
'MANUAL_JOURNAL_ENTRIES_HAVE_NO_BRANCH_ID',
};
export const CONTACTS_CONFIG = [
{
accountBySlug: 'accounts-receivable',
contactService: 'customer',
assignRequired: true,
},
{
accountBySlug: 'accounts-payable',
contactService: 'vendor',
assignRequired: true,
},
];
export const DEFAULT_VIEWS = [];
export const ManualJournalsSampleData = [
{
Date: '2024-02-02',
'Journal No': 'J-100022',
'Reference No.': 'REF-10000',
'Currency Code': '',
'Exchange Rate': '',
'Journal Type': '',
Description: 'Animi quasi qui itaque aut possimus illum est magnam enim.',
Credit: 1000,
Debit: 0,
Note: 'Qui reprehenderit voluptate.',
Account: 'Bank Account',
Contact: '',
Publish: 'T',
},
{
Date: '2024-02-02',
'Journal No': 'J-100022',
'Reference No.': 'REF-10000',
'Currency Code': '',
'Exchange Rate': '',
'Journal Type': '',
Description: 'In assumenda dicta autem non est corrupti non et.',
Credit: 0,
Debit: 1000,
Note: 'Omnis tempora qui fugiat neque dolor voluptatem aut repudiandae nihil.',
Account: 'Bank Account',
Contact: '',
Publish: 'T',
},
];

View File

@@ -0,0 +1,207 @@
import { Model, mixin } from 'objection';
// import TenantModel from 'models/TenantModel';
// import { formatNumber } from 'utils';
// import ModelSetting from './ModelSetting';
// import ManualJournalSettings from './ManualJournal.Settings';
// import CustomViewBaseModel from './CustomViewBaseModel';
// import { DEFAULT_VIEWS } from '@/services/ManualJournals/constants';
// import ModelSearchable from './ModelSearchable';
import { ManualJournalEntry } from './ManualJournalEntry';
import { BaseModel } from '@/models/Model';
import { Document } from '@/modules/ChromiumlyTenancy/models/Document';
export class ManualJournal extends BaseModel {
date: Date;
journalNumber: string;
journalType: string;
reference: string;
amount: number;
currencyCode: string;
exchangeRate: number | null;
publishedAt: Date | string | null;
description: string;
userId?: number;
createdAt?: Date;
updatedAt?: Date;
entries!: ManualJournalEntry[];
attachments!: Document[];
/**
* Table name.
*/
static get tableName() {
return 'manual_journals';
}
/**
* Model timestamps.
*/
get timestamps() {
return ['createdAt', 'updatedAt'];
}
/**
* Virtual attributes.
*/
static get virtualAttributes() {
return ['isPublished', 'amountFormatted'];
}
/**
* Retrieve the amount formatted value.
*/
// get amountFormatted() {
// return formatNumber(this.amount, { currencyCode: this.currencyCode });
// }
/**
* Detarmines whether the invoice is published.
* @return {boolean}
*/
get isPublished() {
return !!this.publishedAt;
}
/**
* Model modifiers.
*/
static get modifiers() {
return {
/**
* Sort by status query.
*/
sortByStatus(query, order) {
query.orderByRaw(`PUBLISHED_AT IS NULL ${order}`);
},
/**
* Filter by draft status.
*/
filterByDraft(query) {
query.whereNull('publishedAt');
},
/**
* Filter by published status.
*/
filterByPublished(query) {
query.whereNotNull('publishedAt');
},
/**
* Filter by the given status.
*/
filterByStatus(query, filterType) {
switch (filterType) {
case 'draft':
query.modify('filterByDraft');
break;
case 'published':
default:
query.modify('filterByPublished');
break;
}
},
};
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const { AccountTransaction } = require('../../Accounts/models/AccountTransaction.model');
const { ManualJournalEntry } = require('./ManualJournalEntry');
const { Document } = require('../../ChromiumlyTenancy/models/Document');
// const { MatchedBankTransaction } = require('models/MatchedBankTransaction');
return {
entries: {
relation: Model.HasManyRelation,
modelClass: ManualJournalEntry,
join: {
from: 'manual_journals.id',
to: 'manual_journals_entries.manualJournalId',
},
filter(query) {
query.orderBy('index', 'ASC');
},
},
transactions: {
relation: Model.HasManyRelation,
modelClass: AccountTransaction,
join: {
from: 'manual_journals.id',
to: 'accounts_transactions.referenceId',
},
filter: (query) => {
query.where('referenceType', 'Journal');
},
},
/**
* Manual journal may has many attached attachments.
*/
attachments: {
relation: Model.ManyToManyRelation,
modelClass: Document,
join: {
from: 'manual_journals.id',
through: {
from: 'document_links.modelId',
to: 'document_links.documentId',
},
to: 'documents.id',
},
filter(query) {
query.where('model_ref', 'ManualJournal');
},
},
/**
* Manual journal may belongs to matched bank transaction.
*/
// matchedBankTransaction: {
// relation: Model.BelongsToOneRelation,
// modelClass: MatchedBankTransaction,
// join: {
// from: 'manual_journals.id',
// to: 'matched_bank_transactions.referenceId',
// },
// filter(query) {
// query.where('reference_type', 'ManualJournal');
// },
// },
};
}
// static get meta() {
// return ManualJournalSettings;
// }
// /**
// * Retrieve the default custom views, roles and columns.
// */
// static get defaultViews() {
// return DEFAULT_VIEWS;
// }
/**
* Model search attributes.
*/
static get searchRoles() {
return [
{ fieldKey: 'journal_number', comparator: 'contains' },
{ condition: 'or', fieldKey: 'reference', comparator: 'contains' },
{ condition: 'or', fieldKey: 'amount', comparator: 'equals' },
];
}
/**
* Prevents mutate base currency since the model is not empty.
*/
static get preventMutateBaseCurrency() {
return true;
}
}

View File

@@ -0,0 +1,64 @@
import { Model } from 'objection';
import { BaseModel } from '@/models/Model';
export class ManualJournalEntry extends BaseModel {
index: number;
credit: number;
debit: number;
accountId: number;
note: string;
contactId?: number;
branchId!: number;
projectId?: number;
/**
* Table name.
*/
static get tableName() {
return 'manual_journals_entries';
}
/**
* Model timestamps.
*/
get timestamps() {
return [];
}
/**
* Relationship mapping.
*/
static get relationMappings() {
const { Account } = require('../../Accounts/models/Account.model');
const { Contact } = require('../../Contacts/models/Contact');
const { Branch } = require('../../Branches/models/Branch.model');
return {
account: {
relation: Model.BelongsToOneRelation,
modelClass: Account,
join: {
from: 'manual_journals_entries.accountId',
to: 'accounts.id',
},
},
contact: {
relation: Model.BelongsToOneRelation,
modelClass: Contact,
join: {
from: 'manual_journals_entries.contactId',
to: 'contacts.id',
},
},
branch: {
relation: Model.BelongsToOneRelation,
modelClass: Branch,
join: {
from: 'manual_journals_entries.branchId',
to: 'branches.id',
},
},
};
}
}

View File

@@ -0,0 +1,36 @@
import { Inject, Injectable } from '@nestjs/common';
import { ManualJournal } from '../models/ManualJournal';
import { TransformerInjectable } from '@/modules/Transformer/TransformerInjectable.service';
import { ManualJournalTransfromer } from './ManualJournalTransformer';
@Injectable()
export class GetManualJournal {
constructor(
private readonly transformer: TransformerInjectable,
@Inject(ManualJournal.name)
private readonly manualJournalModel: typeof ManualJournal,
) {}
/**
* Retrieve manual journal details with associated journal transactions.
* @param {number} tenantId
* @param {number} manualJournalId
*/
public getManualJournal = async (manualJournalId: number) => {
const manualJournal = await this.manualJournalModel
.query()
.findById(manualJournalId)
.withGraphFetched('entries.account')
.withGraphFetched('entries.contact')
.withGraphFetched('entries.branch')
.withGraphFetched('transactions')
.withGraphFetched('attachments')
.throwIfNotFound();
return this.transformer.transform(
manualJournal,
new ManualJournalTransfromer(),
);
};
}

View File

@@ -0,0 +1,77 @@
// import { Service, Inject } from 'typedi';
// import * as R from 'ramda';
// import {
// IManualJournalsFilter,
// IManualJournal,
// IPaginationMeta,
// IFilterMeta,
// } from '@/interfaces';
// import TenancyService from '@/services/Tenancy/TenancyService';
// import DynamicListingService from '@/services/DynamicListing/DynamicListService';
// import { ManualJournalTransfromer } from './ManualJournalTransformer';
// import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
// @Service()
// export class GetManualJournals {
// @Inject()
// private tenancy: TenancyService;
// @Inject()
// private dynamicListService: DynamicListingService;
// @Inject()
// private transformer: TransformerInjectable;
// /**
// * Parses filter DTO of the manual journals list.
// * @param filterDTO
// */
// private parseListFilterDTO = (filterDTO) => {
// return R.compose(this.dynamicListService.parseStringifiedFilter)(filterDTO);
// };
// /**
// * Retrieve manual journals datatable list.
// * @param {number} tenantId -
// * @param {IManualJournalsFilter} filter -
// */
// public getManualJournals = async (
// tenantId: number,
// filterDTO: IManualJournalsFilter
// ): Promise<{
// manualJournals: IManualJournal[];
// pagination: IPaginationMeta;
// filterMeta: IFilterMeta;
// }> => {
// const { ManualJournal } = this.tenancy.models(tenantId);
// // Parses filter DTO.
// const filter = this.parseListFilterDTO(filterDTO);
// // Dynamic service.
// const dynamicService = await this.dynamicListService.dynamicList(
// tenantId,
// ManualJournal,
// filter
// );
// const { results, pagination } = await ManualJournal.query()
// .onBuild((builder) => {
// dynamicService.buildQuery()(builder);
// builder.withGraphFetched('entries.account');
// })
// .pagination(filter.page - 1, filter.pageSize);
// // Transformes the manual journals models to POJO.
// const manualJournals = await this.transformer.transform(
// tenantId,
// results,
// new ManualJournalTransfromer()
// );
// return {
// manualJournals,
// pagination,
// filterMeta: dynamicService.getResponseMeta(),
// };
// };
// }

View File

@@ -0,0 +1,66 @@
import { Transformer } from '@/modules/Transformer/Transformer';
import { AttachmentTransformer } from '@/modules/Attachments/Attachment.transformer';
import { ManualJournal } from '../models/ManualJournal';
export class ManualJournalTransfromer extends Transformer {
/**
* Include these attributes to expense object.
* @returns {Array}
*/
public includeAttributes = (): string[] => {
return [
'formattedAmount',
'formattedDate',
'formattedPublishedAt',
'formattedCreatedAt',
'attachments',
];
};
/**
* Retrieve formatted journal amount.
* @param {IManualJournal} manualJournal
* @returns {string}
*/
protected formattedAmount = (manualJorunal: ManualJournal): string => {
return this.formatNumber(manualJorunal.amount, {
currencyCode: manualJorunal.currencyCode,
});
};
/**
* Retrieve formatted date.
* @param {ManualJournal} manualJournal
* @returns {string}
*/
protected formattedDate = (manualJorunal: ManualJournal): string => {
return this.formatDate(manualJorunal.date);
};
/**
* Retrieve formatted created at date.
* @param {ManualJournal} manualJournal
* @returns {string}
*/
protected formattedCreatedAt = (manualJorunal: ManualJournal): string => {
return this.formatDate(manualJorunal.createdAt);
};
/**
* Retrieve formatted published at date.
* @param {ManualJournal} manualJournal
* @returns {string}
*/
protected formattedPublishedAt = (manualJorunal: ManualJournal): string => {
return this.formatDate(manualJorunal.publishedAt);
};
/**
* Retrieves the manual journal attachments.
* @param {ManualJournal} manualJorunal
* @returns
*/
protected attachments = (manualJorunal: ManualJournal) => {
return this.item(manualJorunal.attachments, new AttachmentTransformer());
};
}

View File

@@ -0,0 +1,100 @@
import { Knex } from 'knex';
// import { IDynamicListFilterDTO } from './DynamicFilter';
// import { ISystemUser } from './User';
// import { IAccount } from './Account';
// import { AttachmentLinkDTO } from './Attachments';
import { ManualJournal } from '../models/ManualJournal';
import { AttachmentLinkDTO } from '@/modules/Attachments/Attachments.types';
export interface IManualJournalEntryDTO {
index: number;
credit: number;
debit: number;
accountId: number;
note: string;
contactId?: number;
branchId?: number
projectId?: number;
}
export interface IManualJournalDTO {
date: Date;
currencyCode?: string;
exchangeRate?: number;
journalNumber: string;
journalType: string;
reference?: string;
description?: string;
publish?: boolean;
branchId?: number;
entries: IManualJournalEntryDTO[];
attachments?: AttachmentLinkDTO[];
}
// export interface IManualJournalsFilter extends IDynamicListFilterDTO {
// stringifiedFilterRoles?: string;
// page: number;
// pageSize: number;
// }
export interface IManualJournalEventPublishedPayload {
// tenantId: number;
manualJournal: ManualJournal;
// manualJournalId: number;
oldManualJournal: ManualJournal;
trx: Knex.Transaction;
}
export interface IManualJournalPublishingPayload {
oldManualJournal: ManualJournal;
trx: Knex.Transaction;
// tenantId: number;
}
export interface IManualJournalEventDeletedPayload {
// tenantId: number;
manualJournalId: number;
oldManualJournal: ManualJournal;
trx?: Knex.Transaction;
}
export interface IManualJournalDeletingPayload {
// tenantId: number;
oldManualJournal: ManualJournal;
trx: Knex.Transaction;
}
export interface IManualJournalEventEditedPayload {
// tenantId: number;
manualJournal: ManualJournal;
oldManualJournal: ManualJournal;
manualJournalDTO: IManualJournalDTO;
trx: Knex.Transaction;
}
export interface IManualJournalEditingPayload {
// tenantId: number;
oldManualJournal: ManualJournal;
manualJournalDTO: IManualJournalDTO;
trx: Knex.Transaction;
}
export interface IManualJournalCreatingPayload {
// tenantId: number;
manualJournalDTO: IManualJournalDTO;
trx: Knex.Transaction;
}
export interface IManualJournalEventCreatedPayload {
// tenantId: number;
manualJournal: ManualJournal;
// manualJournalId: number;
manualJournalDTO: IManualJournalDTO;
trx: Knex.Transaction;
}
export enum ManualJournalAction {
Create = 'Create',
View = 'View',
Edit = 'Edit',
Delete = 'Delete',
}

View File

@@ -7,18 +7,19 @@ import { findByIsoCountryCode } from '@bigcapital/utils';
// import { getUploadedObjectUri } from '../../services/Attachments/utils';
export class TenantMetadata extends BaseModel {
baseCurrency!: string;
name!: string;
tenantId!: number;
industry!: string;
location!: string;
language!: string;
timezone!: string;
dateFormat!: string;
fiscalYear!: string;
primaryColor!: string;
logoKey!: string;
address!: Record<string, any>;
public baseCurrency!: string;
public name!: string;
public tenantId!: number;
public industry!: string;
public location!: string;
public language!: string;
public timezone!: string;
public dateFormat!: string;
public fiscalYear!: string;
public primaryColor!: string;
public logoKey!: string;
public logoUri!: string;
public address!: Record<string, any>;
/**
* Json schema.

View File

@@ -8,7 +8,7 @@ export class TenantModel extends BaseModel {
public readonly initializedAt: string;
public readonly seededAt: boolean;
public readonly builtAt: string;
public readonly metadata: any;
public readonly metadata: TenantMetadata;
/**
* Table name.

View File

@@ -21,7 +21,8 @@ export class TemplateInjectable {
return templateRender(filename, {
organizationName: organization.metadata.name,
organizationEmail: organization.metadata.email,
// @todo email
organizationEmail: '',
__: this.i18n.t,
...options,
});

View File

@@ -32,6 +32,8 @@ import { CreditNoteAppliedInvoice } from '@/modules/CreditNotes/models/CreditNot
import { CreditNote } from '@/modules/CreditNotes/models/CreditNote';
import { PaymentLink } from '@/modules/PaymentLinks/models/PaymentLink';
import { SaleReceipt } from '@/modules/SaleReceipts/models/SaleReceipt';
import { ManualJournal } from '@/modules/ManualJournals/models/ManualJournal';
import { ManualJournalEntry } from '@/modules/ManualJournals/models/ManualJournalEntry';
const models = [
Item,
@@ -63,7 +65,9 @@ const models = [
CreditNoteAppliedInvoice,
CreditNote,
PaymentLink,
SaleReceipt
SaleReceipt,
ManualJournal,
ManualJournalEntry
];
const modelProviders = models.map((model) => {

View File

@@ -1,4 +1,3 @@
import { Model } from 'objection';
import { BaseModel } from '@/models/Model';
import {
getExlusiveTaxAmount,

View File

@@ -1,4 +1,4 @@
import moment from 'moment';
import * as moment from 'moment';
import * as R from 'ramda';
import { includes, isFunction, isObject, isUndefined, omit } from 'lodash';
// import { EXPORT_DTE_FORMAT } from '@/services/Export/constants';