feat: discount sale and purchase transactions

This commit is contained in:
Ahmed Bouhuolia
2024-11-28 11:14:16 +02:00
parent aa4aaeb612
commit df8391201f
19 changed files with 377 additions and 10 deletions

View File

@@ -183,6 +183,13 @@ export default class VendorCreditController extends BaseController {
check('attachments').isArray().optional(), check('attachments').isArray().optional(),
check('attachments.*.key').exists().isString(), check('attachments.*.key').exists().isString(),
// Discount.
check('discount').optional({ nullable: true }).isNumeric().toFloat(),
check('discount_type').optional({ nullable: true }).isString().trim(),
// Adjustment.
check('adjustment').optional({ nullable: true }).isNumeric().toFloat(),
]; ];
} }

View File

@@ -244,11 +244,19 @@ export default class PaymentReceivesController extends BaseController {
.isNumeric() .isNumeric()
.toInt(), .toInt(),
// Attachments.
check('attachments').isArray().optional(), check('attachments').isArray().optional(),
check('attachments.*.key').exists().isString(), check('attachments.*.key').exists().isString(),
// Pdf template id. // Pdf template id.
check('pdf_template_id').optional({ nullable: true }).isNumeric().toInt(), check('pdf_template_id').optional({ nullable: true }).isNumeric().toInt(),
// Discount.
check('discount').optional({ nullable: true }).isNumeric().toFloat(),
check('discount_type').optional({ nullable: true }).isString().trim(),
// Adjustment.
check('adjustment').optional({ nullable: true }).isNumeric().toFloat(),
]; ];
} }

View File

@@ -3,6 +3,7 @@ import { body, check, param, query } from 'express-validator';
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import { import {
AbilitySubject, AbilitySubject,
DiscountType,
ISaleEstimateDTO, ISaleEstimateDTO,
SaleEstimateAction, SaleEstimateAction,
SaleEstimateMailOptionsDTO, SaleEstimateMailOptionsDTO,
@@ -195,11 +196,21 @@ export default class SalesEstimatesController extends BaseController {
check('terms_conditions').optional().trim(), check('terms_conditions').optional().trim(),
check('send_to_email').optional().trim(), check('send_to_email').optional().trim(),
// # Attachments
check('attachments').isArray().optional(), check('attachments').isArray().optional(),
check('attachments.*.key').exists().isString(), check('attachments.*.key').exists().isString(),
// Pdf template id. // # Pdf template id.
check('pdf_template_id').optional({ nullable: true }).isNumeric().toInt(), check('pdf_template_id').optional({ nullable: true }).isNumeric().toInt(),
// # Discount
check('discount').optional().isNumeric().toFloat(),
check('discount_type')
.default(DiscountType.Amount)
.isIn([DiscountType.Amount, DiscountType.Percentage]),
// # Adjustment
check('adjustment').optional().isNumeric().toFloat(),
]; ];
} }

View File

@@ -0,0 +1,24 @@
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.up = function(knex) {
return knex.schema.alterTable('sales_estimates', (table) => {
table.decimal('discount', 10, 2).nullable().after('credited_amount');
table.string('discount_type').nullable().after('discount');
table.decimal('adjustment', 10, 2).nullable().after('discount_type');
});
};
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.down = function(knex) {
return knex.schema.alterTable('sales_estimates', (table) => {
table.dropColumn('discount');
table.dropColumn('discount_type');
table.dropColumn('adjustment');
});
};

View File

@@ -0,0 +1,24 @@
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.up = function(knex) {
return knex.schema.alterTable('sales_receipts', (table) => {
table.decimal('discount', 10, 2).nullable().after('amount');
table.string('discount_type').nullable().after('discount');
table.decimal('adjustment', 10, 2).nullable().after('discount_type');
});
};
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.down = function(knex) {
return knex.schema.alterTable('sales_receipts', (table) => {
table.dropColumn('discount');
table.dropColumn('discount_type');
table.dropColumn('adjustment');
});
};

