fix: discount transactions GL entries

This commit is contained in:
Ahmed Bouhuolia
2024-12-08 14:20:11 +02:00
parent 14ae978bde
commit 46719ef361
17 changed files with 375 additions and 58 deletions

View File

@@ -1,3 +1,14 @@
export const OtherExpensesAccount = {
name: 'Other Expenses',
slug: 'other-expenses',
account_type: 'other-expense',
code: '40011',
description: '',
active: 1,
index: 1,
predefined: 1,
};
export const TaxPayableAccount = {
name: 'Tax Payable',
slug: 'tax-payable',
@@ -39,8 +50,38 @@ export const StripeClearingAccount = {
code: '100020',
active: true,
index: 1,
predefined: true,
}
predefined: true,
};
export const DiscountExpenseAccount = {
name: 'Discount',
slug: 'discount',
account_type: 'other-income',
code: '40008',
active: true,
index: 1,
predefined: true,
};
export const PurchaseDiscountAccount = {
name: 'Purchase Discount',
slug: 'purchase-discount',
account_type: 'other-expense',
code: '40009',
active: true,
index: 1,
predefined: true,
};
export const OtherChargesAccount = {
name: 'Other Charges',
slug: 'other-charges',
account_type: 'other-income',
code: '40010',
active: true,
index: 1,
predefined: true,
};
export default [
{
@@ -231,17 +272,7 @@ export default [
},
// Expenses
{
name: 'Other Expenses',
slug: 'other-expenses',
account_type: 'other-expense',
parent_account_id: null,
code: '40001',
description: '',
active: 1,
index: 1,
predefined: 1,
},
OtherExpensesAccount,
{
name: 'Cost of Goods Sold',
slug: 'cost-of-goods-sold',
@@ -358,4 +389,7 @@ export default [
},
UnearnedRevenueAccount,
PrepardExpenses,
DiscountExpenseAccount,
PurchaseDiscountAccount,
OtherChargesAccount,
];

View File

@@ -80,6 +80,11 @@ export interface ISaleInvoice {
pdfTemplateId?: number;
paymentMethods?: Array<PaymentIntegrationTransactionLink>;
adjustment?: number;
discount?: number;
discountAmount?: number;
}
export enum DiscountType {

View File

@@ -1,6 +1,7 @@
import { Model, raw, mixin } from 'objection';
import { castArray, defaultTo, difference } from 'lodash';
import moment from 'moment';
import * as R from 'ramda';
import TenantModel from 'models/TenantModel';
import BillSettings from './Bill.Settings';
import ModelSetting from './ModelSetting';
@@ -133,12 +134,11 @@ export default class Bill extends mixin(TenantModel, [
get total() {
const adjustmentAmount = defaultTo(this.adjustment, 0);
return this.isInclusiveTax
? this.subtotal - this.discountAmount - adjustmentAmount
: this.subtotal +
this.taxAmountWithheld -
this.discountAmount -
adjustmentAmount;
return R.compose(
R.add(adjustmentAmount),
R.subtract(this.discountAmount),
R.when(R.always(this.isInclusiveTax), R.add(this.taxAmountWithheld))
)(this.subtotal);
}
/**

View File

@@ -107,7 +107,7 @@ export default class CreditNote extends mixin(TenantModel, [
* @returns {number}
*/
get total() {
return this.subtotal - this.discountAmount - this.adjustment;
return this.subtotal - this.discountAmount + this.adjustment;
}
/**

View File

@@ -116,7 +116,7 @@ export default class SaleEstimate extends mixin(TenantModel, [
get total() {
const adjustmentAmount = defaultTo(this.adjustment, 0);
return this.subtotal - this.discountAmount - adjustmentAmount;
return this.subtotal - this.discountAmount + adjustmentAmount;
}
/**

View File

@@ -1,4 +1,5 @@
import { mixin, Model, raw } from 'objection';
import * as R from 'ramda';
import { castArray, defaultTo, takeWhile } from 'lodash';
import moment from 'moment';
import TenantModel from 'models/TenantModel';
@@ -147,9 +148,7 @@ export default class SaleInvoice extends mixin(TenantModel, [
* @returns {number | null}
*/
get discountPercentage(): number | null {
return this.discountType === DiscountType.Percentage
? this.discount
: null;
return this.discountType === DiscountType.Percentage ? this.discount : null;
}
/**
@@ -158,11 +157,12 @@ export default class SaleInvoice extends mixin(TenantModel, [
*/
get total() {
const adjustmentAmount = defaultTo(this.adjustment, 0);
const differencies = this.discountAmount + adjustmentAmount;
return this.isInclusiveTax
? this.subtotal - differencies
: this.subtotal + this.taxAmountWithheld - differencies;
return R.compose(
R.add(adjustmentAmount),
R.subtract(R.__, this.discountAmount),
R.when(R.always(this.isInclusiveTax), R.add(this.taxAmountWithheld))
)(this.subtotal);
}
/**

View File

@@ -108,7 +108,7 @@ export default class SaleReceipt extends mixin(TenantModel, [
get total() {
const adjustmentAmount = defaultTo(this.adjustment, 0);
return this.subtotal - this.discountAmount - adjustmentAmount;
return this.subtotal - this.discountAmount + adjustmentAmount;
}
/**

View File

@@ -73,7 +73,7 @@ export default class VendorCredit extends mixin(TenantModel, [
* @returns {number}
*/
get total() {
return this.subtotal - this.discountAmount - this.adjustment;
return this.subtotal - this.discountAmount + this.adjustment;
}
/**

View File

@@ -3,7 +3,11 @@ import TenantRepository from '@/repositories/TenantRepository';
import { IAccount } from '@/interfaces';
import { Knex } from 'knex';
import {
DiscountExpenseAccount,
OtherChargesAccount,
OtherExpensesAccount,
PrepardExpenses,
PurchaseDiscountAccount,
StripeClearingAccount,
TaxPayableAccount,
UnearnedRevenueAccount,
@@ -188,9 +192,9 @@ export default class AccountRepository extends TenantRepository {
/**
* Finds or creates the unearned revenue.
* @param {Record<string, string>} extraAttrs
* @param {Knex.Transaction} trx
* @returns
* @param {Record<string, string>} extraAttrs
* @param {Knex.Transaction} trx
* @returns
*/
public async findOrCreateUnearnedRevenue(
extraAttrs: Record<string, string> = {},
@@ -219,9 +223,9 @@ export default class AccountRepository extends TenantRepository {
/**
* Finds or creates the prepard expenses account.
* @param {Record<string, string>} extraAttrs
* @param {Knex.Transaction} trx
* @returns
* @param {Record<string, string>} extraAttrs
* @param {Knex.Transaction} trx
* @returns
*/
public async findOrCreatePrepardExpenses(
extraAttrs: Record<string, string> = {},
@@ -249,12 +253,11 @@ export default class AccountRepository extends TenantRepository {
return result;
}
/**
* Finds or creates the stripe clearing account.
* @param {Record<string, string>} extraAttrs
* @param {Knex.Transaction} trx
* @returns
* @param {Record<string, string>} extraAttrs
* @param {Knex.Transaction} trx
* @returns
*/
public async findOrCreateStripeClearing(
extraAttrs: Record<string, string> = {},
@@ -281,4 +284,114 @@ export default class AccountRepository extends TenantRepository {
}
return result;
}
/**
* Finds or creates the discount expense account.
* @param {Record<string, string>} extraAttrs
* @param {Knex.Transaction} trx
* @returns
*/
public async findOrCreateDiscountAccount(
extraAttrs: Record<string, string> = {},
trx?: Knex.Transaction
) {
// Retrieves the given tenant metadata.
const tenantMeta = await TenantMetadata.query().findOne({
tenantId: this.tenantId,
});
const _extraAttrs = {
currencyCode: tenantMeta.baseCurrency,
...extraAttrs,
};
let result = await this.model
.query(trx)
.findOne({ slug: DiscountExpenseAccount.slug, ..._extraAttrs });
if (!result) {
result = await this.model.query(trx).insertAndFetch({
...DiscountExpenseAccount,
..._extraAttrs,
});
}
return result;
}
public async findOrCreatePurchaseDiscountAccount(
extraAttrs: Record<string, string> = {},
trx?: Knex.Transaction
) {
// Retrieves the given tenant metadata.
const tenantMeta = await TenantMetadata.query().findOne({
tenantId: this.tenantId,
});
const _extraAttrs = {
currencyCode: tenantMeta.baseCurrency,
...extraAttrs,
};
let result = await this.model
.query(trx)
.findOne({ slug: PurchaseDiscountAccount.slug, ..._extraAttrs });
if (!result) {
result = await this.model.query(trx).insertAndFetch({
...PurchaseDiscountAccount,
..._extraAttrs,
});
}
return result;
}
public async findOrCreateOtherChargesAccount(
extraAttrs: Record<string, string> = {},
trx?: Knex.Transaction
) {
// Retrieves the given tenant metadata.
const tenantMeta = await TenantMetadata.query().findOne({
tenantId: this.tenantId,
});
const _extraAttrs = {
currencyCode: tenantMeta.baseCurrency,
...extraAttrs,
};
let result = await this.model
.query(trx)
.findOne({ slug: OtherChargesAccount.slug, ..._extraAttrs });
if (!result) {
result = await this.model.query(trx).insertAndFetch({
...OtherChargesAccount,
..._extraAttrs,
});
}
return result;
}
public async findOrCreateOtherExpensesAccount(
extraAttrs: Record<string, string> = {},
trx?: Knex.Transaction
) {
// Retrieves the given tenant metadata.
const tenantMeta = await TenantMetadata.query().findOne({
tenantId: this.tenantId,
});
const _extraAttrs = {
currencyCode: tenantMeta.baseCurrency,
...extraAttrs,
};
let result = await this.model
.query(trx)
.findOne({ slug: OtherExpensesAccount.slug, ..._extraAttrs });
if (!result) {
result = await this.model.query(trx).insertAndFetch({
...OtherExpensesAccount,
..._extraAttrs,
});
}
return result;
}
}

View File

@@ -52,10 +52,22 @@ export class BillGLEntries {
{},
trx
);
// Find or create other expenses account.
const otherExpensesAccount = await accountRepository.findOrCreateOtherExpensesAccount(
{},
trx
);
// Find or create purchase discount account.
const purchaseDiscountAccount = await accountRepository.findOrCreatePurchaseDiscountAccount(
{},
trx
);
const billLedger = this.getBillLedger(
bill,
APAccount.id,
taxPayableAccount.id
taxPayableAccount.id,
purchaseDiscountAccount.id,
otherExpensesAccount.id
);
// Commit the GL enties on the storage.
await this.ledgerStorage.commit(tenantId, billLedger, trx);
@@ -240,6 +252,51 @@ export class BillGLEntries {
return nonZeroTaxEntries.map(transformTaxEntry);
};
/**
* Retrieves the purchase discount GL entry.
* @param {IBill} bill
* @param {number} purchaseDiscountAccountId
* @returns {ILedgerEntry}
*/
private getPurchaseDiscountEntry = (
bill: IBill,
purchaseDiscountAccountId: number
) => {
const commonEntry = this.getBillCommonEntry(bill);
return {
...commonEntry,
credit: bill.discountAmount,
accountId: purchaseDiscountAccountId,
accountNormal: AccountNormal.DEBIT,
index: 1,
indexGroup: 40,
};
};
/**
* Retrieves the purchase other charges GL entry.
* @param {IBill} bill
* @param {number} otherChargesAccountId
* @returns {ILedgerEntry}
*/
private getAdjustmentEntry = (
bill: IBill,
otherExpensesAccountId: number
) => {
const commonEntry = this.getBillCommonEntry(bill);
return {
...commonEntry,
debit: bill.adjustment < 0 ? bill.adjustment : 0,
credit: bill.adjustment > 0 ? bill.adjustment : 0,
accountId: otherExpensesAccountId,
accountNormal: AccountNormal.DEBIT,
index: 1,
indexGroup: 40,
};
};
/**
* Retrieves the given bill GL entries.
* @param {IBill} bill
@@ -249,7 +306,9 @@ export class BillGLEntries {
private getBillGLEntries = (
bill: IBill,
payableAccountId: number,
taxPayableAccountId: number
taxPayableAccountId: number,
purchaseDiscountAccountId: number,
otherExpensesAccountId: number
): ILedgerEntry[] => {
const payableEntry = this.getBillPayableEntry(payableAccountId, bill);
@@ -262,8 +321,21 @@ export class BillGLEntries {
);
const taxEntries = this.getBillTaxEntries(bill, taxPayableAccountId);
const purchaseDiscountEntry = this.getPurchaseDiscountEntry(
bill,
purchaseDiscountAccountId
);
const adjustmentEntry = this.getAdjustmentEntry(bill, otherExpensesAccountId);
// Allocate cost entries journal entries.
return [payableEntry, ...itemsEntries, ...landedCostEntries, ...taxEntries];
return [
payableEntry,
...itemsEntries,
...landedCostEntries,
...taxEntries,
purchaseDiscountEntry,
adjustmentEntry,
];
};
/**
@@ -275,14 +347,17 @@ export class BillGLEntries {
private getBillLedger = (
bill: IBill,
payableAccountId: number,
taxPayableAccountId: number
taxPayableAccountId: number,
purchaseDiscountAccountId: number,
otherExpensesAccountId: number
) => {
const entries = this.getBillGLEntries(
bill,
payableAccountId,
taxPayableAccountId
taxPayableAccountId,
purchaseDiscountAccountId,
otherExpensesAccountId
);
return new Ledger(entries);
};
}

View File

@@ -44,18 +44,31 @@ export class SaleInvoiceGLEntries {
// Find or create the A/R account.
const ARAccount = await accountRepository.findOrCreateAccountReceivable(
saleInvoice.currencyCode, {}, trx
saleInvoice.currencyCode,
{},
trx
);
// Find or create tax payable account.
const taxPayableAccount = await accountRepository.findOrCreateTaxPayable(
{},
trx
);
// Find or create the discount expense account.
const discountAccount = await accountRepository.findOrCreateDiscountAccount(
{},
trx
);
// Find or create the other charges account.
const otherChargesAccount =
await accountRepository.findOrCreateOtherChargesAccount({}, trx);
// Retrieves the ledger of the invoice.
const ledger = this.getInvoiceGLedger(
saleInvoice,
ARAccount.id,
taxPayableAccount.id
taxPayableAccount.id,
discountAccount.id,
otherChargesAccount.id
);
// Commits the ledger entries to the storage as UOW.
await this.ledegrRepository.commit(tenantId, ledger, trx);
@@ -107,12 +120,16 @@ export class SaleInvoiceGLEntries {
public getInvoiceGLedger = (
saleInvoice: ISaleInvoice,
ARAccountId: number,
taxPayableAccountId: number
taxPayableAccountId: number,
discountAccountId: number,
otherChargesAccountId: number
): ILedger => {
const entries = this.getInvoiceGLEntries(
saleInvoice,
ARAccountId,
taxPayableAccountId
taxPayableAccountId,
discountAccountId,
otherChargesAccountId
);
return new Ledger(entries);
};
@@ -127,6 +144,7 @@ export class SaleInvoiceGLEntries {
): Partial<ILedgerEntry> => ({
credit: 0,
debit: 0,
currencyCode: saleInvoice.currencyCode,
exchangeRate: saleInvoice.exchangeRate,
@@ -249,6 +267,50 @@ export class SaleInvoiceGLEntries {
return nonZeroTaxEntries.map(transformTaxEntry);
};
/**
* Retrieves the invoice discount GL entry.
* @param {ISaleInvoice} saleInvoice
* @param {number} discountAccountId
* @returns {ILedgerEntry}
*/
private getInvoiceDiscountEntry = (
saleInvoice: ISaleInvoice,
discountAccountId: number
): ILedgerEntry => {
const commonEntry = this.getInvoiceGLCommonEntry(saleInvoice);
return {
...commonEntry,
debit: saleInvoice.discountAmount,
accountId: discountAccountId,
accountNormal: AccountNormal.CREDIT,
index: 1,
} as ILedgerEntry;
};
/**
* Retrieves the invoice adjustment GL entry.
* @param {ISaleInvoice} saleInvoice
* @param {number} adjustmentAccountId
* @returns {ILedgerEntry}
*/
private getAdjustmentEntry = (
saleInvoice: ISaleInvoice,
otherChargesAccountId: number
): ILedgerEntry => {
const commonEntry = this.getInvoiceGLCommonEntry(saleInvoice);
const adjustmentAmount = Math.abs(saleInvoice.adjustment);
return {
...commonEntry,
debit: saleInvoice.adjustment < 0 ? adjustmentAmount : 0,
credit: saleInvoice.adjustment > 0 ? adjustmentAmount : 0,
accountId: otherChargesAccountId,
accountNormal: AccountNormal.CREDIT,
index: 1,
};
};
/**
* Retrieves the invoice GL entries.
* @param {ISaleInvoice} saleInvoice
@@ -258,7 +320,9 @@ export class SaleInvoiceGLEntries {
public getInvoiceGLEntries = (
saleInvoice: ISaleInvoice,
ARAccountId: number,
taxPayableAccountId: number
taxPayableAccountId: number,
discountAccountId: number,
otherChargesAccountId: number
): ILedgerEntry[] => {
const receivableEntry = this.getInvoiceReceivableEntry(
saleInvoice,
@@ -271,6 +335,20 @@ export class SaleInvoiceGLEntries {
saleInvoice,
taxPayableAccountId
);
return [receivableEntry, ...creditEntries, ...taxEntries];
const discountEntry = this.getInvoiceDiscountEntry(
saleInvoice,
discountAccountId
);
const adjustmentEntry = this.getAdjustmentEntry(
saleInvoice,
otherChargesAccountId
);
return [
receivableEntry,
...creditEntries,
...taxEntries,
discountEntry,
adjustmentEntry,
];
};
}

View File

@@ -415,7 +415,7 @@ export const useBillTotal = () => {
return R.compose(
R.when(R.always(isExclusiveTax), R.add(totalTaxAmount)),
R.subtract(R.__, discountAmount),
R.subtract(R.__, adjustmentAmount),
R.add(R.__, adjustmentAmount),
)(subtotal);
};

View File

@@ -259,7 +259,10 @@ export const useVendorCreditTotal = () => {
const discountAmount = useVendorCreditDiscountAmount();
const adjustment = useVendorCreditAdjustment();
return subtotal - discountAmount - adjustment;
return R.compose(
R.subtract(R.__, discountAmount),
R.add(R.__, adjustment),
)(subtotal);
};
/**

View File

@@ -263,7 +263,10 @@ export const useCreditNoteTotal = () => {
const discountAmount = useCreditNoteDiscountAmount();
const adjustmentAmount = useCreditNoteAdjustmentAmount();
return subtotal - discountAmount - adjustmentAmount;
return R.compose(
R.subtract(R.__, discountAmount),
R.add(R.__, adjustmentAmount),
)(subtotal);
};
/**

View File

@@ -299,7 +299,10 @@ export const useEstimateTotal = () => {
const discount = useEstimateDiscount();
const adjustment = useEstimateAdjustment();
return subtotal - discount - adjustment;
return R.compose(
R.subtract(R.__, discount),
R.add(R.__, adjustment),
)(subtotal);
};
/**

View File

@@ -455,7 +455,7 @@ export const useInvoiceTotal = () => {
return R.compose(
R.when(R.always(isExclusiveTax), R.add(totalTaxAmount)),
R.subtract(R.__, discountAmount),
R.subtract(R.__, adjustmentAmount),
R.add(R.__, adjustmentAmount),
)(subtotal);
};

View File

@@ -284,7 +284,10 @@ export const useReceiptTotal = () => {
const adjustmentAmount = useReceiptAdjustmentAmount();
const discountAmount = useReceiptDiscountAmount();
return subtotal - discountAmount - adjustmentAmount;
return R.compose(
R.add(R.__, adjustmentAmount),
R.subtract(R.__, discountAmount),
)(subtotal);
};
/**