Compare commits

..

1 Commits

Author SHA1 Message Date
Ahmed Bouhuolia
6b6b73b77c feat: send signup event to Loops (#531)
* feat: send signup event to Loops

* feat: fix
2024-07-17 15:56:05 +02:00
62 changed files with 330 additions and 1779 deletions

View File

@@ -2,14 +2,6 @@
All notable changes to Bigcapital server-side will be in this file.
## [v0.18.0] - 10-08-2024
* feat: Bank rules for automated categorization by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/511
* feat: Categorize & match bank transaction by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/511
* feat: Reconcile match transactions by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/522
* fix: Issues in matching transactions by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/523
* fix: Cashflow transactions types by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/524
## [v0.17.5] - 17-06-2024
* fix: Balance sheet and P/L nested accounts by @abouolia in https://github.com/bigcapitalhq/bigcapital/pull/501

View File

@@ -111,7 +111,6 @@ export default class BillsPayments extends BaseController {
check('vendor_id').exists().isNumeric().toInt(),
check('exchange_rate').optional().isFloat({ gt: 0 }).toFloat(),
check('amount').exists().isNumeric().toFloat(),
check('payment_account_id').exists().isNumeric().toInt(),
check('payment_number').optional({ nullable: true }).trim().escape(),
check('payment_date').exists(),
@@ -119,15 +118,13 @@ export default class BillsPayments extends BaseController {
check('reference').optional().trim().escape(),
check('branch_id').optional({ nullable: true }).isNumeric().toInt(),
check('entries').exists().isArray(),
check('entries').exists().isArray({ min: 1 }),
check('entries.*.index').optional().isNumeric().toInt(),
check('entries.*.bill_id').exists().isNumeric().toInt(),
check('entries.*.payment_amount').exists().isNumeric().toFloat(),
check('attachments').isArray().optional(),
check('attachments.*.key').exists().isString(),
check('prepard_expenses_account_id').optional().isNumeric().toInt(),
];
}

View File

@@ -151,8 +151,6 @@ export default class PaymentReceivesController extends BaseController {
check('exchange_rate').optional().isFloat({ gt: 0 }).toFloat(),
check('payment_date').exists(),
check('amount').exists().isNumeric().toFloat(),
check('reference_no').optional(),
check('deposit_account_id').exists().isNumeric().toInt(),
check('payment_receive_no').optional({ nullable: true }).trim().escape(),
@@ -160,7 +158,7 @@ export default class PaymentReceivesController extends BaseController {
check('branch_id').optional({ nullable: true }).isNumeric().toInt(),
check('entries').isArray(),
check('entries').isArray({ min: 1 }),
check('entries.*.id').optional({ nullable: true }).isNumeric().toInt(),
check('entries.*.index').optional().isNumeric().toInt(),
@@ -169,11 +167,6 @@ export default class PaymentReceivesController extends BaseController {
check('attachments').isArray().optional(),
check('attachments.*.key').exists().isString(),
check('unearned_revenue_account_id')
.optional({ nullable: true })
.isNumeric()
.toInt(),
];
}

View File

@@ -237,4 +237,8 @@ module.exports = {
endpoint: process.env.S3_ENDPOINT,
bucket: process.env.S3_BUCKET || 'bigcapital-documents',
},
loops: {
apiKey: process.env.LOOPS_API_KEY,
},
};

View File

@@ -1,17 +0,0 @@
exports.up = function (knex) {
return knex.schema.table('payment_receives', (table) => {
table.decimal('applied_amount', 13, 3).defaultTo(0);
table
.integer('unearned_revenue_account_id')
.unsigned()
.references('id')
.inTable('accounts');
});
};
exports.down = function (knex) {
return knex.schema.table('payment_receives', (table) => {
table.dropColumn('applied_amount');
table.dropColumn('unearned_revenue_account_id');
});
};

View File

@@ -1,17 +0,0 @@
exports.up = function (knex) {
return knex.schema.table('bills_payments', (table) => {
table.decimal('applied_amount', 13, 3).defaultTo(0);
table
.integer('prepard_expenses_account_id')
.unsigned()
.references('id')
.inTable('accounts');
});
};
exports.down = function (knex) {
return knex.schema.table('bills_payments', (table) => {
table.dropColumn('applied_amount');
table.dropColumn('prepard_expenses_account_id');
});
};

View File

@@ -12,7 +12,8 @@ export default class SeedAccounts extends TenantSeeder {
description: this.i18n.__(account.description),
currencyCode: this.tenant.metadata.baseCurrency,
seededAt: new Date(),
}));
})
);
return knex('accounts').then(async () => {
// Inserts seed entries.
return knex('accounts').insert(data);

View File

@@ -9,28 +9,6 @@ export const TaxPayableAccount = {
predefined: 1,
};
export const UnearnedRevenueAccount = {
name: 'Unearned Revenue',
slug: 'unearned-revenue',
account_type: 'other-current-liability',
parent_account_id: null,
code: '50005',
active: true,
index: 1,
predefined: true,
};
export const PrepardExpenses = {
name: 'Prepaid Expenses',
slug: 'prepaid-expenses',
account_type: 'other-current-asset',
parent_account_id: null,
code: '100010',
active: true,
index: 1,
predefined: true,
};
export default [
{
name: 'Bank Account',
@@ -345,6 +323,4 @@ export default [
index: 1,
predefined: 0,
},
UnearnedRevenueAccount,
PrepardExpenses,
];

View File

@@ -166,10 +166,3 @@ export interface IBillOpenedPayload {
oldBill: IBill;
tenantId: number;
}
export interface IBillPrepardExpensesAppliedEventPayload {
tenantId: number;
billId: number;
trx?: Knex.Transaction;
}

View File

@@ -29,9 +29,6 @@ export interface IBillPayment {
localAmount?: number;
branchId?: number;
prepardExpensesAccountId?: number;
isPrepardExpense: boolean;
}
export interface IBillPaymentEntryDTO {
@@ -41,7 +38,6 @@ export interface IBillPaymentEntryDTO {
export interface IBillPaymentDTO {
vendorId: number;
amount: number;
paymentAccountId: number;
paymentNumber?: string;
paymentDate: Date;
@@ -51,7 +47,6 @@ export interface IBillPaymentDTO {
entries: IBillPaymentEntryDTO[];
branchId?: number;
attachments?: AttachmentLinkDTO[];
prepardExpensesAccountId?: number;
}
export interface IBillReceivePageEntry {
@@ -124,11 +119,3 @@ export enum IPaymentMadeAction {
Delete = 'Delete',
View = 'View',
}
export interface IPaymentPrepardExpensesAppliedEventPayload {
tenantId: number;
billPaymentId: number;
billId: number;
appliedAmount: number;
trx?: Knex.Transaction;
}

View File

@@ -40,7 +40,7 @@ export interface ILedgerEntry {
date: Date | string;
transactionType: string;
transactionSubType?: string;
transactionSubType: string;
transactionId: number;

View File

@@ -25,13 +25,8 @@ export interface IPaymentReceive {
updatedAt: Date;
localAmount?: number;
branchId?: number;
unearnedRevenueAccountId?: number;
}
interface IPaymentReceivedCommonDTO {
unearnedRevenueAccountId?: number;
}
export interface IPaymentReceiveCreateDTO extends IPaymentReceivedCommonDTO {
export interface IPaymentReceiveCreateDTO {
customerId: number;
paymentDate: Date;
amount: number;
@@ -46,7 +41,7 @@ export interface IPaymentReceiveCreateDTO extends IPaymentReceivedCommonDTO {
attachments?: AttachmentLinkDTO[];
}
export interface IPaymentReceiveEditDTO extends IPaymentReceivedCommonDTO {
export interface IPaymentReceiveEditDTO {
customerId: number;
paymentDate: Date;
amount: number;
@@ -189,11 +184,3 @@ export interface PaymentReceiveMailPresendEvent {
paymentReceiveId: number;
messageOptions: PaymentReceiveMailOptsDTO;
}
export interface PaymentReceiveUnearnedRevenueAppliedEventPayload {
tenantId: number;
paymentReceiveId: number;
saleInvoiceId: number;
appliedAmount: number;
trx?: Knex.Transaction;
}

View File

@@ -216,9 +216,3 @@ export interface ISaleInvoiceMailSent {
saleInvoiceId: number;
messageOptions: SendInvoiceMailDTO;
}
export interface SaleInvoiceAppliedUnearnedRevenueOnCreatedEventPayload {
tenantId: number;
saleInvoiceId: number;
trx?: Knex.Transaction;
}

View File

@@ -113,8 +113,7 @@ import { UnlinkBankRuleOnDeleteBankRule } from '@/services/Banking/Rules/events/
import { DecrementUncategorizedTransactionOnMatching } from '@/services/Banking/Matching/events/DecrementUncategorizedTransactionsOnMatch';
import { DecrementUncategorizedTransactionOnExclude } from '@/services/Banking/Exclude/events/DecrementUncategorizedTransactionOnExclude';
import { DecrementUncategorizedTransactionOnCategorize } from '@/services/Cashflow/subscribers/DecrementUncategorizedTransactionOnCategorize';
import { AutoApplyUnearnedRevenueOnInvoiceCreated } from '@/services/Sales/PaymentReceives/events/AutoApplyUnearnedRevenueOnInvoiceCreated';
import { AutoApplyPrepardExpensesOnBillCreated } from '@/services/Purchases/Bills/events/AutoApplyPrepardExpensesOnBillCreated';
import { LoopsEventsSubscriber } from '@/services/Loops/LoopsEventsSubscriber';
export default () => {
return new EventPublisher();
@@ -276,5 +275,8 @@ export const susbcribers = () => {
// Plaid
RecognizeSyncedBankTranasctions,
// Loops
LoopsEventsSubscriber
];
};

View File

@@ -525,9 +525,9 @@ export default class Bill extends mixin(TenantModel, [
return notFoundBillsIds;
}
static changePaymentAmount(billId, amount, trx) {
static changePaymentAmount(billId, amount) {
const changeMethod = amount > 0 ? 'increment' : 'decrement';
return this.query(trx)
return this.query()
.where('id', billId)
[changeMethod]('payment_amount', Math.abs(amount));
}

View File

@@ -11,8 +11,6 @@ export default class BillPayment extends mixin(TenantModel, [
CustomViewBaseModel,
ModelSearchable,
]) {
prepardExpensesAccountId: number;
/**
* Table name
*/
@@ -49,14 +47,6 @@ export default class BillPayment extends mixin(TenantModel, [
return BillPaymentSettings;
}
/**
* Detarmines whether the payment is prepard expense.
* @returns {boolean}
*/
get isPrepardExpense() {
return !!this.prepardExpensesAccountId;
}
/**
* Relationship mapping.
*/

View File

@@ -2,12 +2,7 @@ import { Account } from 'models';
import TenantRepository from '@/repositories/TenantRepository';
import { IAccount } from '@/interfaces';
import { Knex } from 'knex';
import {
PrepardExpenses,
TaxPayableAccount,
UnearnedRevenueAccount,
} from '@/database/seeds/data/accounts';
import { TenantMetadata } from '@/system/models';
import { TaxPayableAccount } from '@/database/seeds/data/accounts';
export default class AccountRepository extends TenantRepository {
/**
@@ -184,67 +179,4 @@ export default class AccountRepository extends TenantRepository {
}
return result;
};
/**
* Finds or creates the unearned revenue.
* @param {Record<string, string>} extraAttrs
* @param {Knex.Transaction} trx
* @returns
*/
public async findOrCreateUnearnedRevenue(
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: UnearnedRevenueAccount.slug, ..._extraAttrs });
if (!result) {
result = await this.model.query(trx).insertAndFetch({
...UnearnedRevenueAccount,
..._extraAttrs,
});
}
return result;
}
/**
* Finds or creates the prepard expenses account.
* @param {Record<string, string>} extraAttrs
* @param {Knex.Transaction} trx
* @returns
*/
public async findOrCreatePrepardExpenses(
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: PrepardExpenses.slug, ..._extraAttrs });
if (!result) {
result = await this.model.query(trx).insertAndFetch({
...PrepardExpenses,
..._extraAttrs,
});
}
return result;
}
}

View File

@@ -4,17 +4,12 @@ import CachableRepository from './CachableRepository';
export default class TenantRepository extends CachableRepository {
repositoryName: string;
tenantId: number;
/**
* Constructor method.
* @param {number} tenantId
* @param {number} tenantId
*/
constructor(knex, cache, i18n) {
super(knex, cache, i18n);
}
setTenantId(tenantId: number) {
this.tenantId = tenantId;
}
}
}

View File

@@ -38,24 +38,4 @@ export default class ContactTransfromer extends Transformer {
? this.formatDate(contact.openingBalanceAt)
: '';
};
/**
* Retrieves the unused credit balance.
* @param {IContact} contact
* @returns {number}
*/
protected unusedCredit = (contact: IContact): number => {
return contact.balance > 0 ? 0 : Math.abs(contact.balance);
};
/**
* Retrieves the formatted unused credit balance.
* @param {IContact} contact
* @returns {string}
*/
protected formattedUnusedCredit = (contact: IContact): string => {
const unusedCredit = this.unusedCredit(contact);
return formatNumber(unusedCredit, { currencyCode: contact.currencyCode });
};
}

View File

@@ -12,8 +12,6 @@ export default class CustomerTransfromer extends ContactTransfromer {
'formattedOpeningBalanceAt',
'customerType',
'formattedCustomerType',
'unusedCredit',
'formattedUnusedCredit',
];
};

View File

@@ -45,9 +45,9 @@ export class CustomersApplication {
/**
* Creates a new customer.
* @param {number} tenantId
* @param {ICustomerNewDTO} customerDTO
* @param {ISystemUser} authorizedUser
* @param {number} tenantId
* @param {ICustomerNewDTO} customerDTO
* @param {ISystemUser} authorizedUser
* @returns {Promise<ICustomer>}
*/
public createCustomer = (tenantId: number, customerDTO: ICustomerNewDTO) => {
@@ -56,9 +56,9 @@ export class CustomersApplication {
/**
* Edits details of the given customer.
* @param {number} tenantId
* @param {number} customerId
* @param {ICustomerEditDTO} customerDTO
* @param {number} tenantId
* @param {number} customerId
* @param {ICustomerEditDTO} customerDTO
* @return {Promise<ICustomer>}
*/
public editCustomer = (
@@ -75,9 +75,9 @@ export class CustomersApplication {
/**
* Deletes the given customer and associated transactions.
* @param {number} tenantId
* @param {number} customerId
* @param {ISystemUser} authorizedUser
* @param {number} tenantId
* @param {number} customerId
* @param {ISystemUser} authorizedUser
* @returns {Promise<void>}
*/
public deleteCustomer = (
@@ -94,9 +94,9 @@ export class CustomersApplication {
/**
* Changes the opening balance of the given customer.
* @param {number} tenantId
* @param {number} customerId
* @param {Date|string} openingBalanceEditDTO
* @param {number} tenantId
* @param {number} customerId
* @param {Date|string} openingBalanceEditDTO
* @returns {Promise<ICustomer>}
*/
public editOpeningBalance = (

View File

@@ -1,3 +1,4 @@
import { Service } from 'typedi';
import ContactTransfromer from '../ContactTransformer';
export default class VendorTransfromer extends ContactTransfromer {
@@ -9,9 +10,7 @@ export default class VendorTransfromer extends ContactTransfromer {
return [
'formattedBalance',
'formattedOpeningBalance',
'formattedOpeningBalanceAt',
'unusedCredit',
'formattedUnusedCredit',
'formattedOpeningBalanceAt'
];
};
}

View File

@@ -21,6 +21,7 @@ export const DEFAULT_VIEWS = [
},
];
export const ERRORS = {
OPENING_BALANCE_DATE_REQUIRED: 'OPENING_BALANCE_DATE_REQUIRED',
CONTACT_ALREADY_INACTIVE: 'CONTACT_ALREADY_INACTIVE',

View File

@@ -0,0 +1,51 @@
import axios from 'axios';
import config from '@/config';
import { IAuthSignUpVerifiedEventPayload } from '@/interfaces';
import events from '@/subscribers/events';
import { SystemUser } from '@/system/models';
export class LoopsEventsSubscriber {
/**
* Constructor method.
*/
public attach(bus) {
bus.subscribe(
events.auth.signUpConfirmed,
this.triggerEventOnSignupVerified.bind(this)
);
}
/**
* Once the user verified sends the event to the Loops.
* @param {IAuthSignUpVerifiedEventPayload} param0
*/
public async triggerEventOnSignupVerified({
email,
userId,
}: IAuthSignUpVerifiedEventPayload) {
// Can't continue since the Loops the api key is not configured.
if (!config.loops.apiKey) {
return;
}
const user = await SystemUser.query().findById(userId);
const options = {
method: 'POST',
url: 'https://app.loops.so/api/v1/events/send',
headers: {
Authorization: `Bearer ${config.loops.apiKey}`,
'Content-Type': 'application/json',
},
data: {
email,
userId,
firstName: user.firstName,
lastName: user.lastName,
eventName: 'USER_VERIFIED',
eventProperties: {},
mailingLists: {},
},
};
await axios(options);
}
}

View File

@@ -1,14 +1,8 @@
import moment from 'moment';
import { sumBy, chain } from 'lodash';
import { sumBy } from 'lodash';
import { Service, Inject } from 'typedi';
import { Knex } from 'knex';
import {
AccountNormal,
IBillPayment,
IBillPaymentEntry,
ILedger,
ILedgerEntry,
} from '@/interfaces';
import { AccountNormal, IBillPayment, ILedgerEntry } from '@/interfaces';
import Ledger from '@/services/Accounting/Ledger';
import LedgerStorageService from '@/services/Accounting/LedgerStorageService';
import HasTenancyService from '@/services/Tenancy/TenancyService';
@@ -27,7 +21,6 @@ export class BillPaymentGLEntries {
* @param {number} tenantId
* @param {number} billPaymentId
* @param {Knex.Transaction} trx
* @returns {Promise<void>}
*/
public writePaymentGLEntries = async (
tenantId: number,
@@ -72,7 +65,6 @@ export class BillPaymentGLEntries {
* @param {number} tenantId
* @param {number} billPaymentId
* @param {Knex.Transaction} trx
* @returns {Promise<void>}
*/
public rewritePaymentGLEntries = async (
tenantId: number,
@@ -110,7 +102,7 @@ export class BillPaymentGLEntries {
* @param {IBillPayment} billPayment
* @returns {}
*/
private getPaymentCommonEntry = (billPayment: IBillPayment): ILedgerEntry => {
private getPaymentCommonEntry = (billPayment: IBillPayment) => {
const formattedDate = moment(billPayment.paymentDate).format('YYYY-MM-DD');
return {
@@ -135,7 +127,7 @@ export class BillPaymentGLEntries {
/**
* Calculates the payment total exchange gain/loss.
* @param {IBillPayment} paymentReceive - Payment receive with entries.
* @param {IBillPayment} paymentReceive - Payment receive with entries.
* @returns {number}
*/
private getPaymentExGainOrLoss = (billPayment: IBillPayment): number => {
@@ -149,10 +141,10 @@ export class BillPaymentGLEntries {
/**
* Retrieves the payment exchange gain/loss entries.
* @param {IBillPayment} billPayment -
* @param {number} APAccountId -
* @param {number} gainLossAccountId -
* @param {string} baseCurrency -
* @param {IBillPayment} billPayment -
* @param {number} APAccountId -
* @param {number} gainLossAccountId -
* @param {string} baseCurrency -
* @returns {ILedgerEntry[]}
*/
private getPaymentExGainOrLossEntries = (
@@ -194,7 +186,7 @@ export class BillPaymentGLEntries {
/**
* Retrieves the payment deposit GL entry.
* @param {IBillPayment} billPayment
* @param {IBillPayment} billPayment
* @returns {ILedgerEntry}
*/
private getPaymentGLEntry = (billPayment: IBillPayment): ILedgerEntry => {
@@ -206,7 +198,6 @@ export class BillPaymentGLEntries {
accountId: billPayment.paymentAccountId,
accountNormal: AccountNormal.DEBIT,
index: 2,
indexGroup: 10,
};
};
@@ -235,8 +226,8 @@ export class BillPaymentGLEntries {
/**
* Retrieves the payment GL entries.
* @param {IBillPayment} billPayment
* @param {number} APAccountId
* @param {IBillPayment} billPayment
* @param {number} APAccountId
* @returns {ILedgerEntry[]}
*/
private getPaymentGLEntries = (
@@ -263,53 +254,10 @@ export class BillPaymentGLEntries {
return [paymentEntry, payableEntry, ...exGainLossEntries];
};
/**
*
* BEFORE APPLYING TO PAYMENT TO BILLS.
* -----------------------------------------
* - Cash/Bank - Credit.
* - Prepard Expenses - Debit
*
* AFTER APPLYING BILLS TO PAYMENT.
* -----------------------------------------
* - Prepard Expenses - Credit
* - A/P - Debit
*
* @param {number} APAccountId - A/P account id.
* @param {IBillPayment} billPayment
*/
private getPrepardExpenseGLEntries = (
APAccountId: number,
billPayment: IBillPayment
) => {
const prepardExpenseEntry = this.getPrepardExpenseEntry(billPayment);
const withdrawalEntry = this.getPaymentGLEntry(billPayment);
const paymentLinesEntries = chain(billPayment.entries)
.map((billPaymentEntry) => {
const APEntry = this.getAccountPayablePaymentLineEntry(
APAccountId,
billPayment,
billPaymentEntry
);
const creditPrepardExpenseEntry = this.getCreditPrepardExpenseEntry(
billPayment,
billPaymentEntry
);
return [creditPrepardExpenseEntry, APEntry];
})
.flatten()
.value();
const prepardExpenseEntries = [prepardExpenseEntry, withdrawalEntry];
const combinedEntries = [...prepardExpenseEntries, ...paymentLinesEntries];
return combinedEntries;
};
/**
* Retrieves the bill payment ledger.
* @param {IBillPayment} billPayment
* @param {number} APAccountId
* @param {IBillPayment} billPayment
* @param {number} APAccountId
* @returns {Ledger}
*/
private getBillPaymentLedger = (
@@ -318,79 +266,12 @@ export class BillPaymentGLEntries {
gainLossAccountId: number,
baseCurrency: string
): Ledger => {
const entries = billPayment.isPrepardExpense
? this.getPrepardExpenseGLEntries(APAccountId, billPayment)
: this.getPaymentGLEntries(
billPayment,
APAccountId,
gainLossAccountId,
baseCurrency
);
const entries = this.getPaymentGLEntries(
billPayment,
APAccountId,
gainLossAccountId,
baseCurrency
);
return new Ledger(entries);
};
/**
* Retrieves the prepard expense GL entry.
* @param {IBillPayment} billPayment
* @returns {ILedgerEntry}
*/
private getPrepardExpenseEntry = (
billPayment: IBillPayment
): ILedgerEntry => {
const commonJournal = this.getPaymentCommonEntry(billPayment);
return {
...commonJournal,
debit: billPayment.localAmount,
accountId: billPayment.prepardExpensesAccountId,
accountNormal: AccountNormal.DEBIT,
indexGroup: 10,
index: 1,
};
};
/**
* Retrieves the GL entries of credit prepard expense for the give payment line.
* @param {IBillPayment} billPayment
* @param {IBillPaymentEntry} billPaymentEntry
* @returns {ILedgerEntry}
*/
private getCreditPrepardExpenseEntry = (
billPayment: IBillPayment,
billPaymentEntry: IBillPaymentEntry
) => {
const commonJournal = this.getPaymentCommonEntry(billPayment);
return {
...commonJournal,
credit: billPaymentEntry.paymentAmount,
accountId: billPayment.prepardExpensesAccountId,
accountNormal: AccountNormal.DEBIT,
index: 2,
indexGroup: 20,
};
};
/**
* Retrieves the A/P debit of the payment line.
* @param {number} APAccountId
* @param {IBillPayment} billPayment
* @param {IBillPaymentEntry} billPaymentEntry
* @returns {ILedgerEntry}
*/
private getAccountPayablePaymentLineEntry = (
APAccountId: number,
billPayment: IBillPayment,
billPaymentEntry: IBillPaymentEntry
): ILedgerEntry => {
const commonJournal = this.getPaymentCommonEntry(billPayment);
return {
...commonJournal,
debit: billPaymentEntry.paymentAmount,
accountId: APAccountId,
index: 1,
indexGroup: 20,
};
};
}

View File

@@ -17,10 +17,6 @@ export class PaymentWriteGLEntriesSubscriber {
*/
public attach(bus) {
bus.subscribe(events.billPayment.onCreated, this.handleWriteJournalEntries);
bus.subscribe(
events.billPayment.onPrepardExpensesApplied,
this.handleWritePrepardExpenseGLEntries
);
bus.subscribe(
events.billPayment.onEdited,
this.handleRewriteJournalEntriesOncePaymentEdited
@@ -32,8 +28,7 @@ export class PaymentWriteGLEntriesSubscriber {
}
/**
* Handles bill payment writing journal entries once created.
* @param {IBillPaymentEventCreatedPayload} payload -
* Handle bill payment writing journal entries once created.
*/
private handleWriteJournalEntries = async ({
tenantId,
@@ -49,22 +44,6 @@ export class PaymentWriteGLEntriesSubscriber {
);
};
/**
* Handles rewrite prepard expense GL entries once the bill payment applying to bills.
* @param {IBillPaymentEventCreatedPayload} payload -
*/
private handleWritePrepardExpenseGLEntries = async ({
tenantId,
billPaymentId,
trx,
}: IBillPaymentEventCreatedPayload) => {
await this.billPaymentGLEntries.rewritePaymentGLEntries(
tenantId,
billPaymentId,
trx
);
};
/**
* Handle bill payment re-writing journal entries once the payment transaction be edited.
*/

View File

@@ -4,16 +4,12 @@ import { omit, sumBy } from 'lodash';
import { IBillPayment, IBillPaymentDTO, IVendor } from '@/interfaces';
import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform';
import { formatDateFields } from '@/utils';
import HasTenancyService from '@/services/Tenancy/TenancyService';
@Service()
export class CommandBillPaymentDTOTransformer {
@Inject()
private branchDTOTransform: BranchTransactionDTOTransform;
@Inject()
private tenancy: HasTenancyService;
/**
* Transforms create/edit DTO to model.
* @param {number} tenantId
@@ -27,27 +23,14 @@ export class CommandBillPaymentDTOTransformer {
vendor: IVendor,
oldBillPayment?: IBillPayment
): Promise<IBillPayment> {
const { accountRepository } = this.tenancy.repositories(tenantId);
const appliedAmount = sumBy(billPaymentDTO.entries, 'paymentAmount');
const hasPrepardExpenses = appliedAmount < billPaymentDTO.amount;
const prepardExpensesAccount = hasPrepardExpenses
? await accountRepository.findOrCreatePrepardExpenses()
: null;
const prepardExpensesAccountId =
hasPrepardExpenses && prepardExpensesAccount
? billPaymentDTO.prepardExpensesAccountId ?? prepardExpensesAccount?.id
: billPaymentDTO.prepardExpensesAccountId;
const initialDTO = {
...formatDateFields(omit(billPaymentDTO, ['attachments']), [
'paymentDate',
]),
appliedAmount,
amount: sumBy(billPaymentDTO.entries, 'paymentAmount'),
currencyCode: vendor.currencyCode,
exchangeRate: billPaymentDTO.exchangeRate || 1,
entries: billPaymentDTO.entries,
prepardExpensesAccountId,
};
return R.compose(
this.branchDTOTransform.transformDTO<IBillPayment>(tenantId)

View File

@@ -1,117 +0,0 @@
import { Knex } from 'knex';
import { Inject, Service } from 'typedi';
import PromisePool, { ProcessHandler } from '@supercharge/promise-pool';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import {
IBillPayment,
IBillPrepardExpensesAppliedEventPayload,
IPaymentPrepardExpensesAppliedEventPayload,
} from '@/interfaces';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
@Service()
export class AutoApplyPrepardExpenses {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private eventPublisher: EventPublisher;
/**
* Auto apply prepard expenses to the given bill.
* @param {number} tenantId
* @param {number} billId
* @returns {Promise<void>}
*/
async autoApplyPrepardExpensesToBill(
tenantId: number,
billId: number,
trx?: Knex.Transaction
): Promise<void> {
const { BillPayment, Bill } = this.tenancy.models(tenantId);
const bill = await Bill.query(trx).findById(billId).throwIfNotFound();
const unappliedPayments = await BillPayment.query(trx)
.where('vendorId', bill.vendorId)
.whereRaw('amount - applied_amount > 0')
.whereNotNull('prepardExpensesAccountId');
let unappliedAmount = bill.total;
let appliedTotalAmount = 0; // Total applied amount after applying.
const precessHandler: ProcessHandler<IBillPayment, void> = async (
unappliedPayment: IBillPayment,
index: number,
pool
) => {
const appliedAmount = Math.min(unappliedAmount, unappliedPayment.amount);
unappliedAmount = unappliedAmount - appliedAmount;
appliedTotalAmount += appliedAmount;
// Stop applying once the unapplied amount reach zero or less.
if (appliedAmount <= 0) {
pool.stop();
return;
}
await this.applyBillToPaymentMade(
tenantId,
unappliedPayment.id,
bill.id,
appliedAmount,
trx
);
};
await PromisePool.withConcurrency(1)
.for(unappliedPayments)
.process(precessHandler);
// Increase the paid amount of the purchase invoice.
await Bill.changePaymentAmount(billId, appliedTotalAmount, trx);
// Triggers `onBillPrepardExpensesApplied` event.
await this.eventPublisher.emitAsync(events.bill.onPrepardExpensesApplied, {
tenantId,
billId,
trx,
} as IBillPrepardExpensesAppliedEventPayload);
}
/**
* Apply the given bill to payment made transaction.
* @param {number} tenantId
* @param {number} billPaymentId
* @param {number} billId
* @param {number} appliedAmount
* @param {Knex.Transaction} trx
*/
public applyBillToPaymentMade = async (
tenantId: number,
billPaymentId: number,
billId: number,
appliedAmount: number,
trx?: Knex.Transaction
) => {
const { BillPaymentEntry, BillPayment } = this.tenancy.models(tenantId);
await BillPaymentEntry.query(trx).insert({
billPaymentId,
billId,
paymentAmount: appliedAmount,
});
await BillPayment.query(trx).increment('appliedAmount', appliedAmount);
// Triggers `onBillPaymentPrepardExpensesApplied` event.
await this.eventPublisher.emitAsync(
events.billPayment.onPrepardExpensesApplied,
{
tenantId,
billPaymentId,
billId,
appliedAmount,
trx,
} as IPaymentPrepardExpensesAppliedEventPayload
);
};
}

View File

@@ -1,36 +0,0 @@
import { Inject, Service } from 'typedi';
import { AutoApplyPrepardExpenses } from '../AutoApplyPrepardExpenses';
import events from '@/subscribers/events';
import { IBillCreatedPayload } from '@/interfaces';
@Service()
export class AutoApplyPrepardExpensesOnBillCreated {
@Inject()
private autoApplyPrepardExpenses: AutoApplyPrepardExpenses;
/**
* Constructor method.
*/
public attach(bus) {
bus.subscribe(
events.bill.onCreated,
this.handleAutoApplyPrepardExpensesOnBillCreated.bind(this)
);
}
/**
* Handles the auto apply prepard expenses on bill created.
* @param {IBillCreatedPayload} payload -
*/
private async handleAutoApplyPrepardExpensesOnBillCreated({
tenantId,
billId,
trx,
}: IBillCreatedPayload) {
await this.autoApplyPrepardExpenses.autoApplyPrepardExpensesToBill(
tenantId,
billId,
trx
);
}
}

View File

@@ -1,126 +0,0 @@
import { Inject, Service } from 'typedi';
import { Knex } from 'knex';
import PromisePool, { ProcessHandler } from '@supercharge/promise-pool';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
import {
IPaymentReceive,
PaymentReceiveUnearnedRevenueAppliedEventPayload,
SaleInvoiceAppliedUnearnedRevenueOnCreatedEventPayload,
} from '@/interfaces';
@Service()
export class AutoApplyUnearnedRevenue {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private eventPublisher: EventPublisher;
/**
* Auto apply invoice to advanced payment received transactions.
* @param {number} tenantId
* @param {number} invoiceId
* @returns {Promise<void>}
*/
public async autoApplyUnearnedRevenueToInvoice(
tenantId: number,
saleInvoiceId: number,
trx?: Knex.Transaction
): Promise<void> {
const { PaymentReceive, SaleInvoice } = this.tenancy.models(tenantId);
const invoice = await SaleInvoice.query(trx)
.findById(saleInvoiceId)
.throwIfNotFound();
const unappliedPayments = await PaymentReceive.query(trx)
.where('customerId', invoice.customerId)
.whereRaw('amount - applied_amount > 0')
.whereNotNull('unearnedRevenueAccountId');
let unappliedAmount = invoice.total;
let appliedTotalAmount = 0; // Total applied amount after applying.
const processHandler: ProcessHandler<
IPaymentReceive,
Promise<void>
> = async (unappliedPayment: IPaymentReceive, index: number, pool) => {
const appliedAmount = Math.min(unappliedAmount, unappliedPayment.amount);
unappliedAmount = unappliedAmount - appliedAmount;
appliedTotalAmount += appliedAmount;
// Stop applying once the unapplied amount reache zero or less.
if (appliedAmount <= 0) {
pool.stop();
return;
}
await this.applyInvoiceToPaymentReceived(
tenantId,
unappliedPayment.id,
invoice.id,
appliedAmount,
trx
);
};
await PromisePool.withConcurrency(1)
.for(unappliedPayments)
.process(processHandler);
// Increase the paid amount of the sale invoice.
await SaleInvoice.changePaymentAmount(
saleInvoiceId,
appliedTotalAmount,
trx
);
// Triggers event `onSaleInvoiceUnearnedRevenue`.
await this.eventPublisher.emitAsync(
events.saleInvoice.onUnearnedRevenueApplied,
{
tenantId,
saleInvoiceId,
trx,
} as SaleInvoiceAppliedUnearnedRevenueOnCreatedEventPayload
);
}
/**
* Apply the given invoice to payment received transaction.
* @param {number} tenantId
* @param {number} paymentReceivedId
* @param {number} invoiceId
* @param {number} appliedAmount
* @param {Knex.Transaction} trx
* @returns {Promise<void>}
*/
public applyInvoiceToPaymentReceived = async (
tenantId: number,
paymentReceiveId: number,
invoiceId: number,
appliedAmount: number,
trx?: Knex.Transaction
): Promise<void> => {
const { PaymentReceiveEntry, PaymentReceive } =
this.tenancy.models(tenantId);
await PaymentReceiveEntry.query(trx).insert({
paymentReceiveId,
invoiceId,
paymentAmount: appliedAmount,
});
await PaymentReceive.query(trx).increment('appliedAmount', appliedAmount);
// Triggers the event `onPaymentReceivedUnearnedRevenue`.
await this.eventPublisher.emitAsync(
events.paymentReceive.onUnearnedRevenueApplied,
{
tenantId,
paymentReceiveId,
saleInvoiceId: invoiceId,
appliedAmount,
trx,
} as PaymentReceiveUnearnedRevenueAppliedEventPayload
);
};
}

View File

@@ -11,7 +11,6 @@ import { PaymentReceiveValidators } from './PaymentReceiveValidators';
import { PaymentReceiveIncrement } from './PaymentReceiveIncrement';
import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform';
import { formatDateFields } from '@/utils';
import HasTenancyService from '@/services/Tenancy/TenancyService';
@Service()
export class PaymentReceiveDTOTransformer {
@@ -24,9 +23,6 @@ export class PaymentReceiveDTOTransformer {
@Inject()
private branchDTOTransform: BranchTransactionDTOTransform;
@Inject()
private tenancy: HasTenancyService;
/**
* Transformes the create payment receive DTO to model object.
* @param {number} tenantId
@@ -40,8 +36,7 @@ export class PaymentReceiveDTOTransformer {
paymentReceiveDTO: IPaymentReceiveCreateDTO | IPaymentReceiveEditDTO,
oldPaymentReceive?: IPaymentReceive
): Promise<IPaymentReceive> {
const { accountRepository } = this.tenancy.repositories(tenantId);
const appliedAmount = sumBy(paymentReceiveDTO.entries, 'paymentAmount');
const paymentAmount = sumBy(paymentReceiveDTO.entries, 'paymentAmount');
// Retreive the next invoice number.
const autoNextNumber =
@@ -55,29 +50,17 @@ export class PaymentReceiveDTOTransformer {
this.validators.validatePaymentNoRequire(paymentReceiveNo);
const hasUnearnedPayment = appliedAmount < paymentReceiveDTO.amount;
const unearnedRevenueAccount = hasUnearnedPayment
? await accountRepository.findOrCreateUnearnedRevenue()
: null;
const unearnedRevenueAccountId =
hasUnearnedPayment && unearnedRevenueAccount
? paymentReceiveDTO.unearnedRevenueAccountId ??
unearnedRevenueAccount?.id
: paymentReceiveDTO.unearnedRevenueAccountId;
const initialDTO = {
...formatDateFields(omit(paymentReceiveDTO, ['entries', 'attachments']), [
'paymentDate',
]),
appliedAmount,
amount: paymentAmount,
currencyCode: customer.currencyCode,
...(paymentReceiveNo ? { paymentReceiveNo } : {}),
exchangeRate: paymentReceiveDTO.exchangeRate || 1,
entries: paymentReceiveDTO.entries.map((entry) => ({
...entry,
})),
unearnedRevenueAccountId,
};
return R.compose(
this.branchDTOTransform.transformDTO<IPaymentReceive>(tenantId)

View File

@@ -1,14 +1,19 @@
import { Service, Inject } from 'typedi';
import { sumBy } from 'lodash';
import { Knex } from 'knex';
import Ledger from '@/services/Accounting/Ledger';
import TenancyService from '@/services/Tenancy/TenancyService';
import { IPaymentReceive, ILedgerEntry, AccountNormal } from '@/interfaces';
import {
IPaymentReceive,
ILedgerEntry,
AccountNormal,
IPaymentReceiveGLCommonEntry,
} from '@/interfaces';
import LedgerStorageService from '@/services/Accounting/LedgerStorageService';
import { TenantMetadata } from '@/system/models';
import { PaymentReceivedGLCommon } from './PaymentReceivedGLCommon';
@Service()
export class PaymentReceiveGLEntries extends PaymentReceivedGLCommon {
export class PaymentReceiveGLEntries {
@Inject()
private tenancy: TenancyService;
@@ -17,9 +22,9 @@ export class PaymentReceiveGLEntries extends PaymentReceivedGLCommon {
/**
* Writes payment GL entries to the storage.
* @param {number} tenantId - Tenant id.
* @param {number} paymentReceiveId - Payment received id.
* @param {Knex.Transaction} trx - Knex transaction.
* @param {number} tenantId
* @param {number} paymentReceiveId
* @param {Knex.Transaction} trx
* @returns {Promise<void>}
*/
public writePaymentGLEntries = async (
@@ -29,19 +34,14 @@ export class PaymentReceiveGLEntries extends PaymentReceivedGLCommon {
): Promise<void> => {
const { PaymentReceive } = this.tenancy.models(tenantId);
// Retrieves the given tenant metadata.
const tenantMeta = await TenantMetadata.query().findOne({ tenantId });
// Retrieves the payment receive with associated entries.
const paymentReceive = await PaymentReceive.query(trx)
.findById(paymentReceiveId)
.withGraphFetched('entries.invoice');
// Cannot continue if the received payment is unearned revenue type,
// that type of transactions have different type of GL entries.
if (paymentReceive.unearnedRevenueAccountId) {
return;
}
// Retrieves the given tenant metadata.
const tenantMeta = await TenantMetadata.query().findOne({ tenantId });
// Retrives the payment receive ledger.
const ledger = await this.getPaymentReceiveGLedger(
tenantId,
@@ -53,6 +53,25 @@ export class PaymentReceiveGLEntries extends PaymentReceivedGLCommon {
await this.ledgerStorage.commit(tenantId, ledger, trx);
};
/**
* Reverts the given payment receive GL entries.
* @param {number} tenantId
* @param {number} paymentReceiveId
* @param {Knex.Transaction} trx
*/
public revertPaymentGLEntries = async (
tenantId: number,
paymentReceiveId: number,
trx?: Knex.Transaction
) => {
await this.ledgerStorage.deleteByReference(
tenantId,
paymentReceiveId,
'PaymentReceive',
trx
);
};
/**
* Rewrites the given payment receive GL entries.
* @param {number} tenantId
@@ -73,10 +92,10 @@ export class PaymentReceiveGLEntries extends PaymentReceivedGLCommon {
/**
* Retrieves the payment receive general ledger.
* @param {number} tenantId -
* @param {IPaymentReceive} paymentReceive -
* @param {string} baseCurrencyCode -
* @param {Knex.Transaction} trx -
* @param {number} tenantId -
* @param {IPaymentReceive} paymentReceive -
* @param {string} baseCurrencyCode -
* @param {Knex.Transaction} trx -
* @returns {Ledger}
*/
public getPaymentReceiveGLedger = async (
@@ -107,9 +126,100 @@ export class PaymentReceiveGLEntries extends PaymentReceivedGLCommon {
return new Ledger(ledgerEntries);
};
/**
* Calculates the payment total exchange gain/loss.
* @param {IBillPayment} paymentReceive - Payment receive with entries.
* @returns {number}
*/
private getPaymentExGainOrLoss = (
paymentReceive: IPaymentReceive
): number => {
return sumBy(paymentReceive.entries, (entry) => {
const paymentLocalAmount =
entry.paymentAmount * paymentReceive.exchangeRate;
const invoicePayment = entry.paymentAmount * entry.invoice.exchangeRate;
return paymentLocalAmount - invoicePayment;
});
};
/**
* Retrieves the common entry of payment receive.
* @param {IPaymentReceive} paymentReceive
* @returns {}
*/
private getPaymentReceiveCommonEntry = (
paymentReceive: IPaymentReceive
): IPaymentReceiveGLCommonEntry => {
return {
debit: 0,
credit: 0,
currencyCode: paymentReceive.currencyCode,
exchangeRate: paymentReceive.exchangeRate,
transactionId: paymentReceive.id,
transactionType: 'PaymentReceive',
transactionNumber: paymentReceive.paymentReceiveNo,
referenceNumber: paymentReceive.referenceNo,
date: paymentReceive.paymentDate,
userId: paymentReceive.userId,
createdAt: paymentReceive.createdAt,
branchId: paymentReceive.branchId,
};
};
/**
* Retrieves the payment exchange gain/loss entry.
* @param {IPaymentReceive} paymentReceive -
* @param {number} ARAccountId -
* @param {number} exchangeGainOrLossAccountId -
* @param {string} baseCurrencyCode -
* @returns {ILedgerEntry[]}
*/
private getPaymentExchangeGainLossEntry = (
paymentReceive: IPaymentReceive,
ARAccountId: number,
exchangeGainOrLossAccountId: number,
baseCurrencyCode: string
): ILedgerEntry[] => {
const commonJournal = this.getPaymentReceiveCommonEntry(paymentReceive);
const gainOrLoss = this.getPaymentExGainOrLoss(paymentReceive);
const absGainOrLoss = Math.abs(gainOrLoss);
return gainOrLoss
? [
{
...commonJournal,
currencyCode: baseCurrencyCode,
exchangeRate: 1,
debit: gainOrLoss > 0 ? absGainOrLoss : 0,
credit: gainOrLoss < 0 ? absGainOrLoss : 0,
accountId: ARAccountId,
contactId: paymentReceive.customerId,
index: 3,
accountNormal: AccountNormal.CREDIT,
},
{
...commonJournal,
currencyCode: baseCurrencyCode,
exchangeRate: 1,
credit: gainOrLoss > 0 ? absGainOrLoss : 0,
debit: gainOrLoss < 0 ? absGainOrLoss : 0,
accountId: exchangeGainOrLossAccountId,
index: 3,
accountNormal: AccountNormal.DEBIT,
},
]
: [];
};
/**
* Retrieves the payment deposit GL entry.
* @param {IPaymentReceive} paymentReceive
* @param {IPaymentReceive} paymentReceive
* @returns {ILedgerEntry}
*/
private getPaymentDepositGLEntry = (
@@ -128,8 +238,8 @@ export class PaymentReceiveGLEntries extends PaymentReceivedGLCommon {
/**
* Retrieves the payment receivable entry.
* @param {IPaymentReceive} paymentReceive
* @param {number} ARAccountId
* @param {IPaymentReceive} paymentReceive
* @param {number} ARAccountId
* @returns {ILedgerEntry}
*/
private getPaymentReceivableEntry = (
@@ -152,15 +262,15 @@ export class PaymentReceiveGLEntries extends PaymentReceivedGLCommon {
* Records payment receive journal transactions.
*
* Invoice payment journals.
* ------------
* - Account Receivable -> Debit
* - Payment Account [current asset] -> Credit
* --------
* - Account receivable -> Debit
* - Payment account [current asset] -> Credit
*
* @param {number} tenantId
* @param {IPaymentReceive} paymentRecieve - Payment receive model.
* @param {number} ARAccountId - A/R account id.
* @param {number} exGainOrLossAccountId - Exchange gain/loss account id.
* @param {string} baseCurrency - Base currency code.
* @param {number} tenantId
* @param {IPaymentReceive} paymentRecieve - Payment receive model.
* @param {number} ARAccountId - A/R account id.
* @param {number} exGainOrLossAccountId - Exchange gain/loss account id.
* @param {string} baseCurrency - Base currency code.
* @returns {Promise<ILedgerEntry>}
*/
public getPaymentReceiveGLEntries = (

View File

@@ -107,6 +107,7 @@ export class PaymentReceiveValidators {
const invoicesIds = paymentReceiveEntries.map(
(e: IPaymentReceiveEntryDTO) => e.invoiceId
);
const storedInvoices = await SaleInvoice.query().whereIn('id', invoicesIds);
const storedInvoicesMap = new Map(

View File

@@ -1,123 +0,0 @@
import { Knex } from 'knex';
import { sumBy } from 'lodash';
import {
AccountNormal,
ILedgerEntry,
IPaymentReceive,
IPaymentReceiveGLCommonEntry,
} from '@/interfaces';
import LedgerStorageService from '@/services/Accounting/LedgerStorageService';
export class PaymentReceivedGLCommon {
private ledgerStorage: LedgerStorageService;
/**
* Retrieves the common entry of payment receive.
* @param {IPaymentReceive} paymentReceive
* @returns {IPaymentReceiveGLCommonEntry}
*/
protected getPaymentReceiveCommonEntry = (
paymentReceive: IPaymentReceive
): IPaymentReceiveGLCommonEntry => {
return {
debit: 0,
credit: 0,
currencyCode: paymentReceive.currencyCode,
exchangeRate: paymentReceive.exchangeRate,
transactionId: paymentReceive.id,
transactionType: 'PaymentReceive',
transactionNumber: paymentReceive.paymentReceiveNo,
referenceNumber: paymentReceive.referenceNo,
date: paymentReceive.paymentDate,
userId: paymentReceive.userId,
createdAt: paymentReceive.createdAt,
branchId: paymentReceive.branchId,
};
};
/**
* Retrieves the payment exchange gain/loss entry.
* @param {IPaymentReceive} paymentReceive -
* @param {number} ARAccountId -
* @param {number} exchangeGainOrLossAccountId -
* @param {string} baseCurrencyCode -
* @returns {ILedgerEntry[]}
*/
protected getPaymentExchangeGainLossEntry = (
paymentReceive: IPaymentReceive,
ARAccountId: number,
exchangeGainOrLossAccountId: number,
baseCurrencyCode: string
): ILedgerEntry[] => {
const commonJournal = this.getPaymentReceiveCommonEntry(paymentReceive);
const gainOrLoss = this.getPaymentExGainOrLoss(paymentReceive);
const absGainOrLoss = Math.abs(gainOrLoss);
return gainOrLoss
? [
{
...commonJournal,
currencyCode: baseCurrencyCode,
exchangeRate: 1,
debit: gainOrLoss > 0 ? absGainOrLoss : 0,
credit: gainOrLoss < 0 ? absGainOrLoss : 0,
accountId: ARAccountId,
contactId: paymentReceive.customerId,
index: 3,
accountNormal: AccountNormal.CREDIT,
},
{
...commonJournal,
currencyCode: baseCurrencyCode,
exchangeRate: 1,
credit: gainOrLoss > 0 ? absGainOrLoss : 0,
debit: gainOrLoss < 0 ? absGainOrLoss : 0,
accountId: exchangeGainOrLossAccountId,
index: 3,
accountNormal: AccountNormal.DEBIT,
},
]
: [];
};
/**
* Calculates the payment total exchange gain/loss.
* @param {IBillPayment} paymentReceive - Payment receive with entries.
* @returns {number}
*/
private getPaymentExGainOrLoss = (
paymentReceive: IPaymentReceive
): number => {
return sumBy(paymentReceive.entries, (entry) => {
const paymentLocalAmount =
entry.paymentAmount * paymentReceive.exchangeRate;
const invoicePayment = entry.paymentAmount * entry.invoice.exchangeRate;
return paymentLocalAmount - invoicePayment;
});
};
/**
* Reverts the given payment receive GL entries.
* @param {number} tenantId
* @param {number} paymentReceiveId
* @param {Knex.Transaction} trx
*/
public revertPaymentGLEntries = async (
tenantId: number,
paymentReceiveId: number,
trx?: Knex.Transaction
) => {
await this.ledgerStorage.deleteByReference(
tenantId,
paymentReceiveId,
'PaymentReceive',
trx
);
};
}

View File

@@ -1,252 +0,0 @@
import * as R from 'ramda';
import { Knex } from 'knex';
import { Inject, Service } from 'typedi';
import { flatten } from 'lodash';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { TenantMetadata } from '@/system/models';
import { PaymentReceivedGLCommon } from './PaymentReceivedGLCommon';
import {
AccountNormal,
ILedgerEntry,
IPaymentReceive,
IPaymentReceiveEntry,
} from '@/interfaces';
import LedgerStorageService from '@/services/Accounting/LedgerStorageService';
import Ledger from '@/services/Accounting/Ledger';
@Service()
export class PaymentReceivedUnearnedGLEntries extends PaymentReceivedGLCommon {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private ledgerStorage: LedgerStorageService;
/**
* Writes payment GL entries to the storage.
* @param {number} tenantId
* @param {number} paymentReceiveId
* @param {Knex.Transaction} trx
* @returns {Promise<void>}
*/
public writePaymentGLEntries = async (
tenantId: number,
paymentReceiveId: number,
trx?: Knex.Transaction
): Promise<void> => {
const { PaymentReceive } = this.tenancy.models(tenantId);
// Retrieves the payment receive with associated entries.
const paymentReceive = await PaymentReceive.query(trx)
.findById(paymentReceiveId)
.withGraphFetched('entries.invoice');
// Stop early if
if (!paymentReceive.unearnedRevenueAccountId) {
return;
}
// Retrieves the given tenant metadata.
const tenantMeta = await TenantMetadata.query().findOne({ tenantId });
const ledger = await this.getPaymentReceiveGLedger(
tenantId,
paymentReceive
);
// Commit the ledger entries to the storage.
await this.ledgerStorage.commit(tenantId, ledger, trx);
};
/**
* Rewrites the given payment receive GL entries.
* @param {number} tenantId
* @param {number} paymentReceiveId
* @param {Knex.Transaction} trx
*/
public rewritePaymentGLEntries = async (
tenantId: number,
paymentReceiveId: number,
trx?: Knex.Transaction
) => {
// Reverts the payment GL entries.
await this.revertPaymentGLEntries(tenantId, paymentReceiveId, trx);
// Writes the payment GL entries.
await this.writePaymentGLEntries(tenantId, paymentReceiveId, trx);
};
/**
* Retrieves the payment received GL entries.
* @param {number} tenantId
* @param {IPaymentReceive} paymentReceive
* @returns {Promise<Ledger>}
*/
private getPaymentReceiveGLedger = async (
tenantId: number,
paymentReceive: IPaymentReceive
) => {
const { accountRepository } = this.tenancy.repositories(tenantId);
// Retrieve the A/R account of the given currency.
const receivableAccount =
await accountRepository.findOrCreateAccountReceivable(
paymentReceive.currencyCode
);
// Retrieve the payment GL entries.
const entries = this.getPaymentGLEntries(
receivableAccount.id,
paymentReceive
);
const unearnedRevenueEntries =
this.getUnearnedRevenueEntries(paymentReceive);
const combinedEntries = [...unearnedRevenueEntries, ...entries];
return new Ledger(combinedEntries);
};
/**
* Retrieve the payment received GL entries.
* @param {number} ARAccountId - A/R account id.
* @param {IPaymentReceive} paymentReceive -
* @returns {Array<ILedgerEntry>}
*/
private getPaymentGLEntries = R.curry(
(
ARAccountId: number,
paymentReceive: IPaymentReceive
): Array<ILedgerEntry> => {
const getPaymentEntryGLEntries = this.getPaymentEntryGLEntries(
ARAccountId,
paymentReceive
);
const entriesGroup = paymentReceive.entries.map((paymentEntry) => {
return getPaymentEntryGLEntries(paymentEntry);
});
return flatten(entriesGroup);
}
);
/**
* Retrieve the payment entry GL entries.
* @param {IPaymentReceiveEntry} paymentReceivedEntry -
* @param {IPaymentReceive} paymentReceive -
* @returns {Array<ILedgerEntry>}
*/
private getPaymentEntryGLEntries = R.curry(
(
ARAccountId: number,
paymentReceive: IPaymentReceive,
paymentReceivedEntry: IPaymentReceiveEntry
): Array<ILedgerEntry> => {
const unearnedRevenueEntry = this.getDebitUnearnedRevenueGLEntry(
paymentReceivedEntry.paymentAmount,
paymentReceive
);
const AREntry = this.getPaymentReceivableEntry(
paymentReceivedEntry.paymentAmount,
paymentReceive,
ARAccountId
);
return [unearnedRevenueEntry, AREntry];
}
);
/**
* Retrieves the payment deposit GL entry.
* @param {IPaymentReceive} paymentReceive
* @returns {ILedgerEntry}
*/
private getDebitUnearnedRevenueGLEntry = (
amount: number,
paymentReceive: IPaymentReceive
): ILedgerEntry => {
const commonJournal = this.getPaymentReceiveCommonEntry(paymentReceive);
return {
...commonJournal,
debit: amount,
accountId: paymentReceive.unearnedRevenueAccountId,
accountNormal: AccountNormal.CREDIT,
index: 2,
indexGroup: 20,
};
};
/**
* Retrieves the payment receivable entry.
* @param {IPaymentReceive} paymentReceive
* @param {number} ARAccountId
* @returns {ILedgerEntry}
*/
private getPaymentReceivableEntry = (
amount: number,
paymentReceive: IPaymentReceive,
ARAccountId: number
): ILedgerEntry => {
const commonJournal = this.getPaymentReceiveCommonEntry(paymentReceive);
return {
...commonJournal,
credit: amount,
contactId: paymentReceive.customerId,
accountId: ARAccountId,
accountNormal: AccountNormal.DEBIT,
index: 1,
indexGroup: 20,
};
};
/**
* Retrieves the unearned revenue entries.
* @param {IPaymentReceive} paymentReceived -
* @returns {Array<ILedgerEntry>}
*/
private getUnearnedRevenueEntries = (
paymentReceive: IPaymentReceive
): Array<ILedgerEntry> => {
const depositEntry = this.getDepositPaymentGLEntry(paymentReceive);
const unearnedEntry = this.getUnearnedRevenueEntry(paymentReceive);
return [depositEntry, unearnedEntry];
};
/**
* Retrieve the payment deposit entry.
* @param {IPaymentReceive} paymentReceived -
* @returns {ILedgerEntry}
*/
private getDepositPaymentGLEntry = (
paymentReceive: IPaymentReceive
): ILedgerEntry => {
const commonJournal = this.getPaymentReceiveCommonEntry(paymentReceive);
return {
...commonJournal,
debit: paymentReceive.amount,
accountId: paymentReceive.depositAccountId,
accountNormal: AccountNormal.DEBIT,
indexGroup: 10,
index: 1,
};
};
/**
* Retrieve the unearned revenue entry.
* @param {IPaymentReceive} paymentReceived -
* @returns {ILedgerEntry}
*/
private getUnearnedRevenueEntry = (
paymentReceived: IPaymentReceive
): ILedgerEntry => {
const commonJournal = this.getPaymentReceiveCommonEntry(paymentReceived);
return {
...commonJournal,
credit: paymentReceived.amount,
accountId: paymentReceived.unearnedRevenueAccountId,
accountNormal: AccountNormal.CREDIT,
indexGroup: 10,
index: 1,
};
};
}

View File

@@ -1,34 +0,0 @@
import { Inject, Service } from 'typedi';
import events from '@/subscribers/events';
import { AutoApplyUnearnedRevenue } from '../AutoApplyUnearnedRevenue';
@Service()
export class AutoApplyUnearnedRevenueOnInvoiceCreated {
@Inject()
private autoApplyUnearnedRevenue: AutoApplyUnearnedRevenue;
/**
* Constructor method.
*/
public attach(bus) {
bus.subscribe(
events.saleInvoice.onCreated,
this.handleAutoApplyUnearnedRevenueOnInvoiceCreated.bind(this)
);
}
/**
* Handles the auto apply unearned revenue on invoice creating.
* @param
*/
private async handleAutoApplyUnearnedRevenueOnInvoiceCreated({
tenantId,
saleInvoice,
trx,
}) {
await this.autoApplyUnearnedRevenue.autoApplyUnearnedRevenueToInvoice(
tenantId,
saleInvoice.id,
trx
);
}
}

View File

@@ -77,12 +77,7 @@ export default class HasTenancyService {
const knex = this.knex(tenantId);
const i18n = this.i18n(tenantId);
const repositories = tenantRepositoriesLoader(knex, cache, i18n);
Object.values(repositories).forEach((repository) => {
repository.setTenantId(tenantId);
});
return repositories;
return tenantRepositoriesLoader(knex, cache, i18n);
});
}

View File

@@ -3,20 +3,15 @@ import {
IPaymentReceiveCreatedPayload,
IPaymentReceiveDeletedPayload,
IPaymentReceiveEditedPayload,
PaymentReceiveUnearnedRevenueAppliedEventPayload,
} from '@/interfaces';
import events from '@/subscribers/events';
import { PaymentReceiveGLEntries } from '@/services/Sales/PaymentReceives/PaymentReceiveGLEntries';
import { PaymentReceivedUnearnedGLEntries } from '@/services/Sales/PaymentReceives/PaymentReceivedUnearnedGLEntries';
@Service()
export default class PaymentReceivesWriteGLEntriesSubscriber {
@Inject()
private paymentReceiveGLEntries: PaymentReceiveGLEntries;
@Inject()
private paymentReceivedUnearnedGLEntries: PaymentReceivedUnearnedGLEntries;
/**
* Attaches events with handlers.
*/
@@ -37,7 +32,6 @@ export default class PaymentReceivesWriteGLEntriesSubscriber {
/**
* Handle journal entries writing once the payment receive created.
* @param {IPaymentReceiveCreatedPayload} payload -
*/
private handleWriteJournalEntriesOnceCreated = async ({
tenantId,
@@ -49,21 +43,14 @@ export default class PaymentReceivesWriteGLEntriesSubscriber {
paymentReceiveId,
trx
);
await this.paymentReceivedUnearnedGLEntries.writePaymentGLEntries(
tenantId,
paymentReceiveId,
trx
);
};
/**
* Handle journal entries writing once the payment receive edited.
* @param {IPaymentReceiveEditedPayload} payload -
*/
private handleOverwriteJournalEntriesOnceEdited = async ({
tenantId,
paymentReceive,
paymentReceiveId,
trx,
}: IPaymentReceiveEditedPayload) => {
await this.paymentReceiveGLEntries.rewritePaymentGLEntries(
@@ -71,16 +58,10 @@ export default class PaymentReceivesWriteGLEntriesSubscriber {
paymentReceive.id,
trx
);
await this.paymentReceivedUnearnedGLEntries.rewritePaymentGLEntries(
tenantId,
paymentReceiveId,
trx
);
};
/**
* Handles revert journal entries once deleted.
* @param {IPaymentReceiveDeletedPayload} payload -
*/
private handleRevertJournalEntriesOnceDeleted = async ({
tenantId,

View File

@@ -40,6 +40,13 @@ export default {
baseCurrencyUpdated: 'onOrganizationBaseCurrencyUpdated',
},
/**
* User subscription events.
*/
subscription: {
onSubscribed: 'onOrganizationSubscribed',
},
/**
* Tenants managment service.
*/
@@ -142,8 +149,6 @@ export default {
onMailReminderSend: 'onSaleInvoiceMailReminderSend',
onMailReminderSent: 'onSaleInvoiceMailReminderSent',
onUnearnedRevenueApplied: 'onSaleInvoiceUnearnedRevenue',
},
/**
@@ -232,8 +237,6 @@ export default {
onPreMailSend: 'onPaymentReceivePreMailSend',
onMailSend: 'onPaymentReceiveMailSend',
onMailSent: 'onPaymentReceiveMailSent',
onUnearnedRevenueApplied: 'onPaymentReceivedUnearnedRevenue',
},
/**
@@ -254,8 +257,6 @@ export default {
onOpening: 'onBillOpening',
onOpened: 'onBillOpened',
onPrepardExpensesApplied: 'onBillPrepardExpensesApplied'
},
/**
@@ -273,8 +274,6 @@ export default {
onPublishing: 'onBillPaymentPublishing',
onPublished: 'onBillPaymentPublished',
onPrepardExpensesApplied: 'onBillPaymentPrepardExpensesApplied'
},
/**

View File

@@ -4,9 +4,9 @@ export const ACCOUNT_TYPE = {
BANK: 'bank',
ACCOUNTS_RECEIVABLE: 'accounts-receivable',
INVENTORY: 'inventory',
OTHER_CURRENT_ASSET: 'other-current-asset',
OTHER_CURRENT_ASSET: 'other-ACCOUNT_PARENT_TYPE.CURRENT_ASSET',
FIXED_ASSET: 'fixed-asset',
NON_CURRENT_ASSET: 'non-current-asset',
NON_CURRENT_ASSET: 'non-ACCOUNT_PARENT_TYPE.CURRENT_ASSET',
ACCOUNTS_PAYABLE: 'accounts-payable',
CREDIT_CARD: 'credit-card',

View File

@@ -160,13 +160,6 @@ export function useCustomersTableColumns() {
width: 85,
clickable: true,
},
{
id: 'credit_balance',
Header: 'Credit Balance',
accessor: 'formatted_unused_credit',
width: 100,
align: 'right',
},
{
id: 'balance',
Header: intl.get('receivable_balance'),

View File

@@ -1,9 +0,0 @@
import { ExcessPaymentDialog } from './dialogs/PaymentMadeExcessDialog';
export function PaymentMadeDialogs() {
return (
<>
<ExcessPaymentDialog dialogName={'payment-made-excessed-payment'} />
</>
);
}

View File

@@ -2,7 +2,7 @@
import React, { useMemo } from 'react';
import intl from 'react-intl-universal';
import classNames from 'classnames';
import { Formik, Form, FormikHelpers } from 'formik';
import { Formik, Form } from 'formik';
import { Intent } from '@blueprintjs/core';
import { sumBy, defaultTo } from 'lodash';
import { useHistory } from 'react-router-dom';
@@ -14,7 +14,6 @@ import PaymentMadeFloatingActions from './PaymentMadeFloatingActions';
import PaymentMadeFooter from './PaymentMadeFooter';
import PaymentMadeFormBody from './PaymentMadeFormBody';
import PaymentMadeFormTopBar from './PaymentMadeFormTopBar';
import { PaymentMadeDialogs } from './PaymentMadeDialogs';
import { PaymentMadeInnerProvider } from './PaymentMadeInnerProvider';
import { usePaymentMadeFormContext } from './PaymentMadeFormProvider';
@@ -22,7 +21,6 @@ import { compose, orderingLinesIndexes } from '@/utils';
import withSettings from '@/containers/Settings/withSettings';
import withCurrentOrganization from '@/containers/Organization/withCurrentOrganization';
import withDialogActions from '@/containers/Dialog/withDialogActions';
import {
EditPaymentMadeFormSchema,
@@ -33,7 +31,6 @@ import {
transformToEditForm,
transformErrors,
transformFormToRequest,
getPaymentExcessAmountFromValues,
} from './utils';
/**
@@ -45,9 +42,6 @@ function PaymentMadeForm({
// #withCurrentOrganization
organization: { base_currency },
// #withDialogActions
openDialog,
}) {
const history = useHistory();
@@ -60,7 +54,6 @@ function PaymentMadeForm({
submitPayload,
createPaymentMadeMutate,
editPaymentMadeMutate,
isExcessConfirmed,
} = usePaymentMadeFormContext();
// Form initial values.
@@ -83,11 +76,13 @@ function PaymentMadeForm({
// Handle the form submit.
const handleSubmitForm = (
values,
{ setSubmitting, resetForm, setFieldError }: FormikHelpers<any>,
{ setSubmitting, resetForm, setFieldError },
) => {
setSubmitting(true);
// Total payment amount of entries.
const totalPaymentAmount = sumBy(values.entries, 'payment_amount');
if (values.amount <= 0) {
if (totalPaymentAmount <= 0) {
AppToaster.show({
message: intl.get('you_cannot_make_payment_with_zero_total_amount'),
intent: Intent.DANGER,
@@ -95,16 +90,6 @@ function PaymentMadeForm({
setSubmitting(false);
return;
}
const excessAmount = getPaymentExcessAmountFromValues(values);
// Show the confirmation popup if the excess amount bigger than zero and
// has not been confirmed yet.
if (excessAmount > 0 && !isExcessConfirmed) {
openDialog('payment-made-excessed-payment');
setSubmitting(false);
return;
}
// Transformes the form values to request body.
const form = transformFormToRequest(values);
@@ -134,12 +119,11 @@ function PaymentMadeForm({
}
setSubmitting(false);
};
if (!isNewMode) {
return editPaymentMadeMutate([paymentMadeId, form])
.then(onSaved)
.catch(onError);
editPaymentMadeMutate([paymentMadeId, form]).then(onSaved).catch(onError);
} else {
return createPaymentMadeMutate(form).then(onSaved).catch(onError);
createPaymentMadeMutate(form).then(onSaved).catch(onError);
}
};
@@ -165,7 +149,6 @@ function PaymentMadeForm({
<PaymentMadeFormBody />
<PaymentMadeFooter />
<PaymentMadeFloatingActions />
<PaymentMadeDialogs />
</PaymentMadeInnerProvider>
</Form>
</Formik>
@@ -180,5 +163,4 @@ export default compose(
preferredPaymentAccount: parseInt(billPaymentSettings?.withdrawalAccount),
})),
withCurrentOrganization(),
withDialogActions,
)(PaymentMadeForm);

View File

@@ -1,23 +1,17 @@
// @ts-nocheck
import React from 'react';
import styled from 'styled-components';
import { useFormikContext } from 'formik';
import {
T,
TotalLines,
TotalLine,
TotalLineBorderStyle,
TotalLineTextStyle,
FormatNumber,
} from '@/components';
import { usePaymentMadeExcessAmount, usePaymentMadeTotals } from './utils';
import { usePaymentMadeTotals } from './utils';
export function PaymentMadeFormFooterRight() {
const { formattedSubtotal, formattedTotal } = usePaymentMadeTotals();
const excessAmount = usePaymentMadeExcessAmount();
const {
values: { currency_code: currencyCode },
} = useFormikContext();
return (
<PaymentMadeTotalLines labelColWidth={'180px'} amountColWidth={'180px'}>
@@ -31,11 +25,6 @@ export function PaymentMadeFormFooterRight() {
value={formattedTotal}
textStyle={TotalLineTextStyle.Bold}
/>
<TotalLine
title={'Excess Amount'}
value={<FormatNumber value={excessAmount} currency={currencyCode} />}
textStyle={TotalLineTextStyle.Regular}
/>
</PaymentMadeTotalLines>
);
}

View File

@@ -1,12 +1,12 @@
// @ts-nocheck
import React from 'react';
import React, { useMemo } from 'react';
import classNames from 'classnames';
import { useFormikContext } from 'formik';
import { sumBy } from 'lodash';
import { CLASSES } from '@/constants/classes';
import { Money, FormattedMessage as T } from '@/components';
import PaymentMadeFormHeaderFields from './PaymentMadeFormHeaderFields';
import { usePaymentmadeTotalAmount } from './utils';
/**
* Payment made header form.
@@ -14,10 +14,11 @@ import { usePaymentmadeTotalAmount } from './utils';
function PaymentMadeFormHeader() {
// Formik form context.
const {
values: { currency_code },
values: { entries, currency_code },
} = useFormikContext();
const totalAmount = usePaymentmadeTotalAmount();
// Calculate the payment amount of the entries.
const amountPaid = useMemo(() => sumBy(entries, 'payment_amount'), [entries]);
return (
<div className={classNames(CLASSES.PAGE_FORM_HEADER)}>
@@ -29,9 +30,8 @@ function PaymentMadeFormHeader() {
<span class="big-amount__label">
<T id={'amount_received'} />
</span>
<h1 class="big-amount__number">
<Money amount={totalAmount} currency={currency_code} />
<Money amount={amountPaid} currency={currency_code} />
</h1>
</div>
</div>

View File

@@ -2,7 +2,6 @@
import React, { useMemo } from 'react';
import styled from 'styled-components';
import classNames from 'classnames';
import { isEmpty, toSafeInteger } from 'lodash';
import {
FormGroup,
InputGroup,
@@ -14,6 +13,7 @@ import {
import { DateInput } from '@blueprintjs/datetime';
import { FastField, Field, useFormikContext, ErrorMessage } from 'formik';
import { FormattedMessage as T, VendorsSelect } from '@/components';
import { toSafeInteger } from 'lodash';
import { CLASSES } from '@/constants/classes';
import {
@@ -68,7 +68,7 @@ function PaymentMadeFormHeaderFields({ organization: { base_currency } }) {
const fullAmount = safeSumBy(newEntries, 'payment_amount');
setFieldValue('entries', newEntries);
setFieldValue('amount', fullAmount);
setFieldValue('full_amount', fullAmount);
};
// Handles the full-amount field blur.
@@ -115,10 +115,10 @@ function PaymentMadeFormHeaderFields({ organization: { base_currency } }) {
</FastField>
{/* ------------ Full amount ------------ */}
<Field name={'amount'}>
<Field name={'full_amount'}>
{({
form: {
values: { currency_code, entries },
values: { currency_code },
},
field: { value },
meta: { error, touched },
@@ -129,30 +129,28 @@ function PaymentMadeFormHeaderFields({ organization: { base_currency } }) {
className={('form-group--full-amount', Classes.FILL)}
intent={inputIntent({ error, touched })}
labelInfo={<Hint />}
helperText={<ErrorMessage name="amount" />}
helperText={<ErrorMessage name="full_amount" />}
>
<ControlGroup>
<InputPrependText text={currency_code} />
<MoneyInputGroup
value={value}
onChange={(value) => {
setFieldValue('amount', value);
setFieldValue('full_amount', value);
}}
onBlurValue={onFullAmountBlur}
/>
</ControlGroup>
{!isEmpty(entries) && (
<Button
onClick={handleReceiveFullAmountClick}
className={'receive-full-amount'}
small={true}
minimal={true}
>
<T id={'receive_full_amount'} /> (
<Money amount={payableFullAmount} currency={currency_code} />)
</Button>
)}
<Button
onClick={handleReceiveFullAmountClick}
className={'receive-full-amount'}
small={true}
minimal={true}
>
<T id={'receive_full_amount'} /> (
<Money amount={payableFullAmount} currency={currency_code} />)
</Button>
</FormGroup>
)}
</Field>

View File

@@ -1,5 +1,5 @@
// @ts-nocheck
import React, { createContext, useContext, useState } from 'react';
import React, { createContext, useContext } from 'react';
import { Features } from '@/constants';
import { useFeatureCan } from '@/hooks/state';
import {
@@ -71,8 +71,6 @@ function PaymentMadeFormProvider({ query, paymentMadeId, ...props }) {
const isFeatureLoading = isBranchesLoading;
const [isExcessConfirmed, setIsExcessConfirmed] = useState<boolean>(false);
// Provider payload.
const provider = {
paymentMadeId,
@@ -100,9 +98,6 @@ function PaymentMadeFormProvider({ query, paymentMadeId, ...props }) {
setSubmitPayload,
setPaymentVendorId,
isExcessConfirmed,
setIsExcessConfirmed,
};
return (

View File

@@ -1,37 +0,0 @@
// @ts-nocheck
import React from 'react';
import { Dialog, DialogSuspense } from '@/components';
import withDialogRedux from '@/components/DialogReduxConnect';
import { compose } from '@/utils';
const ExcessPaymentDialogContent = React.lazy(() =>
import('./PaymentMadeExcessDialogContent').then((module) => ({
default: module.ExcessPaymentDialogContent,
})),
);
/**
* Exess payment dialog of the payment made form.
*/
function ExcessPaymentDialogRoot({ dialogName, isOpen }) {
return (
<Dialog
name={dialogName}
title={'Excess Payment'}
isOpen={isOpen}
canEscapeJeyClose={true}
autoFocus={true}
style={{ width: 500 }}
>
<DialogSuspense>
<ExcessPaymentDialogContent dialogName={dialogName} />
</DialogSuspense>
</Dialog>
);
}
export const ExcessPaymentDialog = compose(withDialogRedux())(
ExcessPaymentDialogRoot,
);
ExcessPaymentDialog.displayName = 'ExcessPaymentDialog';

View File

@@ -1,147 +0,0 @@
// @ts-nocheck
import * as R from 'ramda';
import * as Yup from 'yup';
import React, { useMemo } from 'react';
import { Button, Classes, Intent } from '@blueprintjs/core';
import { Form, Formik, FormikHelpers, useFormikContext } from 'formik';
import { AccountsSelect, FFormGroup, FormatNumber } from '@/components';
import { usePaymentMadeFormContext } from '../../PaymentMadeFormProvider';
import withDialogActions from '@/containers/Dialog/withDialogActions';
import { ACCOUNT_TYPE } from '@/constants';
import { usePaymentMadeExcessAmount } from '../../utils';
interface ExcessPaymentValues {
accountId: string;
}
const initialValues = {
accountId: '',
} as ExcessPaymentValues;
const Schema = Yup.object().shape({
accountId: Yup.number().required(),
});
const DEFAULT_ACCOUNT_SLUG = 'prepaid-expenses';
function ExcessPaymentDialogContentRoot({ dialogName, closeDialog }) {
const {
setFieldValue,
submitForm,
values: { currency_code: currencyCode },
} = useFormikContext();
const { setIsExcessConfirmed } = usePaymentMadeFormContext();
// Handles the form submitting.
const handleSubmit = (
values: ExcessPaymentValues,
{ setSubmitting }: FormikHelpers<ExcessPaymentValues>,
) => {
setFieldValue(values.accountId);
setSubmitting(true);
setIsExcessConfirmed(true);
return submitForm().then(() => {
setSubmitting(false);
closeDialog(dialogName);
});
};
// Handle close button click.
const handleCloseBtn = () => {
closeDialog(dialogName);
};
// Retrieves the default excess account id.
const defaultAccountId = useDefaultExcessPaymentDeposit();
const excessAmount = usePaymentMadeExcessAmount();
return (
<Formik
initialValues={{
...initialValues,
accountId: defaultAccountId,
}}
validationSchema={Schema}
onSubmit={handleSubmit}
>
<Form>
<ExcessPaymentDialogContentForm
excessAmount={
<FormatNumber value={excessAmount} currency={currencyCode} />
}
onClose={handleCloseBtn}
/>
</Form>
</Formik>
);
}
export const ExcessPaymentDialogContent = R.compose(withDialogActions)(
ExcessPaymentDialogContentRoot,
);
interface ExcessPaymentDialogContentFormProps {
excessAmount: string | number | React.ReactNode;
onClose?: () => void;
}
function ExcessPaymentDialogContentForm({
excessAmount,
onClose,
}: ExcessPaymentDialogContentFormProps) {
const { submitForm, isSubmitting } = useFormikContext();
const { accounts } = usePaymentMadeFormContext();
const handleCloseBtn = () => {
onClose && onClose();
};
return (
<>
<div className={Classes.DIALOG_BODY}>
<p style={{ marginBottom: 20 }}>
Would you like to record the excess amount of{' '}
<strong>{excessAmount}</strong> as advanced payment from the customer.
</p>
<FFormGroup
name={'accountId'}
label={'The excessed amount will be deposited in the'}
helperText={
'Only other current asset and non current asset accounts will show.'
}
>
<AccountsSelect
name={'accountId'}
items={accounts}
buttonProps={{ small: true }}
filterByTypes={[
ACCOUNT_TYPE.OTHER_CURRENT_ASSET,
ACCOUNT_TYPE.NON_CURRENT_ASSET,
]}
/>
</FFormGroup>
</div>
<div className={Classes.DIALOG_FOOTER}>
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
<Button
intent={Intent.PRIMARY}
loading={isSubmitting}
onClick={() => submitForm()}
>
Save Payment
</Button>
<Button onClick={handleCloseBtn}>Cancel</Button>
</div>
</div>
</>
);
}
const useDefaultExcessPaymentDeposit = () => {
const { accounts } = usePaymentMadeFormContext();
return useMemo(() => {
return accounts?.find((a) => a.slug === DEFAULT_ACCOUNT_SLUG)?.id;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
};

View File

@@ -1 +0,0 @@
export * from './PaymentMadeExcessDialog';

View File

@@ -37,7 +37,7 @@ export const defaultPaymentMadeEntry = {
// Default initial values of payment made.
export const defaultPaymentMade = {
amount: '',
full_amount: '',
vendor_id: '',
payment_account_id: '',
payment_date: moment(new Date()).format('YYYY-MM-DD'),
@@ -53,10 +53,10 @@ export const defaultPaymentMade = {
export const transformToEditForm = (paymentMade, paymentMadeEntries) => {
const attachments = transformAttachmentsToForm(paymentMade);
const appliedAmount = safeSumBy(paymentMadeEntries, 'payment_amount');
return {
...transformToForm(paymentMade, defaultPaymentMade),
full_amount: safeSumBy(paymentMadeEntries, 'payment_amount'),
entries: [
...paymentMadeEntries.map((paymentMadeEntry) => ({
...transformToForm(paymentMadeEntry, defaultPaymentMadeEntry),
@@ -177,30 +177,6 @@ export const usePaymentMadeTotals = () => {
};
};
export const usePaymentmadeTotalAmount = () => {
const {
values: { amount },
} = useFormikContext();
return amount;
};
export const usePaymentMadeAppliedAmount = () => {
const {
values: { entries },
} = useFormikContext();
// Retrieves the invoice entries total.
return React.useMemo(() => sumBy(entries, 'payment_amount'), [entries]);
};
export const usePaymentMadeExcessAmount = () => {
const appliedAmount = usePaymentMadeAppliedAmount();
const totalAmount = usePaymentmadeTotalAmount();
return Math.abs(totalAmount - appliedAmount);
};
/**
* Detarmines whether the bill has foreign customer.
* @returns {boolean}
@@ -215,10 +191,3 @@ export const usePaymentMadeIsForeignCustomer = () => {
);
return isForeignCustomer;
};
export const getPaymentExcessAmountFromValues = (values) => {
const appliedAmount = sumBy(values.entries, 'payment_amount');
const totalAmount = values.amount;
return Math.abs(totalAmount - appliedAmount);
};

View File

@@ -1,5 +1,5 @@
// @ts-nocheck
import React, { useMemo, useRef } from 'react';
import React, { useMemo } from 'react';
import { sumBy, isEmpty, defaultTo } from 'lodash';
import intl from 'react-intl-universal';
import classNames from 'classnames';
@@ -21,7 +21,6 @@ import { PaymentReceiveInnerProvider } from './PaymentReceiveInnerProvider';
import withSettings from '@/containers/Settings/withSettings';
import withCurrentOrganization from '@/containers/Organization/withCurrentOrganization';
import withDialogActions from '@/containers/Dialog/withDialogActions';
import {
EditPaymentReceiveFormSchema,
@@ -37,7 +36,6 @@ import {
transformFormToRequest,
transformErrors,
resetFormState,
getExceededAmountFromValues,
} from './utils';
import { PaymentReceiveSyncIncrementSettingsToForm } from './components';
@@ -53,9 +51,6 @@ function PaymentReceiveForm({
// #withCurrentOrganization
organization: { base_currency },
// #withDialogActions
openDialog,
}) {
const history = useHistory();
@@ -68,7 +63,6 @@ function PaymentReceiveForm({
submitPayload,
editPaymentReceiveMutate,
createPaymentReceiveMutate,
isExcessConfirmed,
} = usePaymentReceiveFormContext();
// Payment receive number.
@@ -100,16 +94,18 @@ function PaymentReceiveForm({
preferredDepositAccount,
],
);
// Handle form submit.
const handleSubmitForm = (
values,
{ setSubmitting, resetForm, setFieldError },
) => {
setSubmitting(true);
const exceededAmount = getExceededAmountFromValues(values);
// Validates the amount should be bigger than zero.
if (values.amount <= 0) {
// Calculates the total payment amount of entries.
const totalPaymentAmount = sumBy(values.entries, 'payment_amount');
if (totalPaymentAmount <= 0) {
AppToaster.show({
message: intl.get('you_cannot_make_payment_with_zero_total_amount'),
intent: Intent.DANGER,
@@ -117,13 +113,6 @@ function PaymentReceiveForm({
setSubmitting(false);
return;
}
// Show the confirm popup if the excessed amount bigger than zero and
// excess confirmation has not been confirmed yet.
if (exceededAmount > 0 && !isExcessConfirmed) {
setSubmitting(false);
openDialog('payment-received-excessed-payment');
return;
}
// Transformes the form values to request body.
const form = transformFormToRequest(values);
@@ -159,11 +148,11 @@ function PaymentReceiveForm({
};
if (paymentReceiveId) {
return editPaymentReceiveMutate([paymentReceiveId, form])
editPaymentReceiveMutate([paymentReceiveId, form])
.then(onSaved)
.catch(onError);
} else {
return createPaymentReceiveMutate(form).then(onSaved).catch(onError);
createPaymentReceiveMutate(form).then(onSaved).catch(onError);
}
};
return (
@@ -213,5 +202,4 @@ export default compose(
preferredDepositAccount: paymentReceiveSettings?.preferredDepositAccount,
})),
withCurrentOrganization(),
withDialogActions,
)(PaymentReceiveForm);

View File

@@ -2,7 +2,6 @@
import React from 'react';
import { useFormikContext } from 'formik';
import PaymentReceiveNumberDialog from '@/containers/Dialogs/PaymentReceiveNumberDialog';
import { ExcessPaymentDialog } from './dialogs/ExcessPaymentDialog';
/**
* Payment receive form dialogs.
@@ -22,12 +21,9 @@ export default function PaymentReceiveFormDialogs() {
};
return (
<>
<PaymentReceiveNumberDialog
dialogName={'payment-receive-number-form'}
onConfirm={handleUpdatePaymentNumber}
/>
<ExcessPaymentDialog dialogName={'payment-received-excessed-payment'} />
</>
<PaymentReceiveNumberDialog
dialogName={'payment-receive-number-form'}
onConfirm={handleUpdatePaymentNumber}
/>
);
}

View File

@@ -7,16 +7,11 @@ import {
TotalLine,
TotalLineBorderStyle,
TotalLineTextStyle,
FormatNumber,
} from '@/components';
import {
usePaymentReceiveTotals,
usePaymentReceivedTotalExceededAmount,
} from './utils';
import { usePaymentReceiveTotals } from './utils';
export function PaymentReceiveFormFootetRight() {
const { formattedSubtotal, formattedTotal } = usePaymentReceiveTotals();
const exceededAmount = usePaymentReceivedTotalExceededAmount();
return (
<PaymentReceiveTotalLines labelColWidth={'180px'} amountColWidth={'180px'}>
@@ -30,11 +25,6 @@ export function PaymentReceiveFormFootetRight() {
value={formattedTotal}
textStyle={TotalLineTextStyle.Bold}
/>
<TotalLine
title={'Exceeded Amount'}
value={<FormatNumber value={exceededAmount} />}
textStyle={TotalLineTextStyle.Regular}
/>
</PaymentReceiveTotalLines>
);
}

View File

@@ -30,9 +30,15 @@ function PaymentReceiveFormHeader() {
function PaymentReceiveFormBigTotal() {
// Formik form context.
const {
values: { currency_code, amount },
values: { currency_code, entries },
} = useFormikContext();
// Calculates the total payment amount from due amount.
const paymentFullAmount = useMemo(
() => sumBy(entries, 'payment_amount'),
[entries],
);
return (
<div className={classNames(CLASSES.PAGE_FORM_HEADER_BIG_NUMBERS)}>
<div class="big-amount">
@@ -40,7 +46,7 @@ function PaymentReceiveFormBigTotal() {
<T id={'amount_received'} />
</span>
<h1 class="big-amount__number">
<Money amount={amount} currency={currency_code} />
<Money amount={paymentFullAmount} currency={currency_code} />
</h1>
</div>
</div>

View File

@@ -1,5 +1,5 @@
// @ts-nocheck
import React, { createContext, useContext, useState } from 'react';
import React, { createContext, useContext } from 'react';
import { Features } from '@/constants';
import { useFeatureCan } from '@/hooks/state';
import { DashboardInsider } from '@/components';
@@ -74,8 +74,6 @@ function PaymentReceiveFormProvider({ query, paymentReceiveId, ...props }) {
const { mutateAsync: editPaymentReceiveMutate } = useEditPaymentReceive();
const { mutateAsync: createPaymentReceiveMutate } = useCreatePaymentReceive();
const [isExcessConfirmed, setIsExcessConfirmed] = useState<boolean>(false);
// Provider payload.
const provider = {
paymentReceiveId,
@@ -99,9 +97,6 @@ function PaymentReceiveFormProvider({ query, paymentReceiveId, ...props }) {
editPaymentReceiveMutate,
createPaymentReceiveMutate,
isExcessConfirmed,
setIsExcessConfirmed,
};
return (

View File

@@ -11,7 +11,7 @@ import {
Button,
} from '@blueprintjs/core';
import { DateInput } from '@blueprintjs/datetime';
import { isEmpty, toSafeInteger } from 'lodash';
import { toSafeInteger } from 'lodash';
import { FastField, Field, useFormikContext, ErrorMessage } from 'formik';
import {
@@ -124,11 +124,11 @@ export default function PaymentReceiveHeaderFields() {
</FastField>
{/* ------------ Full amount ------------ */}
<Field name={'amount'}>
<Field name={'full_amount'}>
{({
form: {
setFieldValue,
values: { currency_code, entries },
values: { currency_code },
},
field: { value, onChange },
meta: { error, touched },
@@ -146,23 +146,21 @@ export default function PaymentReceiveHeaderFields() {
<MoneyInputGroup
value={value}
onChange={(value) => {
setFieldValue('amount', value);
setFieldValue('full_amount', value);
}}
onBlurValue={onFullAmountBlur}
/>
</ControlGroup>
{!isEmpty(entries) && (
<Button
onClick={handleReceiveFullAmountClick}
className={'receive-full-amount'}
small={true}
minimal={true}
>
<T id={'receive_full_amount'} /> (
<Money amount={totalDueAmount} currency={currency_code} />)
</Button>
)}
<Button
onClick={handleReceiveFullAmountClick}
className={'receive-full-amount'}
small={true}
minimal={true}
>
<T id={'receive_full_amount'} /> (
<Money amount={totalDueAmount} currency={currency_code} />)
</Button>
</FormGroup>
)}
</Field>

View File

@@ -1,37 +0,0 @@
// @ts-nocheck
import React from 'react';
import { Dialog, DialogSuspense } from '@/components';
import withDialogRedux from '@/components/DialogReduxConnect';
import { compose } from '@/utils';
const ExcessPaymentDialogContent = React.lazy(() =>
import('./ExcessPaymentDialogContent').then((module) => ({
default: module.ExcessPaymentDialogContent,
})),
);
/**
* Excess payment dialog of the payment received form.
*/
function ExcessPaymentDialogRoot({ dialogName, isOpen }) {
return (
<Dialog
name={dialogName}
title={'Excess Payment'}
isOpen={isOpen}
canEscapeJeyClose={true}
autoFocus={true}
style={{ width: 500 }}
>
<DialogSuspense>
<ExcessPaymentDialogContent dialogName={dialogName} />
</DialogSuspense>
</Dialog>
);
}
export const ExcessPaymentDialog = compose(withDialogRedux())(
ExcessPaymentDialogRoot,
);
ExcessPaymentDialog.displayName = 'ExcessPaymentDialog';

View File

@@ -1,138 +0,0 @@
// @ts-nocheck
import { useMemo } from 'react';
import * as Yup from 'yup';
import * as R from 'ramda';
import { Button, Classes, Intent } from '@blueprintjs/core';
import { Form, Formik, FormikHelpers, useFormikContext } from 'formik';
import { AccountsSelect, FFormGroup, FormatNumber } from '@/components';
import { usePaymentReceiveFormContext } from '../../PaymentReceiveFormProvider';
import withDialogActions from '@/containers/Dialog/withDialogActions';
import { usePaymentReceivedTotalExceededAmount } from '../../utils';
import { ACCOUNT_TYPE } from '@/constants';
interface ExcessPaymentValues {
accountId: string;
}
const initialValues = {
accountId: '',
} as ExcessPaymentValues;
const Schema = Yup.object().shape({
accountId: Yup.number().required(),
});
const DEFAULT_ACCOUNT_SLUG = 'unearned-revenue';
export function ExcessPaymentDialogContentRoot({ dialogName, closeDialog }) {
const {
setFieldValue,
submitForm,
values: { currency_code: currencyCode },
} = useFormikContext();
const { setIsExcessConfirmed } = usePaymentReceiveFormContext();
const initialAccountId = useDefaultExcessPaymentDeposit();
const exceededAmount = usePaymentReceivedTotalExceededAmount();
const handleSubmit = (
values: ExcessPaymentValues,
{ setSubmitting }: FormikHelpers<ExcessPaymentValues>,
) => {
setSubmitting(true);
setIsExcessConfirmed(true);
setFieldValue('unearned_revenue_account_id', values.accountId);
submitForm().then(() => {
closeDialog(dialogName);
setSubmitting(false);
});
};
const handleClose = () => {
closeDialog(dialogName);
};
return (
<Formik
initialValues={{
...initialValues,
accountId: initialAccountId,
}}
validationSchema={Schema}
onSubmit={handleSubmit}
>
<Form>
<ExcessPaymentDialogContentForm
exceededAmount={
<FormatNumber value={exceededAmount} currency={currencyCode} />
}
onClose={handleClose}
/>
</Form>
</Formik>
);
}
export const ExcessPaymentDialogContent = R.compose(withDialogActions)(
ExcessPaymentDialogContentRoot,
);
function ExcessPaymentDialogContentForm({ onClose, exceededAmount }) {
const { accounts } = usePaymentReceiveFormContext();
const { submitForm, isSubmitting } = useFormikContext();
const handleCloseBtn = () => {
onClose && onClose();
};
return (
<>
<div className={Classes.DIALOG_BODY}>
<p style={{ marginBottom: 20 }}>
Would you like to record the excess amount of{' '}
<strong>{exceededAmount}</strong> as advanced payment from the
customer.
</p>
<FFormGroup
name={'accountId'}
label={'The excessed amount will be deposited in the'}
helperText={
'Only other other current liability and non current liability accounts will show.'
}
>
<AccountsSelect
name={'accountId'}
items={accounts}
buttonProps={{ small: true }}
filterByTypes={[
ACCOUNT_TYPE.OTHER_CURRENT_LIABILITY,
ACCOUNT_TYPE.NON_CURRENT_LIABILITY,
]}
/>
</FFormGroup>
</div>
<div className={Classes.DIALOG_FOOTER}>
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
<Button
intent={Intent.PRIMARY}
loading={isSubmitting}
disabled={isSubmitting}
onClick={() => submitForm()}
>
Save Payment
</Button>
<Button onClick={handleCloseBtn}>Cancel</Button>
</div>
</div>
</>
);
}
const useDefaultExcessPaymentDeposit = () => {
const { accounts } = usePaymentReceiveFormContext();
return useMemo(() => {
return accounts?.find((a) => a.slug === DEFAULT_ACCOUNT_SLUG)?.id;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
};

View File

@@ -42,13 +42,12 @@ export const defaultPaymentReceive = {
// Holds the payment number that entered manually only.
payment_receive_no_manually: '',
statement: '',
amount: '',
full_amount: '',
currency_code: '',
branch_id: '',
exchange_rate: 1,
entries: [],
attachments: [],
unearned_revenue_account_id: '',
attachments: []
};
export const defaultRequestPaymentEntry = {
@@ -250,30 +249,6 @@ export const usePaymentReceiveTotals = () => {
};
};
export const usePaymentReceivedTotalAppliedAmount = () => {
const {
values: { entries },
} = useFormikContext();
// Retrieves the invoice entries total.
return React.useMemo(() => sumBy(entries, 'payment_amount'), [entries]);
};
export const usePaymentReceivedTotalAmount = () => {
const {
values: { amount },
} = useFormikContext();
return amount;
};
export const usePaymentReceivedTotalExceededAmount = () => {
const totalAmount = usePaymentReceivedTotalAmount();
const totalApplied = usePaymentReceivedTotalAppliedAmount();
return Math.abs(totalAmount - totalApplied);
};
/**
* Detarmines whether the payment has foreign customer.
* @returns {boolean}
@@ -298,11 +273,3 @@ export const resetFormState = ({ initialValues, values, resetForm }) => {
},
});
};
export const getExceededAmountFromValues = (values) => {
const totalApplied = sumBy(values.entries, 'payment_amount');
const totalAmount = values.amount;
return totalAmount - totalApplied;
}

View File

@@ -183,13 +183,6 @@ export function useVendorsTableColumns() {
width: 85,
clickable: true,
},
{
id: 'credit_balance',
Header: 'Credit Balance',
accessor: 'formatted_unused_credit',
width: 100,
align: 'right',
},
{
id: 'balance',
Header: intl.get('receivable_balance'),