View File

@@ -0,0 +1,26 @@
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.up = function(knex) {
return knex.schema.alterTable('bills', (table) => {
// Discount.
table.decimal('discount', 10, 2).nullable().after('amount');
table.string('discount_type').nullable().after('discount');
// Adjustment.
table.decimal('adjustment', 10, 2).nullable().after('discount_type');
});
};
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.down = function(knex) {
return knex.schema.alterTable('bills', (table) => {
table.dropColumn('discount');
table.dropColumn('discount_type');
table.dropColumn('adjustment');
});
};

View File

@@ -0,0 +1,23 @@
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.up = function(knex) {
return knex.schema.alterTable('credit_notes', (table) => {
table.decimal('discount', 10, 2).nullable().after('credited_amount');
table.string('discount_type').nullable().after('discount');
table.decimal('adjustment', 10, 2).nullable().after('discount_type');
});
};
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.down = function(knex) {
return knex.schema.alterTable('credit_notes', (table) => {
table.dropColumn('discount');
table.dropColumn('discount_type');
table.dropColumn('adjustment');
});
};

View File

@@ -3,6 +3,7 @@ import { IDynamicListFilterDTO } from './DynamicFilter';
import { IItemEntry, IItemEntryDTO } from './ItemEntry'; import { IItemEntry, IItemEntryDTO } from './ItemEntry';
import { IBillLandedCost } from './LandedCost'; import { IBillLandedCost } from './LandedCost';
import { AttachmentLinkDTO } from './Attachments'; import { AttachmentLinkDTO } from './Attachments';
import { DiscountType } from './SaleInvoice';
export interface IBillDTO { export interface IBillDTO {
vendorId: number; vendorId: number;
@@ -22,6 +23,13 @@ export interface IBillDTO {
projectId?: number; projectId?: number;
isInclusiveTax?: boolean; isInclusiveTax?: boolean;
attachments?: AttachmentLinkDTO[]; attachments?: AttachmentLinkDTO[];
// # Discount
discount?: number;
discountType?: DiscountType;
// # Adjustment
adjustment?: number;
} }
export interface IBillEditDTO { export interface IBillEditDTO {

View File

@@ -1,5 +1,5 @@
import { Knex } from 'knex'; import { Knex } from 'knex';
import { IDynamicListFilter, IItemEntry } from '@/interfaces'; import { DiscountType, IDynamicListFilter, IItemEntry } from '@/interfaces';
import { ILedgerEntry } from './Ledger'; import { ILedgerEntry } from './Ledger';
import { AttachmentLinkDTO } from './Attachments'; import { AttachmentLinkDTO } from './Attachments';
@@ -23,6 +23,9 @@ export interface ICreditNoteNewDTO {
branchId?: number; branchId?: number;
warehouseId?: number; warehouseId?: number;
attachments?: AttachmentLinkDTO[]; attachments?: AttachmentLinkDTO[];
discount?: number;
discountType?: DiscountType;
adjustment?: number;
} }
export interface ICreditNoteEditDTO { export interface ICreditNoteEditDTO {

View File

@@ -3,6 +3,7 @@ import { IItemEntry, IItemEntryDTO } from './ItemEntry';
import { IDynamicListFilterDTO } from '@/interfaces/DynamicFilter'; import { IDynamicListFilterDTO } from '@/interfaces/DynamicFilter';
import { CommonMailOptions, CommonMailOptionsDTO } from './Mailable'; import { CommonMailOptions, CommonMailOptionsDTO } from './Mailable';
import { AttachmentLinkDTO } from './Attachments'; import { AttachmentLinkDTO } from './Attachments';
import { DiscountType } from './SaleInvoice';
export interface ISaleEstimate { export interface ISaleEstimate {
id?: number; id?: number;
@@ -40,6 +41,13 @@ export interface ISaleEstimateDTO {
branchId?: number; branchId?: number;
warehouseId?: number; warehouseId?: number;
attachments?: AttachmentLinkDTO[]; attachments?: AttachmentLinkDTO[];
// # Discount
discount?: number;
discountType?: DiscountType;
// # Adjustment
adjustment?: number;
} }
export interface ISalesEstimatesFilter extends IDynamicListFilterDTO { export interface ISalesEstimatesFilter extends IDynamicListFilterDTO {

View File

@@ -82,6 +82,11 @@ export interface ISaleInvoice {
paymentMethods?: Array<PaymentIntegrationTransactionLink>; paymentMethods?: Array<PaymentIntegrationTransactionLink>;
} }
export enum DiscountType {
Percentage = 'Percentage',
Amount = 'Amount',
}
export interface ISaleInvoiceDTO { export interface ISaleInvoiceDTO {
invoiceDate: Date; invoiceDate: Date;
dueDate: Date; dueDate: Date;
@@ -105,7 +110,7 @@ export interface ISaleInvoiceDTO {
// # Discount // # Discount
discount?: number; discount?: number;
discountType?: string; discountType?: DiscountType;
// # Adjustments // # Adjustments
adjustments?: string; adjustments?: string;

View File

@@ -2,6 +2,7 @@ import { Knex } from 'knex';
import { IItemEntry } from './ItemEntry'; import { IItemEntry } from './ItemEntry';
import { CommonMailOptions, CommonMailOptionsDTO } from './Mailable'; import { CommonMailOptions, CommonMailOptionsDTO } from './Mailable';
import { AttachmentLinkDTO } from './Attachments'; import { AttachmentLinkDTO } from './Attachments';
import { DiscountType } from './SaleInvoice';
export interface ISaleReceipt { export interface ISaleReceipt {
id?: number; id?: number;
@@ -47,6 +48,11 @@ export interface ISaleReceiptDTO {
entries: any[]; entries: any[];
branchId?: number; branchId?: number;
attachments?: AttachmentLinkDTO[]; attachments?: AttachmentLinkDTO[];
discount?: number;
discountType?: DiscountType;
adjustment?: number;
} }
export interface ISalesReceiptsService { export interface ISalesReceiptsService {

View File

@@ -1,4 +1,4 @@
import { IDynamicListFilter, IItemEntry, IItemEntryDTO } from '@/interfaces'; import { DiscountType, IDynamicListFilter, IItemEntry, IItemEntryDTO } from '@/interfaces';
import { Knex } from 'knex'; import { Knex } from 'knex';
import { AttachmentLinkDTO } from './Attachments'; import { AttachmentLinkDTO } from './Attachments';
@@ -63,6 +63,11 @@ export interface IVendorCreditDTO {
branchId?: number; branchId?: number;
warehouseId?: number; warehouseId?: number;
attachments?: AttachmentLinkDTO[]; attachments?: AttachmentLinkDTO[];
discount?: number;
discountType?: DiscountType;
adjustment?: number;
} }
export interface IVendorCreditCreateDTO extends IVendorCreditDTO {} export interface IVendorCreditCreateDTO extends IVendorCreditDTO {}

View File

@@ -1,5 +1,5 @@
import { Model, raw, mixin } from 'objection'; import { Model, raw, mixin } from 'objection';
import { castArray, difference } from 'lodash'; import { castArray, defaultTo, difference } from 'lodash';
import moment from 'moment'; import moment from 'moment';
import TenantModel from 'models/TenantModel'; import TenantModel from 'models/TenantModel';
import BillSettings from './Bill.Settings'; import BillSettings from './Bill.Settings';
@@ -7,6 +7,7 @@ import ModelSetting from './ModelSetting';
import CustomViewBaseModel from './CustomViewBaseModel'; import CustomViewBaseModel from './CustomViewBaseModel';
import { DEFAULT_VIEWS } from '@/services/Purchases/Bills/constants'; import { DEFAULT_VIEWS } from '@/services/Purchases/Bills/constants';
import ModelSearchable from './ModelSearchable'; import ModelSearchable from './ModelSearchable';
import { DiscountType } from '@/interfaces';
export default class Bill extends mixin(TenantModel, [ export default class Bill extends mixin(TenantModel, [
ModelSetting, ModelSetting,
@@ -21,6 +22,11 @@ export default class Bill extends mixin(TenantModel, [
public taxAmountWithheld: number; public taxAmountWithheld: number;
public exchangeRate: number; public exchangeRate: number;
public discount: number;
public discountType: DiscountType;
public adjustment: number;
/** /**
* Timestamps columns. * Timestamps columns.
*/ */
@@ -103,9 +109,15 @@ export default class Bill extends mixin(TenantModel, [
* @returns {number} * @returns {number}
*/ */
get total() { get total() {
const discountAmount = this.discountType === DiscountType.Amount
? this.discount
: this.subtotal * (this.discount / 100);
const adjustmentAmount = defaultTo(this.adjustment, 0);
return this.isInclusiveTax return this.isInclusiveTax
? this.subtotal ? this.subtotal - discountAmount - adjustmentAmount
: this.subtotal + this.taxAmountWithheld; : this.subtotal + this.taxAmountWithheld - discountAmount - adjustmentAmount;
} }
/** /**

View File

@@ -5,12 +5,20 @@ import CustomViewBaseModel from './CustomViewBaseModel';
import { DEFAULT_VIEWS } from '@/services/CreditNotes/constants'; import { DEFAULT_VIEWS } from '@/services/CreditNotes/constants';
import ModelSearchable from './ModelSearchable'; import ModelSearchable from './ModelSearchable';
import CreditNoteMeta from './CreditNote.Meta'; import CreditNoteMeta from './CreditNote.Meta';
import { DiscountType } from '@/interfaces';
export default class CreditNote extends mixin(TenantModel, [ export default class CreditNote extends mixin(TenantModel, [
ModelSetting, ModelSetting,
CustomViewBaseModel, CustomViewBaseModel,
ModelSearchable, ModelSearchable,
]) { ]) {
public amount: number;
public exchangeRate: number;
public openedAt: Date;
public discount: number;
public discountType: DiscountType;
public adjustment: number;
/** /**
* Table name * Table name
*/ */
@@ -48,6 +56,42 @@ export default class CreditNote extends mixin(TenantModel, [
return this.amount * this.exchangeRate; return this.amount * this.exchangeRate;
} }
/**
* Credit note subtotal.
* @returns {number}
*/
get subtotal() {
return this.amount;
}
/**
* Credit note subtotal in local currency.
* @returns {number}
*/
get subtotalLocal() {
return this.subtotal * this.exchangeRate;
}
/**
* Credit note total.
* @returns {number}
*/
get total() {
const discountAmount = this.discountType === DiscountType.Amount
? this.discount
: this.subtotal * (this.discount / 100);
return this.subtotal - discountAmount - this.adjustment;
}
/**
* Credit note total in local currency.
* @returns {number}
*/
get totalLocal() {
return this.total * this.exchangeRate;
}
/** /**
* Detarmines whether the credit note is draft. * Detarmines whether the credit note is draft.
* @returns {boolean} * @returns {boolean}

View File

@@ -7,12 +7,22 @@ import ModelSetting from './ModelSetting';
import CustomViewBaseModel from './CustomViewBaseModel'; import CustomViewBaseModel from './CustomViewBaseModel';
import { DEFAULT_VIEWS } from '@/services/Sales/Estimates/constants'; import { DEFAULT_VIEWS } from '@/services/Sales/Estimates/constants';
import ModelSearchable from './ModelSearchable'; import ModelSearchable from './ModelSearchable';
import { DiscountType } from '@/interfaces';
import { defaultTo } from 'lodash';
export default class SaleEstimate extends mixin(TenantModel, [ export default class SaleEstimate extends mixin(TenantModel, [
ModelSetting, ModelSetting,
CustomViewBaseModel, CustomViewBaseModel,
ModelSearchable, ModelSearchable,
]) { ]) {
public amount: number;
public exchangeRate: number;
public discount: number;
public discountType: DiscountType;
public adjustment: number;
/** /**
* Table name * Table name
*/ */
@@ -49,6 +59,44 @@ export default class SaleEstimate extends mixin(TenantModel, [
return this.amount * this.exchangeRate; return this.amount * this.exchangeRate;
} }
/**
* Estimate subtotal.
* @returns {number}
*/
get subtotal() {
return this.amount;;
}
/**
* Estimate subtotal in local currency.
* @returns {number}
*/
get subtotalLocal() {
return this.localAmount;
}
/**
* Estimate total.
* @returns {number}
*/
get total() {
const discountAmount = this.discountType === DiscountType.Amount
? this.discount
: this.subtotal * (this.discount / 100);
const adjustmentAmount = defaultTo(this.adjustment, 0);
return this.subtotal - discountAmount - adjustmentAmount;
}
/**
* Estimate total in local currency.
* @returns {number}
*/
get totalLocal() {
return this.total * this.exchangeRate;
}
/** /**
* Detarmines whether the sale estimate converted to sale invoice. * Detarmines whether the sale estimate converted to sale invoice.
* @return {boolean} * @return {boolean}

View File

@@ -1,5 +1,5 @@
import { mixin, Model, raw } from 'objection'; import { mixin, Model, raw } from 'objection';
import { castArray, takeWhile } from 'lodash'; import { castArray, defaultTo, takeWhile } from 'lodash';
import moment from 'moment'; import moment from 'moment';
import TenantModel from 'models/TenantModel'; import TenantModel from 'models/TenantModel';
import ModelSetting from './ModelSetting'; import ModelSetting from './ModelSetting';
@@ -7,6 +7,7 @@ import SaleInvoiceMeta from './SaleInvoice.Settings';
import CustomViewBaseModel from './CustomViewBaseModel'; import CustomViewBaseModel from './CustomViewBaseModel';
import { DEFAULT_VIEWS } from '@/services/Sales/Invoices/constants'; import { DEFAULT_VIEWS } from '@/services/Sales/Invoices/constants';
import ModelSearchable from './ModelSearchable'; import ModelSearchable from './ModelSearchable';
import { DiscountType } from '@/interfaces';
export default class SaleInvoice extends mixin(TenantModel, [ export default class SaleInvoice extends mixin(TenantModel, [
ModelSetting, ModelSetting,
@@ -23,6 +24,10 @@ export default class SaleInvoice extends mixin(TenantModel, [
public writtenoffAt: Date; public writtenoffAt: Date;
public dueDate: Date; public dueDate: Date;
public deliveredAt: Date; public deliveredAt: Date;
public discount: number;
public discountType: DiscountType;
public adjustments: number;
/** /**
* Table name * Table name
@@ -130,9 +135,16 @@ export default class SaleInvoice extends mixin(TenantModel, [
* @returns {number} * @returns {number}
*/ */
get total() { get total() {
const discountAmount = this.discountType === DiscountType.Amount
? this.discount
: this.subtotal * (this.discount / 100);
const adjustmentAmount = defaultTo(this.adjustments, 0);
const differencies = discountAmount + adjustmentAmount;
return this.isInclusiveTax return this.isInclusiveTax
? this.subtotal ? this.subtotal - differencies
: this.subtotal + this.taxAmountWithheld; : this.subtotal + this.taxAmountWithheld - differencies;
} }
/** /**

View File

@@ -5,12 +5,23 @@ import SaleReceiptSettings from './SaleReceipt.Settings';
import CustomViewBaseModel from './CustomViewBaseModel'; import CustomViewBaseModel from './CustomViewBaseModel';
import { DEFAULT_VIEWS } from '@/services/Sales/Receipts/constants'; import { DEFAULT_VIEWS } from '@/services/Sales/Receipts/constants';
import ModelSearchable from './ModelSearchable'; import ModelSearchable from './ModelSearchable';
import { DiscountType } from '@/interfaces';
import { defaultTo } from 'lodash';
export default class SaleReceipt extends mixin(TenantModel, [ export default class SaleReceipt extends mixin(TenantModel, [
ModelSetting, ModelSetting,
CustomViewBaseModel, CustomViewBaseModel,
ModelSearchable, ModelSearchable,
]) { ]) {
public amount: number;
public exchangeRate: number;
public closedAt: Date;
public discount: number;
public discountType: DiscountType;
public adjustment: number;
/** /**
* Table name * Table name
*/ */
@@ -40,6 +51,44 @@ export default class SaleReceipt extends mixin(TenantModel, [
return this.amount * this.exchangeRate; return this.amount * this.exchangeRate;
} }
/**
* Receipt subtotal.
* @returns {number}
*/
get subtotal() {
return this.amount;
}
/**
* Receipt subtotal in local currency.
* @returns {number}
*/
get subtotalLocal() {
return this.localAmount;
}
/**
* Receipt total.
* @returns {number}
*/
get total() {
const discountAmount = this.discountType === DiscountType.Amount
? this.discount
: this.subtotal * (this.discount / 100);
const adjustmentAmount = defaultTo(this.adjustment, 0);
return this.subtotal - discountAmount - adjustmentAmount;
}
/**
* Receipt total in local currency.
* @returns {number}
*/
get totalLocal() {
return this.total * this.exchangeRate;
}
/** /**
* Detarmine whether the sale receipt closed. * Detarmine whether the sale receipt closed.
* @return {boolean} * @return {boolean}

View File

@@ -6,12 +6,20 @@ import CustomViewBaseModel from './CustomViewBaseModel';
import { DEFAULT_VIEWS } from '@/services/Purchases/VendorCredits/constants'; import { DEFAULT_VIEWS } from '@/services/Purchases/VendorCredits/constants';
import ModelSearchable from './ModelSearchable'; import ModelSearchable from './ModelSearchable';
import VendorCreditMeta from './VendorCredit.Meta'; import VendorCreditMeta from './VendorCredit.Meta';
import { DiscountType } from '@/interfaces';
export default class VendorCredit extends mixin(TenantModel, [ export default class VendorCredit extends mixin(TenantModel, [
ModelSetting, ModelSetting,
CustomViewBaseModel, CustomViewBaseModel,
ModelSearchable, ModelSearchable,
]) { ]) {
public amount: number;
public exchangeRate: number;
public openedAt: Date;
public discount: number;
public discountType: DiscountType;
public adjustment: number;
/** /**
* Table name * Table name
*/ */
@@ -34,6 +42,42 @@ export default class VendorCredit extends mixin(TenantModel, [
return this.amount * this.exchangeRate; return this.amount * this.exchangeRate;
} }
/**
* Vendor credit subtotal.
* @returns {number}
*/
get subtotal() {
return this.amount;
}
/**
* Vendor credit subtotal in local currency.
* @returns {number}
*/
get subtotalLocal() {
return this.subtotal * this.exchangeRate;
}
/**
* Vendor credit total.
* @returns {number}
*/
get total() {
const discountAmount = this.discountType === DiscountType.Amount
? this.discount
: this.subtotal * (this.discount / 100);
return this.subtotal - discountAmount - this.adjustment;
}
/**
* Vendor credit total in local currency.
* @returns {number}
*/
get totalLocal() {
return this.total * this.exchangeRate;
}
/** /**
* Model modifiers. * Model modifiers.
*/ */