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.*.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()
.toInt(),
// Attachments.
check('attachments').isArray().optional(),
check('attachments.*.key').exists().isString(),
// Pdf template id.
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 {
AbilitySubject,
DiscountType,
ISaleEstimateDTO,
SaleEstimateAction,
SaleEstimateMailOptionsDTO,
@@ -195,11 +196,21 @@ export default class SalesEstimatesController extends BaseController {
check('terms_conditions').optional().trim(),
check('send_to_email').optional().trim(),
// # Attachments
check('attachments').isArray().optional(),
check('attachments.*.key').exists().isString(),
// Pdf template id.
// # Pdf template id.
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 { IBillLandedCost } from './LandedCost';
import { AttachmentLinkDTO } from './Attachments';
import { DiscountType } from './SaleInvoice';
export interface IBillDTO {
vendorId: number;
@@ -22,6 +23,13 @@ export interface IBillDTO {
projectId?: number;
isInclusiveTax?: boolean;
attachments?: AttachmentLinkDTO[];
// # Discount
discount?: number;
discountType?: DiscountType;
// # Adjustment
adjustment?: number;
}
export interface IBillEditDTO {

View File

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

View File

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

View File

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

View File

@@ -2,6 +2,7 @@ import { Knex } from 'knex';
import { IItemEntry } from './ItemEntry';
import { CommonMailOptions, CommonMailOptionsDTO } from './Mailable';
import { AttachmentLinkDTO } from './Attachments';
import { DiscountType } from './SaleInvoice';
export interface ISaleReceipt {
id?: number;
@@ -47,6 +48,11 @@ export interface ISaleReceiptDTO {
entries: any[];
branchId?: number;
attachments?: AttachmentLinkDTO[];
discount?: number;
discountType?: DiscountType;
adjustment?: number;
}
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 { AttachmentLinkDTO } from './Attachments';
@@ -63,6 +63,11 @@ export interface IVendorCreditDTO {
branchId?: number;
warehouseId?: number;
attachments?: AttachmentLinkDTO[];
discount?: number;
discountType?: DiscountType;
adjustment?: number;
}
export interface IVendorCreditCreateDTO extends IVendorCreditDTO {}

View File

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

View File

@@ -5,12 +5,20 @@ import CustomViewBaseModel from './CustomViewBaseModel';
import { DEFAULT_VIEWS } from '@/services/CreditNotes/constants';
import ModelSearchable from './ModelSearchable';
import CreditNoteMeta from './CreditNote.Meta';
import { DiscountType } from '@/interfaces';
export default class CreditNote extends mixin(TenantModel, [
ModelSetting,
CustomViewBaseModel,
ModelSearchable,
]) {
public amount: number;
public exchangeRate: number;
public openedAt: Date;
public discount: number;
public discountType: DiscountType;
public adjustment: number;
/**
* Table name
*/
@@ -48,6 +56,42 @@ export default class CreditNote extends mixin(TenantModel, [
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.
* @returns {boolean}

View File

@@ -7,12 +7,22 @@ import ModelSetting from './ModelSetting';
import CustomViewBaseModel from './CustomViewBaseModel';
import { DEFAULT_VIEWS } from '@/services/Sales/Estimates/constants';
import ModelSearchable from './ModelSearchable';
import { DiscountType } from '@/interfaces';
import { defaultTo } from 'lodash';
export default class SaleEstimate extends mixin(TenantModel, [
ModelSetting,
CustomViewBaseModel,
ModelSearchable,
]) {
public amount: number;
public exchangeRate: number;
public discount: number;
public discountType: DiscountType;
public adjustment: number;
/**
* Table name
*/
@@ -49,6 +59,44 @@ export default class SaleEstimate extends mixin(TenantModel, [
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.
* @return {boolean}

View File

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

View File

@@ -5,12 +5,23 @@ import SaleReceiptSettings from './SaleReceipt.Settings';
import CustomViewBaseModel from './CustomViewBaseModel';
import { DEFAULT_VIEWS } from '@/services/Sales/Receipts/constants';
import ModelSearchable from './ModelSearchable';
import { DiscountType } from '@/interfaces';
import { defaultTo } from 'lodash';
export default class SaleReceipt extends mixin(TenantModel, [
ModelSetting,
CustomViewBaseModel,
ModelSearchable,
]) {
public amount: number;
public exchangeRate: number;
public closedAt: Date;
public discount: number;
public discountType: DiscountType;
public adjustment: number;
/**
* Table name
*/
@@ -40,6 +51,44 @@ export default class SaleReceipt extends mixin(TenantModel, [
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.
* @return {boolean}

View File

@@ -6,12 +6,20 @@ import CustomViewBaseModel from './CustomViewBaseModel';
import { DEFAULT_VIEWS } from '@/services/Purchases/VendorCredits/constants';
import ModelSearchable from './ModelSearchable';
import VendorCreditMeta from './VendorCredit.Meta';
import { DiscountType } from '@/interfaces';
export default class VendorCredit extends mixin(TenantModel, [
ModelSetting,
CustomViewBaseModel,
ModelSearchable,
]) {
public amount: number;
public exchangeRate: number;
public openedAt: Date;
public discount: number;
public discountType: DiscountType;
public adjustment: number;
/**
* Table name
*/
@@ -34,6 +42,42 @@ export default class VendorCredit extends mixin(TenantModel, [
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.
*/