feat(sales): currency code associated from invoice customer.

feat(purchases): currency code associated from vendor customer.
This commit is contained in:
a.bouhuolia
2021-03-08 09:47:04 +02:00
parent 6ec4ee4f0f
commit 3a3d881f67
26 changed files with 292 additions and 196 deletions

View File

@@ -103,7 +103,7 @@ export default class ExchangeRatesController extends BaseController {
const { tenantId } = req;
const filter = {
page: 1,
pageSize: 100,
pageSize: 12,
filterRoles: [],
columnSortBy: 'created_at',
sortOrder: 'asc',

View File

@@ -48,7 +48,7 @@ export default class BillsController extends BaseController {
);
router.post(
'/:id', [
...this.billValidationSchema,
...this.billEditValidationSchema,
...this.specificBillValidationSchema,
],
this.validationResult,
@@ -106,7 +106,6 @@ export default class BillsController extends BaseController {
check('entries').isArray({ min: 1 }),
check('entries.*.id').optional().isNumeric().toInt(),
check('entries.*.index').exists().isNumeric().toInt(),
check('entries.*.item_id').exists().isNumeric().toInt(),
check('entries.*.rate').exists().isNumeric().toFloat(),
@@ -121,7 +120,7 @@ export default class BillsController extends BaseController {
*/
get billEditValidationSchema() {
return [
check('bill_number').exists().trim().escape(),
check('bill_number').optional().trim().escape(),
check('reference_no').optional().trim().escape(),
check('bill_date').exists().isISO8601(),
check('due_date').optional().isISO8601(),

View File

@@ -407,6 +407,11 @@ export default class BillsPayments extends BaseController {
errors: [{ type: 'BILLS_NOT_FOUND', code: 1000 }],
});
}
if (error.errorType === 'PAYMENT_NUMBER_SHOULD_NOT_MODIFY') {
return res.status(400).send({
errors: [{ type: 'PAYMENT_NUMBER_SHOULD_NOT_MODIFY', code: 1100 }],
});
}
}
next(error);
}

View File

@@ -91,6 +91,7 @@ export default class PaymentReceivesController extends BaseController {
*/
get paymentReceiveSchema(): ValidationChain[] {
return [
check('customer_id').exists().isNumeric().toInt(),
check('payment_date').exists(),
check('reference_no').optional(),
check('deposit_account_id').exists().isNumeric().toInt(),
@@ -132,7 +133,6 @@ export default class PaymentReceivesController extends BaseController {
*/
get newPaymentReceiveValidation() {
return [
check('customer_id').exists().isNumeric().toInt(),
...this.paymentReceiveSchema,
];
}
@@ -449,6 +449,11 @@ export default class PaymentReceivesController extends BaseController {
errors: [{ type: 'PAYMENT_RECEIVE_NO_IS_REQUIRED', code: 1100 }],
});
}
if (error.errorType === 'PAYMENT_CUSTOMER_SHOULD_NOT_UPDATE') {
return res.boom.badRequest(null, {
errors: [{ type: 'PAYMENT_CUSTOMER_SHOULD_NOT_UPDATE', code: 1200 }],
});
}
}
next(error);
}

View File

@@ -301,6 +301,7 @@ export default class SaleInvoicesController extends BaseController {
filterMeta,
pagination,
} = await this.saleInvoiceService.salesInvoicesList(tenantId, filter);
return res.status(200).send({
sales_invoices: salesInvoices,
pagination: this.transfromToResponse(pagination),

View File

@@ -4,6 +4,7 @@ exports.up = function(knex) {
return knex.schema.createTable('sales_estimates', (table) => {
table.increments();
table.decimal('amount', 13, 3);
table.string('currency_code', 3);
table.integer('customer_id').unsigned().index().references('id').inTable('contacts');
table.date('estimate_date').index();
table.date('expiration_date').index();

View File

@@ -3,6 +3,7 @@ exports.up = function(knex) {
return knex.schema.createTable('sales_receipts', table => {
table.increments();
table.decimal('amount', 13, 3);
table.string('currency_code', 3);
table.integer('deposit_account_id').unsigned().index().references('id').inTable('accounts');
table.integer('customer_id').unsigned().index().references('id').inTable('contacts');
table.date('receipt_date').index();

View File

@@ -13,6 +13,7 @@ exports.up = function(knex) {
table.decimal('balance', 13, 3);
table.decimal('payment_amount', 13, 3);
table.string('currency_code', 3);
table.string('inv_lot_number').index();

View File

@@ -6,6 +6,7 @@ exports.up = function(knex) {
table.integer('customer_id').unsigned().index().references('id').inTable('contacts');
table.date('payment_date').index();
table.decimal('amount', 13, 3).defaultTo(0);
table.string('currency_code', 3);
table.string('reference_no').index();
table.integer('deposit_account_id').unsigned().references('id').inTable('accounts');
table.string('payment_receive_no').nullable();

View File

@@ -10,6 +10,7 @@ exports.up = function(knex) {
table.string('status').index();
table.text('note');
table.decimal('amount', 13, 3).defaultTo(0);
table.string('currency_code');
table.decimal('payment_amount', 13, 3).defaultTo(0);
table.string('inv_lot_number').index();
table.date('opened_at').index();

View File

@@ -4,6 +4,7 @@ exports.up = function(knex) {
table.increments();
table.integer('vendor_id').unsigned().index().references('id').inTable('contacts');
table.decimal('amount', 13, 3).defaultTo(0);
table.string('currency_code');
table.integer('payment_account_id').unsigned().references('id').inTable('accounts');
table.string('payment_number').nullable().index();
table.date('payment_date').index();

View File

@@ -25,6 +25,7 @@ export interface IPaymentReceiveCreateDTO {
};
export interface IPaymentReceiveEditDTO {
customerId: number,
paymentDate: Date,
amount: number,
referenceNo: string,

View File

@@ -4,6 +4,7 @@ import { IDynamicListFilterDTO } from 'interfaces/DynamicFilter';
export interface ISaleEstimate {
id?: number,
amount: number,
currencyCode: string,
customerId: number,
estimateDate: Date,
estimateNumber: string,

View File

@@ -5,6 +5,7 @@ export interface ISaleInvoice {
id: number,
balance: number,
paymentAmount: number,
currencyCode: string,
invoiceDate: Date,
dueDate: Date,
dueAmount: number,

View File

@@ -10,6 +10,7 @@ export interface ISaleReceipt {
receiptMessage: string;
receiptNumber: string;
amount: number;
currencyCode: string,
statement: string;
closedAt: Date | string;
entries: any[];

View File

@@ -63,6 +63,11 @@ export default class ContactsService {
* @param {IContactNewDTO | IContactEditDTO} contactDTO
*/
private transformContactObj(contactDTO: IContactNewDTO | IContactEditDTO) {
const baseCurrency = 'USD';
const currencyCode = typeof contactDTO.currencyCode !== 'undefined'
? contactDTO.currencyCode
: baseCurrency;
return {
...omit(contactDTO, [
'billingAddress1',
@@ -74,6 +79,7 @@ export default class ContactsService {
billing_address_2: contactDTO?.billingAddress2,
shipping_address_1: contactDTO?.shippingAddress1,
shipping_address_2: contactDTO?.shippingAddress2,
...(currencyCode ? ({ currencyCode }) : {}),
};
}
@@ -99,7 +105,6 @@ export default class ContactsService {
contactService,
...contactObj,
});
this.logger.info('[contacts] contact inserted successfully.', {
tenantId,
contact,
@@ -123,12 +128,12 @@ export default class ContactsService {
const { contactRepository } = this.tenancy.repositories(tenantId);
const contactObj = this.transformContactObj(contactDTO);
// Retrieve the given contact by id or throw not found service error.
const contact = await this.getContactByIdOrThrowError(
tenantId,
contactId,
contactService
);
this.logger.info('[contacts] trying to edit the given contact details.', {
tenantId,
contactId,
@@ -196,7 +201,7 @@ export default class ContactsService {
const dynamicList = await this.dynamicListService.dynamicList(
tenantId,
Contact,
contactsFilter,
contactsFilter
);
// Retrieve contacts list by the given query.
const contacts = await Contact.query().onBuild((builder) => {

View File

@@ -116,7 +116,7 @@ export default class VendorsService {
* @param {number} tenantId
* @param {number} customerId
*/
private getVendorByIdOrThrowError(tenantId: number, customerId: number) {
public getVendorByIdOrThrowError(tenantId: number, customerId: number) {
return this.contactService.getContactByIdOrThrowError(
tenantId,
customerId,

View File

@@ -54,9 +54,9 @@ export default class InventoryService {
/**
* Computes the given item cost and records the inventory lots transactions
* and journal entries based on the cost method FIFO, LIFO or average cost rate.
* @param {number} tenantId -
* @param {Date} fromDate -
* @param {number} itemId -
* @param {number} tenantId - Tenant id.
* @param {Date} fromDate - From date.
* @param {number} itemId - Item id.
*/
async computeItemCost(tenantId: number, fromDate: Date, itemId: number) {
const { Item } = this.tenancy.models(tenantId);

View File

@@ -27,6 +27,7 @@ import DynamicListingService from 'services/DynamicListing/DynamicListService';
import { entriesAmountDiff, formatDateFields } from 'utils';
import { ServiceError } from 'exceptions';
import { ACCOUNT_PARENT_TYPE } from 'data/AccountTypes';
import VendorsService from 'services/Contacts/VendorsService';
const ERRORS = {
BILL_VENDOR_NOT_FOUND: 'VENDOR_NOT_FOUND',
@@ -38,6 +39,7 @@ const ERRORS = {
BILL_ENTRIES_IDS_NOT_FOUND: 'BILL_ENTRIES_IDS_NOT_FOUND',
BILL_PAYMENT_ENTRIES_NOT_FOUND: 'BILL_PAYMENT_ENTRIES_NOT_FOUND',
INVALID_BILL_PAYMENT_AMOUNT: 'INVALID_BILL_PAYMENT_AMOUNT',
PAYMENT_NUMBER_SHOULD_NOT_MODIFY: 'PAYMENT_NUMBER_SHOULD_NOT_MODIFY',
};
/**
@@ -58,6 +60,9 @@ export default class BillPaymentsService {
@Inject()
dynamicListService: DynamicListingService;
@Inject()
vendorsService: VendorsService;
@EventDispatcher()
eventDispatcher: EventDispatcherInterface;
@@ -118,7 +123,6 @@ export default class BillPaymentsService {
const paymentAccount = await accountRepository.findOneById(
paymentAccountId
);
if (!paymentAccount) {
throw new ServiceError(ERRORS.PAYMENT_ACCOUNT_NOT_FOUND);
}
@@ -263,6 +267,45 @@ export default class BillPaymentsService {
}
}
/**
* * Validate the payment vendor whether modified.
* @param {string} billPaymentNo
*/
validateVendorNotModified(
billPaymentDTO: IBillPaymentDTO,
oldBillPayment: IBillPayment
) {
if (billPaymentDTO.vendorId !== oldBillPayment.vendorId) {
throw new ServiceError(ERRORS.PAYMENT_NUMBER_SHOULD_NOT_MODIFY);
}
}
/**
* Transforms create/edit DTO to model.
* @param {number} tenantId
* @param {IBillPaymentDTO} billPaymentDTO - Bill payment.
* @param {IBillPayment} oldBillPayment - Old bill payment.
* @return {Promise<IBillPayment>}
*/
async transformDTOToModel(
tenantId: number,
billPaymentDTO: IBillPaymentDTO,
oldBillPayment?: IBillPayment
): Promise<IBillPayment> {
// Retrieve vendor details by the given vendor id.
const vendor = await this.vendorsService.getVendorByIdOrThrowError(
tenantId,
billPaymentDTO.vendorId
);
return {
amount: sumBy(billPaymentDTO.entries, 'paymentAmount'),
currencyCode: vendor.currencyCode,
...formatDateFields(billPaymentDTO, ['paymentDate']),
entries: billPaymentDTO.entries,
};
}
/**
* Creates a new bill payment transcations and store it to the storage
* with associated bills entries and journal transactions.
@@ -288,11 +331,11 @@ export default class BillPaymentsService {
});
const { BillPayment } = this.tenancy.models(tenantId);
const billPaymentObj = {
amount: sumBy(billPaymentDTO.entries, 'paymentAmount'),
...formatDateFields(billPaymentDTO, ['paymentDate']),
};
// Transform create DTO to model object.
const billPaymentObj = await this.transformDTOToModel(
tenantId,
billPaymentDTO
);
// Validate vendor existance on the storage.
await this.getVendorOrThrowError(tenantId, billPaymentObj.vendorId);
@@ -301,7 +344,6 @@ export default class BillPaymentsService {
tenantId,
billPaymentObj.paymentAccountId
);
// Validate the payment number uniquiness.
if (billPaymentObj.paymentNumber) {
await this.validatePaymentNumber(tenantId, billPaymentObj.paymentNumber);
@@ -312,15 +354,13 @@ export default class BillPaymentsService {
billPaymentObj.entries,
billPaymentDTO.vendorId
);
// Validates the bills due payment amount.
await this.validateBillsDueAmount(tenantId, billPaymentObj.entries);
const billPayment = await BillPayment.query().insertGraphAndFetch({
...omit(billPaymentObj, ['entries']),
entries: billPaymentDTO.entries,
...billPaymentObj,
});
// Triggers `onBillPaymentCreated` event.
await this.eventDispatcher.dispatch(events.billPayment.onCreated, {
tenantId,
billPayment,
@@ -363,11 +403,14 @@ export default class BillPaymentsService {
tenantId,
billPaymentId
);
const billPaymentObj = {
amount: sumBy(billPaymentDTO.entries, 'paymentAmount'),
...formatDateFields(billPaymentDTO, ['paymentDate']),
};
// Transform bill payment DTO to model object.
const billPaymentObj = await this.transformDTOToModel(
tenantId,
billPaymentDTO,
oldBillPayment
);
// Validate vendor not modified.
this.validateVendorNotModified(billPaymentDTO, oldBillPayment);
// Validate vendor existance on the storage.
await this.getVendorOrThrowError(tenantId, billPaymentObj.vendorId);
@@ -377,28 +420,24 @@ export default class BillPaymentsService {
tenantId,
billPaymentObj.paymentAccountId
);
// Validate the items entries IDs existance on the storage.
await this.validateEntriesIdsExistance(
tenantId,
billPaymentId,
billPaymentObj.entries
);
// Validate the bills existance and associated to the given vendor.
await this.validateBillsExistance(
tenantId,
billPaymentObj.entries,
billPaymentDTO.vendorId
);
// Validates the bills due payment amount.
await this.validateBillsDueAmount(
tenantId,
billPaymentObj.entries,
oldBillPayment.entries
);
// Validate the payment number uniquiness.
if (billPaymentObj.paymentNumber) {
await this.validatePaymentNumber(
@@ -409,8 +448,7 @@ export default class BillPaymentsService {
}
const billPayment = await BillPayment.query().upsertGraphAndFetch({
id: billPaymentId,
...omit(billPaymentObj, ['entries']),
entries: billPaymentDTO.entries,
...billPaymentObj,
});
await this.eventDispatcher.dispatch(events.billPayment.onEdited, {
tenantId,

View File

@@ -27,6 +27,7 @@ import ItemsService from 'services/Items/ItemsService';
import ItemsEntriesService from 'services/Items/ItemsEntriesService';
import JournalCommands from 'services/Accounting/JournalCommands';
import JournalPosterService from 'services/Sales/JournalPosterService';
import VendorsService from 'services/Contacts/VendorsService';
import { ERRORS } from './constants';
/**
@@ -46,7 +47,7 @@ export default class BillsService extends SalesInvoicesCost {
@Inject()
tenancy: TenancyService;
@EventDispatcher()
eventDispatcher: EventDispatcherInterface;
@@ -62,6 +63,9 @@ export default class BillsService extends SalesInvoicesCost {
@Inject()
journalPosterService: JournalPosterService;
@Inject()
vendorsService: VendorsService;
/**
* Validates whether the vendor is exist.
* @async
@@ -136,16 +140,25 @@ export default class BillsService extends SalesInvoicesCost {
}
/**
* Converts bill DTO to model.
* Validate the bill number require.
* @param {string} billNo -
*/
validateBillNoRequire(billNo: string) {
if (!billNo) {
throw new ServiceError(ERRORS.BILL_NO_IS_REQUIRED);
}
}
/**
* Converts create bill DTO to model.
* @param {number} tenantId
* @param {IBillDTO} billDTO
* @param {IBill} oldBill
*
* @returns {IBill}
*/
private async billDTOToModel(
tenantId: number,
billDTO: IBillDTO | IBillEditDTO,
billDTO: IBillDTO,
authorizedUser: ISystemUser,
oldBill?: IBill
) {
@@ -157,15 +170,25 @@ export default class BillsService extends SalesInvoicesCost {
}));
const amount = sumBy(entries, 'amount');
// Bill number from DTO or from auto-increment.
const billNumber = billDTO.billNumber || oldBill?.billNumber;
// Retrieve vendor details by the given vendor id.
const vendor = await this.vendorsService.getVendorByIdOrThrowError(
tenantId,
billDTO.vendorId
);
return {
...formatDateFields(omit(billDTO, ['open', 'entries']), [
'billDate',
'dueDate',
]),
amount,
currencyCode: vendor.currencyCode,
billNumber,
entries: entries.map((entry) => ({
reference_type: 'Bill',
...omit(entry, ['amount', 'id']),
...omit(entry, ['amount']),
})),
// Avoid rewrite the open date in edit mode when already opened.
...(billDTO.open &&
@@ -205,16 +228,14 @@ export default class BillsService extends SalesInvoicesCost {
const billObj = await this.billDTOToModel(
tenantId,
billDTO,
authorizedUser,
null
authorizedUser
);
// Retrieve vendor or throw not found service error.
await this.getVendorOrThrowError(tenantId, billDTO.vendorId);
// Validate the bill number uniqiness on the storage.
if (billDTO.billNumber) {
await this.validateBillNumberExists(tenantId, billDTO.billNumber);
}
await this.validateBillNumberExists(tenantId, billDTO.billNumber);
// Validate items IDs existance.
await this.itemsEntriesService.validateItemsIdsExistance(
tenantId,
@@ -277,7 +298,6 @@ export default class BillsService extends SalesInvoicesCost {
authorizedUser,
oldBill
);
// Retrieve vendor details or throw not found service error.
await this.getVendorOrThrowError(tenantId, billDTO.vendorId);
@@ -292,7 +312,6 @@ export default class BillsService extends SalesInvoicesCost {
'Bill',
billDTO.entries
);
// Validate the items ids existance on the storage.
await this.itemsEntriesService.validateItemsIdsExistance(
tenantId,

View File

@@ -7,4 +7,5 @@ export const ERRORS = {
BILL_ENTRIES_IDS_NOT_FOUND: 'BILL_ENTRIES_IDS_NOT_FOUND',
NOT_PURCHASE_ABLE_ITEMS: 'NOT_PURCHASE_ABLE_ITEMS',
BILL_ALREADY_OPEN: 'BILL_ALREADY_OPEN',
BILL_NO_IS_REQUIRED: 'BILL_NO_IS_REQUIRED'
};

View File

@@ -42,7 +42,9 @@ const ERRORS = {
INVOICES_IDS_NOT_FOUND: 'INVOICES_IDS_NOT_FOUND',
ENTRIES_IDS_NOT_EXISTS: 'ENTRIES_IDS_NOT_EXISTS',
INVOICES_NOT_DELIVERED_YET: 'INVOICES_NOT_DELIVERED_YET',
PAYMENT_RECEIVE_NO_IS_REQUIRED: 'PAYMENT_RECEIVE_NO_IS_REQUIRED'
PAYMENT_RECEIVE_NO_IS_REQUIRED: 'PAYMENT_RECEIVE_NO_IS_REQUIRED',
PAYMENT_RECEIVE_NO_REQUIRED: 'PAYMENT_RECEIVE_NO_REQUIRED',
PAYMENT_CUSTOMER_SHOULD_NOT_UPDATE: 'PAYMENT_CUSTOMER_SHOULD_NOT_UPDATE',
};
/**
* Payment receive service.
@@ -264,28 +266,6 @@ export default class PaymentReceiveService {
}
}
/**
* Retrieve estimate number to object model.
* @param {number} tenantId
* @param {IPaymentReceiveCreateDTO | IPaymentReceiveEditDTO} paymentReceiveDTO - Payment receive DTO.
* @param {IPaymentReceive} oldPaymentReceive - Old payment model object.
*/
transformPaymentNumberToModel(
tenantId: number,
paymentReceiveDTO: IPaymentReceiveCreateDTO | IPaymentReceiveEditDTO,
oldPaymentReceive?: IPaymentReceive
): string {
// Retreive the next invoice number.
const autoNextNumber = this.getNextPaymentReceiveNumber(tenantId);
if (paymentReceiveDTO.paymentReceiveNo) {
return paymentReceiveDTO.paymentReceiveNo;
}
return oldPaymentReceive
? oldPaymentReceive.paymentReceiveNo
: autoNextNumber;
}
/**
* Validate the payment receive entries IDs existance.
* @param {number} tenantId
@@ -315,32 +295,69 @@ export default class PaymentReceiveService {
}
}
/**
* Validates the payment receive number require.
* @param {string} paymentReceiveNo
*/
validatePaymentNoRequire(paymentReceiveNo: string) {
if (!paymentReceiveNo) {
throw new ServiceError(ERRORS.PAYMENT_RECEIVE_NO_REQUIRED);
}
}
/**
* Validate the payment customer whether modified.
* @param {IPaymentReceiveEditDTO} paymentReceiveDTO
* @param {IPaymentReceive} oldPaymentReceive
*/
validateCustomerNotModified(
paymentReceiveDTO: IPaymentReceiveEditDTO,
oldPaymentReceive: IPaymentReceive
) {
if (paymentReceiveDTO.customerId !== oldPaymentReceive.customerId) {
throw new ServiceError(ERRORS.PAYMENT_CUSTOMER_SHOULD_NOT_UPDATE);
}
}
/**
* Transformes the create payment receive DTO to model object.
* @param {number} tenantId
* @param {IPaymentReceiveCreateDTO} paymentReceiveDTO
* @param {IPaymentReceiveCreateDTO|IPaymentReceiveEditDTO} paymentReceiveDTO - Payment receive DTO.
* @param {IPaymentReceive} oldPaymentReceive -
* @return {IPaymentReceive}
*/
transformPaymentReceiveDTOToModel(
async transformPaymentReceiveDTOToModel(
tenantId: number,
paymentReceiveDTO: IPaymentReceiveCreateDTO | IPaymentReceiveEditDTO,
oldPaymentReceive?: IPaymentReceive
): IPaymentReceive {
): Promise<IPaymentReceive> {
const paymentAmount = sumBy(paymentReceiveDTO.entries, 'paymentAmount');
// Retrieve the next payment receive number.
const paymentReceiveNo = this.transformPaymentNumberToModel(
// Retrieve customer details.
const customer = await this.customersService.getCustomerByIdOrThrowError(
tenantId,
paymentReceiveDTO,
oldPaymentReceive
paymentReceiveDTO.customerId
);
// Retreive the next invoice number.
const autoNextNumber = this.getNextPaymentReceiveNumber(tenantId);
// Retrieve the next payment receive number.
const paymentReceiveNo =
paymentReceiveDTO.paymentReceiveNo ||
oldPaymentReceive?.paymentReceiveNo ||
autoNextNumber;
this.validatePaymentNoRequire(paymentReceiveNo);
return {
amount: paymentAmount,
currencyCode: customer.currencyCode,
...formatDateFields(omit(paymentReceiveDTO, ['entries']), [
'paymentDate',
]),
...(paymentReceiveNo ? { paymentReceiveNo } : {}),
entries: paymentReceiveDTO.entries.map((entry) => ({
...omit(entry, ['id']),
...entry,
})),
};
}
@@ -360,13 +377,10 @@ export default class PaymentReceiveService {
const { PaymentReceive } = this.tenancy.models(tenantId);
// Transformes the payment receive DTO to model.
const paymentReceiveObj = this.transformPaymentReceiveDTOToModel(
const paymentReceiveObj = await this.transformPaymentReceiveDTOToModel(
tenantId,
paymentReceiveDTO
);
// Validate payment receive is required.
this.validatePaymentReceiveNoRequire(paymentReceiveObj);
// Validate payment receive number uniquiness.
await this.validatePaymentReceiveNoExistance(
tenantId,
@@ -393,7 +407,6 @@ export default class PaymentReceiveService {
tenantId,
paymentReceiveDTO.entries
);
this.logger.info('[payment_receive] inserting to the storage.');
const paymentReceive = await PaymentReceive.query().insertGraphAndFetch({
...paymentReceiveObj,
@@ -447,13 +460,13 @@ export default class PaymentReceiveService {
paymentReceiveId
);
// Transformes the payment receive DTO to model.
const paymentReceiveObj = this.transformPaymentReceiveDTOToModel(
const paymentReceiveObj = await this.transformPaymentReceiveDTOToModel(
tenantId,
paymentReceiveDTO,
oldPaymentReceive
);
// Validate payment receive number existance.
this.validatePaymentReceiveNoRequire(paymentReceiveObj);
// Validate customer whether modified.
this.validateCustomerNotModified(paymentReceiveDTO, oldPaymentReceive);
// Validate payment receive number uniquiness.
if (paymentReceiveDTO.paymentReceiveNo) {
@@ -527,7 +540,7 @@ export default class PaymentReceiveService {
const { PaymentReceive, PaymentReceiveEntry } = this.tenancy.models(
tenantId
);
// Retreive payment receive or throw not found service error.
const oldPaymentReceive = await this.getPaymentReceiveOrThrowError(
tenantId,
paymentReceiveId

View File

@@ -31,7 +31,7 @@ const ERRORS = {
SALE_ESTIMATE_ALREADY_REJECTED: 'SALE_ESTIMATE_ALREADY_REJECTED',
SALE_ESTIMATE_ALREADY_APPROVED: 'SALE_ESTIMATE_ALREADY_APPROVED',
SALE_ESTIMATE_NOT_DELIVERED: 'SALE_ESTIMATE_NOT_DELIVERED',
SALE_ESTIMATE_NO_IS_REQUIRED: 'SALE_ESTIMATE_NO_IS_REQUIRED'
SALE_ESTIMATE_NO_IS_REQUIRED: 'SALE_ESTIMATE_NO_IS_REQUIRED',
};
/**
@@ -157,25 +157,34 @@ export default class SaleEstimateService {
}
/**
* Transform DTO object ot model object.
* Transform create DTO object ot model object.
* @param {number} tenantId
* @param {ISaleEstimateDTO} saleEstimateDTO
* @param {ISaleEstimate} oldSaleEstimate
* @param {ISaleEstimateDTO} saleEstimateDTO - Sale estimate DTO.
* @return {ISaleEstimate}
*/
transformDTOToModel(
async transformDTOToModel(
tenantId: number,
estimateDTO: ISaleEstimateDTO,
oldSaleEstimate?: ISaleEstimate
): ISaleEstimate {
): Promise<ISaleEstimate> {
const { ItemEntry } = this.tenancy.models(tenantId);
const amount = sumBy(estimateDTO.entries, (e) => ItemEntry.calcAmount(e));
// Retreive the next invoice number.
const autoNextNumber = this.getNextEstimateNumber(tenantId);
// Retreive the next estimate number.
const estimateNumber = this.transformEstimateNumberToModel(
const estimateNumber = estimateDTO.estimateNumber ||
oldSaleEstimate?.estimateNumber ||
autoNextNumber;
// Validate the sale estimate number require.
this.validateEstimateNoRequire(estimateNumber);
// Retrieve customer details.
const customer = await this.customersService.getCustomerByIdOrThrowError(
tenantId,
estimateDTO,
oldSaleEstimate
estimateDTO.customerId
);
return {
@@ -184,25 +193,26 @@ export default class SaleEstimateService {
'estimateDate',
'expirationDate',
]),
currencyCode: customer.currencyCode,
...(estimateNumber ? { estimateNumber } : {}),
entries: estimateDTO.entries.map((entry) => ({
reference_type: 'SaleEstimate',
...omit(entry, ['total', 'amount', 'id']),
...entry,
})),
// Avoid rewrite the deliver date in edit mode when already published.
...(estimateDTO.delivered &&
!oldSaleEstimate?.deliveredAt && {
deliveredAt: moment().toMySqlDateTime(),
}),
!oldSaleEstimate?.deliveredAt && {
deliveredAt: moment().toMySqlDateTime(),
}),
};
}
/**
* Validate the sale estimate number require.
* @param {ISaleEstimate} saleInvoiceObj
*/
validateEstimateNoRequire(saleInvoiceObj: ISaleEstimate) {
if (!saleInvoiceObj.estimateNumber) {
validateEstimateNoRequire(estimateNumber: string) {
if (!estimateNumber) {
throw new ServiceError(ERRORS.SALE_ESTIMATE_NO_IS_REQUIRED);
}
}
@@ -223,27 +233,25 @@ export default class SaleEstimateService {
this.logger.info('[sale_estimate] inserting sale estimate to the storage.');
// Transform DTO object ot model object.
const estimateObj = this.transformDTOToModel(tenantId, estimateDTO);
// Validate the sale estimate number require.
this.validateEstimateNoRequire(estimateObj);
const estimateObj = await this.transformDTOToModel(
tenantId,
estimateDTO
);
// Validate estimate number uniquiness on the storage.
if (estimateObj.estimateNumber) {
await this.validateEstimateNumberExistance(
tenantId,
estimateObj.estimateNumber
);
}
await this.validateEstimateNumberExistance(
tenantId,
estimateObj.estimateNumber
);
// Retrieve the given customer or throw not found service error.
await this.customersService.getCustomer(tenantId, estimateDTO.customerId);
await this.customersService.getCustomerByIdOrThrowError(
tenantId,
estimateDTO.customerId
);
// Validate items IDs existance on the storage.
await this.itemsEntriesService.validateItemsIdsExistance(
tenantId,
estimateDTO.entries
);
// Validate non-sellable items.
await this.itemsEntriesService.validateNonSellableEntriesItems(
tenantId,
@@ -284,14 +292,11 @@ export default class SaleEstimateService {
estimateId
);
// Transform DTO object ot model object.
const estimateObj = this.transformDTOToModel(
const estimateObj = await this.transformDTOToModel(
tenantId,
estimateDTO,
oldSaleEstimate
);
// Validate the sale estimate number require.
this.validateEstimateNoRequire(estimateObj);
// Validate estimate number uniquiness on the storage.
if (estimateDTO.estimateNumber) {
await this.validateEstimateNumberExistance(
@@ -301,13 +306,15 @@ export default class SaleEstimateService {
);
}
// Retrieve the given customer or throw not found service error.
await this.customersService.getCustomer(tenantId, estimateDTO.customerId);
await this.customersService.getCustomerByIdOrThrowError(
tenantId,
estimateDTO.customerId
);
// Validate sale estimate entries existance.
await this.itemsEntriesService.validateEntriesIdsExistance(
tenantId,
estimateId,
'SaleEstiamte',
'SaleEstimate',
estimateDTO.entries
);
// Validate items IDs existance on the storage.

View File

@@ -181,46 +181,36 @@ export default class SaleInvoicesService {
}
/**
* Retrieve invoice number to object model.
* @param tenantId
* @param saleInvoiceDTO
* @param oldSaleInvoice
* Transformes the create DTO to invoice object model.
* @param {ISaleInvoiceCreateDTO} saleInvoiceDTO - Sale invoice DTO.
* @param {ISaleInvoice} oldSaleInvoice - Old sale invoice.
* @return {ISaleInvoice}
*/
transformInvoiceNumberToModel(
private async transformDTOToModel(
tenantId: number,
saleInvoiceDTO: ISaleInvoiceCreateDTO | ISaleInvoiceEditDTO,
oldSaleInvoice?: ISaleInvoice
): string {
// Retreive the next invoice number.
const autoNextNumber = this.getNextInvoiceNumber(tenantId);
if (saleInvoiceDTO.invoiceNo) {
return saleInvoiceDTO.invoiceNo;
}
return oldSaleInvoice ? oldSaleInvoice.invoiceNo : autoNextNumber;
}
/**
* Transform DTO object to model object.
* @param {number} tenantId - Tenant id.
* @param {ISaleInvoiceDTO} saleInvoiceDTO - Sale invoice DTO.
*/
transformDTOToModel(
tenantId: number,
saleInvoiceDTO: ISaleInvoiceCreateDTO | ISaleInvoiceEditDTO,
oldSaleInvoice?: ISaleInvoice
): ISaleInvoice {
): Promise<ISaleInvoice> {
const { ItemEntry } = this.tenancy.models(tenantId);
const balance = sumBy(saleInvoiceDTO.entries, (e) =>
ItemEntry.calcAmount(e)
);
const invoiceNo = this.transformInvoiceNumberToModel(
// Retrieve customer details.
const customer = await this.customersService.getCustomerByIdOrThrowError(
tenantId,
saleInvoiceDTO,
oldSaleInvoice
saleInvoiceDTO.customerId
);
// Retreive the next invoice number.
const autoNextNumber = this.getNextInvoiceNumber(tenantId);
// Invoice number.
const invoiceNo =
saleInvoiceDTO.invoiceNo || oldSaleInvoice?.invoiceNo || autoNextNumber;
// Validate the invoice is required.
this.validateInvoiceNoRequire(invoiceNo);
return {
...formatDateFields(
omit(saleInvoiceDTO, ['delivered', 'entries', 'fromEstimateId']),
@@ -228,20 +218,17 @@ export default class SaleInvoicesService {
),
// Avoid rewrite the deliver date in edit mode when already published.
balance,
currencyCode: customer.currencyCode,
...(saleInvoiceDTO.delivered &&
!oldSaleInvoice?.deliveredAt && {
deliveredAt: moment().toMySqlDateTime(),
}),
// Avoid add payment amount in edit mode.
...(!oldSaleInvoice
? {
paymentAmount: 0,
}
: {}),
// Avoid override payment amount in edit mode.
...(!oldSaleInvoice && { paymentAmount: 0 }),
...(invoiceNo ? { invoiceNo } : {}),
entries: saleInvoiceDTO.entries.map((entry) => ({
referenceType: 'SaleInvoice',
...omit(entry, ['amount', 'id']),
...entry,
})),
};
}
@@ -250,8 +237,8 @@ export default class SaleInvoicesService {
* Validate the invoice number require.
* @param {ISaleInvoice} saleInvoiceObj
*/
validateInvoiceNoRequire(saleInvoiceObj: ISaleInvoice) {
if (!saleInvoiceObj.invoiceNo) {
validateInvoiceNoRequire(invoiceNo: string) {
if (!invoiceNo) {
throw new ServiceError(ERRORS.SALE_INVOICE_NO_IS_REQUIRED);
}
}
@@ -272,19 +259,15 @@ export default class SaleInvoicesService {
const { saleInvoiceRepository } = this.tenancy.repositories(tenantId);
// Transform DTO object to model object.
const saleInvoiceObj = this.transformDTOToModel(
const saleInvoiceObj = await this.transformDTOToModel(
tenantId,
saleInvoiceDTO,
null
saleInvoiceDTO
);
this.validateInvoiceNoRequire(saleInvoiceObj);
// Validate customer existance.
await this.customersService.getCustomerByIdOrThrowError(
tenantId,
saleInvoiceDTO.customerId
);
// Validate sale invoice number uniquiness.
if (saleInvoiceObj.invoiceNo) {
await this.validateInvoiceNumberUnique(
@@ -354,7 +337,7 @@ export default class SaleInvoicesService {
saleInvoiceId
);
// Transform DTO object to model object.
const saleInvoiceObj = this.transformDTOToModel(
const saleInvoiceObj = await this.transformDTOToModel(
tenantId,
saleInvoiceDTO,
oldSaleInvoice
@@ -396,10 +379,10 @@ export default class SaleInvoicesService {
);
this.logger.info('[sale_invoice] trying to update sale invoice.');
const saleInvoice: ISaleInvoice = await saleInvoiceRepository.update(
{ ...omit(saleInvoiceObj, ['paymentAmount']) },
{ id: saleInvoiceId }
);
const saleInvoice: ISaleInvoice = await saleInvoiceRepository.upsertGraph({
id: saleInvoiceId,
...saleInvoiceObj,
});
// Triggers `onSaleInvoiceEdited` event.
await this.eventDispatcher.dispatch(events.saleInvoice.onEdited, {
tenantId,

View File

@@ -20,6 +20,7 @@ import { ItemEntry } from 'models';
import InventoryService from 'services/Inventory/Inventory';
import { ACCOUNT_PARENT_TYPE } from 'data/AccountTypes';
import AutoIncrementOrdersService from './AutoIncrementOrdersService';
import CustomersService from 'services/Contacts/CustomersService';
const ERRORS = {
SALE_RECEIPT_NOT_FOUND: 'SALE_RECEIPT_NOT_FOUND',
@@ -27,7 +28,7 @@ const ERRORS = {
DEPOSIT_ACCOUNT_NOT_CURRENT_ASSET: 'DEPOSIT_ACCOUNT_NOT_CURRENT_ASSET',
SALE_RECEIPT_NUMBER_NOT_UNIQUE: 'SALE_RECEIPT_NUMBER_NOT_UNIQUE',
SALE_RECEIPT_IS_ALREADY_CLOSED: 'SALE_RECEIPT_IS_ALREADY_CLOSED',
SALE_RECEIPT_NO_IS_REQUIRED: 'SALE_RECEIPT_NO_IS_REQUIRED'
SALE_RECEIPT_NO_IS_REQUIRED: 'SALE_RECEIPT_NO_IS_REQUIRED',
};
@Service()
@@ -56,6 +57,9 @@ export default class SalesReceiptService {
@Inject()
autoIncrementOrdersService: AutoIncrementOrdersService;
@Inject()
customersService: CustomersService;
/**
* Validate whether sale receipt exists on the storage.
* @param {number} tenantId -
@@ -139,8 +143,8 @@ export default class SalesReceiptService {
* Validate the sale receipt number require.
* @param {ISaleReceipt} saleReceipt
*/
validateReceiptNoRequire(saleReceipt: ISaleReceipt) {
if (!saleReceipt.receiptNumber) {
validateReceiptNoRequire(receiptNumber: string) {
if (!receiptNumber) {
throw new ServiceError(ERRORS.SALE_RECEIPT_NO_IS_REQUIRED);
}
}
@@ -189,40 +193,52 @@ export default class SalesReceiptService {
}
/**
* Transform DTO object to model object.
* Transform create DTO object to model object.
* @param {ISaleReceiptDTO} saleReceiptDTO -
* @param {ISaleReceipt} oldSaleReceipt -
* @returns {ISaleReceipt}
*/
transformObjectDTOToModel(
async transformDTOToModel(
tenantId: number,
saleReceiptDTO: ISaleReceiptDTO,
oldSaleReceipt?: ISaleReceipt
): ISaleReceipt {
): Promise<ISaleReceipt> {
const amount = sumBy(saleReceiptDTO.entries, (e) =>
ItemEntry.calcAmount(e)
);
// Retreive the next invoice number.
const autoNextNumber = this.getNextReceiptNumber(tenantId);
// Retreive the receipt number.
const receiptNumber = this.transformReceiptNumberToModel(
const receiptNumber =
saleReceiptDTO.receiptNumber ||
oldSaleReceipt?.receiptNumber ||
autoNextNumber;
// Validate receipt number require.
this.validateReceiptNoRequire(receiptNumber);
// Retrieve customer details.
const customer = await this.customersService.getCustomerByIdOrThrowError(
tenantId,
saleReceiptDTO,
oldSaleReceipt
saleReceiptDTO.customerId
);
return {
amount,
currencyCode: customer.currencyCode,
...formatDateFields(omit(saleReceiptDTO, ['closed', 'entries']), [
'receiptDate',
]),
...(receiptNumber ? { receiptNumber } : {}),
receiptNumber,
// Avoid rewrite the deliver date in edit mode when already published.
...(saleReceiptDTO.closed &&
!oldSaleReceipt?.closedAt && {
!oldSaleReceipt.closedAt && {
closedAt: moment().toMySqlDateTime(),
}),
entries: saleReceiptDTO.entries.map((entry) => ({
reference_type: 'SaleReceipt',
...omit(entry, ['id', 'amount']),
...entry,
})),
};
}
@@ -240,13 +256,10 @@ export default class SalesReceiptService {
const { SaleReceipt } = this.tenancy.models(tenantId);
// Transform sale receipt DTO to model.
const saleReceiptObj = this.transformObjectDTOToModel(
const saleReceiptObj = await this.transformDTOToModel(
tenantId,
saleReceiptDTO
);
// Validate receipt number is required.
this.validateReceiptNoRequire(saleReceiptObj);
// Validate receipt deposit account existance and type.
await this.validateReceiptDepositAccountExistance(
tenantId,
@@ -308,14 +321,11 @@ export default class SalesReceiptService {
saleReceiptId
);
// Transform sale receipt DTO to model.
const saleReceiptObj = this.transformObjectDTOToModel(
const saleReceiptObj = await this.transformDTOToModel(
tenantId,
saleReceiptDTO,
oldSaleReceipt
);
// Validate receipt number is required.
this.validateReceiptNoRequire(saleReceiptObj);
// Validate receipt deposit account existance and type.
await this.validateReceiptDepositAccountExistance(
tenantId,

View File

@@ -3,17 +3,20 @@ import { On, EventSubscriber } from 'event-dispatch';
import events from 'subscribers/events';
import TenancyService from 'services/Tenancy/TenancyService';
import SettingsService from 'services/Settings/SettingsService';
import SalesReceiptService from 'services/Sales/SalesReceipts';
@EventSubscriber()
export default class SaleReceiptSubscriber {
logger: any;
tenancy: TenancyService;
settingsService: SettingsService;
saleReceiptsService: SalesReceiptService;
constructor() {
this.logger = Container.get('logger');
this.tenancy = Container.get(TenancyService);
this.settingsService = Container.get(SettingsService);
this.saleReceiptsService = Container.get(SalesReceiptService);
}
/**
@@ -21,9 +24,6 @@ export default class SaleReceiptSubscriber {
*/
@On(events.saleReceipt.onCreated)
public async handleReceiptNextNumberIncrement({ tenantId, saleReceiptId }) {
await this.settingsService.incrementNextNumber(tenantId, {
key: 'next_number',
group: 'sales_receipts',
});
await this.saleReceiptsService.incrementNextReceiptNumber(tenantId);
}
}