Compare commits

..

1 Commits

Author SHA1 Message Date
Ahmed Bouhuolia
889b0cec4b fix(server): sale receipt cost gl entries 2026-01-25 22:20:28 +02:00
846 changed files with 6136 additions and 5710 deletions

View File

@@ -167,9 +167,6 @@
"**/*.(t|j)s" "**/*.(t|j)s"
], ],
"coverageDirectory": "../coverage", "coverageDirectory": "../coverage",
"testEnvironment": "node", "testEnvironment": "node"
"moduleNameMapper": {
"^@/(.*)$": "<rootDir>/$1"
}
} }
} }

View File

@@ -1,27 +1,4 @@
import { chain, mapKeys } from 'lodash'; // import { getTransactionsLockingSettingsSchema } from '@/api/controllers/TransactionsLocking/utils';
const getTransactionsLockingSettingsSchema = (modules: string[]) => {
const moduleSchema = {
active: { type: 'boolean' },
lock_to_date: { type: 'date' },
unlock_from_date: { type: 'date' },
unlock_to_date: { type: 'date' },
lock_reason: { type: 'string' },
unlock_reason: { type: 'string' },
};
return chain(modules)
.map((module: string) => {
return mapKeys(moduleSchema, (value, key: string) => `${module}.${key}`);
})
.flattenDeep()
.reduce((result, value) => {
return {
...result,
...value,
};
}, {})
.value();
};
export const SettingsOptions = { export const SettingsOptions = {
organization: { organization: {
@@ -246,12 +223,12 @@ export const SettingsOptions = {
'locking-type': { 'locking-type': {
type: 'string', type: 'string',
}, },
...getTransactionsLockingSettingsSchema([ // ...getTransactionsLockingSettingsSchema([
'all', // 'all',
'sales', // 'sales',
'purchases', // 'purchases',
'financial', // 'financial',
]), // ]),
}, },
features: { features: {
'multi-warehouses': { 'multi-warehouses': {

View File

@@ -3,7 +3,6 @@
"field.description": "Description", "field.description": "Description",
"field.slug": "Account slug", "field.slug": "Account slug",
"field.code": "Account code", "field.code": "Account code",
"field.code_hint": "Unique number to identify the account.",
"field.root_type": "Root type", "field.root_type": "Root type",
"field.normal": "Account normal", "field.normal": "Account normal",
"field.normal.credit": "Credit", "field.normal.credit": "Credit",
@@ -14,6 +13,5 @@
"field.balance": "Balance", "field.balance": "Balance",
"field.bank_balance": "Bank Balance", "field.bank_balance": "Bank Balance",
"field.parent_account": "Parent Account", "field.parent_account": "Parent Account",
"field.created_at": "Created at", "field.created_at": "Created at"
"field.account_hint": "Matches the account name or code."
} }

View File

@@ -1,27 +0,0 @@
{
"field.vendor": "Vendor",
"field.bill_number": "Bill No.",
"field.bill_date": "Date",
"field.due_date": "Due Date",
"field.reference_no": "Reference No.",
"field.exchange_rate": "Exchange Rate",
"field.note": "Note",
"field.open": "Open",
"field.entries": "Entries",
"field.item": "Item",
"field.item_hint": "Matches the item name or code.",
"field.rate": "Rate",
"field.quantity": "Quantity",
"field.description": "Line Description",
"field.amount": "Amount",
"field.payment_amount": "Payment Amount",
"field.status": "Status",
"field.status.paid": "Paid",
"field.status.partially-paid": "Partially Paid",
"field.status.overdue": "Overdue",
"field.status.unpaid": "Unpaid",
"field.status.opened": "Opened",
"field.status.draft": "Draft",
"field.created_at": "Created At"
}

View File

@@ -1,15 +0,0 @@
{
"field.vendor": "Vendor",
"field.payment_date": "Payment Date",
"field.payment_number": "Payment No.",
"field.payment_account": "Payment Account",
"field.exchange_rate": "Exchange Rate",
"field.note": "Note",
"field.reference": "Reference",
"field.entries": "Entries",
"field.entries.bill": "Bill",
"field.entries.payment_amount": "Payment Amount",
"field.payment_number_hint": "The payment number should be unique.",
"field.bill_hint": "Matches the bill number."
}

View File

@@ -1,16 +0,0 @@
{
"field.customer": "Customer",
"field.exchange_rate": "Exchange Rate",
"field.credit_note_date": "Credit Note Date",
"field.reference_no": "Reference No.",
"field.note": "Note",
"field.terms_conditions": "Terms & Conditions",
"field.credit_note_number": "Credit Note Number",
"field.open": "Open",
"field.entries": "Entries",
"field.item": "Item",
"field.rate": "Rate",
"field.quantity": "Quantity",
"field.description": "Description"
}

View File

@@ -1,21 +0,0 @@
{
"field.customer": "Customer",
"field.estimate_date": "Estimate Date",
"field.expiration_date": "Expiration Date",
"field.estimate_number": "Estimate No.",
"field.reference_no": "Reference No.",
"field.exchange_rate": "Exchange Rate",
"field.currency": "Currency",
"field.note": "Note",
"field.terms_conditions": "Terms & Conditions",
"field.delivered": "Delivered",
"field.entries": "Entries",
"field.amount": "Amount",
"field.status": "Status",
"field.status.draft": "Draft",
"field.status.delivered": "Delivered",
"field.status.rejected": "Rejected",
"field.status.approved": "Approved",
"field.created_at": "Created At"
}

View File

@@ -26,7 +26,6 @@
"field.due_amount": "Due amount", "field.due_amount": "Due amount",
"field.delivered": "Delivered", "field.delivered": "Delivered",
"field.item_name": "Item Name", "field.item_name": "Item Name",
"field.item_hint": "Matches the item name or code.",
"field.rate": "Rate", "field.rate": "Rate",
"field.quantity": "Quantity", "field.quantity": "Quantity",
"field.description": "Description", "field.description": "Description",
@@ -39,7 +38,5 @@
"field.status.draft": "Draft", "field.status.draft": "Draft",
"field.created_at": "Created at", "field.created_at": "Created at",
"field.currency": "Currency", "field.currency": "Currency",
"field.entries": "Entries", "field.entries": "Entries"
"field.branch": "Branch",
"field.warehouse": "Warehouse"
} }

View File

@@ -17,7 +17,6 @@
"field.quantity_on_hand": "Quantity on Hand", "field.quantity_on_hand": "Quantity on Hand",
"field.note": "Note", "field.note": "Note",
"field.category": "Category", "field.category": "Category",
"field.category_hint": "Matches the category name.",
"field.active": "Active", "field.active": "Active",
"field.created_at": "Created At" "field.created_at": "Created At"
} }

View File

@@ -1,17 +0,0 @@
{
"field.customer": "Customer",
"field.payment_date": "Payment Date",
"field.amount": "Amount",
"field.reference_no": "Reference No.",
"field.deposit_account": "Deposit Account",
"field.payment_receive_no": "Payment No.",
"field.statement": "Statement",
"field.entries": "Entries",
"field.exchange_rate": "Exchange Rate",
"field.invoice": "Invoice",
"field.entries.payment_amount": "Payment Amount",
"field.created_at": "Created At",
"field.payment_no_hint": "The payment number should be unique.",
"field.invoice_hint": "Matches the invoice number."
}

View File

@@ -10,21 +10,5 @@
"paper.receipt_amount": "Receipt amount", "paper.receipt_amount": "Receipt amount",
"paper.total": "Total", "paper.total": "Total",
"paper.balance_due": "Balance Due", "paper.balance_due": "Balance Due",
"paper.payment_amount": "Payment Amount", "paper.payment_amount": "Payment Amount"
"field.receipt_date": "Receipt Date",
"field.customer": "Customer",
"field.deposit_account": "Deposit Account",
"field.exchange_rate": "Exchange Rate",
"field.receipt_number": "Receipt Number",
"field.reference_no": "Reference No.",
"field.closed": "Closed",
"field.entries": "Entries",
"field.statement": "Statement",
"field.receipt_message": "Receipt Message",
"field.amount": "Amount",
"field.status": "Status",
"field.status.draft": "Draft",
"field.status.closed": "Closed",
"field.created_at": "Created At"
} }

View File

@@ -2,18 +2,5 @@
"view.draft": "Draft", "view.draft": "Draft",
"view.published": "Published", "view.published": "Published",
"view.open": "Open", "view.open": "Open",
"view.closed": "Closed", "view.closed": "Closed"
"field.vendor": "Vendor",
"field.vendor_credit_number": "Vendor Credit No.",
"field.vendor_credit_date": "Vendor Credit Date",
"field.reference_no": "Reference No.",
"field.exchange_rate": "Exchange Rate",
"field.note": "Note",
"field.open": "Open",
"field.entries": "Entries",
"field.item": "Item",
"field.rate": "Rate",
"field.quantity": "Quantity",
"field.description": "Description"
} }

View File

@@ -156,7 +156,7 @@ export const AccountMeta = {
minLength: 3, minLength: 3,
maxLength: 6, maxLength: 6,
unique: true, unique: true,
importHint: 'account.field.code_hint', importHint: 'Unique number to identify the account.',
}, },
accountType: { accountType: {
name: 'account.field.type', name: 'account.field.type',

View File

@@ -1,7 +1,7 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { Knex } from 'knex'; import { Knex } from 'knex';
import * as yup from 'yup'; import * as yup from 'yup';
import * as uniqid from 'uniqid'; import uniqid from 'uniqid';
import { Importable } from '../../Import/Importable'; import { Importable } from '../../Import/Importable';
import { CreateUncategorizedTransactionService } from './CreateUncategorizedTransaction.service'; import { CreateUncategorizedTransactionService } from './CreateUncategorizedTransaction.service';
import { ImportableContext } from '../../Import/interfaces'; import { ImportableContext } from '../../Import/interfaces';
@@ -9,10 +9,8 @@ import { BankTransactionsSampleData } from '../../BankingTransactions/constants'
import { Account } from '@/modules/Accounts/models/Account.model'; import { Account } from '@/modules/Accounts/models/Account.model';
import { CreateUncategorizedTransactionDTO } from '../types/BankingCategorize.types'; import { CreateUncategorizedTransactionDTO } from '../types/BankingCategorize.types';
import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel'; import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
import { ImportableService } from '../../Import/decorators/Import.decorator';
import { UncategorizedBankTransaction } from '../../BankingTransactions/models/UncategorizedBankTransaction';
@Injectable() @Injectable()
@ImportableService({ name: UncategorizedBankTransaction.name })
export class UncategorizedTransactionsImportable extends Importable { export class UncategorizedTransactionsImportable extends Importable {
constructor( constructor(
private readonly createUncategorizedTransaction: CreateUncategorizedTransactionService, private readonly createUncategorizedTransaction: CreateUncategorizedTransactionService,

View File

@@ -1,6 +1,6 @@
import * as moment from 'moment'; import * as moment from 'moment';
import * as R from 'ramda'; import * as R from 'ramda';
import { isEmpty, round, sumBy } from 'lodash'; import { isEmpty, sumBy } from 'lodash';
import { ERRORS, MatchedTransactionPOJO } from './types'; import { ERRORS, MatchedTransactionPOJO } from './types';
import { ServiceError } from '../Items/ServiceError'; import { ServiceError } from '../Items/ServiceError';
@@ -22,24 +22,18 @@ export const sortClosestMatchTransactions = (
}; };
export const sumMatchTranasctions = (transactions: Array<any>) => { export const sumMatchTranasctions = (transactions: Array<any>) => {
const total = transactions.reduce( return transactions.reduce(
(sum, item) => { (total, item) =>
const amount = parseFloat(item.amount) || 0; total +
const multiplier = item.transactionNormal === 'debit' ? 1 : -1; (item.transactionNormal === 'debit' ? 1 : -1) * parseFloat(item.amount),
return sum + multiplier * amount;
},
0 0
); );
// Round to 2 decimal places to avoid floating-point precision issues
return round(total, 2);
}; };
export const sumUncategorizedTransactions = ( export const sumUncategorizedTransactions = (
uncategorizedTransactions: Array<any> uncategorizedTransactions: Array<any>
) => { ) => {
const total = sumBy(uncategorizedTransactions, 'amount'); return sumBy(uncategorizedTransactions, 'amount');
// Round to 2 decimal places to avoid floating-point precision issues
return round(total, 2);
}; };
export const validateUncategorizedTransactionsNotMatched = ( export const validateUncategorizedTransactionsNotMatched = (

View File

@@ -34,7 +34,7 @@ export class MatchBankTransactions {
private readonly uncategorizedBankTransactionModel: TenantModelProxy< private readonly uncategorizedBankTransactionModel: TenantModelProxy<
typeof UncategorizedBankTransaction typeof UncategorizedBankTransaction
>, >,
) { } ) {}
/** /**
* Validates the match bank transactions DTO. * Validates the match bank transactions DTO.
@@ -100,10 +100,7 @@ export class MatchBankTransactions {
); );
// Validates the total given matching transcations whether is not equal // Validates the total given matching transcations whether is not equal
// uncategorized transaction amount. // uncategorized transaction amount.
// Use tolerance-based comparison to handle floating-point precision issues if (totalUncategorizedTransactions !== totalMatchedTranasctions) {
const tolerance = 0.01; // Allow 0.01 difference for floating-point precision
const difference = Math.abs(totalUncategorizedTransactions - totalMatchedTranasctions);
if (difference > tolerance) {
throw new ServiceError(ERRORS.TOTAL_MATCHING_TRANSACTIONS_INVALID); throw new ServiceError(ERRORS.TOTAL_MATCHING_TRANSACTIONS_INVALID);
} }
} }

View File

@@ -12,30 +12,24 @@ export class MatchTransactionsTypes {
private static registry: MatchTransactionsTypesRegistry; private static registry: MatchTransactionsTypesRegistry;
/** /**
* Constructor method. * Consttuctor method.
*/ */
constructor( constructor() {
private readonly getMatchedInvoicesService: GetMatchedTransactionsByInvoices,
private readonly getMatchedBillsService: GetMatchedTransactionsByBills,
private readonly getMatchedExpensesService: GetMatchedTransactionsByExpenses,
private readonly getMatchedManualJournalsService: GetMatchedTransactionsByManualJournals,
private readonly getMatchedCashflowService: GetMatchedTransactionsByCashflow,
) {
this.boot(); this.boot();
} }
get registered() { get registered() {
return [ return [
{ type: 'SaleInvoice', service: this.getMatchedInvoicesService }, { type: 'SaleInvoice', service: GetMatchedTransactionsByInvoices },
{ type: 'Bill', service: this.getMatchedBillsService }, { type: 'Bill', service: GetMatchedTransactionsByBills },
{ type: 'Expense', service: this.getMatchedExpensesService }, { type: 'Expense', service: GetMatchedTransactionsByExpenses },
{ {
type: 'ManualJournal', type: 'ManualJournal',
service: this.getMatchedManualJournalsService, service: GetMatchedTransactionsByManualJournals,
}, },
{ {
type: 'CashflowTransaction', type: 'CashflowTransaction',
service: this.getMatchedCashflowService, service: GetMatchedTransactionsByCashflow,
}, },
]; ];
} }
@@ -56,13 +50,14 @@ export class MatchTransactionsTypes {
* Boots all the registered importables. * Boots all the registered importables.
*/ */
public boot() { public boot() {
const instance = MatchTransactionsTypesRegistry.getInstance(); if (!MatchTransactionsTypes.registry) {
const instance = MatchTransactionsTypesRegistry.getInstance();
// Always register services to ensure they're available this.registered.forEach((registered) => {
this.registered.forEach((registered) => { // const serviceInstanace = Container.get(registered.service);
instance.register(registered.type, registered.service); // instance.register(registered.type, serviceInstanace);
}); });
MatchTransactionsTypes.registry = instance;
MatchTransactionsTypes.registry = instance; }
} }
} }

View File

@@ -83,4 +83,4 @@ const models = [
CreateBankTransactionService CreateBankTransactionService
], ],
}) })
export class BankingTransactionsModule { } export class BankingTransactionsModule {}

View File

@@ -1,72 +0,0 @@
export const UncategorizedBankTransactionMeta = {
defaultFilterField: 'createdAt',
defaultSort: {
sortOrder: 'DESC',
sortField: 'created_at',
},
importable: true,
fields: {
date: {
name: 'Date',
column: 'date',
fieldType: 'date',
},
payee: {
name: 'Payee',
column: 'payee',
fieldType: 'text',
},
description: {
name: 'Description',
column: 'description',
fieldType: 'text',
},
referenceNo: {
name: 'Reference No.',
column: 'reference_no',
fieldType: 'text',
},
amount: {
name: 'Amount',
column: 'Amount',
fieldType: 'numeric',
required: true,
},
account: {
name: 'Account',
column: 'account_id',
fieldType: 'relation',
to: { model: 'Account', to: 'id' },
},
createdAt: {
name: 'Created At',
column: 'createdAt',
fieldType: 'date',
importable: false,
},
},
fields2: {
date: {
name: 'Date',
fieldType: 'date',
required: true,
},
payee: {
name: 'Payee',
fieldType: 'text',
},
description: {
name: 'Description',
fieldType: 'text',
},
referenceNo: {
name: 'Reference No.',
fieldType: 'text',
},
amount: {
name: 'Amount',
fieldType: 'number',
required: true,
},
},
};

View File

