feat: rewrite repositories with base entity repository class.

feat: sales and purchases status.
feat: sales and purchases auto-increment number.
fix: settings find query with extra columns.
This commit is contained in:
Ahmed Bouhuolia
2020-12-13 19:50:59 +02:00
parent e9e4ddaee0
commit 188e411f02
78 changed files with 1634 additions and 869 deletions

View File

@@ -119,7 +119,7 @@ export default class PaymentReceiveService {
const { accountTypeRepository, accountRepository } = this.tenancy.repositories(tenantId);
const currentAssetTypes = await accountTypeRepository.getByChildType('current_asset');
const depositAccount = await accountRepository.findById(depositAccountId);
const depositAccount = await accountRepository.findOneById(depositAccountId);
const currentAssetTypesIds = currentAssetTypes.map(type => type.id);

View File

@@ -12,6 +12,7 @@ import ItemsEntriesService from 'services/Items/ItemsEntriesService';
import events from 'subscribers/events';
import { ServiceError } from 'exceptions';
import CustomersService from 'services/Contacts/CustomersService';
import moment from 'moment';
const ERRORS = {
@@ -19,6 +20,8 @@ const ERRORS = {
CUSTOMER_NOT_FOUND: 'CUSTOMER_NOT_FOUND',
SALE_ESTIMATE_NUMBER_EXISTANCE: 'SALE_ESTIMATE_NUMBER_EXISTANCE',
ITEMS_IDS_NOT_EXISTS: 'ITEMS_IDS_NOT_EXISTS',
SALE_ESTIMATE_ALREADY_DELIVERED: 'SALE_ESTIMATE_ALREADY_DELIVERED',
SALE_ESTIMATE_CONVERTED_TO_INVOICE: 'SALE_ESTIMATE_CONVERTED_TO_INVOICE'
};
/**
* Sale estimate service.
@@ -80,6 +83,39 @@ export default class SaleEstimateService {
}
}
/**
* Transform DTO object ot model object.
* @param {number} tenantId
* @param {ISaleEstimateDTO} saleEstimateDTO
* @param {ISaleEstimate} oldSaleEstimate
* @return {ISaleEstimate}
*/
transformDTOToModel(
tenantId: number,
estimateDTO: ISaleEstimateDTO,
oldSaleEstimate?: ISaleEstimate,
): ISaleEstimate {
const { ItemEntry } = this.tenancy.models(tenantId);
const amount = sumBy(estimateDTO.entries, e => ItemEntry.calcAmount(e));
return {
amount,
...formatDateFields(
omit(estimateDTO, ['delivered', 'entries']),
['estimateDate', 'expirationDate']
),
entries: estimateDTO.entries.map((entry) => ({
reference_type: 'SaleEstimate',
...omit(entry, ['total', 'amount', 'id']),
})),
// Avoid rewrite the deliver date in edit mode when already published.
...(estimateDTO.delivered && (!oldSaleEstimate?.deliveredAt)) && ({
deliveredAt: moment().toMySqlDateTime(),
}),
};
}
/**
* Creates a new estimate with associated entries.
* @async
@@ -87,16 +123,16 @@ export default class SaleEstimateService {
* @param {EstimateDTO} estimate
* @return {Promise<ISaleEstimate>}
*/
public async createEstimate(tenantId: number, estimateDTO: ISaleEstimateDTO): Promise<ISaleEstimate> {
const { SaleEstimate, ItemEntry } = this.tenancy.models(tenantId);
public async createEstimate(
tenantId: number,
estimateDTO: ISaleEstimateDTO
): Promise<ISaleEstimate> {
const { SaleEstimate } = this.tenancy.models(tenantId);
this.logger.info('[sale_estimate] inserting sale estimate to the storage.');
const amount = sumBy(estimateDTO.entries, e => ItemEntry.calcAmount(e));
const estimateObj = {
amount,
...formatDateFields(estimateDTO, ['estimateDate', 'expirationDate']),
};
// Transform DTO object ot model object.
const estimateObj = this.transformDTOToModel(tenantId, estimateDTO);
// Validate estimate number uniquiness on the storage.
if (estimateDTO.estimateNumber) {
@@ -112,13 +148,7 @@ export default class SaleEstimateService {
await this.itemsEntriesService.validateNonSellableEntriesItems(tenantId, estimateDTO.entries);
const saleEstimate = await SaleEstimate.query()
.upsertGraphAndFetch({
...omit(estimateObj, ['entries']),
entries: estimateObj.entries.map((entry) => ({
reference_type: 'SaleEstimate',
...omit(entry, ['total', 'amount', 'id']),
}))
});
.upsertGraphAndFetch({ ...estimateObj });
this.logger.info('[sale_estimate] insert sale estimated success.');
await this.eventDispatcher.dispatch(events.saleEstimate.onCreated, {
@@ -136,15 +166,16 @@ export default class SaleEstimateService {
* @param {EstimateDTO} estimate
* @return {void}
*/
public async editEstimate(tenantId: number, estimateId: number, estimateDTO: ISaleEstimateDTO): Promise<ISaleEstimate> {
const { SaleEstimate, ItemEntry } = this.tenancy.models(tenantId);
public async editEstimate(
tenantId: number,
estimateId: number,
estimateDTO: ISaleEstimateDTO
): Promise<ISaleEstimate> {
const { SaleEstimate } = this.tenancy.models(tenantId);
const oldSaleEstimate = await this.getSaleEstimateOrThrowError(tenantId, estimateId);
const amount = sumBy(estimateDTO.entries, (e) => ItemEntry.calcAmount(e));
const estimateObj = {
amount,
...formatDateFields(estimateDTO, ['estimateDate', 'expirationDate']),
};
// Transform DTO object ot model object.
const estimateObj = this.transformDTOToModel(tenantId, estimateDTO, oldSaleEstimate);
// Validate estimate number uniquiness on the storage.
if (estimateDTO.estimateNumber) {
@@ -166,11 +197,7 @@ export default class SaleEstimateService {
const saleEstimate = await SaleEstimate.query()
.upsertGraphAndFetch({
id: estimateId,
...omit(estimateObj, ['entries']),
entries: estimateObj.entries.map((entry) => ({
reference_type: 'SaleEstimate',
...omit(entry, ['total', 'amount']),
})),
...estimateObj
});
await this.eventDispatcher.dispatch(events.saleEstimate.onEdited, {
@@ -194,6 +221,11 @@ export default class SaleEstimateService {
// Retrieve sale estimate or throw not found service error.
const oldSaleEstimate = await this.getSaleEstimateOrThrowError(tenantId, estimateId);
// Throw error if the sale estimate converted to sale invoice.
if (oldSaleEstimate.convertedToInvoiceId) {
throw new ServiceError(ERRORS.SALE_ESTIMATE_CONVERTED_TO_INVOICE);
}
this.logger.info('[sale_estimate] delete sale estimate and associated entries from the storage.');
await ItemEntry.query()
.where('reference_id', estimateId)
@@ -254,4 +286,70 @@ export default class SaleEstimateService {
filterMeta: dynamicFilter.getResponseMeta(),
};
}
/**
* Converts estimate to invoice.
* @param {number} tenantId -
* @param {number} estimateId -
* @return {Promise<void>}
*/
async convertEstimateToInvoice(
tenantId: number,
estimateId: number,
invoiceId: number,
): Promise<void> {
const { SaleEstimate } = this.tenancy.models(tenantId);
// Retrieve details of the given sale estimate.
const saleEstimate = await this.getSaleEstimateOrThrowError(tenantId, estimateId);
await SaleEstimate.query().where('id', estimateId).patch({
convertedToInvoiceId: invoiceId,
convertedToInvoiceAt: moment().toMySqlDateTime(),
});
}
/**
* Unlink the converted sale estimates from the given sale invoice.
* @param {number} tenantId -
* @param {number} invoiceId -
* @return {Promise<void>}
*/
async unlinkConvertedEstimateFromInvoice(
tenantId: number,
invoiceId: number,
): Promise<void> {
const { SaleEstimate } = this.tenancy.models(tenantId);
await SaleEstimate.query().where({
convertedToInvoiceId: invoiceId,
}).patch({
convertedToInvoiceId: null,
convertedToInvoiceAt: null,
});
}
/**
* Mark the sale estimate as delivered.
* @param {number} tenantId - Tenant id.
* @param {number} saleEstimateId - Sale estimate id.
*/
public async deliverSaleEstimate(
tenantId: number,
saleEstimateId: number,
): Promise<void> {
const { SaleEstimate } = this.tenancy.models(tenantId);
// Retrieve details of the given sale estimate id.
const saleEstimate = await this.getSaleEstimateOrThrowError(tenantId, saleEstimateId);
// Throws error in case the sale estimate already published.
if (saleEstimate.isDelivered) {
throw new ServiceError(ERRORS.SALE_ESTIMATE_ALREADY_DELIVERED);
}
// Record the delivered at on the storage.
await SaleEstimate.query().where('id', saleEstimateId).patch({
deliveredAt: moment().toMySqlDateTime()
});
}
}

View File

@@ -1,5 +1,6 @@
import { Service, Inject } from 'typedi';
import { omit, sumBy, difference, pick, chain } from 'lodash';
import { omit, sumBy, pick, chain } from 'lodash';
import moment from 'moment';
import {
EventDispatcher,
EventDispatcherInterface,
@@ -10,7 +11,9 @@ import {
IItemEntry,
ISalesInvoicesFilter,
IPaginationMeta,
IFilterMeta
IFilterMeta,
ISaleInvoiceCreateDTO,
ISaleInvoiceEditDTO,
} from 'interfaces';
import events from 'subscribers/events';
import JournalPoster from 'services/Accounting/JournalPoster';
@@ -23,11 +26,13 @@ import { ServiceError } from 'exceptions';
import ItemsService from 'services/Items/ItemsService';
import ItemsEntriesService from 'services/Items/ItemsEntriesService';
import CustomersService from 'services/Contacts/CustomersService';
import SaleEstimateService from 'services/Sales/SalesEstimate';
const ERRORS = {
INVOICE_NUMBER_NOT_UNIQUE: 'INVOICE_NUMBER_NOT_UNIQUE',
SALE_INVOICE_NOT_FOUND: 'SALE_INVOICE_NOT_FOUND',
SALE_INVOICE_ALREADY_DELIVERED: 'SALE_INVOICE_ALREADY_DELIVERED',
ENTRIES_ITEMS_IDS_NOT_EXISTS: 'ENTRIES_ITEMS_IDS_NOT_EXISTS',
NOT_SELLABLE_ITEMS: 'NOT_SELLABLE_ITEMS',
SALE_INVOICE_NO_NOT_UNIQUE: 'SALE_INVOICE_NO_NOT_UNIQUE'
@@ -63,6 +68,9 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
@Inject()
customersService: CustomersService;
@Inject()
saleEstimatesService: SaleEstimateService;
/**
*
* Validate whether sale invoice number unqiue on the storage.
@@ -101,6 +109,33 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
return saleInvoice;
}
/**
* Transform DTO object to model object.
* @param {number} tenantId - Tenant id.
* @param {ISaleInvoiceOTD} saleInvoiceDTO - Sale invoice DTO.
*/
transformDTOToModel(
tenantId: number,
saleInvoiceDTO: ISaleInvoiceCreateDTO|ISaleInvoiceEditDTO,
oldSaleInvoice?: ISaleInvoice
): ISaleInvoice {
const { ItemEntry } = this.tenancy.models(tenantId);
const balance = sumBy(saleInvoiceDTO.entries, e => ItemEntry.calcAmount(e));
return {
...formatDateFields(
omit(saleInvoiceDTO, ['delivered']),
['invoiceDate', 'dueDate']
),
// Avoid rewrite the deliver date in edit mode when already published.
...(saleInvoiceDTO.delivered && (!oldSaleInvoice?.deliveredAt)) && ({
deliveredAt: moment().toMySqlDateTime(),
}),
balance,
paymentAmount: 0,
}
}
/**
* Creates a new sale invoices and store it to the storage
* with associated to entries and journal transactions.
@@ -109,18 +144,16 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
* @param {ISaleInvoice} saleInvoiceDTO -
* @return {ISaleInvoice}
*/
public async createSaleInvoice(tenantId: number, saleInvoiceDTO: ISaleInvoiceOTD): Promise<ISaleInvoice> {
const { SaleInvoice, ItemEntry } = this.tenancy.models(tenantId);
const balance = sumBy(saleInvoiceDTO.entries, e => ItemEntry.calcAmount(e));
public async createSaleInvoice(
tenantId: number,
saleInvoiceDTO: ISaleInvoiceCreateDTO
): Promise<ISaleInvoice> {
const { SaleInvoice } = this.tenancy.models(tenantId);
const invLotNumber = 1;
const saleInvoiceObj: ISaleInvoice = {
...formatDateFields(saleInvoiceDTO, ['invoiceDate', 'dueDate']),
balance,
paymentAmount: 0,
// invLotNumber,
};
// Transform DTO object to model object.
const saleInvoiceObj = this.transformDTOToModel(tenantId, saleInvoiceDTO);
// Validate customer existance.
await this.customersService.getCustomerByIdOrThrowError(tenantId, saleInvoiceDTO.customerId);
@@ -131,6 +164,8 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
}
// Validate items ids existance.
await this.itemsEntriesService.validateItemsIdsExistance(tenantId, saleInvoiceDTO.entries);
// Validate items should be sellable items.
await this.itemsEntriesService.validateNonSellableEntriesItems(tenantId, saleInvoiceDTO.entries);
this.logger.info('[sale_invoice] inserting sale invoice to the storage.');
@@ -165,11 +200,8 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
const balance = sumBy(saleInvoiceDTO.entries, e => ItemEntry.calcAmount(e));
const oldSaleInvoice = await this.getInvoiceOrThrowError(tenantId, saleInvoiceId);
const saleInvoiceObj = {
...formatDateFields(saleInvoiceDTO, ['invoiceDate', 'dueDate']),
balance,
// invLotNumber: oldSaleInvoice.invLotNumber,
};
// Transform DTO object to model object.
const saleInvoiceObj = this.transformDTOToModel(tenantId, saleInvoiceDTO, oldSaleInvoice);
// Validate customer existance.
await this.customersService.getCustomerByIdOrThrowError(tenantId, saleInvoiceDTO.customerId);
@@ -203,10 +235,34 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
await this.eventDispatcher.dispatch(events.saleInvoice.onEdited, {
saleInvoice, oldSaleInvoice, tenantId, saleInvoiceId,
});
return saleInvoice;
}
/**
* Deliver the given sale invoice.
* @param {number} tenantId - Tenant id.
* @param {number} saleInvoiceId - Sale invoice id.
* @return {Promise<void>}
*/
public async deliverSaleInvoice(
tenantId: number,
saleInvoiceId: number,
): Promise<void> {
const { saleInvoiceRepository } = this.tenancy.repositories(tenantId);
// Retrieve details of the given sale invoice id.
const saleInvoice = await this.getInvoiceOrThrowError(tenantId, saleInvoiceId);
// Throws error in case the sale invoice already published.
if (saleInvoice.isDelivered) {
throw new ServiceError(ERRORS.SALE_INVOICE_ALREADY_DELIVERED);
}
// Record the delivered at on the storage.
await saleInvoiceRepository.update({
deliveredAt: moment().toMySqlDateTime()
}, { id: saleInvoiceId });
}
/**
* Deletes the given sale invoice with associated entries
* and journal transactions.
@@ -218,6 +274,12 @@ export default class SaleInvoicesService extends SalesInvoicesCost {
const oldSaleInvoice = await this.getInvoiceOrThrowError(tenantId, saleInvoiceId);
// Unlink the converted sale estimates from the given sale invoice.
await this.saleEstimatesService.unlinkConvertedEstimateFromInvoice(
tenantId,
saleInvoiceId,
);
this.logger.info('[sale_invoice] delete sale invoice with entries.');
await SaleInvoice.query().where('id', saleInvoiceId).delete();
await ItemEntry.query()

View File

@@ -66,12 +66,12 @@ export default class SalesReceiptService {
*/
async validateReceiptDepositAccountExistance(tenantId: number, accountId: number) {
const { accountRepository, accountTypeRepository } = this.tenancy.repositories(tenantId);
const depositAccount = await accountRepository.findById(accountId);
const depositAccount = await accountRepository.findOneById(accountId);
if (!depositAccount) {
throw new ServiceError(ERRORS.DEPOSIT_ACCOUNT_NOT_FOUND);
}
const depositAccountType = await accountTypeRepository.getTypeMeta(depositAccount.accountTypeId);
const depositAccountType = await accountTypeRepository.findOneById(depositAccount.accountTypeId);
if (!depositAccountType || depositAccountType.childRoot === 'current_asset') {
throw new ServiceError(ERRORS.DEPOSIT_ACCOUNT_NOT_CURRENT_ASSET);