diff --git a/packages/server/src/api/controllers/Purchases/VendorCredit.ts b/packages/server/src/api/controllers/Purchases/VendorCredit.ts index e97fbf687..e44906e40 100644 --- a/packages/server/src/api/controllers/Purchases/VendorCredit.ts +++ b/packages/server/src/api/controllers/Purchases/VendorCredit.ts @@ -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(), ]; } diff --git a/packages/server/src/api/controllers/Sales/CreditNotes.ts b/packages/server/src/api/controllers/Sales/CreditNotes.ts index a88b45939..6f7a9a6ea 100644 --- a/packages/server/src/api/controllers/Sales/CreditNotes.ts +++ b/packages/server/src/api/controllers/Sales/CreditNotes.ts @@ -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(), ]; } diff --git a/packages/server/src/api/controllers/Sales/SalesEstimates.ts b/packages/server/src/api/controllers/Sales/SalesEstimates.ts index 8f284023c..dbbef3e7b 100644 --- a/packages/server/src/api/controllers/Sales/SalesEstimates.ts +++ b/packages/server/src/api/controllers/Sales/SalesEstimates.ts @@ -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(), ]; } diff --git a/packages/server/src/database/migrations/20241128081259_add_discount_to_estimates_table.js b/packages/server/src/database/migrations/20241128081259_add_discount_to_estimates_table.js new file mode 100644 index 000000000..e8ab28a7a --- /dev/null +++ b/packages/server/src/database/migrations/20241128081259_add_discount_to_estimates_table.js @@ -0,0 +1,24 @@ +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +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 } + */ +exports.down = function(knex) { + return knex.schema.alterTable('sales_estimates', (table) => { + table.dropColumn('discount'); + table.dropColumn('discount_type'); + table.dropColumn('adjustment'); + }); +}; diff --git a/packages/server/src/database/migrations/20241128084550_add_discount_to_receipts_table.js b/packages/server/src/database/migrations/20241128084550_add_discount_to_receipts_table.js new file mode 100644 index 000000000..8b46955cc --- /dev/null +++ b/packages/server/src/database/migrations/20241128084550_add_discount_to_receipts_table.js @@ -0,0 +1,24 @@ +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +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 } + */ +exports.down = function(knex) { + return knex.schema.alterTable('sales_receipts', (table) => { + table.dropColumn('discount'); + table.dropColumn('discount_type'); + table.dropColumn('adjustment'); + }); +}; diff --git a/packages/server/src/database/migrations/20241128085243_add_discount_to_bills_table.js b/packages/server/src/database/migrations/20241128085243_add_discount_to_bills_table.js new file mode 100644 index 000000000..9de24d9ab --- /dev/null +++ b/packages/server/src/database/migrations/20241128085243_add_discount_to_bills_table.js @@ -0,0 +1,26 @@ +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +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 } + */ +exports.down = function(knex) { + return knex.schema.alterTable('bills', (table) => { + table.dropColumn('discount'); + table.dropColumn('discount_type'); + table.dropColumn('adjustment'); + }); +}; diff --git a/packages/server/src/database/migrations/20241128090222_add_discount_to_credit_notes_table.js b/packages/server/src/database/migrations/20241128090222_add_discount_to_credit_notes_table.js new file mode 100644 index 000000000..f59624313 --- /dev/null +++ b/packages/server/src/database/migrations/20241128090222_add_discount_to_credit_notes_table.js @@ -0,0 +1,23 @@ +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +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 } + */ +exports.down = function(knex) { + return knex.schema.alterTable('credit_notes', (table) => { + table.dropColumn('discount'); + table.dropColumn('discount_type'); + table.dropColumn('adjustment'); + }); +}; diff --git a/packages/server/src/interfaces/Bill.ts b/packages/server/src/interfaces/Bill.ts index a130a62ba..6e5391ee8 100644 --- a/packages/server/src/interfaces/Bill.ts +++ b/packages/server/src/interfaces/Bill.ts @@ -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 { diff --git a/packages/server/src/interfaces/CreditNote.ts b/packages/server/src/interfaces/CreditNote.ts index 035778273..4f0944aaa 100644 --- a/packages/server/src/interfaces/CreditNote.ts +++ b/packages/server/src/interfaces/CreditNote.ts @@ -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 { diff --git a/packages/server/src/interfaces/SaleEstimate.ts b/packages/server/src/interfaces/SaleEstimate.ts index 72dd4cd09..d03f9e2db 100644 --- a/packages/server/src/interfaces/SaleEstimate.ts +++ b/packages/server/src/interfaces/SaleEstimate.ts @@ -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 { diff --git a/packages/server/src/interfaces/SaleInvoice.ts b/packages/server/src/interfaces/SaleInvoice.ts index 33f0fa14c..7eebc6fa1 100644 --- a/packages/server/src/interfaces/SaleInvoice.ts +++ b/packages/server/src/interfaces/SaleInvoice.ts @@ -82,6 +82,11 @@ export interface ISaleInvoice { paymentMethods?: Array; } +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; diff --git a/packages/server/src/interfaces/SaleReceipt.ts b/packages/server/src/interfaces/SaleReceipt.ts index f44e995d3..3f1ac38d1 100644 --- a/packages/server/src/interfaces/SaleReceipt.ts +++ b/packages/server/src/interfaces/SaleReceipt.ts @@ -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 { diff --git a/packages/server/src/interfaces/VendorCredit.ts b/packages/server/src/interfaces/VendorCredit.ts index 879b6b783..29254337f 100644 --- a/packages/server/src/interfaces/VendorCredit.ts +++ b/packages/server/src/interfaces/VendorCredit.ts @@ -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 {} diff --git a/packages/server/src/models/Bill.ts b/packages/server/src/models/Bill.ts index f4e313e72..0a296238a 100644 --- a/packages/server/src/models/Bill.ts +++ b/packages/server/src/models/Bill.ts @@ -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; } /** diff --git a/packages/server/src/models/CreditNote.ts b/packages/server/src/models/CreditNote.ts index 02c456f9d..6caa254d5 100644 --- a/packages/server/src/models/CreditNote.ts +++ b/packages/server/src/models/CreditNote.ts @@ -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} diff --git a/packages/server/src/models/SaleEstimate.ts b/packages/server/src/models/SaleEstimate.ts index 8fd31e2d4..714bcc6f2 100644 --- a/packages/server/src/models/SaleEstimate.ts +++ b/packages/server/src/models/SaleEstimate.ts @@ -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} diff --git a/packages/server/src/models/SaleInvoice.ts b/packages/server/src/models/SaleInvoice.ts index 883b39ce6..6f258cd6e 100644 --- a/packages/server/src/models/SaleInvoice.ts +++ b/packages/server/src/models/SaleInvoice.ts @@ -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; } /** diff --git a/packages/server/src/models/SaleReceipt.ts b/packages/server/src/models/SaleReceipt.ts index 9ac76640e..d441e8572 100644 --- a/packages/server/src/models/SaleReceipt.ts +++ b/packages/server/src/models/SaleReceipt.ts @@ -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} diff --git a/packages/server/src/models/VendorCredit.ts b/packages/server/src/models/VendorCredit.ts index c7de45e54..2a7833ace 100644 --- a/packages/server/src/models/VendorCredit.ts +++ b/packages/server/src/models/VendorCredit.ts @@ -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. */