@@ -2,10 +2,7 @@
import * as moment from 'moment'; import * as moment from 'moment';
import { Model } from 'objection'; import { Model } from 'objection';
import { TenantBaseModel } from '@/modules/System/models/TenantBaseModel'; import { TenantBaseModel } from '@/modules/System/models/TenantBaseModel';
import { UncategorizedBankTransactionMeta } from './UncategorizedBankTransaction.meta';
import { InjectModelMeta } from '@/modules/Tenancy/TenancyModels/decorators/InjectModelMeta.decorator';
@InjectModelMeta(UncategorizedBankTransactionMeta)
export class UncategorizedBankTransaction extends TenantBaseModel { export class UncategorizedBankTransaction extends TenantBaseModel {
readonly amount!: number; readonly amount!: number;
readonly date!: Date | string; readonly date!: Date | string;

View File

@@ -104,12 +104,6 @@ export class BillPaymentResponseDto {
@ApiProperty({ description: 'The formatted amount', example: '100.00 USD' }) @ApiProperty({ description: 'The formatted amount', example: '100.00 USD' })
formattedAmount: string; formattedAmount: string;
@ApiProperty({ description: 'The formatted total', example: '100.00 USD' })
formattedTotal: string;
@ApiProperty({ description: 'The formatted subtotal', example: '100.00 USD' })
formattedSubtotal: string;
@ApiProperty({ @ApiProperty({
description: 'The date when the payment was created', description: 'The date when the payment was created',
example: '2024-01-01T12:00:00Z', example: '2024-01-01T12:00:00Z',

View File

@@ -167,7 +167,7 @@ export const BillPaymentMeta = {
name: 'bill_payment.field.payment_number', name: 'bill_payment.field.payment_number',
fieldType: 'text', fieldType: 'text',
unique: true, unique: true,
importHint: 'bill_payment.field.payment_number_hint', importHint: 'The payment number should be unique.',
}, },
paymentAccountId: { paymentAccountId: {
name: 'bill_payment.field.payment_account', name: 'bill_payment.field.payment_account',
@@ -175,7 +175,7 @@ export const BillPaymentMeta = {
relationModel: 'Account', relationModel: 'Account',
relationImportMatch: ['name', 'code'], relationImportMatch: ['name', 'code'],
required: true, required: true,
importHint: 'account.field.account_hint', importHint: 'Matches the account name or code.',
}, },
exchangeRate: { exchangeRate: {
name: 'bill_payment.field.exchange_rate', name: 'bill_payment.field.exchange_rate',
@@ -203,7 +203,7 @@ export const BillPaymentMeta = {
relationModel: 'Bill', relationModel: 'Bill',
relationImportMatch: 'billNumber', relationImportMatch: 'billNumber',
required: true, required: true,
importHint: 'bill_payment.field.bill_hint', importHint: 'Matches the bill number.',
}, },
paymentAmount: { paymentAmount: {
name: 'bill_payment.field.entries.payment_amount', name: 'bill_payment.field.entries.payment_amount',
@@ -213,7 +213,7 @@ export const BillPaymentMeta = {
}, },
}, },
branchId: { branchId: {
name: 'invoice.field.branch', name: 'Branch',
fieldType: 'relation', fieldType: 'relation',
relationModel: 'Branch', relationModel: 'Branch',
relationImportMatch: ['name', 'code'], relationImportMatch: ['name', 'code'],

View File

@@ -13,8 +13,6 @@ export class BillPaymentTransformer extends Transformer {
'formattedPaymentDate', 'formattedPaymentDate',
'formattedCreatedAt', 'formattedCreatedAt',
'formattedAmount', 'formattedAmount',
'formattedTotal',
'formattedSubtotal',
'entries', 'entries',
'attachments', 'attachments',
]; ];
@@ -49,29 +47,6 @@ export class BillPaymentTransformer extends Transformer {
}); });
}; };
/**
* Retrieves the formatted total.
* @param {IBillPayment} billPayment
* @returns {string}
*/
protected formattedTotal = (billPayment: BillPayment): string => {
return this.formatNumber(billPayment.amount, {
currencyCode: billPayment.currencyCode,
money: true,
});
};
/**
* Retrieves the formatted subtotal.
* @param {IBillPayment} billPayment
* @returns {string}
*/
protected formattedSubtotal = (billPayment: BillPayment): string => {
return this.formatNumber(billPayment.amount, {
currencyCode: billPayment.currencyCode,
});
};
/** /**
* Retreives the bill payment entries. * Retreives the bill payment entries.
*/ */

View File

@@ -184,76 +184,76 @@ export const BillMeta = {
}, },
fields2: { fields2: {
billNumber: { billNumber: {
name: 'bill.field.bill_number', name: 'Bill No.',
fieldType: 'text', fieldType: 'text',
required: true, required: true,
}, },
referenceNo: { referenceNo: {
name: 'bill.field.reference_no', name: 'Reference No.',
fieldType: 'text', fieldType: 'text',
}, },
billDate: { billDate: {
name: 'bill.field.bill_date', name: 'Date',
fieldType: 'date', fieldType: 'date',
required: true, required: true,
}, },
dueDate: { dueDate: {
name: 'bill.field.due_date', name: 'Due Date',
fieldType: 'date', fieldType: 'date',
required: true, required: true,
}, },
vendorId: { vendorId: {
name: 'bill.field.vendor', name: 'Vendor',
fieldType: 'relation', fieldType: 'relation',
relationModel: 'Contact', relationModel: 'Contact',
relationImportMatch: 'displayName', relationImportMatch: 'displayName',
required: true, required: true,
}, },
exchangeRate: { exchangeRate: {
name: 'bill.field.exchange_rate', name: 'Exchange Rate',
fieldType: 'number', fieldType: 'number',
}, },
note: { note: {
name: 'bill.field.note', name: 'Note',
fieldType: 'text', fieldType: 'text',
}, },
open: { open: {
name: 'bill.field.open', name: 'Open',
fieldType: 'boolean', fieldType: 'boolean',
}, },
entries: { entries: {
name: 'bill.field.entries', name: 'Entries',
fieldType: 'collection', fieldType: 'collection',
collectionOf: 'object', collectionOf: 'object',
collectionMinLength: 1, collectionMinLength: 1,
required: true, required: true,
fields: { fields: {
itemId: { itemId: {
name: 'bill.field.item', name: 'Item',
fieldType: 'relation', fieldType: 'relation',
relationModel: 'Item', relationModel: 'Item',
relationImportMatch: ['name', 'code'], relationImportMatch: ['name', 'code'],
required: true, required: true,
importHint: 'bill.field.item_hint', importHint: 'Matches the item name or code.',
}, },
rate: { rate: {
name: 'bill.field.rate', name: 'Rate',
fieldType: 'number', fieldType: 'number',
required: true, required: true,
}, },
quantity: { quantity: {
name: 'bill.field.quantity', name: 'Quantity',
fieldType: 'number', fieldType: 'number',
required: true, required: true,
}, },
description: { description: {
name: 'bill.field.description', name: 'Line Description',
fieldType: 'text', fieldType: 'text',
}, },
}, },
}, },
branchId: { branchId: {
name: 'invoice.field.branch', name: 'Branch',
fieldType: 'relation', fieldType: 'relation',
relationModel: 'Branch', relationModel: 'Branch',
relationImportMatch: ['name', 'code'], relationImportMatch: ['name', 'code'],
@@ -261,7 +261,7 @@ export const BillMeta = {
required: true, required: true,
}, },
warehouseId: { warehouseId: {
name: 'invoice.field.warehouse', name: 'Warehouse',
fieldType: 'relation', fieldType: 'relation',
relationModel: 'Warehouse', relationModel: 'Warehouse',
relationImportMatch: ['name', 'code'], relationImportMatch: ['name', 'code'],

View File

@@ -94,7 +94,6 @@ export class BillTransformer extends Transformer {
protected formattedDueAmount = (bill: Bill): string => { protected formattedDueAmount = (bill: Bill): string => {
return this.formatNumber(bill.dueAmount, { return this.formatNumber(bill.dueAmount, {
currencyCode: bill.currencyCode, currencyCode: bill.currencyCode,
money: true,
}); });
}; };
@@ -170,7 +169,6 @@ export class BillTransformer extends Transformer {
protected totalFormatted = (bill: Bill): string => { protected totalFormatted = (bill: Bill): string => {
return this.formatNumber(bill.total, { return this.formatNumber(bill.total, {
currencyCode: bill.currencyCode, currencyCode: bill.currencyCode,
money: true,
}); });
}; };

View File

@@ -1,6 +1,5 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { ToNumber, IsOptional } from '@/common/decorators/Validators'; import { IsNotEmpty, IsOptional, IsPositive, IsString } from 'class-validator';
import { IsDateString, IsNotEmpty, IsPositive, IsString } from 'class-validator';
import { IsDate } from 'class-validator'; import { IsDate } from 'class-validator';
import { IsNumber } from 'class-validator'; import { IsNumber } from 'class-validator';
@@ -11,13 +10,8 @@ export class CreditNoteRefundDto {
description: 'The id of the from account', description: 'The id of the from account',
example: 1, example: 1,
}) })
@ApiProperty({
description: 'The id of the from account',
example: 1,
})
fromAccountId: number; fromAccountId: number;
@ToNumber()
@IsNumber() @IsNumber()
@IsPositive() @IsPositive()
@IsNotEmpty() @IsNotEmpty()
@@ -27,7 +21,6 @@ export class CreditNoteRefundDto {
}) })
amount: number; amount: number;
@ToNumber()
@IsNumber() @IsNumber()
@IsOptional() @IsOptional()
@IsPositive() @IsPositive()
@@ -37,23 +30,23 @@ export class CreditNoteRefundDto {
}) })
exchangeRate?: number; exchangeRate?: number;
@IsOptional()
@IsString() @IsString()
@IsNotEmpty()
@ApiProperty({ @ApiProperty({
description: 'The reference number of the credit note refund', description: 'The reference number of the credit note refund',
example: '123456', example: '123456',
}) })
referenceNo: string; referenceNo: string;
@IsOptional()
@IsString() @IsString()
@IsNotEmpty()
@ApiProperty({ @ApiProperty({
description: 'The description of the credit note refund', description: 'The description of the credit note refund',
example: 'Credit note refund', example: 'Credit note refund',
}) })
description: string; description: string;
@IsDateString() @IsDate()
@IsNotEmpty() @IsNotEmpty()
@ApiProperty({ @ApiProperty({
description: 'The date of the credit note refund', description: 'The date of the credit note refund',
@@ -61,7 +54,6 @@ export class CreditNoteRefundDto {
}) })
date: Date; date: Date;
@ToNumber()
@IsNumber() @IsNumber()
@IsOptional() @IsOptional()
@ApiProperty({ @ApiProperty({

View File

@@ -164,73 +164,73 @@ export const CreditNoteMeta = {
}, },
fields2: { fields2: {
customerId: { customerId: {
name: 'credit_note.field.customer', name: 'Customer',
fieldType: 'relation', fieldType: 'relation',
relationModel: 'Contact', relationModel: 'Contact',
relationImportMatch: 'displayName', relationImportMatch: 'displayName',
required: true, required: true,
}, },
exchangeRate: { exchangeRate: {
name: 'credit_note.field.exchange_rate', name: 'Exchange Rate',
fieldType: 'number', fieldType: 'number',
}, },
creditNoteDate: { creditNoteDate: {
name: 'credit_note.field.credit_note_date', name: 'Credit Note Date',
fieldType: 'date', fieldType: 'date',
required: true, required: true,
}, },
referenceNo: { referenceNo: {
name: 'credit_note.field.reference_no', name: 'Reference No.',
fieldType: 'text', fieldType: 'text',
}, },
note: { note: {
name: 'credit_note.field.note', name: 'Note',
fieldType: 'text', fieldType: 'text',
}, },
termsConditions: { termsConditions: {
name: 'credit_note.field.terms_conditions', name: 'Terms & Conditions',
fieldType: 'text', fieldType: 'text',
}, },
creditNoteNumber: { creditNoteNumber: {
name: 'credit_note.field.credit_note_number', name: 'Credit Note Number',
fieldType: 'text', fieldType: 'text',
}, },
open: { open: {
name: 'credit_note.field.open', name: 'Open',
fieldType: 'boolean', fieldType: 'boolean',
}, },
entries: { entries: {
name: 'credit_note.field.entries', name: 'Entries',
fieldType: 'collection', fieldType: 'collection',
collectionOf: 'object', collectionOf: 'object',
collectionMinLength: 1, collectionMinLength: 1,
fields: { fields: {
itemId: { itemId: {
name: 'credit_note.field.item', name: 'Item',
fieldType: 'relation', fieldType: 'relation',
relationModel: 'Item', relationModel: 'Item',
relationImportMatch: ['name', 'code'], relationImportMatch: ['name', 'code'],
required: true, required: true,
importHint: 'invoice.field.item_hint', importHint: 'Matches the item name or code.',
}, },
rate: { rate: {
name: 'credit_note.field.rate', name: 'Rate',
fieldType: 'number', fieldType: 'number',
required: true, required: true,
}, },
quantity: { quantity: {
name: 'credit_note.field.quantity', name: 'Quantity',
fieldType: 'number', fieldType: 'number',
required: true, required: true,
}, },
description: { description: {
name: 'credit_note.field.description', name: 'Description',
fieldType: 'text', fieldType: 'text',
}, },
}, },
}, },
branchId: { branchId: {
name: 'invoice.field.branch', name: 'Branch',
fieldType: 'relation', fieldType: 'relation',
relationModel: 'Branch', relationModel: 'Branch',
relationImportMatch: ['name', 'code'], relationImportMatch: ['name', 'code'],
@@ -238,7 +238,7 @@ export const CreditNoteMeta = {
required: true, required: true,
}, },
warehouseId: { warehouseId: {
name: 'invoice.field.warehouse', name: 'Warehouse',
fieldType: 'relation', fieldType: 'relation',
relationModel: 'Warehouse', relationModel: 'Warehouse',
relationImportMatch: ['name', 'code'], relationImportMatch: ['name', 'code'],

View File

@@ -90,7 +90,7 @@ export class CreditNoteTransformer extends Transformer {
* @returns {string} * @returns {string}
*/ */
protected formattedSubtotal = (credit): string => { protected formattedSubtotal = (credit): string => {
return this.formatNumber(credit.amount); return this.formatNumber(credit.amount, { money: false });
}; };
/** /**
@@ -130,7 +130,7 @@ export class CreditNoteTransformer extends Transformer {
* @returns {string} * @returns {string}
*/ */
protected adjustmentFormatted = (credit): string => { protected adjustmentFormatted = (credit): string => {
return this.formatNumber(credit.adjustment, { return this.formatMoney(credit.adjustment, {
currencyCode: credit.currencyCode, currencyCode: credit.currencyCode,
excerptZero: true, excerptZero: true,
}); });
@@ -156,7 +156,6 @@ export class CreditNoteTransformer extends Transformer {
protected totalFormatted = (credit): string => { protected totalFormatted = (credit): string => {
return this.formatNumber(credit.total, { return this.formatNumber(credit.total, {
currencyCode: credit.currencyCode, currencyCode: credit.currencyCode,
money: true,
}); });
}; };
@@ -168,7 +167,6 @@ export class CreditNoteTransformer extends Transformer {
protected totalLocalFormatted = (credit): string => { protected totalLocalFormatted = (credit): string => {
return this.formatNumber(credit.totalLocal, { return this.formatNumber(credit.totalLocal, {
currencyCode: credit.currencyCode, currencyCode: credit.currencyCode,
money: true,
}); });
}; };

View File

@@ -1,10 +1,10 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { renderCreditNotePaperTemplateHtml } from '@bigcapital/pdf-templates';
import { GetCreditNoteService } from './GetCreditNote.service'; import { GetCreditNoteService } from './GetCreditNote.service';
import { CreditNoteBrandingTemplate } from './CreditNoteBrandingTemplate.service'; import { CreditNoteBrandingTemplate } from './CreditNoteBrandingTemplate.service';
import { transformCreditNoteToPdfTemplate } from '../utils'; import { transformCreditNoteToPdfTemplate } from '../utils';
import { CreditNote } from '../models/CreditNote'; import { CreditNote } from '../models/CreditNote';
import { ChromiumlyTenancy } from '@/modules/ChromiumlyTenancy/ChromiumlyTenancy.service'; import { ChromiumlyTenancy } from '@/modules/ChromiumlyTenancy/ChromiumlyTenancy.service';
import { TemplateInjectable } from '@/modules/TemplateInjectable/TemplateInjectable.service';
import { EventEmitter2 } from '@nestjs/event-emitter'; import { EventEmitter2 } from '@nestjs/event-emitter';
import { PdfTemplateModel } from '@/modules/PdfTemplate/models/PdfTemplate'; import { PdfTemplateModel } from '@/modules/PdfTemplate/models/PdfTemplate';
import { CreditNotePdfTemplateAttributes } from '../types/CreditNotes.types'; import { CreditNotePdfTemplateAttributes } from '../types/CreditNotes.types';
@@ -15,6 +15,7 @@ import { TenantModelProxy } from '@/modules/System/models/TenantBaseModel';
export class GetCreditNotePdf { export class GetCreditNotePdf {
/** /**
* @param {ChromiumlyTenancy} chromiumlyTenancy - Chromiumly tenancy service. * @param {ChromiumlyTenancy} chromiumlyTenancy - Chromiumly tenancy service.
* @param {TemplateInjectable} templateInjectable - Template injectable service.
* @param {GetCreditNote} getCreditNoteService - Get credit note service. * @param {GetCreditNote} getCreditNoteService - Get credit note service.
* @param {CreditNoteBrandingTemplate} creditNoteBrandingTemplate - Credit note branding template service. * @param {CreditNoteBrandingTemplate} creditNoteBrandingTemplate - Credit note branding template service.
* @param {EventEmitter2} eventPublisher - Event publisher service. * @param {EventEmitter2} eventPublisher - Event publisher service.
@@ -23,6 +24,7 @@ export class GetCreditNotePdf {
*/ */
constructor( constructor(
private readonly chromiumlyTenancy: ChromiumlyTenancy, private readonly chromiumlyTenancy: ChromiumlyTenancy,
private readonly templateInjectable: TemplateInjectable,
private readonly getCreditNoteService: GetCreditNoteService, private readonly getCreditNoteService: GetCreditNoteService,
private readonly creditNoteBrandingTemplate: CreditNoteBrandingTemplate, private readonly creditNoteBrandingTemplate: CreditNoteBrandingTemplate,
private readonly eventPublisher: EventEmitter2, private readonly eventPublisher: EventEmitter2,
@@ -34,40 +36,23 @@ export class GetCreditNotePdf {
private readonly pdfTemplateModel: TenantModelProxy< private readonly pdfTemplateModel: TenantModelProxy<
typeof PdfTemplateModel typeof PdfTemplateModel
>, >,
) { } ) {}
/** /**
* Retrieves credit note html content. * Retrieves sale invoice pdf content.
* @param {number} creditNoteId - Credit note id.
* @returns {Promise<string>}
*/
public async getCreditNoteHtml(creditNoteId: number): Promise<string> {
const brandingAttributes =
await this.getCreditNoteBrandingAttributes(creditNoteId);
// Map attributes to match the React component props
// The branding template returns companyLogoUri, but type may have companyLogo
const props = {
...brandingAttributes,
companyLogoUri:
(brandingAttributes as any).companyLogoUri ||
(brandingAttributes as any).companyLogo ||
'',
};
return renderCreditNotePaperTemplateHtml(props);
}
/**
* Retrieves credit note pdf content.
* @param {number} creditNoteId - Credit note id. * @param {number} creditNoteId - Credit note id.
* @returns {Promise<[Buffer, string]>} * @returns {Promise<[Buffer, string]>}
*/ */
public async getCreditNotePdf( public async getCreditNotePdf(
creditNoteId: number, creditNoteId: number,
): Promise<[Buffer, string]> { ): Promise<[Buffer, string]> {
const brandingAttributes =
await this.getCreditNoteBrandingAttributes(creditNoteId);
const htmlContent = await this.templateInjectable.render(
'modules/credit-note-standard',
brandingAttributes,
);
const filename = await this.getCreditNoteFilename(creditNoteId); const filename = await this.getCreditNoteFilename(creditNoteId);
const htmlContent = await this.getCreditNoteHtml(creditNoteId);
const document = const document =
await this.chromiumlyTenancy.convertHtmlContent(htmlContent); await this.chromiumlyTenancy.convertHtmlContent(htmlContent);

View File

@@ -5,10 +5,10 @@ import { ExpensesSampleData } from './constants';
import { CreateExpense } from './commands/CreateExpense.service'; import { CreateExpense } from './commands/CreateExpense.service';
import { CreateExpenseDto } from './dtos/Expense.dto'; import { CreateExpenseDto } from './dtos/Expense.dto';
import { ImportableService } from '../Import/decorators/Import.decorator'; import { ImportableService } from '../Import/decorators/Import.decorator';
import { Expense } from './models/Expense.model'; import { ManualJournal } from '../ManualJournals/models/ManualJournal';
@Injectable() @Injectable()
@ImportableService({ name: Expense.name }) @ImportableService({ name: ManualJournal.name })
export class ExpensesImportable extends Importable { export class ExpensesImportable extends Importable {
constructor(private readonly createExpenseService: CreateExpense) { constructor(private readonly createExpenseService: CreateExpense) {
super(); super();

View File

@@ -135,7 +135,7 @@ export const ExpenseMeta = {
relationModel: 'Account', relationModel: 'Account',
relationImportMatch: ['name', 'code'], relationImportMatch: ['name', 'code'],
required: true, required: true,
importHint: 'account.field.account_hint', importHint: 'Matches the account name or code.',
}, },
referenceNo: { referenceNo: {
name: 'expense.field.reference_no', name: 'expense.field.reference_no',
@@ -169,7 +169,7 @@ export const ExpenseMeta = {
relationModel: 'Account', relationModel: 'Account',
relationImportMatch: ['name', 'code'], relationImportMatch: ['name', 'code'],
required: true, required: true,
importHint: 'account.field.account_hint', importHint: 'Matches the account name or code.',
}, },
amount: { amount: {
name: 'expense.field.amount', name: 'expense.field.amount',
@@ -187,7 +187,7 @@ export const ExpenseMeta = {
fieldType: 'boolean', fieldType: 'boolean',
}, },
branchId: { branchId: {
name: 'invoice.field.branch', name: 'Branch',
fieldType: 'relation', fieldType: 'relation',
relationModel: 'Branch', relationModel: 'Branch',
relationImportMatch: ['name', 'code'], relationImportMatch: ['name', 'code'],

View File

@@ -1,13 +1,14 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { ChromiumlyTenancy } from '../ChromiumlyTenancy/ChromiumlyTenancy.service'; import { ChromiumlyTenancy } from '../ChromiumlyTenancy/ChromiumlyTenancy.service';
import { renderExportResourceTableTemplateHtml } from '@bigcapital/pdf-templates'; import { TemplateInjectable } from '../TemplateInjectable/TemplateInjectable.service';
import { mapPdfRows } from './utils'; import { mapPdfRows } from './utils';
@Injectable() @Injectable()
export class ExportPdf { export class ExportPdf {
constructor( constructor(
private readonly templateInjectable: TemplateInjectable,
private readonly chromiumlyTenancy: ChromiumlyTenancy, private readonly chromiumlyTenancy: ChromiumlyTenancy,
) { } ) {}
/** /**
* Generates the pdf table sheet for the given data and columns. * Generates the pdf table sheet for the given data and columns.
@@ -18,18 +19,21 @@ export class ExportPdf {
* @returns * @returns
*/ */
public async pdf( public async pdf(
columns: { accessor: string; name?: string; style?: string; group?: string }[], columns: { accessor: string },
data: Record<string, any>, data: Record<string, any>,
sheetTitle: string = '', sheetTitle: string = '',
sheetDescription: string = '' sheetDescription: string = ''
) { ) {
const rows = mapPdfRows(columns, data); const rows = mapPdfRows(columns, data);
const htmlContent = renderExportResourceTableTemplateHtml({ const htmlContent = await this.templateInjectable.render(
table: { rows, columns }, 'modules/export-resource-table',
sheetTitle, {
sheetDescription, table: { rows, columns },
}); sheetTitle,
sheetDescription,
}
);
// Convert the HTML content to PDF // Convert the HTML content to PDF
return this.chromiumlyTenancy.convertHtmlContent(htmlContent, { return this.chromiumlyTenancy.convertHtmlContent(htmlContent, {
margins: { top: 0.2, bottom: 0.2, left: 0.2, right: 0.2 }, margins: { top: 0.2, bottom: 0.2, left: 0.2, right: 0.2 },

View File

@@ -211,7 +211,7 @@ export class InventoryValuationSheet extends FinancialSheet {
* Detarmines whether the items post filter is active. * Detarmines whether the items post filter is active.
*/ */
private isItemsPostFilter = (): boolean => { private isItemsPostFilter = (): boolean => {
return !isEmpty(this.query.itemsIds); return isEmpty(this.query.itemsIds);
}; };
/** /**

View File

@@ -18,7 +18,7 @@ export class InventoryValuationSheetService {
private readonly inventoryValuationMeta: InventoryValuationMetaInjectable, private readonly inventoryValuationMeta: InventoryValuationMetaInjectable,
private readonly eventPublisher: EventEmitter2, private readonly eventPublisher: EventEmitter2,
private readonly inventoryValuationSheetRepository: InventoryValuationSheetRepository, private readonly inventoryValuationSheetRepository: InventoryValuationSheetRepository,
) { } ) {}
/** /**
* Inventory valuation sheet. * Inventory valuation sheet.

View File

@@ -172,10 +172,7 @@ export class TrialBalanceSheet extends FinancialSheet {
private filterNoneTransactions = ( private filterNoneTransactions = (
accountNode: ITrialBalanceAccount accountNode: ITrialBalanceAccount
): boolean => { ): boolean => {
const accountLedger = this.repository.totalAccountsLedger.whereAccountId( return false === this.repository.totalAccountsLedger.isEmpty();
accountNode.id,
);
return !accountLedger.isEmpty();
}; };
/** /**

View File

@@ -22,7 +22,7 @@ export class ImportFileCommon {
private readonly importFileValidator: ImportFileDataValidator, private readonly importFileValidator: ImportFileDataValidator,
private readonly resource: ResourceService, private readonly resource: ResourceService,
private readonly importableRegistry: ImportableRegistry, private readonly importableRegistry: ImportableRegistry,
) { } ) {}
/** /**
* Imports the given parsed data to the resource storage through registered importable service. * Imports the given parsed data to the resource storage through registered importable service.

View File

@@ -18,7 +18,7 @@ import { CurrencyParsingDTOs } from './_constants';
export class ImportFileDataTransformer { export class ImportFileDataTransformer {
constructor( constructor(
private readonly resource: ResourceService, private readonly resource: ResourceService,
) { } ) {}
/** /**
* Parses the given sheet data before passing to the service layer. * Parses the given sheet data before passing to the service layer.
@@ -55,8 +55,9 @@ export class ImportFileDataTransformer {
/** /**
* Aggregates parsed data based on resource metadata configuration. * Aggregates parsed data based on resource metadata configuration.
* @param {string} resourceName - The resource name. * @param {number} tenantId
* @param {Record<string, any>} parsedData - The parsed data to aggregate. * @param {string} resourceName
* @param {Record<string, any>} parsedData
* @returns {Record<string, any>[]} * @returns {Record<string, any>[]}
*/ */
public aggregateParsedValues( public aggregateParsedValues(
@@ -109,11 +110,8 @@ export class ImportFileDataTransformer {
valueDTOs: Record<string, any>[], valueDTOs: Record<string, any>[],
trx?: Knex.Transaction trx?: Knex.Transaction
): Promise<Record<string, any>[]> { ): Promise<Record<string, any>[]> {
// Create a model resolver function that uses ResourceService // const tenantModels = this.tenancy.models(tenantId);
const modelResolver = (modelName: string) => { const _valueParser = valueParser(fields, {}, trx);
return this.resource.getResourceModel(modelName)();
};
const _valueParser = valueParser(fields, modelResolver, trx);
const _keyParser = parseKey(fields); const _keyParser = parseKey(fields);
const parseAsync = async (valueDTO) => { const parseAsync = async (valueDTO) => {

View File

@@ -19,8 +19,7 @@ export class ImportFileDataValidator {
/** /**
* Validates the given mapped DTOs and returns errors with their index. * Validates the given mapped DTOs and returns errors with their index.
* @param {ResourceMetaFieldsMap} importableFields - Already localized fields from ResourceService * @param {Record<string, any>} mappedDTOs
* @param {Record<string, any>} data
* @returns {Promise<void | ImportInsertError[]>} * @returns {Promise<void | ImportInsertError[]>}
*/ */
public async validateData( public async validateData(

View File

@@ -24,7 +24,7 @@ export class ImportFileUploadService {
@Inject(ImportModel.name) @Inject(ImportModel.name)
private readonly importModel: typeof ImportModel, private readonly importModel: typeof ImportModel,
) { } ) {}
/** /**
* Imports the specified file for the given resource. * Imports the specified file for the given resource.
@@ -84,7 +84,7 @@ export class ImportFileUploadService {
} catch (error) { } catch (error) {
throw error; throw error;
} }
const _params = await this.importFileCommon.transformParams(resource, params); const _params = this.importFileCommon.transformParams(resource, params);
const paramsStringified = JSON.stringify(_params); const paramsStringified = JSON.stringify(_params);
const tenant = await this.tenancyContext.getTenant(); const tenant = await this.tenancyContext.getTenant();

View File

@@ -6,7 +6,7 @@ import { ContextIdFactory, ModuleRef } from '@nestjs/core';
@Injectable() @Injectable()
export class ImportableRegistry { export class ImportableRegistry {
constructor(private readonly moduleRef: ModuleRef) { } constructor(private readonly moduleRef: ModuleRef) {}
/** /**
* Retrieves the importable service instance of the given resource name. * Retrieves the importable service instance of the given resource name.
* @param {string} name * @param {string} name
@@ -15,12 +15,6 @@ export class ImportableRegistry {
public async getImportable(name: string) { public async getImportable(name: string) {
const _name = this.sanitizeResourceName(name); const _name = this.sanitizeResourceName(name);
const importable = getImportableService(_name); const importable = getImportableService(_name);
if (!importable) {
throw new Error(
`No importable service found for resource "${_name}". Make sure the resource has an @ImportableService decorator registered.`,
);
}
const contextId = ContextIdFactory.create(); const contextId = ContextIdFactory.create();
const importableInstance = await this.moduleRef.resolve(importable, contextId, { const importableInstance = await this.moduleRef.resolve(importable, contextId, {

View File

@@ -1,287 +0,0 @@
import { aggregate } from './_utils';
describe('aggregate', () => {
describe('basic aggregation', () => {
it('should aggregate entries with matching comparator attribute', () => {
const input = [
{ id: 1, name: 'John', entries: ['entry1'] },
{ id: 2, name: 'Jane', entries: ['entry2'] },
{ id: 1, name: 'John', entries: ['entry3'] },
];
const result = aggregate(input, 'id', 'entries');
expect(result).toHaveLength(2);
expect(result[0]).toEqual({
id: 1,
name: 'John',
entries: ['entry1', 'entry3'],
});
expect(result[1]).toEqual({
id: 2,
name: 'Jane',
entries: ['entry2'],
});
});
it('should preserve order of first occurrence', () => {
const input = [
{ id: 2, entries: ['a'] },
{ id: 1, entries: ['b'] },
{ id: 2, entries: ['c'] },
];
const result = aggregate(input, 'id', 'entries');
expect(result[0].id).toBe(2);
expect(result[1].id).toBe(1);
});
});
describe('no matching entries', () => {
it('should return all entries unchanged when no comparator matches', () => {
const input = [
{ id: 1, name: 'John', entries: ['entry1'] },
{ id: 2, name: 'Jane', entries: ['entry2'] },
{ id: 3, name: 'Bob', entries: ['entry3'] },
];
const result = aggregate(input, 'id', 'entries');
expect(result).toHaveLength(3);
expect(result).toEqual(input);
});
});
describe('edge cases', () => {
it('should return empty array when input is empty', () => {
const result = aggregate([], 'id', 'entries');
expect(result).toEqual([]);
expect(result).toHaveLength(0);
});
it('should return single entry unchanged when input has one item', () => {
const input = [{ id: 1, name: 'John', entries: ['entry1'] }];
const result = aggregate(input, 'id', 'entries');
expect(result).toHaveLength(1);
expect(result[0]).toEqual({
id: 1,
name: 'John',
entries: ['entry1'],
});
});
it('should handle multiple entries with same comparator value', () => {
const input = [
{ id: 1, entries: ['a'] },
{ id: 1, entries: ['b'] },
{ id: 1, entries: ['c'] },
{ id: 1, entries: ['d'] },
];
const result = aggregate(input, 'id', 'entries');
expect(result).toHaveLength(1);
expect(result[0].entries).toEqual(['a', 'b', 'c', 'd']);
});
});
describe('different comparator attributes', () => {
it('should work with string comparator attribute', () => {
const input = [
{ name: 'Product A', category: 'Electronics', entries: ['item1'] },
{ name: 'Product B', category: 'Books', entries: ['item2'] },
{ name: 'Product C', category: 'Electronics', entries: ['item3'] },
];
const result = aggregate(input, 'category', 'entries');
expect(result).toHaveLength(2);
expect(result[0]).toEqual({
name: 'Product A',
category: 'Electronics',
entries: ['item1', 'item3'],
});
expect(result[1]).toEqual({
name: 'Product B',
category: 'Books',
entries: ['item2'],
});
});
it('should not aggregate items with undefined comparator values', () => {
const input = [
{ id: undefined, entries: ['a'] },
{ id: 1, entries: ['b'] },
{ id: undefined, entries: ['c'] },
];
const result = aggregate(input, 'id', 'entries');
expect(result).toHaveLength(3);
// Items with undefined id are NOT aggregated - each remains separate
expect(result[0].entries).toEqual(['a']);
expect(result[1].entries).toEqual(['b']);
expect(result[2].entries).toEqual(['c']);
});
it('should handle null comparator values separately', () => {
const input = [
{ id: null, entries: ['a'] },
{ id: 1, entries: ['b'] },
{ id: null, entries: ['c'] },
];
const result = aggregate(input, 'id', 'entries');
expect(result).toHaveLength(3);
expect(result[0].entries).toEqual(['a']);
expect(result[1].entries).toEqual(['b']);
expect(result[2].entries).toEqual(['c']);
});
it('should not aggregate items missing the comparatorAttr property', () => {
const input = [
{ id: 1, entries: ['a'] },
{ name: 'No ID', entries: ['b'] }, // missing 'id' property
{ id: 1, entries: ['c'] },
{ entries: ['d'] }, // also missing 'id' property
];
const result = aggregate(input, 'id', 'entries');
// 3 entries: aggregated id:1, and two separate items without 'id' property
expect(result).toHaveLength(3);
// Items with id: 1 are aggregated
expect(result[0]).toEqual({ id: 1, entries: ['a', 'c'] });
// Items missing 'id' are NOT aggregated - each remains separate
expect(result[1]).toEqual({ name: 'No ID', entries: ['b'] });
expect(result[2]).toEqual({ entries: ['d'] });
});
});
describe('different group attributes', () => {
it('should work with different groupOn attribute name', () => {
const input = [
{ id: 1, items: ['item1'] },
{ id: 1, items: ['item2'] },
{ id: 2, items: ['item3'] },
];
const result = aggregate(input, 'id', 'items');
expect(result).toHaveLength(2);
expect(result[0].items).toEqual(['item1', 'item2']);
expect(result[1].items).toEqual(['item3']);
});
});
describe('complex entries', () => {
it('should aggregate entries containing objects', () => {
const input = [
{ invoiceId: 'INV-001', entries: [{ itemId: 1, quantity: 2 }] },
{ invoiceId: 'INV-002', entries: [{ itemId: 2, quantity: 1 }] },
{ invoiceId: 'INV-001', entries: [{ itemId: 3, quantity: 5 }] },
];
const result = aggregate(input, 'invoiceId', 'entries');
expect(result).toHaveLength(2);
expect(result[0].entries).toEqual([
{ itemId: 1, quantity: 2 },
{ itemId: 3, quantity: 5 },
]);
expect(result[1].entries).toEqual([{ itemId: 2, quantity: 1 }]);
});
it('should aggregate entries with multiple items in each entry', () => {
const input = [
{ id: 1, entries: ['a', 'b'] },
{ id: 1, entries: ['c', 'd'] },
{ id: 2, entries: ['e'] },
];
const result = aggregate(input, 'id', 'entries');
expect(result).toHaveLength(2);
expect(result[0].entries).toEqual(['a', 'b', 'c', 'd']);
expect(result[1].entries).toEqual(['e']);
});
});
describe('numeric comparator values', () => {
it('should correctly compare numeric values', () => {
const input = [
{ id: 1, entries: ['a'] },
{ id: 2, entries: ['b'] },
{ id: 1, entries: ['c'] },
];
const result = aggregate(input, 'id', 'entries');
expect(result).toHaveLength(2);
expect(result.find((r) => r.id === 1).entries).toEqual(['a', 'c']);
expect(result.find((r) => r.id === 2).entries).toEqual(['b']);
});
it('should treat 0 as a valid comparator value', () => {
const input = [
{ id: 0, entries: ['a'] },
{ id: 1, entries: ['b'] },
{ id: 0, entries: ['c'] },
];
const result = aggregate(input, 'id', 'entries');
expect(result).toHaveLength(2);
expect(result[0].entries).toEqual(['a', 'c']);
expect(result[1].entries).toEqual(['b']);
});
});
describe('preserving other properties', () => {
it('should preserve all properties from the first matching entry', () => {
const input = [
{ id: 1, name: 'First', extra: 'data1', entries: ['a'] },
{ id: 1, name: 'Second', extra: 'data2', entries: ['b'] },
];
const result = aggregate(input, 'id', 'entries');
expect(result).toHaveLength(1);
expect(result[0].name).toBe('First');
expect(result[0].extra).toBe('data1');
expect(result[0].entries).toEqual(['a', 'b']);
});
});
describe('empty entries arrays', () => {
it('should handle empty entries arrays', () => {
const input = [
{ id: 1, entries: [] },
{ id: 1, entries: ['a'] },
];
const result = aggregate(input, 'id', 'entries');
expect(result).toHaveLength(1);
expect(result[0].entries).toEqual(['a']);
});
it('should handle all empty entries arrays', () => {
const input = [
{ id: 1, entries: [] },
{ id: 1, entries: [] },
];
const result = aggregate(input, 'id', 'entries');
expect(result).toHaveLength(1);
expect(result[0].entries).toEqual([]);
});
});
});

View File

@@ -253,28 +253,28 @@ export const getResourceColumns = (resourceColumns: {
}) => { }) => {
const mapColumn = const mapColumn =
(group: string) => (group: string) =>
([fieldKey, { name, importHint, required, order, ...field }]: [ ([fieldKey, { name, importHint, required, order, ...field }]: [
string, string,
IModelMetaField2, IModelMetaField2,
]) => { ]) => {
const extra: Record<string, any> = {}; const extra: Record<string, any> = {};
const key = fieldKey; const key = fieldKey;
if (group) { if (group) {
extra.group = group; extra.group = group;
} }
if (field.fieldType === 'collection') { if (field.fieldType === 'collection') {
extra.fields = mapColumns(field.fields, key); extra.fields = mapColumns(field.fields, key);
} }
return { return {
key, key,
name, name,
required, required,
hint: importHint, hint: importHint,
order, order,
...extra, ...extra,
};
}; };
};
const sortColumn = (a, b) => const sortColumn = (a, b) =>
a.order && b.order ? a.order - b.order : a.order ? -1 : b.order ? 1 : 0; a.order && b.order ? a.order - b.order : a.order ? -1 : b.order ? 1 : 0;
@@ -284,54 +284,52 @@ export const getResourceColumns = (resourceColumns: {
return R.compose(transformInputToGroupedFields, mapColumns)(resourceColumns); return R.compose(transformInputToGroupedFields, mapColumns)(resourceColumns);
}; };
export type ModelResolver = (modelName: string) => any;
// Prases the given object value based on the field key type. // Prases the given object value based on the field key type.
export const valueParser = export const valueParser =
(fields: ResourceMetaFieldsMap, modelResolver: ModelResolver, trx?: Knex.Transaction) => (fields: ResourceMetaFieldsMap, tenantModels: any, trx?: Knex.Transaction) =>
async (value: any, key: string, group = '') => { async (value: any, key: string, group = '') => {
let _value = value; let _value = value;
const fieldKey = key.includes('.') ? key.split('.')[0] : key; const fieldKey = key.includes('.') ? key.split('.')[0] : key;
const field = group ? fields[group]?.fields[fieldKey] : fields[fieldKey]; const field = group ? fields[group]?.fields[fieldKey] : fields[fieldKey];
// Parses the boolean value. // Parses the boolean value.
if (field.fieldType === 'boolean') { if (field.fieldType === 'boolean') {
_value = parseBoolean(value); _value = parseBoolean(value);
// Parses the enumeration value. // Parses the enumeration value.
} else if (field.fieldType === 'enumeration') { } else if (field.fieldType === 'enumeration') {
const option = get(field, 'options', []).find( const option = get(field, 'options', []).find(
(option) => option.label?.toLowerCase() === value?.toLowerCase(), (option) => option.label?.toLowerCase() === value?.toLowerCase(),
); );
_value = get(option, 'key'); _value = get(option, 'key');
// Parses the numeric value. // Parses the numeric value.
} else if (field.fieldType === 'number') { } else if (field.fieldType === 'number') {
_value = multiNumberParse(value); _value = multiNumberParse(value);
// Parses the relation value. // Parses the relation value.
} else if (field.fieldType === 'relation') { } else if (field.fieldType === 'relation') {
const RelationModel = modelResolver(field.relationModel); const RelationModel = tenantModels[field.relationModel];
if (!RelationModel) { if (!RelationModel) {
throw new Error(`The relation model of ${key} field is not exist.`); throw new Error(`The relation model of ${key} field is not exist.`);
}
const relationQuery = RelationModel.query(trx);
const relationKeys = castArray(field?.relationImportMatch);
relationQuery.where(function () {
relationKeys.forEach((relationKey: string) => {
this.orWhereRaw('LOWER(??) = LOWER(?)', [relationKey, value]);
});
});
const result = await relationQuery.first();
_value = get(result, 'id');
} else if (field.fieldType === 'collection') {
const ObjectFieldKey = key.includes('.') ? key.split('.')[1] : key;
const _valueParser = valueParser(fields, modelResolver);
_value = await _valueParser(value, ObjectFieldKey, fieldKey);
} }
return _value; const relationQuery = RelationModel.query(trx);
}; const relationKeys = castArray(field?.relationImportMatch);
relationQuery.where(function () {
relationKeys.forEach((relationKey: string) => {
this.orWhereRaw('LOWER(??) = LOWER(?)', [relationKey, value]);
});
});
const result = await relationQuery.first();
_value = get(result, 'id');
} else if (field.fieldType === 'collection') {
const ObjectFieldKey = key.includes('.') ? key.split('.')[1] : key;
const _valueParser = valueParser(fields, tenantModels);
_value = await _valueParser(value, ObjectFieldKey, fieldKey);
}
return _value;
};
/** /**
* Parses the field key and detarmines the key path. * Parses the field key and detarmines the key path.
@@ -404,17 +402,12 @@ export function aggregate(
groupOn: string, groupOn: string,
): Array<Record<string, any>> { ): Array<Record<string, any>> {
return input.reduce((acc, curr) => { return input.reduce((acc, curr) => {
// Skip aggregation if the current item doesn't have the comparator attribute
if (curr[comparatorAttr] === undefined || curr[comparatorAttr] === null) {
acc.push({ ...curr });
return acc;
}
const existingEntry = acc.find( const existingEntry = acc.find(
(entry) => entry[comparatorAttr] === curr[comparatorAttr], (entry) => entry[comparatorAttr] === curr[comparatorAttr],
); );
if (existingEntry) { if (existingEntry) {
existingEntry[groupOn].push(...curr[groupOn]); existingEntry[groupOn].push(...curr.entries);
} else { } else {
acc.push({ ...curr }); acc.push({ ...curr });
} }

View File

@@ -93,7 +93,7 @@ export class InventoryComputeCostService {
*/ */
async scheduleComputeItemCost(itemId: number, startingDate: Date | string) { async scheduleComputeItemCost(itemId: number, startingDate: Date | string) {
const debounceKey = `inventory-cost-compute-debounce:${itemId}`; const debounceKey = `inventory-cost-compute-debounce:${itemId}`;
const debounceTime = 1000 * 10; // 10 seconds const debounceTime = 1000 * 60; // 1 minute
// Generate a unique job ID or use a custom identifier // Generate a unique job ID or use a custom identifier
const jobId = `task-${Date.now()}-${Math.random().toString(36).substring(2)}`; const jobId = `task-${Date.now()}-${Math.random().toString(36).substring(2)}`;

View File

@@ -2,8 +2,7 @@ import { EventEmitter2 } from '@nestjs/event-emitter';
import { Processor, WorkerHost } from '@nestjs/bullmq'; import { Processor, WorkerHost } from '@nestjs/bullmq';
import { Scope } from '@nestjs/common'; import { Scope } from '@nestjs/common';
import { Job } from 'bullmq'; import { Job } from 'bullmq';
import { ClsService, UseCls } from 'nestjs-cls'; import { ClsService } from 'nestjs-cls';
import * as moment from 'moment';
import { TenantJobPayload } from '@/interfaces/Tenant'; import { TenantJobPayload } from '@/interfaces/Tenant';
import { InventoryComputeCostService } from '../commands/InventoryComputeCost.service'; import { InventoryComputeCostService } from '../commands/InventoryComputeCost.service';
import { events } from '@/common/events/events'; import { events } from '@/common/events/events';
@@ -15,7 +14,7 @@ import { Process } from '@nestjs/bull';
interface ComputeItemCostJobPayload extends TenantJobPayload { interface ComputeItemCostJobPayload extends TenantJobPayload {
itemId: number; itemId: number;
startingDate: Date | string; startingDate: Date;
} }
@Processor({ @Processor({
name: ComputeItemCostQueue, name: ComputeItemCostQueue,
@@ -40,34 +39,28 @@ export class ComputeItemCostProcessor extends WorkerHost {
* @param {Job<ComputeItemCostJobPayload>} job - The job to process * @param {Job<ComputeItemCostJobPayload>} job - The job to process
*/ */
@Process(ComputeItemCostQueueJob) @Process(ComputeItemCostQueueJob)
@UseCls()
async process(job: Job<ComputeItemCostJobPayload>) { async process(job: Job<ComputeItemCostJobPayload>) {
const { itemId, startingDate, organizationId, userId } = job.data; const { itemId, startingDate, organizationId, userId } = job.data;
// Parse startingDate using moment to handle both Date and string formats console.log(`Compute item cost for item ${itemId} started`);
const startingDateObj = moment(startingDate).toDate();
console.log(`[info] Compute item cost for item ${itemId} started`, {
payload: job.data,
jobId: job.id
});
this.clsService.set('organizationId', organizationId); this.clsService.set('organizationId', organizationId);
this.clsService.set('userId', userId); this.clsService.set('userId', userId);
try { try {
await this.inventoryComputeCostService.computeItemCost( await this.inventoryComputeCostService.computeItemCost(
startingDateObj, startingDate,
itemId, itemId,
); );
// Emit job completed event // Emit job completed event
await this.eventEmitter.emitAsync( await this.eventEmitter.emitAsync(
events.inventory.onComputeItemCostJobCompleted, events.inventory.onComputeItemCostJobCompleted,
{ startingDate: startingDateObj, itemId, organizationId, userId }, { startingDate, itemId, organizationId, userId },
); );
console.log(`[info] Compute item cost for item ${itemId} completed successfully`);
console.log(`Compute item cost for item ${itemId} completed`);
} catch (error) { } catch (error) {
console.error(`[error] Error computing item cost for item ${itemId}:`, error); console.error('Error computing item cost:', error);
console.error('Error stack:', error instanceof Error ? error.stack : 'No stack trace');
throw error; throw error;
} }
} }

View File

@@ -267,28 +267,28 @@ export const ItemMeta = {
fieldType: 'relation', fieldType: 'relation',
relationModel: 'Account', relationModel: 'Account',
relationImportMatch: ['name', 'code'], relationImportMatch: ['name', 'code'],
importHint: 'account.field.account_hint', importHint: 'Matches the account name or code.',
}, },
sellAccountId: { sellAccountId: {
name: 'item.field.sell_account', name: 'item.field.sell_account',
fieldType: 'relation', fieldType: 'relation',
relationModel: 'Account', relationModel: 'Account',
relationImportMatch: ['name', 'code'], relationImportMatch: ['name', 'code'],
importHint: 'account.field.account_hint', importHint: 'Matches the account name or code.',
}, },
inventoryAccountId: { inventoryAccountId: {
name: 'item.field.inventory_account', name: 'item.field.inventory_account',
fieldType: 'relation', fieldType: 'relation',
relationModel: 'Account', relationModel: 'Account',
relationImportMatch: ['name', 'code'], relationImportMatch: ['name', 'code'],
importHint: 'account.field.account_hint', importHint: 'Matches the account name or code.',
}, },
sellDescription: { sellDescription: {
name: 'item.field.sell_description', name: 'Sell Description',
fieldType: 'text', fieldType: 'text',
}, },
purchaseDescription: { purchaseDescription: {
name: 'item.field.purchase_description', name: 'Purchase Description',
fieldType: 'text', fieldType: 'text',
}, },
note: { note: {
@@ -300,7 +300,7 @@ export const ItemMeta = {
fieldType: 'relation', fieldType: 'relation',
relationModel: 'ItemCategory', relationModel: 'ItemCategory',
relationImportMatch: ['name'], relationImportMatch: ['name'],
importHint: 'item.field.category_hint', importHint: 'Matches the category name.',
}, },
active: { active: {
name: 'item.field.active', name: 'item.field.active',

View File

@@ -74,9 +74,6 @@ export class PaymentReceivedResponseDto {
@ApiProperty({ description: 'The formatted amount', example: '100.00' }) @ApiProperty({ description: 'The formatted amount', example: '100.00' })
formattedAmount: string; formattedAmount: string;
@ApiProperty({ description: 'The formatted total', example: '100.00 USD' })
formattedTotal: string;
@ApiProperty({ description: 'The currency code', example: 'USD' }) @ApiProperty({ description: 'The currency code', example: 'USD' })
currencyCode: string; currencyCode: string;

View File

@@ -165,12 +165,12 @@ export const PaymentReceivedMeta = {
relationModel: 'Account', relationModel: 'Account',
relationImportMatch: ['name', 'code'], relationImportMatch: ['name', 'code'],
required: true, required: true,
importHint: 'account.field.account_hint', importHint: 'Matches the account name or code.',
}, },
paymentReceiveNo: { paymentReceiveNo: {
name: 'payment_receive.field.payment_receive_no', name: 'payment_receive.field.payment_receive_no',
fieldType: 'text', fieldType: 'text',
importHint: 'payment_receive.field.payment_no_hint', importHint: 'The payment number should be unique.',
}, },
statement: { statement: {
name: 'payment_receive.field.statement', name: 'payment_receive.field.statement',
@@ -189,7 +189,7 @@ export const PaymentReceivedMeta = {
relationModel: 'SaleInvoice', relationModel: 'SaleInvoice',
relationImportMatch: 'invoiceNo', relationImportMatch: 'invoiceNo',
required: true, required: true,
importHint: 'payment_receive.field.invoice_hint', importHint: 'Matches the invoice number.',
}, },
paymentAmount: { paymentAmount: {
name: 'payment_receive.field.entries.payment_amount', name: 'payment_receive.field.entries.payment_amount',
@@ -199,7 +199,7 @@ export const PaymentReceivedMeta = {
}, },
}, },
branchId: { branchId: {
name: 'invoice.field.branch', name: 'Branch',
fieldType: 'relation', fieldType: 'relation',
relationModel: 'Branch', relationModel: 'Branch',
relationImportMatch: ['name', 'code'], relationImportMatch: ['name', 'code'],

View File

@@ -11,7 +11,6 @@ export class PaymentReceiveTransfromer extends Transformer {
public includeAttributes = (): string[] => { public includeAttributes = (): string[] => {
return [ return [
'subtotalFormatted', 'subtotalFormatted',
'formatttedTotal',
'formattedPaymentDate', 'formattedPaymentDate',
'formattedCreatedAt', 'formattedCreatedAt',
'formattedAmount', 'formattedAmount',
@@ -46,18 +45,7 @@ export class PaymentReceiveTransfromer extends Transformer {
protected subtotalFormatted = (payment: PaymentReceived): string => { protected subtotalFormatted = (payment: PaymentReceived): string => {
return this.formatNumber(payment.amount, { return this.formatNumber(payment.amount, {
currencyCode: payment.currencyCode, currencyCode: payment.currencyCode,
}); money: false,
};
/**
* Retrieves the formatted total.
* @param {PaymentReceived} payment
* @returns {string}
*/
protected formatttedTotal = (payment: PaymentReceived): string => {
return this.formatNumber(payment.amount, {
currencyCode: payment.currencyCode,
money: true,
}); });
}; };
@@ -78,7 +66,7 @@ export class PaymentReceiveTransfromer extends Transformer {
* @returns {string} * @returns {string}
*/ */
protected formattedExchangeRate = (payment: PaymentReceived): string => { protected formattedExchangeRate = (payment: PaymentReceived): string => {
return this.formatNumber(payment.exchangeRate); return this.formatNumber(payment.exchangeRate, { money: false });
}; };
/** /**

View File

@@ -1,6 +1,5 @@
import { ModuleRef } from '@nestjs/core'; import { ModuleRef } from '@nestjs/core';
import { pickBy, mapValues } from 'lodash'; import { pickBy } from 'lodash';
import { I18nService } from 'nestjs-i18n';
import { WarehousesSettings } from '../Warehouses/WarehousesSettings'; import { WarehousesSettings } from '../Warehouses/WarehousesSettings';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { BranchesSettingsService } from '../Branches/BranchesSettings'; import { BranchesSettingsService } from '../Branches/BranchesSettings';
@@ -21,8 +20,7 @@ export class ResourceService {
private readonly branchesSettings: BranchesSettingsService, private readonly branchesSettings: BranchesSettingsService,
private readonly warehousesSettings: WarehousesSettings, private readonly warehousesSettings: WarehousesSettings,
private readonly moduleRef: ModuleRef, private readonly moduleRef: ModuleRef,
private readonly i18nService: I18nService, ) {}
) { }
/** /**
* Retrieve resource model object. * Retrieve resource model object.
@@ -98,45 +96,7 @@ export class ResourceService {
}; };
/** /**
* Localizes a single field by translating its name and importHint. * Retrieve the resource fields.
* @param {IModelMetaField2} field - The field to localize.
* @returns {IModelMetaField2} - The localized field.
*/
private localizeField(field: IModelMetaField2): IModelMetaField2 {
const localizedField = {
...field,
name: this.i18nService.t(field.name, { defaultValue: field.name }),
} as IModelMetaField2;
if (field.importHint) {
localizedField.importHint = this.i18nService.t(field.importHint, {
defaultValue: field.importHint,
});
}
// Recursively localize nested fields (for collection types)
if (field.fields) {
localizedField.fields = this.localizeFields(
field.fields as unknown as Record<string, IModelMetaField2>,
) as unknown as typeof field.fields;
}
return localizedField;
}
/**
* Localizes all fields in a fields map.
* @param {Record<string, IModelMetaField2>} fields - The fields to localize.
* @returns {Record<string, IModelMetaField2>} - The localized fields.
*/
private localizeFields(
fields: Record<string, IModelMetaField2>,
): Record<string, IModelMetaField2> {
return mapValues(fields, (field) => this.localizeField(field));
}
/**
* Retrieve the resource fields with localized names and hints.
* @param {string} modelName * @param {string} modelName
* @returns {IModelMetaField2} * @returns {IModelMetaField2}
*/ */
@@ -144,11 +104,8 @@ export class ResourceService {
[key: string]: IModelMetaField2; [key: string]: IModelMetaField2;
} { } {
const meta = this.getResourceMeta(modelName); const meta = this.getResourceMeta(modelName);
const filteredFields = this.filterSupportFeatures(meta.fields2);
return this.localizeFields( return this.filterSupportFeatures(meta.fields2);
filteredFields as Record<string, IModelMetaField2>,
);
} }
/** /**

View File

@@ -191,52 +191,52 @@ export const SaleEstimateMeta = {
}, },
fields2: { fields2: {
customerId: { customerId: {
name: 'estimate.field.customer', name: 'Customer',
fieldType: 'relation', fieldType: 'relation',
relationModel: 'Contact', relationModel: 'Contact',
relationImportMatch: ['displayName'], relationImportMatch: ['displayName'],
required: true, required: true,
}, },
estimateDate: { estimateDate: {
name: 'estimate.field.estimate_date', name: 'Estimate Date',
fieldType: 'date', fieldType: 'date',
required: true, required: true,
}, },
expirationDate: { expirationDate: {
name: 'estimate.field.expiration_date', name: 'Expiration Date',
fieldType: 'date', fieldType: 'date',
required: true, required: true,
}, },
estimateNumber: { estimateNumber: {
name: 'estimate.field.estimate_number', name: 'Estimate No.',
fieldType: 'text', fieldType: 'text',
}, },
reference: { reference: {
name: 'estimate.field.reference_no', name: 'Reference No.',
fieldType: 'text', fieldType: 'text',
}, },
exchangeRate: { exchangeRate: {
name: 'estimate.field.exchange_rate', name: 'Exchange Rate',
fieldType: 'number', fieldType: 'number',
}, },
currencyCode: { currencyCode: {
name: 'estimate.field.currency', name: 'Currency',
fieldType: 'text', fieldType: 'text',
}, },
note: { note: {
name: 'estimate.field.note', name: 'Note',
fieldType: 'text', fieldType: 'text',
}, },
termsConditions: { termsConditions: {
name: 'estimate.field.terms_conditions', name: 'Terms & Conditions',
fieldType: 'text', fieldType: 'text',
}, },
delivered: { delivered: {
name: 'estimate.field.delivered', name: 'Delivered',
type: 'boolean', type: 'boolean',
}, },
entries: { entries: {
name: 'estimate.field.entries', name: 'Entries',
fieldType: 'collection', fieldType: 'collection',
collectionOf: 'object', collectionOf: 'object',
collectionMinLength: 1, collectionMinLength: 1,
@@ -248,7 +248,7 @@ export const SaleEstimateMeta = {
relationModel: 'Item', relationModel: 'Item',
relationImportMatch: ['name', 'code'], relationImportMatch: ['name', 'code'],
required: true, required: true,
importHint: 'invoice.field.item_hint', importHint: 'Matches the item name or code.',
}, },
rate: { rate: {
name: 'invoice.field.rate', name: 'invoice.field.rate',
@@ -261,13 +261,13 @@ export const SaleEstimateMeta = {
required: true, required: true,
}, },
description: { description: {
name: 'invoice.field.description', name: 'Line Description',
fieldType: 'text', fieldType: 'text',
}, },
}, },
}, },
branchId: { branchId: {
name: 'invoice.field.branch', name: 'Branch',
fieldType: 'relation', fieldType: 'relation',
relationModel: 'Branch', relationModel: 'Branch',
relationImportMatch: ['name', 'code'], relationImportMatch: ['name', 'code'],
@@ -275,7 +275,7 @@ export const SaleEstimateMeta = {
required: true, required: true,
}, },
warehouseId: { warehouseId: {
name: 'invoice.field.warehouse', name: 'Warehouse',
fieldType: 'relation', fieldType: 'relation',
relationModel: 'Warehouse', relationModel: 'Warehouse',
relationImportMatch: ['name', 'code'], relationImportMatch: ['name', 'code'],

View File

@@ -17,7 +17,7 @@ export class SaleEstimateTransfromer extends Transformer {
'formattedDeliveredAtDate', 'formattedDeliveredAtDate',
'formattedApprovedAtDate', 'formattedApprovedAtDate',
'formattedRejectedAtDate', 'formattedRejectedAtDate',
'discountAmountFormatted', 'discountAmountFormatted',
'discountPercentageFormatted', 'discountPercentageFormatted',
'adjustmentFormatted', 'adjustmentFormatted',
@@ -135,7 +135,7 @@ export class SaleEstimateTransfromer extends Transformer {
* @returns {string} * @returns {string}
*/ */
protected adjustmentFormatted = (estimate: SaleEstimate): string => { protected adjustmentFormatted = (estimate: SaleEstimate): string => {
return this.formatNumber(estimate.adjustment, { return this.formatMoney(estimate.adjustment, {
currencyCode: estimate.currencyCode, currencyCode: estimate.currencyCode,
excerptZero: true, excerptZero: true,
}); });

View File

@@ -19,7 +19,7 @@ export class SaleInvoiceCostGLEntries {
private readonly inventoryCostLotTracker: TenantModelProxy< private readonly inventoryCostLotTracker: TenantModelProxy<
typeof InventoryCostLotTracker typeof InventoryCostLotTracker
>, >,
) { } ) {}
/** /**
* Writes journal entries from sales invoices. * Writes journal entries from sales invoices.

View File

@@ -5,10 +5,10 @@ import { Importable } from '@/modules/Import/Importable';
import { CreateSaleInvoiceDto } from '../dtos/SaleInvoice.dto'; import { CreateSaleInvoiceDto } from '../dtos/SaleInvoice.dto';
import { SaleInvoicesSampleData } from '../constants'; import { SaleInvoicesSampleData } from '../constants';
import { ImportableService } from '@/modules/Import/decorators/Import.decorator'; import { ImportableService } from '@/modules/Import/decorators/Import.decorator';
import { SaleInvoice } from '../models/SaleInvoice'; import { ManualJournal } from '@/modules/ManualJournals/models/ManualJournal';
@Injectable() @Injectable()
@ImportableService({ name: SaleInvoice.name }) @ImportableService({ name: ManualJournal.name })
export class SaleInvoicesImportable extends Importable { export class SaleInvoicesImportable extends Importable {
constructor(private readonly createInvoiceService: CreateSaleInvoice) { constructor(private readonly createInvoiceService: CreateSaleInvoice) {
super(); super();

View File

@@ -259,7 +259,7 @@ export const SaleInvoiceMeta = {
relationModel: 'Item', relationModel: 'Item',
relationImportMatch: ['name', 'code'], relationImportMatch: ['name', 'code'],
required: true, required: true,
importHint: 'invoice.field.item_hint', importHint: 'Matches the item name or code.',
}, },
rate: { rate: {
name: 'invoice.field.rate', name: 'invoice.field.rate',
@@ -283,7 +283,7 @@ export const SaleInvoiceMeta = {
printable: false, printable: false,
}, },
branchId: { branchId: {
name: 'invoice.field.branch', name: 'Branch',
fieldType: 'relation', fieldType: 'relation',
relationModel: 'Branch', relationModel: 'Branch',
relationImportMatch: ['name', 'code'], relationImportMatch: ['name', 'code'],
@@ -291,7 +291,7 @@ export const SaleInvoiceMeta = {
required: true, required: true,
}, },
warehouseId: { warehouseId: {
name: 'invoice.field.warehouse', name: 'Warehouse',
fieldType: 'relation', fieldType: 'relation',
relationModel: 'Warehouse', relationModel: 'Warehouse',
relationImportMatch: ['name', 'code'], relationImportMatch: ['name', 'code'],

View File

@@ -70,7 +70,6 @@ export class SaleInvoiceTransformer extends Transformer {
protected dueAmountFormatted = (invoice: SaleInvoice): string => { protected dueAmountFormatted = (invoice: SaleInvoice): string => {
return this.formatNumber(invoice.dueAmount, { return this.formatNumber(invoice.dueAmount, {
currencyCode: invoice.currencyCode, currencyCode: invoice.currencyCode,
money: true
}); });
}; };
@@ -114,6 +113,7 @@ export class SaleInvoiceTransformer extends Transformer {
protected subtotalFormatted = (invoice: SaleInvoice): string => { protected subtotalFormatted = (invoice: SaleInvoice): string => {
return this.formatNumber(invoice.subtotal, { return this.formatNumber(invoice.subtotal, {
currencyCode: this.context.organization.baseCurrency, currencyCode: this.context.organization.baseCurrency,
money: false,
}); });
}; };
@@ -170,7 +170,6 @@ export class SaleInvoiceTransformer extends Transformer {
protected totalFormatted = (invoice: SaleInvoice): string => { protected totalFormatted = (invoice: SaleInvoice): string => {
return this.formatNumber(invoice.total, { return this.formatNumber(invoice.total, {
currencyCode: invoice.currencyCode, currencyCode: invoice.currencyCode,
money: true
}); });
}; };
@@ -213,7 +212,7 @@ export class SaleInvoiceTransformer extends Transformer {
* @returns {string} * @returns {string}
*/ */
protected adjustmentFormatted = (invoice: SaleInvoice): string => { protected adjustmentFormatted = (invoice: SaleInvoice): string => {
return this.formatNumber(invoice.adjustment, { return this.formatMoney(invoice.adjustment, {
currencyCode: invoice.currencyCode, currencyCode: invoice.currencyCode,
}) })
} }

View File

@@ -10,7 +10,7 @@ import { events } from '@/common/events/events';
@Injectable() @Injectable()
export class InvoiceGLEntriesSubscriber { export class InvoiceGLEntriesSubscriber {
constructor(public readonly saleInvoiceGLEntries: SaleInvoiceGLEntries) { } constructor(public readonly saleInvoiceGLEntries: SaleInvoiceGLEntries) {}
/** /**
* Records journal entries of the non-inventory invoice. * Records journal entries of the non-inventory invoice.

View File

@@ -0,0 +1,143 @@
import * as R from 'ramda';
import { Knex } from 'knex';
import { Inject, Injectable } from '@nestjs/common';
import { TenantModelProxy } from '../System/models/TenantBaseModel';
import { InventoryCostLotTracker } from '../InventoryCost/models/InventoryCostLotTracker';
import { LedgerStorageService } from '../Ledger/LedgerStorage.service';
import { groupInventoryTransactionsByTypeId } from '../InventoryCost/utils';
import { Ledger } from '../Ledger/Ledger';
import { AccountNormal } from '@/interfaces/Account';
import { ILedgerEntry } from '../Ledger/types/Ledger.types';
import { increment } from '@/utils/increment';
@Injectable()
export class SaleReceiptCostGLEntries {
constructor(
private readonly ledgerStorage: LedgerStorageService,
@Inject(InventoryCostLotTracker.name)
private readonly inventoryCostLotTracker: TenantModelProxy<
typeof InventoryCostLotTracker
>,
) {}
/**
* Writes journal entries from sales receipts.
* @param {Date} startingDate - Starting date.
* @param {Knex.Transaction} trx - Transaction.
*/
public writeInventoryCostJournalEntries = async (
startingDate: Date,
trx?: Knex.Transaction,
): Promise<void> => {
const inventoryCostLotTrans = await this.inventoryCostLotTracker()
.query()
.where('direction', 'OUT')
.where('transaction_type', 'SaleReceipt')
.where('cost', '>', 0)
.modify('filterDateRange', startingDate)
.orderBy('date', 'ASC')
.withGraphFetched('receipt')
.withGraphFetched('item')
.withGraphFetched('itemEntry');
const ledger = this.getInventoryCostLotsLedger(inventoryCostLotTrans);
await this.ledgerStorage.commit(ledger, trx);
};
/**
* Retrieves the inventory cost lots ledger.
*/
private getInventoryCostLotsLedger = (
inventoryCostLots: InventoryCostLotTracker[],
) => {
const inventoryTransactions =
groupInventoryTransactionsByTypeId(inventoryCostLots);
const entries = inventoryTransactions
.map(this.getSaleReceiptCostGLEntries)
.flat();
return new Ledger(entries);
};
/**
* Builds the common GL entry fields for a sale receipt cost.
*/
private getReceiptCostGLCommonEntry = (
inventoryCostLot: InventoryCostLotTracker,
) => {
return {
currencyCode: inventoryCostLot.receipt.currencyCode,
exchangeRate: inventoryCostLot.receipt.exchangeRate,
transactionType: inventoryCostLot.transactionType,
transactionId: inventoryCostLot.transactionId,
transactionNumber: inventoryCostLot.receipt.receiptNumber,
referenceNumber: inventoryCostLot.receipt.referenceNo,
date: inventoryCostLot.date,
indexGroup: 20,
costable: true,
createdAt: inventoryCostLot.createdAt,
debit: 0,
credit: 0,
branchId: inventoryCostLot.receipt.branchId,
};
};
/**
* Retrieves the inventory cost GL entry for a single lot.
*/
private getInventoryCostGLEntry = R.curry(
(
getIndexIncrement: () => number,
inventoryCostLot: InventoryCostLotTracker,
): ILedgerEntry[] => {
const commonEntry = this.getReceiptCostGLCommonEntry(inventoryCostLot);
const costAccountId =
inventoryCostLot.costAccountId || inventoryCostLot.item.costAccountId;
const description = inventoryCostLot.itemEntry?.description || null;
const costEntry = {
...commonEntry,
debit: inventoryCostLot.cost,
accountId: costAccountId,
accountNormal: AccountNormal.DEBIT,
itemId: inventoryCostLot.itemId,
note: description,
index: getIndexIncrement(),
};
const inventoryEntry = {
...commonEntry,
credit: inventoryCostLot.cost,
accountId: inventoryCostLot.item.inventoryAccountId,
accountNormal: AccountNormal.DEBIT,
itemId: inventoryCostLot.itemId,
note: description,
index: getIndexIncrement(),
};
return [costEntry, inventoryEntry];
},
);
/**
* Builds GL entries for a group of sale receipt cost lots.
* - Cost of goods sold -> Debit
* - Inventory assets -> Credit
*/
public getSaleReceiptCostGLEntries = (
inventoryCostLots: InventoryCostLotTracker[],
): ILedgerEntry[] => {
const getIndexIncrement = increment(0);
const getInventoryLotEntry =
this.getInventoryCostGLEntry(getIndexIncrement);
return inventoryCostLots.map((t) => getInventoryLotEntry(t)).flat();
};
}

View File

@@ -40,6 +40,8 @@ import { SaleReceiptsImportable } from './commands/SaleReceiptsImportable';
import { GetSaleReceiptMailStateService } from './queries/GetSaleReceiptMailState.service'; import { GetSaleReceiptMailStateService } from './queries/GetSaleReceiptMailState.service';
import { GetSaleReceiptMailTemplateService } from './queries/GetSaleReceiptMailTemplate.service'; import { GetSaleReceiptMailTemplateService } from './queries/GetSaleReceiptMailTemplate.service';
import { SaleReceiptAutoIncrementSubscriber } from './subscribers/SaleReceiptAutoIncrementSubscriber'; import { SaleReceiptAutoIncrementSubscriber } from './subscribers/SaleReceiptAutoIncrementSubscriber';
import { SaleReceiptCostGLEntriesSubscriber } from './subscribers/SaleReceiptCostGLEntriesSubscriber';
import { SaleReceiptCostGLEntries } from './SaleReceiptCostGLEntries';
import { BulkDeleteSaleReceiptsService } from './BulkDeleteSaleReceipts.service'; import { BulkDeleteSaleReceiptsService } from './BulkDeleteSaleReceipts.service';
import { ValidateBulkDeleteSaleReceiptsService } from './ValidateBulkDeleteSaleReceipts.service'; import { ValidateBulkDeleteSaleReceiptsService } from './ValidateBulkDeleteSaleReceipts.service';
@@ -87,6 +89,8 @@ import { ValidateBulkDeleteSaleReceiptsService } from './ValidateBulkDeleteSaleR
GetSaleReceiptMailStateService, GetSaleReceiptMailStateService,
GetSaleReceiptMailTemplateService, GetSaleReceiptMailTemplateService,
SaleReceiptAutoIncrementSubscriber, SaleReceiptAutoIncrementSubscriber,
SaleReceiptCostGLEntries,
SaleReceiptCostGLEntriesSubscriber,
BulkDeleteSaleReceiptsService, BulkDeleteSaleReceiptsService,
ValidateBulkDeleteSaleReceiptsService, ValidateBulkDeleteSaleReceiptsService,
], ],

View File

@@ -1,148 +0,0 @@
// import { Service, Inject } from 'typedi';
// import * as R from 'ramda';
// import { Knex } from 'knex';
// import { AccountNormal, IInventoryLotCost, ILedgerEntry } from '@/interfaces';
// import { increment } from 'utils';
// import HasTenancyService from '@/services/Tenancy/TenancyService';
// import Ledger from '@/services/Accounting/Ledger';
// import LedgerStorageService from '@/services/Accounting/LedgerStorageService';
// import { groupInventoryTransactionsByTypeId } from '../../Inventory/utils';
// @Service()
// export class SaleReceiptCostGLEntries {
// @Inject()
// private tenancy: HasTenancyService;
// @Inject()
// private ledgerStorage: LedgerStorageService;
// /**
// * Writes journal entries from sales invoices.
// * @param {number} tenantId - The tenant id.
// * @param {Date} startingDate - Starting date.
// * @param {boolean} override
// */
// public writeInventoryCostJournalEntries = async (
// tenantId: number,
// startingDate: Date,
// trx?: Knex.Transaction
// ): Promise<void> => {
// const { InventoryCostLotTracker } = this.tenancy.models(tenantId);
// const inventoryCostLotTrans = await InventoryCostLotTracker.query()
// .where('direction', 'OUT')
// .where('transaction_type', 'SaleReceipt')
// .where('cost', '>', 0)
// .modify('filterDateRange', startingDate)
// .orderBy('date', 'ASC')
// .withGraphFetched('receipt')
// .withGraphFetched('item');
// const ledger = this.getInventoryCostLotsLedger(inventoryCostLotTrans);
// // Commit the ledger to the storage.
// await this.ledgerStorage.commit(tenantId, ledger, trx);
// };
// /**
// * Retrieves the inventory cost lots ledger.
// * @param {} inventoryCostLots
// * @returns {Ledger}
// */
// private getInventoryCostLotsLedger = (
// inventoryCostLots: IInventoryLotCost[]
// ) => {
// // Groups the inventory cost lots transactions.
// const inventoryTransactions =
// groupInventoryTransactionsByTypeId(inventoryCostLots);
// //
// const entries = inventoryTransactions
// .map(this.getSaleInvoiceCostGLEntries)
// .flat();
// return new Ledger(entries);
// };
// /**
// *
// * @param {IInventoryLotCost} inventoryCostLot
// * @returns {}
// */
// private getInvoiceCostGLCommonEntry = (
// inventoryCostLot: IInventoryLotCost
// ) => {
// return {
// currencyCode: inventoryCostLot.receipt.currencyCode,
// exchangeRate: inventoryCostLot.receipt.exchangeRate,
// transactionType: inventoryCostLot.transactionType,
// transactionId: inventoryCostLot.transactionId,
// date: inventoryCostLot.date,
// indexGroup: 20,
// costable: true,
// createdAt: inventoryCostLot.createdAt,
// debit: 0,
// credit: 0,
// branchId: inventoryCostLot.receipt.branchId,
// };
// };
// /**
// * Retrieves the inventory cost GL entry.
// * @param {IInventoryLotCost} inventoryLotCost
// * @returns {ILedgerEntry[]}
// */
// private getInventoryCostGLEntry = R.curry(
// (
// getIndexIncrement,
// inventoryCostLot: IInventoryLotCost
// ): ILedgerEntry[] => {
// const commonEntry = this.getInvoiceCostGLCommonEntry(inventoryCostLot);
// const costAccountId =
// inventoryCostLot.costAccountId || inventoryCostLot.item.costAccountId;
// // XXX Debit - Cost account.
// const costEntry = {
// ...commonEntry,
// debit: inventoryCostLot.cost,
// accountId: costAccountId,
// accountNormal: AccountNormal.DEBIT,
// itemId: inventoryCostLot.itemId,
// index: getIndexIncrement(),
// };
// // XXX Credit - Inventory account.
// const inventoryEntry = {
// ...commonEntry,
// credit: inventoryCostLot.cost,
// accountId: inventoryCostLot.item.inventoryAccountId,
// accountNormal: AccountNormal.DEBIT,
// itemId: inventoryCostLot.itemId,
// index: getIndexIncrement(),
// };
// return [costEntry, inventoryEntry];
// }
// );
// /**
// * Writes journal entries for given sale invoice.
// * -------
// * - Cost of goods sold -> Debit -> YYYY
// * - Inventory assets -> Credit -> YYYY
// * --------
// * @param {ISaleInvoice} saleInvoice
// * @param {JournalPoster} journal
// */
// public getSaleInvoiceCostGLEntries = (
// inventoryCostLots: IInventoryLotCost[]
// ): ILedgerEntry[] => {
// const getIndexIncrement = increment(0);
// const getInventoryLotEntry =
// this.getInventoryCostGLEntry(getIndexIncrement);
// return inventoryCostLots.map(getInventoryLotEntry).flat();
// };
// }

View File

@@ -186,42 +186,42 @@ export const SaleReceiptMeta = {
}, },
fields2: { fields2: {
receiptDate: { receiptDate: {
name: 'receipt.field.receipt_date', name: 'Receipt Date',
fieldType: 'date', fieldType: 'date',
required: true, required: true,
}, },
customerId: { customerId: {
name: 'receipt.field.customer', name: 'Customer',
fieldType: 'relation', fieldType: 'relation',
relationModel: 'Contact', relationModel: 'Contact',
relationImportMatch: 'displayName', relationImportMatch: 'displayName',
required: true, required: true,
}, },
depositAccountId: { depositAccountId: {
name: 'receipt.field.deposit_account', name: 'Deposit Account',
fieldType: 'relation', fieldType: 'relation',
relationModel: 'Account', relationModel: 'Account',
relationImportMatch: ['name', 'code'], relationImportMatch: ['name', 'code'],
required: true, required: true,
}, },
exchangeRate: { exchangeRate: {
name: 'receipt.field.exchange_rate', name: 'Exchange Rate',
fieldType: 'number', fieldType: 'number',
}, },
receiptNumber: { receiptNumber: {
name: 'receipt.field.receipt_number', name: 'Receipt Number',
fieldType: 'text', fieldType: 'text',
}, },
referenceNo: { referenceNo: {
name: 'receipt.field.reference_no', name: 'Reference No.',
fieldType: 'text', fieldType: 'text',
}, },
closed: { closed: {
name: 'receipt.field.closed', name: 'Closed',
fieldType: 'boolean', fieldType: 'boolean',
}, },
entries: { entries: {
name: 'receipt.field.entries', name: 'Entries',
fieldType: 'collection', fieldType: 'collection',
collectionOf: 'object', collectionOf: 'object',
collectionMinLength: 1, collectionMinLength: 1,
@@ -233,7 +233,7 @@ export const SaleReceiptMeta = {
relationModel: 'Item', relationModel: 'Item',
relationImportMatch: ['name', 'code'], relationImportMatch: ['name', 'code'],
required: true, required: true,
importHint: 'invoice.field.item_hint', importHint: 'Matches the item name or code.',
}, },
rate: { rate: {
name: 'invoice.field.rate', name: 'invoice.field.rate',
@@ -252,15 +252,15 @@ export const SaleReceiptMeta = {
}, },
}, },
statement: { statement: {
name: 'receipt.field.statement', name: 'Statement',
fieldType: 'text', fieldType: 'text',
}, },
receiptMessage: { receiptMessage: {
name: 'receipt.field.receipt_message', name: 'Receipt Message',
fieldType: 'text', fieldType: 'text',
}, },
branchId: { branchId: {
name: 'invoice.field.branch', name: 'Branch',
fieldType: 'relation', fieldType: 'relation',
relationModel: 'Branch', relationModel: 'Branch',
relationImportMatch: ['name', 'code'], relationImportMatch: ['name', 'code'],
@@ -268,7 +268,7 @@ export const SaleReceiptMeta = {
required: true, required: true,
}, },
warehouseId: { warehouseId: {
name: 'invoice.field.warehouse', name: 'Warehouse',
fieldType: 'relation', fieldType: 'relation',
relationModel: 'Warehouse', relationModel: 'Warehouse',
relationImportMatch: ['name', 'code'], relationImportMatch: ['name', 'code'],

View File

@@ -1,36 +1,26 @@
// import { Inject, Service } from 'typedi'; import { Injectable } from '@nestjs/common';
// import events from '@/subscribers/events'; import { OnEvent } from '@nestjs/event-emitter';
// import { IInventoryCostLotsGLEntriesWriteEvent } from '@/interfaces'; import { events } from '@/common/events/events';
// import { SaleReceiptCostGLEntries } from '../SaleReceiptCostGLEntries'; import { IInventoryCostLotsGLEntriesWriteEvent } from '@/modules/InventoryCost/types/InventoryCost.types';
import { SaleReceiptCostGLEntries } from '../SaleReceiptCostGLEntries';
// @Service() @Injectable()
// export class SaleReceiptCostGLEntriesSubscriber { export class SaleReceiptCostGLEntriesSubscriber {
// @Inject() constructor(
// private saleReceiptCostEntries: SaleReceiptCostGLEntries; private readonly saleReceiptCostEntries: SaleReceiptCostGLEntries,
) {}
// /** /**
// * Attaches events. * Writes the receipts cost GL entries once the inventory cost lots are written.
// */ */
// public attach(bus) { @OnEvent(events.inventory.onCostLotsGLEntriesWrite)
// bus.subscribe( async writeReceiptsCostEntriesOnCostLotsWritten({
// events.inventory.onCostLotsGLEntriesWrite, trx,
// this.writeJournalEntriesOnceWriteoffCreate startingDate,
// ); }: IInventoryCostLotsGLEntriesWriteEvent) {
// } await this.saleReceiptCostEntries.writeInventoryCostJournalEntries(
startingDate,
// /** trx,
// * Writes the receipts cost GL entries once the inventory cost lots be written. );
// * @param {IInventoryCostLotsGLEntriesWriteEvent} }
// */ }
// private writeJournalEntriesOnceWriteoffCreate = async ({
// trx,
// startingDate,
// tenantId,
// }: IInventoryCostLotsGLEntriesWriteEvent) => {
// await this.saleReceiptCostEntries.writeInventoryCostJournalEntries(
// tenantId,
// startingDate,
// trx
// );
// };
// }

View File

@@ -107,4 +107,4 @@ const modelProviders = models.map((model) => RegisterTenancyModel(model));
imports: [...modelProviders], imports: [...modelProviders],
exports: [...modelProviders], exports: [...modelProviders],
}) })
export class TenancyModelsModule { } export class TenancyModelsModule {}

View File

@@ -26,7 +26,7 @@ export class TransactionsLockingService {
constructor( constructor(
private readonly transactionsLockingRepo: TransactionsLockingRepository, private readonly transactionsLockingRepo: TransactionsLockingRepository,
private readonly eventPublisher: EventEmitter2, private readonly eventPublisher: EventEmitter2,
) { } ) {}
/** /**
* Enable/disable all transacations locking. * Enable/disable all transacations locking.

View File

@@ -203,7 +203,6 @@ export class Transformer<T = {}, ExtraContext = {}> {
protected formatMoney(money, options?) { protected formatMoney(money, options?) {
return formatNumber(money, { return formatNumber(money, {
currencyCode: this.context.organization.baseCurrency, currencyCode: this.context.organization.baseCurrency,
money: true,
...options, ...options,
}); });
} }

View File

@@ -177,70 +177,70 @@ export const VendorCreditMeta = {
}, },
fields2: { fields2: {
vendorId: { vendorId: {
name: 'vendor_credit.field.vendor', name: 'Vendor',
fieldType: 'relation', fieldType: 'relation',
relationModel: 'Contact', relationModel: 'Contact',
relationImportMatch: 'displayName', relationImportMatch: 'displayName',
required: true, required: true,
}, },
exchangeRate: { exchangeRate: {
name: 'vendor_credit.field.exchange_rate', name: 'Echange Rate',
fieldType: 'text', fieldType: 'text',
}, },
vendorCreditNumber: { vendorCreditNumber: {
name: 'vendor_credit.field.vendor_credit_number', name: 'Vendor Credit No.',
fieldType: 'text', fieldType: 'text',
}, },
referenceNo: { referenceNo: {
name: 'vendor_credit.field.reference_no', name: 'Refernece No.',
fieldType: 'text', fieldType: 'text',
}, },
vendorCreditDate: { vendorCreditDate: {
name: 'vendor_credit.field.vendor_credit_date', name: 'Vendor Credit Date',
fieldType: 'date', fieldType: 'date',
required: true, required: true,
}, },
note: { note: {
name: 'vendor_credit.field.note', name: 'Note',
fieldType: 'text', fieldType: 'text',
}, },
open: { open: {
name: 'vendor_credit.field.open', name: 'Open',
fieldType: 'boolean', fieldType: 'boolean',
}, },
entries: { entries: {
name: 'vendor_credit.field.entries', name: 'Entries',
fieldType: 'collection', fieldType: 'collection',
collectionOf: 'object', collectionOf: 'object',
collectionMinLength: 1, collectionMinLength: 1,
required: true, required: true,
fields: { fields: {
itemId: { itemId: {
name: 'vendor_credit.field.item', name: 'Item Name',
fieldType: 'relation', fieldType: 'relation',
relationModel: 'Item', relationModel: 'Item',
relationImportMatch: ['name', 'code'], relationImportMatch: ['name', 'code'],
required: true, required: true,
importHint: 'invoice.field.item_hint', importHint: 'Matches the item name or code.',
}, },
rate: { rate: {
name: 'vendor_credit.field.rate', name: 'Rate',
fieldType: 'number', fieldType: 'number',
required: true, required: true,
}, },
quantity: { quantity: {
name: 'vendor_credit.field.quantity', name: 'Quantity',
fieldType: 'number', fieldType: 'number',
required: true, required: true,
}, },
description: { description: {
name: 'vendor_credit.field.description', name: 'Description',
fieldType: 'text', fieldType: 'text',
}, },
}, },
}, },
branchId: { branchId: {
name: 'invoice.field.branch', name: 'Branch',
fieldType: 'relation', fieldType: 'relation',
relationModel: 'Branch', relationModel: 'Branch',
relationImportMatch: ['name', 'code'], relationImportMatch: ['name', 'code'],
@@ -248,7 +248,7 @@ export const VendorCreditMeta = {
required: true required: true
}, },
warehouseId: { warehouseId: {
name: 'invoice.field.warehouse', name: 'Warehouse',
fieldType: 'relation', fieldType: 'relation',
relationModel: 'Warehouse', relationModel: 'Warehouse',
relationImportMatch: ['name', 'code'], relationImportMatch: ['name', 'code'],

View File

@@ -2,35 +2,19 @@ import { Injectable } from '@nestjs/common';
import { DeleteRefundVendorCreditService } from './commands/DeleteRefundVendorCredit.service'; import { DeleteRefundVendorCreditService } from './commands/DeleteRefundVendorCredit.service';
import { RefundVendorCredit } from './models/RefundVendorCredit'; import { RefundVendorCredit } from './models/RefundVendorCredit';
import { CreateRefundVendorCredit } from './commands/CreateRefundVendorCredit.service'; import { CreateRefundVendorCredit } from './commands/CreateRefundVendorCredit.service';
import { IRefundVendorCreditDTO } from './types/VendorCreditRefund.types';
import { RefundVendorCreditDto } from './dtos/RefundVendorCredit.dto'; import { RefundVendorCreditDto } from './dtos/RefundVendorCredit.dto';
import { GetRefundVendorCreditsService } from './queries/GetRefundVendorCredits.service';
import { IRefundVendorCreditPOJO } from './types/VendorCreditRefund.types';
@Injectable() @Injectable()
export class VendorCreditsRefundApplication { export class VendorCreditsRefundApplication {
/** /**
* @param {CreateRefundVendorCredit} createRefundVendorCreditService * @param {CreateRefundVendorCredit} createRefundVendorCreditService
* @param {DeleteRefundVendorCreditService} deleteRefundVendorCreditService * @param {DeleteRefundVendorCreditService} deleteRefundVendorCreditService
* @param {GetRefundVendorCreditsService} getRefundVendorCreditsService
*/ */
constructor( constructor(
private readonly createRefundVendorCreditService: CreateRefundVendorCredit, private readonly createRefundVendorCreditService: CreateRefundVendorCredit,
private readonly deleteRefundVendorCreditService: DeleteRefundVendorCreditService, private readonly deleteRefundVendorCreditService: DeleteRefundVendorCreditService,
private readonly getRefundVendorCreditsService: GetRefundVendorCreditsService, ) {}
) { }
/**
* Retrieve the vendor credit refunds graph.
* @param {number} vendorCreditId - Vendor credit id.
* @returns {Promise<IRefundVendorCreditPOJO[]>}
*/
public getVendorCreditRefunds(
vendorCreditId: number,
): Promise<IRefundVendorCreditPOJO[]> {
return this.getRefundVendorCreditsService.getVendorCreditRefunds(
vendorCreditId,
);
}
/** /**
* Creates a refund vendor credit. * Creates a refund vendor credit.

View File

@@ -1,4 +1,4 @@
import { Body, Controller, Delete, Get, Param, Post } from '@nestjs/common'; import { Body, Controller, Delete, Param, Post } from '@nestjs/common';
import { VendorCreditsRefundApplication } from './VendorCreditsRefund.application'; import { VendorCreditsRefundApplication } from './VendorCreditsRefund.application';
import { RefundVendorCredit } from './models/RefundVendorCredit'; import { RefundVendorCredit } from './models/RefundVendorCredit';
import { ApiOperation, ApiTags } from '@nestjs/swagger'; import { ApiOperation, ApiTags } from '@nestjs/swagger';
@@ -9,22 +9,7 @@ import { RefundVendorCreditDto } from './dtos/RefundVendorCredit.dto';
export class VendorCreditsRefundController { export class VendorCreditsRefundController {
constructor( constructor(
private readonly vendorCreditsRefundApplication: VendorCreditsRefundApplication, private readonly vendorCreditsRefundApplication: VendorCreditsRefundApplication,
) { } ) {}
/**
* Retrieve the vendor credit refunds graph.
* @param {number} vendorCreditId - Vendor credit id.
* @returns {Promise<IRefundVendorCreditPOJO[]>}
*/
@Get(':vendorCreditId/refund')
@ApiOperation({ summary: 'Retrieve the vendor credit refunds graph.' })
public getVendorCreditRefunds(
@Param('vendorCreditId') vendorCreditId: string,
) {
return this.vendorCreditsRefundApplication.getVendorCreditRefunds(
Number(vendorCreditId),
);
}
/** /**
* Creates a refund vendor credit. * Creates a refund vendor credit.
@@ -32,7 +17,7 @@ export class VendorCreditsRefundController {
* @param {IRefundVendorCreditDTO} refundVendorCreditDTO * @param {IRefundVendorCreditDTO} refundVendorCreditDTO
* @returns {Promise<RefundVendorCredit>} * @returns {Promise<RefundVendorCredit>}
*/ */
@Post(':vendorCreditId/refund') @Post(':vendorCreditId/refunds')
@ApiOperation({ summary: 'Create a refund for the given vendor credit.' }) @ApiOperation({ summary: 'Create a refund for the given vendor credit.' })
public async createRefundVendorCredit( public async createRefundVendorCredit(
@Param('vendorCreditId') vendorCreditId: string, @Param('vendorCreditId') vendorCreditId: string,

View File

@@ -1,12 +1,10 @@
import { IsOptional, ToNumber } from '@/common/decorators/Validators';
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { IsDateString, Min } from 'class-validator'; import { Min } from 'class-validator';
import { IsString } from 'class-validator'; import { IsString } from 'class-validator';
import { IsDate } from 'class-validator'; import { IsDate } from 'class-validator';
import { IsNotEmpty, IsNumber, IsPositive } from 'class-validator'; import { IsNotEmpty, IsNumber, IsOptional, IsPositive } from 'class-validator';
export class RefundVendorCreditDto { export class RefundVendorCreditDto {
@ToNumber()
@IsNumber() @IsNumber()
@IsNotEmpty() @IsNotEmpty()
@Min(0) @Min(0)
@@ -34,7 +32,6 @@ export class RefundVendorCreditDto {
}) })
depositAccountId: number; depositAccountId: number;
@IsOptional()
@IsString() @IsString()
@IsNotEmpty() @IsNotEmpty()
@ApiProperty({ @ApiProperty({
@@ -43,7 +40,7 @@ export class RefundVendorCreditDto {
}) })
description: string; description: string;
@IsDateString() @IsDate()
@IsNotEmpty() @IsNotEmpty()
@ApiProperty({ @ApiProperty({
description: 'The date of the refund', description: 'The date of the refund',

View File

@@ -1,37 +0,0 @@
import { Controller, Get, Param } from '@nestjs/common';
import { WarehousesApplication } from './WarehousesApplication.service';
import {
ApiOperation,
ApiParam,
ApiResponse,
ApiTags,
} from '@nestjs/swagger';
import { ApiCommonHeaders } from '@/common/decorators/ApiCommonHeaders';
@Controller('items')
@ApiTags('Warehouses')
@ApiCommonHeaders()
export class WarehouseItemsController {
constructor(private warehousesApplication: WarehousesApplication) { }
@Get(':id/warehouses')
@ApiOperation({
summary: 'Retrieves the item associated warehouses.',
})
@ApiResponse({
status: 200,
description:
'The item associated warehouses have been successfully retrieved.',
})
@ApiResponse({ status: 404, description: 'The item not found.' })
@ApiParam({
name: 'id',
required: true,
type: Number,
description: 'The item id',
})
getItemWarehouses(@Param('id') itemId: string) {
return this.warehousesApplication.getItemWarehouses(Number(itemId));
}
}

View File

@@ -85,4 +85,10 @@ export class WarehousesController {
markWarehousePrimary(@Param('id') warehouseId: string) { markWarehousePrimary(@Param('id') warehouseId: string) {
return this.warehousesApplication.markWarehousePrimary(Number(warehouseId)); return this.warehousesApplication.markWarehousePrimary(Number(warehouseId));
} }
@Get('items/:itemId')
@ApiOperation({ summary: 'Get item warehouses' })
getItemWarehouses(@Param('itemId') itemId: string) {
return this.warehousesApplication.getItemWarehouses(Number(itemId));
}
} }

View File

@@ -7,7 +7,6 @@ import { CreateWarehouse } from './commands/CreateWarehouse.service';
import { EditWarehouse } from './commands/EditWarehouse.service'; import { EditWarehouse } from './commands/EditWarehouse.service';
import { DeleteWarehouseService } from './commands/DeleteWarehouse.service'; import { DeleteWarehouseService } from './commands/DeleteWarehouse.service';
import { WarehousesController } from './Warehouses.controller'; import { WarehousesController } from './Warehouses.controller';
import { WarehouseItemsController } from './WarehouseItems.controller';
import { GetWarehouse } from './queries/GetWarehouse'; import { GetWarehouse } from './queries/GetWarehouse';
import { WarehouseMarkPrimary } from './commands/WarehouseMarkPrimary.service'; import { WarehouseMarkPrimary } from './commands/WarehouseMarkPrimary.service';
import { GetWarehouses } from './queries/GetWarehouses'; import { GetWarehouses } from './queries/GetWarehouses';
@@ -48,7 +47,7 @@ const models = [RegisterTenancyModel(Warehouse)];
@Module({ @Module({
imports: [TenancyDatabaseModule, ...models], imports: [TenancyDatabaseModule, ...models],
controllers: [WarehousesController, WarehouseItemsController], controllers: [WarehousesController],
providers: [ providers: [
CreateWarehouse, CreateWarehouse,
EditWarehouse, EditWarehouse,
@@ -91,6 +90,6 @@ const models = [RegisterTenancyModel(Warehouse)];
InventoryTransactionsWarehouses, InventoryTransactionsWarehouses,
ValidateWarehouseExistance ValidateWarehouseExistance
], ],
exports: [WarehousesSettings, WarehouseTransactionDTOTransform, WarehousesApplication, ...models], exports: [WarehousesSettings, WarehouseTransactionDTOTransform, ...models],
}) })
export class WarehousesModule {} export class WarehousesModule {}

View File

@@ -8,7 +8,7 @@
"@bigcapital/utils": "*", "@bigcapital/utils": "*",
"@blueprintjs-formik/core": "^0.3.7", "@blueprintjs-formik/core": "^0.3.7",
"@blueprintjs-formik/datetime": "^0.4.0", "@blueprintjs-formik/datetime": "^0.4.0",
"@blueprintjs-formik/select": "^0.4.5", "@blueprintjs-formik/select": "^0.3.5",
"@blueprintjs/colors": "4.1.19", "@blueprintjs/colors": "4.1.19",
"@blueprintjs/core": "^4.20.2", "@blueprintjs/core": "^4.20.2",
"@blueprintjs/datetime": "^4.4.37", "@blueprintjs/datetime": "^4.4.37",

View File

@@ -96,7 +96,7 @@ export function AccountsMultiSelect({
}; };
return ( return (
<FMultiSelect <FMultiSelect<AccountSelect>
{...rest} {...rest}
items={filteredAccounts} items={filteredAccounts}
valueAccessor={'id'} valueAccessor={'id'}

View File

@@ -6,7 +6,7 @@ import { MenuItem } from '@blueprintjs/core';
import { MenuItemNestedText, FSelect } from '@/components'; import { MenuItemNestedText, FSelect } from '@/components';
import { accountPredicate } from './_components'; import { accountPredicate } from './_components';
import { DialogsName } from '@/constants/dialogs'; import { DialogsName } from '@/constants/dialogs';
import { withDialogActions } from '@/containers/Dialog/withDialogActions'; import withDialogActions from '@/containers/Dialog/withDialogActions';
import { usePreprocessingAccounts } from './_hooks'; import { usePreprocessingAccounts } from './_hooks';
// Create new account renderer. // Create new account renderer.

View File

@@ -1,34 +1,23 @@
import React, { useCallback, ComponentType } from 'react'; // @ts-nocheck
import React, { useCallback, useMemo } from 'react';
import * as R from 'ramda';
import intl from 'react-intl-universal'; import intl from 'react-intl-universal';
import classNames from 'classnames';
import { MenuItem } from '@blueprintjs/core'; import { MenuItem } from '@blueprintjs/core';
import { ItemRenderer, ItemPredicate } from '@blueprintjs/select';
import { CLASSES } from '@/constants/classes';
import { DialogsName } from '@/constants/dialogs'; import { DialogsName } from '@/constants/dialogs';
import { FSuggest, Suggest, FormattedMessage as T } from '@/components';
import { withDialogActions } from '@/containers/Dialog/withDialogActions';
import { usePreprocessingAccounts } from './_hooks';
// Account interface import {
interface Account { FSuggest,
id: number; MenuItemNestedText,
name: string; FormattedMessage as T,
code: string; } from '@/components';
account_level?: number; import { nestedArrayToflatten, filterAccountsByQuery } from '@/utils';
account_type?: string; import withDialogActions from '@/containers/Dialog/withDialogActions';
account_parent_type?: string;
account_root_type?: string;
account_normal?: string;
}
// Types for renderers and predicates
type AccountItemRenderer = ItemRenderer<Account>;
type AccountItemPredicate = ItemPredicate<Account>;
// Create new account renderer. // Create new account renderer.
const createNewItemRenderer = ( const createNewItemRenderer = (query, active, handleClick) => {
query: string,
active: boolean,
handleClick: (event: React.MouseEvent<HTMLElement>) => void,
): React.ReactElement => {
return ( return (
<MenuItem <MenuItem
icon="add" icon="add"
@@ -40,17 +29,12 @@ const createNewItemRenderer = (
}; };
// Create new item from the given query string. // Create new item from the given query string.
const createNewItemFromQuery = (name: string): Partial<Account> => { const createNewItemFromQuery = (name) => {
return { name }; return { name };
}; };
// Filters accounts items. // Filters accounts items.
const filterAccountsPredicater: AccountItemPredicate = ( const filterAccountsPredicater = (query, account, _index, exactMatch) => {
query: string,
account: Account,
_index?: number,
exactMatch?: boolean,
): boolean => {
const normalizedTitle = account.name.toLowerCase(); const normalizedTitle = account.name.toLowerCase();
const normalizedQuery = query.toLowerCase(); const normalizedQuery = query.toLowerCase();
@@ -61,139 +45,77 @@ const filterAccountsPredicater: AccountItemPredicate = (
} }
}; };
// Account item renderer for Suggest (non-Formik) /**
const accountItemRenderer: AccountItemRenderer = ( * Accounts suggest field.
item: Account, */
{ handleClick, modifiers }, function AccountsSuggestFieldRoot({
): React.ReactElement | null => {
if (!modifiers.matchesPredicate) {
return null;
}
return (
<MenuItem
active={modifiers.active}
disabled={modifiers.disabled}
label={item.code}
key={item.id}
text={item.name}
onClick={handleClick}
/>
);
};
// Input value renderer for Suggest (non-Formik)
const inputValueRenderer = (item: Account | null): string => {
if (item) {
return item.name || '';
}
return '';
};
// Props specific to the HOC (excluding component's own props)
interface AccountsSuggestFieldOwnProps {
// #withDialogActions // #withDialogActions
openDialog: (name: string, payload?: any) => void; openDialog,
// #ownProps // #ownProps
items: Account[]; accounts,
defaultSelectText?: string; defaultSelectText = intl.formatMessage({ id: 'select_account' }),
filterByParentTypes?: string[];
filterByTypes?: string[];
filterByNormal?: string;
filterByRootTypes?: string[];
allowCreate?: boolean;
}
// Props that the HOC provides to the wrapped component (should be omitted from external props) filterByParentTypes = [],
type ProvidedSuggestProps = filterByTypes = [],
| 'items' filterByNormal,
| 'itemPredicate' filterByRootTypes = [],
| 'onCreateItemSelect'
| 'valueAccessor'
| 'textAccessor'
| 'labelAccessor'
| 'resetOnClose'
| 'createNewItemRenderer'
| 'createNewItemFromQuery';
// Utility type to extract props from a component allowCreate,
type ComponentProps<C> = C extends ComponentType<infer P> ? P : never;
/** ...suggestProps
* HOC for Accounts Suggest Field logic. }) {
* Returns a component that accepts the wrapped component's props minus the ones provided by the HOC. const flattenAccounts = useMemo(
*/ () => nestedArrayToflatten(accounts),
function withAccountsSuggestFieldLogic<C extends ComponentType<any>>( [accounts],
Component: C, );
): ComponentType< const filteredAccounts = useMemo(
AccountsSuggestFieldOwnProps & Omit<ComponentProps<C>, ProvidedSuggestProps> () =>
> { filterAccountsByQuery(flattenAccounts, {
return function AccountsSuggestFieldLogic({ filterByParentTypes,
// #withDialogActions filterByTypes,
openDialog, filterByNormal,
filterByRootTypes,
// #ownProps }),
items, [
defaultSelectText = intl.formatMessage({ id: 'select_account' }), flattenAccounts,
filterByParentTypes = [],
filterByTypes = [],
filterByNormal,
filterByRootTypes = [],
allowCreate,
// SuggestProps - props that will be passed to Suggest/FSuggest
...suggestProps
}: AccountsSuggestFieldOwnProps &
Omit<ComponentProps<C>, ProvidedSuggestProps>) {
const filteredAccounts = usePreprocessingAccounts(items, {
filterByParentTypes, filterByParentTypes,
filterByTypes, filterByTypes,
filterByNormal: filterByNormal ? [filterByNormal] : [], filterByNormal,
filterByRootTypes, filterByRootTypes,
}); ],
const handleCreateItemSelect = useCallback( );
(item: Account | Partial<Account>) => { const handleCreateItemSelect = useCallback(
if (!('id' in item) || !item.id) { (item) => {
openDialog(DialogsName.AccountForm); if (!item.id) {
} openDialog(DialogsName.AccountForm);
}, }
[openDialog], },
); [openDialog],
// Maybe inject new item props to select component. );
const maybeCreateNewItemRenderer = allowCreate // Maybe inject new item props to select component.
? createNewItemRenderer const maybeCreateNewItemRenderer = allowCreate ? createNewItemRenderer : null;
: undefined; const maybeCreateNewItemFromQuery = allowCreate
const maybeCreateNewItemFromQuery = allowCreate ? createNewItemFromQuery
? createNewItemFromQuery : null;
: undefined;
// Build the SuggestProps to pass to the component return (
const processedSuggestProps = { <FSuggest
items: filteredAccounts, items={filteredAccounts}
itemPredicate: filterAccountsPredicater, onCreateItemSelect={handleCreateItemSelect}
onCreateItemSelect: handleCreateItemSelect, valueAccessor="id"
valueAccessor: 'id' as const, textAccessor="name"
textAccessor: 'name' as const, labelAccessor="code"
labelAccessor: 'code' as const, inputProps={{ placeholder: defaultSelectText }}
inputProps: { placeholder: defaultSelectText }, resetOnClose
resetOnClose: true, popoverProps={{ minimal: true, boundary: 'window' }}
popoverProps: { minimal: true, boundary: 'window' as const }, createNewItemRenderer={maybeCreateNewItemRenderer}
createNewItemRenderer: maybeCreateNewItemRenderer, createNewItemFromQuery={maybeCreateNewItemFromQuery}
createNewItemFromQuery: maybeCreateNewItemFromQuery, {...suggestProps}
...suggestProps, />
} as ComponentProps<C>; );
return <Component {...processedSuggestProps} />;
};
} }
const AccountsSuggestFieldWithLogic = withAccountsSuggestFieldLogic(Suggest);
const FAccountsSuggestFieldWithLogic = withAccountsSuggestFieldLogic(FSuggest);
export const AccountsSuggestField = withDialogActions( export const AccountsSuggestField = R.compose(withDialogActions)(
AccountsSuggestFieldWithLogic, AccountsSuggestFieldRoot,
);
export const FAccountsSuggestField = withDialogActions(
FAccountsSuggestFieldWithLogic,
); );

View File

@@ -1,5 +1,5 @@
// @ts-nocheck // @ts-nocheck
import React, { useMemo } from 'react'; import React from 'react';
import { Position, Checkbox, InputGroup } from '@blueprintjs/core'; import { Position, Checkbox, InputGroup } from '@blueprintjs/core';
import { DateInput } from '@blueprintjs/datetime'; import { DateInput } from '@blueprintjs/datetime';
import moment from 'moment'; import moment from 'moment';
@@ -7,29 +7,23 @@ import intl from 'react-intl-universal';
import { isUndefined } from 'lodash'; import { isUndefined } from 'lodash';
import { useAutofocus } from '@/hooks'; import { useAutofocus } from '@/hooks';
import { T, Choose } from '@/components'; import { T, Choose, ListSelect } from '@/components';
import { Select } from '@/components/Forms';
import { momentFormatter } from '@/utils'; import { momentFormatter } from '@/utils';
function AdvancedFilterEnumerationField({ options, value, ...rest }) { function AdvancedFilterEnumerationField({ options, value, ...rest }) {
const selectedItem = useMemo(
() => options.find((opt) => opt.key === value) || null,
[options, value],
);
return ( return (
<Select <ListSelect
items={options} items={options}
selectedItem={selectedItem} selectedItem={value}
popoverProps={{ popoverProps={{
fill: true, fill: true,
inline: true, inline: true,
minimal: true, minimal: true,
captureDismiss: true, captureDismiss: true,
}} }}
placeholder={<T id={'filter.select_option'} />} defaultText={<T id={'filter.select_option'} />}
textAccessor={'label'} textProp={'label'}
valueAccessor={'key'} selectedItemProp={'key'}
{...rest} {...rest}
/> />
); );

View File

@@ -11,7 +11,7 @@ import { AppIntlProvider } from './AppIntlProvider';
import { useSplashLoading } from '@/hooks/state'; import { useSplashLoading } from '@/hooks/state';
import { useWatchImmediate } from '../hooks'; import { useWatchImmediate } from '../hooks';
import { withDashboardActions } from '@/containers/Dashboard/withDashboardActions'; import withDashboardActions from '@/containers/Dashboard/withDashboardActions';
const SUPPORTED_LOCALES = [ const SUPPORTED_LOCALES = [
{ name: 'English', value: 'en' }, { name: 'English', value: 'en' },

View File

@@ -20,3 +20,12 @@ export function BranchSelect({ branches, ...rest }) {
/> />
); );
} }
/**
*
* @param {*} param0
* @returns
*/
export function BranchSelectButton({ label, ...rest }) {
return <Button text={label} {...rest} />;
}

View File

@@ -2,7 +2,7 @@
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import * as R from 'ramda'; import * as R from 'ramda';
import { FMultiSelect } from '../Forms'; import { FMultiSelect } from '../Forms';
import { withDrawerActions } from '@/containers/Drawer/withDrawerActions'; import withDrawerActions from '@/containers/Drawer/withDrawerActions';
import { DRAWERS } from '@/constants/drawers'; import { DRAWERS } from '@/constants/drawers';
/** /**

View File

@@ -3,7 +3,7 @@ import React from 'react';
import * as R from 'ramda'; import * as R from 'ramda';
import { ButtonLink } from '../Button'; import { ButtonLink } from '../Button';
import { withDrawerActions } from '@/containers/Drawer/withDrawerActions'; import withDrawerActions from '@/containers/Drawer/withDrawerActions';
import { DRAWERS } from '@/constants/drawers'; import { DRAWERS } from '@/constants/drawers';
function CustomerDrawerLinkComponent({ function CustomerDrawerLinkComponent({

View File

@@ -4,7 +4,7 @@ import * as R from 'ramda';
import { useFormikContext } from 'formik'; import { useFormikContext } from 'formik';
import { createNewItemFromQuery, createNewItemRenderer } from './utils'; import { createNewItemFromQuery, createNewItemRenderer } from './utils';
import { FSelect } from '../Forms'; import { FSelect } from '../Forms';
import { withDrawerActions } from '@/containers/Drawer/withDrawerActions'; import withDrawerActions from '@/containers/Drawer/withDrawerActions';
import { useCreateAutofillListener } from '@/hooks/state/autofill'; import { useCreateAutofillListener } from '@/hooks/state/autofill';
import { DRAWERS } from '@/constants/drawers'; import { DRAWERS } from '@/constants/drawers';

View File

@@ -5,7 +5,7 @@ import { useHistory } from 'react-router-dom';
import { If, Icon } from '@/components'; import { If, Icon } from '@/components';
import { FormattedMessage as T } from '@/components'; import { FormattedMessage as T } from '@/components';
import { withDashboard } from '@/containers/Dashboard/withDashboard'; import withDashboard from '@/containers/Dashboard/withDashboard';
import { compose } from '@/utils'; import { compose } from '@/utils';
function DashboardBackLink({ dashboardBackLink, breadcrumbs }) { function DashboardBackLink({ dashboardBackLink, breadcrumbs }) {

View File

@@ -1,11 +1,11 @@
// @ts-nocheck // @ts-nocheck
import React, { useEffect, Suspense } from 'react'; import React, { useEffect, Suspense } from 'react';
import { CLASSES } from '@/constants/classes'; import { CLASSES } from '@/constants/classes';
import { withDashboardActions } from '@/containers/Dashboard/withDashboardActions'; import withDashboardActions from '@/containers/Dashboard/withDashboardActions';
import { compose } from '@/utils'; import { compose } from '@/utils';
import { Spinner } from '@blueprintjs/core'; import { Spinner } from '@blueprintjs/core';
import { withUniversalSearchActions } from '@/containers/UniversalSearch/withUniversalSearchActions'; import withUniversalSearchActions from '@/containers/UniversalSearch/withUniversalSearchActions';
/** /**
* Dashboard pages wrapper. * Dashboard pages wrapper.

View File

@@ -3,7 +3,7 @@ import React, { useState, useRef } from 'react';
import SplitPane from 'react-split-pane'; import SplitPane from 'react-split-pane';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import { withDashboard } from '@/containers/Dashboard/withDashboard'; import withDashboard from '@/containers/Dashboard/withDashboard';
import { compose } from '@/utils'; import { compose } from '@/utils';
function DashboardSplitPane({ function DashboardSplitPane({

View File

@@ -21,10 +21,10 @@ import DashboardTopbarUser from '@/components/Dashboard/TopbarUser';
import DashboardBreadcrumbs from '@/components/Dashboard/DashboardBreadcrumbs'; import DashboardBreadcrumbs from '@/components/Dashboard/DashboardBreadcrumbs';
import DashboardBackLink from '@/components/Dashboard/DashboardBackLink'; import DashboardBackLink from '@/components/Dashboard/DashboardBackLink';
import { withUniversalSearchActions } from '@/containers/UniversalSearch/withUniversalSearchActions'; import withUniversalSearchActions from '@/containers/UniversalSearch/withUniversalSearchActions';
import { withDashboardActions } from '@/containers/Dashboard/withDashboardActions'; import withDashboardActions from '@/containers/Dashboard/withDashboardActions';
import { withDashboard } from '@/containers/Dashboard/withDashboard'; import withDashboard from '@/containers/Dashboard/withDashboard';
import { withDialogActions } from '@/containers/Dialog/withDialogActions'; import withDialogActions from '@/containers/Dialog/withDialogActions';
import QuickNewDropdown from '@/containers/QuickNewDropdown/QuickNewDropdown'; import QuickNewDropdown from '@/containers/QuickNewDropdown/QuickNewDropdown';
import { import {

View File

@@ -3,9 +3,9 @@ import React from 'react';
import { useHotkeys } from 'react-hotkeys-hook'; import { useHotkeys } from 'react-hotkeys-hook';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { getDashboardRoutes } from '@/routes/dashboard'; import { getDashboardRoutes } from '@/routes/dashboard';
import { withDashboardActions } from '@/containers/Dashboard/withDashboardActions'; import withDashboardActions from '@/containers/Dashboard/withDashboardActions';
import { withDialogActions } from '@/containers/Dialog/withDialogActions'; import withDialogActions from '@/containers/Dialog/withDialogActions';
import { withUniversalSearchActions } from '@/containers/UniversalSearch/withUniversalSearchActions'; import withUniversalSearchActions from '@/containers/UniversalSearch/withUniversalSearchActions';
import { compose } from '@/utils'; import { compose } from '@/utils';

View File

@@ -1,7 +1,7 @@
// @ts-nocheck // @ts-nocheck
import * as R from 'ramda'; import * as R from 'ramda';
import BigcapitalLoading from './BigcapitalLoading'; import BigcapitalLoading from './BigcapitalLoading';
import { withDashboard } from '@/containers/Dashboard/withDashboard'; import withDashboard from '@/containers/Dashboard/withDashboard';
function SplashScreenComponent({ splashScreenLoading }) { function SplashScreenComponent({ splashScreenLoading }) {
return splashScreenLoading ? <BigcapitalLoading /> : null; return splashScreenLoading ? <BigcapitalLoading /> : null;

View File

@@ -13,7 +13,7 @@ import { FormattedMessage as T } from '@/components';
import { useAuthActions } from '@/hooks/state'; import { useAuthActions } from '@/hooks/state';
import { withDialogActions } from '@/containers/Dialog/withDialogActions'; import withDialogActions from '@/containers/Dialog/withDialogActions';
import { useAuthenticatedAccount } from '@/hooks/query'; import { useAuthenticatedAccount } from '@/hooks/query';
import { firstLettersArgs, compose } from '@/utils'; import { firstLettersArgs, compose } from '@/utils';

View File

@@ -58,9 +58,9 @@ export default function AccountCellRenderer({
{...formGroupProps} {...formGroupProps}
> >
<AccountsSuggestField <AccountsSuggestField
items={accounts} accounts={accounts}
onItemSelect={handleAccountSelected} onAccountSelected={handleAccountSelected}
selectedValue={initialValue} selectedAccountId={initialValue}
filterByRootTypes={filterAccountsByRootTypes} filterByRootTypes={filterAccountsByRootTypes}
filterByTypes={filterAccountsByTypes} filterByTypes={filterAccountsByTypes}
inputProps={{ inputProps={{

View File

@@ -1,7 +1,7 @@
// @ts-nocheck // @ts-nocheck
import React from 'react'; import React from 'react';
import { Dialog } from '@blueprintjs/core'; import { Dialog } from '@blueprintjs/core';
import { withDialogActions } from '@/containers/Dialog/withDialogActions'; import withDialogActions from '@/containers/Dialog/withDialogActions';
import { compose } from '@/utils'; import { compose } from '@/utils';
import '@/style/components/Dialog/Dialog.scss'; import '@/style/components/Dialog/Dialog.scss';

View File

@@ -5,7 +5,7 @@ import { Position, Drawer } from '@blueprintjs/core';
import '@/style/components/Drawer.scss'; import '@/style/components/Drawer.scss';
import { DrawerProvider } from './DrawerProvider'; import { DrawerProvider } from './DrawerProvider';
import { withDrawerActions } from '@/containers/Drawer/withDrawerActions'; import withDrawerActions from '@/containers/Drawer/withDrawerActions';
import { compose } from '@/utils'; import { compose } from '@/utils';
/** /**

View File

@@ -3,7 +3,7 @@ import React from 'react';
import { FormattedMessage as T } from '@/components'; import { FormattedMessage as T } from '@/components';
import { Classes, Icon, H4, Button } from '@blueprintjs/core'; import { Classes, Icon, H4, Button } from '@blueprintjs/core';
import { withDrawerActions } from '@/containers/Drawer/withDrawerActions'; import withDrawerActions from '@/containers/Drawer/withDrawerActions';
import { useDrawerContext } from './DrawerProvider'; import { useDrawerContext } from './DrawerProvider';
import { compose } from '@/utils'; import { compose } from '@/utils';

View File

@@ -5,7 +5,7 @@ import * as R from 'ramda';
import { DetailItem } from '@/components'; import { DetailItem } from '@/components';
import { isEqual } from 'lodash'; import { isEqual } from 'lodash';
import { withCurrentOrganization } from '@/containers/Organization/withCurrentOrganization'; import withCurrentOrganization from '@/containers/Organization/withCurrentOrganization';
/** /**
* Detail exchange rate item. * Detail exchange rate item.

View File

@@ -1,7 +1,7 @@
// @ts-nocheck // @ts-nocheck
import React from 'react'; import React from 'react';
import * as R from 'ramda'; import * as R from 'ramda';
import { withFeatureCan } from './withFeatureCan'; import withFeatureCan from './withFeatureCan';
function FeatureCanJSX({ feature, children, isFeatureCan }) { function FeatureCanJSX({ feature, children, isFeatureCan }) {
return isFeatureCan && children; return isFeatureCan && children;

View File

@@ -2,7 +2,7 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { getDashboardFeaturesSelector } from '@/store/dashboard/dashboard.selectors'; import { getDashboardFeaturesSelector } from '@/store/dashboard/dashboard.selectors';
export const withFeatureCan = (mapState) => { export default (mapState) => {
const featuresSelector = getDashboardFeaturesSelector(); const featuresSelector = getDashboardFeaturesSelector();
const mapStateToProps = (state, props) => { const mapStateToProps = (state, props) => {

View File

@@ -10,16 +10,7 @@ import {
TextArea, TextArea,
HTMLSelect, HTMLSelect,
} from '@blueprintjs-formik/core'; } from '@blueprintjs-formik/core';
import { import { MultiSelect, SuggestField } from '@blueprintjs-formik/select';
MultiSelect,
Suggest,
Select,
FormikMultiSelect,
FormikSuggest,
withFormikMultiSelect,
withFormikSuggest,
withFormikSelect,
} from '@blueprintjs-formik/select';
import { DateInput, TimezoneSelect } from '@blueprintjs-formik/datetime'; import { DateInput, TimezoneSelect } from '@blueprintjs-formik/datetime';
import { FSelect } from './Select'; import { FSelect } from './Select';
@@ -31,17 +22,11 @@ export {
RadioGroup as FRadioGroup, RadioGroup as FRadioGroup,
Switch as FSwitch, Switch as FSwitch,
FSelect, FSelect,
FormikMultiSelect as FMultiSelect, MultiSelect as FMultiSelect,
EditableText as FEditableText, EditableText as FEditableText,
FormikSuggest as FSuggest, SuggestField as FSuggest,
TextArea as FTextArea, TextArea as FTextArea,
DateInput as FDateInput, DateInput as FDateInput,
HTMLSelect as FHTMLSelect, HTMLSelect as FHTMLSelect,
TimezoneSelect as FTimezoneSelect, TimezoneSelect as FTimezoneSelect,
Suggest,
MultiSelect,
Select,
withFormikSelect,
withFormikMultiSelect,
withFormikSuggest,
}; };

View File

@@ -1,7 +1,7 @@
// @ts-nocheck // @ts-nocheck
import React from 'react'; import React from 'react';
import { Intent } from '@blueprintjs/core'; import { Intent } from '@blueprintjs/core';
import { Field, FastField, getIn } from 'formik'; import { Field, getIn } from 'formik';
import { CurrencyInput } from './MoneyInputGroup'; import { CurrencyInput } from './MoneyInputGroup';
const fieldToMoneyInputGroup = ({ const fieldToMoneyInputGroup = ({
@@ -32,7 +32,6 @@ function FieldToMoneyInputGroup({ ...props }) {
return <CurrencyInput {...fieldToMoneyInputGroup(props)} />; return <CurrencyInput {...fieldToMoneyInputGroup(props)} />;
} }
export function FMoneyInputGroup({ fastField, ...props }) { export function FMoneyInputGroup({ ...props }) {
const FieldComponent = fastField ? FastField : Field; return <Field {...props} component={FieldToMoneyInputGroup} />;
return <FieldComponent {...props} component={FieldToMoneyInputGroup} />;
} }

View File

@@ -1,7 +1,7 @@
// @ts-nocheck // @ts-nocheck
import React from 'react'; import React from 'react';
import { Button } from '@blueprintjs/core'; import { Button } from '@blueprintjs/core';
import { FormikSelect } from '@blueprintjs-formik/select'; import { Select } from '@blueprintjs-formik/select';
import styled from 'styled-components'; import styled from 'styled-components';
import clsx from 'classnames'; import clsx from 'classnames';
@@ -14,7 +14,7 @@ export function FSelect({ ...props }) {
className={clsx({ 'is-selected': !!text }, props.className)} className={clsx({ 'is-selected': !!text }, props.className)}
/> />
); );
return <FormikSelect input={input} fill={true} {...props} />; return <Select input={input} fill={true} {...props} />;
} }
export const SelectButton = styled(Button)` export const SelectButton = styled(Button)`

View File

@@ -3,8 +3,8 @@ import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Redirect } from 'react-router-dom'; import { Redirect } from 'react-router-dom';
import { compose } from '@/utils'; import { compose } from '@/utils';
import { withAuthentication } from '@/containers/Authentication/withAuthentication'; import withAuthentication from '@/containers/Authentication/withAuthentication';
import { withOrganization } from '@/containers/Organization/withOrganization'; import withOrganization from '@/containers/Organization/withOrganization';
/** /**
* Ensures organization is not ready. * Ensures organization is not ready.

View File

@@ -4,8 +4,8 @@ import { connect } from 'react-redux';
import { Redirect } from 'react-router-dom'; import { Redirect } from 'react-router-dom';
import { compose } from '@/utils'; import { compose } from '@/utils';
import { withAuthentication } from '@/containers/Authentication/withAuthentication'; import withAuthentication from '@/containers/Authentication/withAuthentication';
import { withOrganization } from '@/containers/Organization/withOrganization'; import withOrganization from '@/containers/Organization/withOrganization';
function EnsureOrganizationIsReady({ function EnsureOrganizationIsReady({
// #ownProps // #ownProps

Some files were not shown because too many files have changed in this diff Show More