Compare commits

...

25 Commits

Author SHA1 Message Date
Ahmed Bouhuolia
1d42a1c67a refactor(webapp): bound Formik fields across all components 2026-01-01 16:24:00 +02:00
Ahmed Bouhuolia
339289be9f refactor(export): move PDF table template to shared package 2025-12-29 23:54:43 +02:00
Ahmed Bouhuolia
350d229e98 feat(transactions-locking): enable settings schema and add dark mode support 2025-12-29 23:35:34 +02:00
Ahmed Bouhuolia
8152a16fd5 Merge pull request #881 from bigcapitalhq/bugs-bashing
bugs bashing
2025-12-29 22:08:56 +02:00
Ahmed Bouhuolia
00aad6e35c wip 2025-12-29 22:06:49 +02:00
Ahmed Bouhuolia
30d8fdb4c0 fix: running compute item cost processor 2025-12-28 12:30:06 +02:00
Ahmed Bouhuolia
872fc661ce bugs bashing 2025-12-28 12:01:24 +02:00
Ahmed Bouhuolia
054cd1fae4 Merge pull request #880 from bigcapitalhq/fix-dark-mode-bank-transaction-drawer
fix: darkmode bank transaction drawer
2025-12-23 20:00:50 +02:00
Ahmed Bouhuolia
7cb169bce9 fix: darkmode bank transaction drawer 2025-12-23 19:57:31 +02:00
Ahmed Bouhuolia
f2663c4af3 Merge pull request #879 from bigcapitalhq/refactor-bound-formik-fields
refactor(webapp): bound Formik fields
2025-12-22 23:28:17 +02:00
Ahmed Bouhuolia
6fea7779da refactor(webapp): bound Formik fields 2025-12-22 23:25:43 +02:00
Ahmed Bouhuolia
c00af18327 Merge pull request #878 from bigcapitalhq/fix-match-bank-transactions
fix: match uncategorized bank transactions
2025-12-22 23:06:36 +02:00
Ahmed Bouhuolia
37f0f4e227 fix: match uncategorized bank transactions 2025-12-22 23:02:08 +02:00
Ahmed Bouhuolia
8662c5899e Merge pull request #877 from bigcapitalhq/fix-import-bank-transactions
fix: import bank transactions
2025-12-22 22:52:34 +02:00
Ahmed Bouhuolia
a9a7cd8617 fix: import bank transactions 2025-12-22 22:49:58 +02:00
Ahmed Bouhuolia
e50fc3b523 Merge pull request #876 from bigcapitalhq/refactor-date-input
refactor: date input field
2025-12-21 23:39:09 +02:00
Ahmed Bouhuolia
b294a72a26 refactor: date input field 2025-12-21 23:34:11 +02:00
Ahmed Bouhuolia
62ae49941b Merge pull request #875 from bigcapitalhq/fix-accounts-suggest-field
fix: accounts suggest field
2025-12-21 16:15:39 +02:00
Ahmed Bouhuolia
31f5cbf335 fix: accounts suggest field 2025-12-21 16:11:01 +02:00
Ahmed Bouhuolia
b22328cff9 Merge pull request #874 from bigcapitalhq/feature/20251218134811
fix: import module bugs
2025-12-18 21:25:34 +02:00
Ahmed Bouhuolia
58f609353c fix: import bugs 2025-12-18 21:21:54 +02:00
Ahmed Bouhuolia
8a2a8eed3b fix: import rows aggregator 2025-12-18 20:44:05 +02:00
Ahmed Bouhuolia
636d206b0e fix: bugs sprint 2025-12-18 13:48:12 +02:00
Ahmed Bouhuolia
63922c391a fix: formatted money attributes 2025-12-14 16:51:06 +02:00
Ahmed Bouhuolia
6ecfe1ff12 fix: remove the auth body background 2025-12-14 14:30:25 +02:00
820 changed files with 4883 additions and 5703 deletions

View File

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

View File

@@ -1,4 +1,27 @@
// import { getTransactionsLockingSettingsSchema } from '@/api/controllers/TransactionsLocking/utils'; import { chain, mapKeys } from 'lodash';
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: {
@@ -223,12 +246,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,6 +3,7 @@
"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",
@@ -13,5 +14,6 @@
"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

@@ -0,0 +1,27 @@
{
"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

@@ -0,0 +1,15 @@
{
"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

@@ -0,0 +1,16 @@
{
"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

@@ -0,0 +1,21 @@
{
"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,6 +26,7 @@
"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",
@@ -38,5 +39,7 @@
"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,6 +17,7 @@
"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

@@ -0,0 +1,17 @@
{
"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,5 +10,21 @@
"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,5 +2,18 @@
"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: 'Unique number to identify the account.', importHint: 'account.field.code_hint',
}, },
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 uniqid from 'uniqid'; import * as 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,8 +9,10 @@ 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, sumBy } from 'lodash'; import { isEmpty, round, sumBy } from 'lodash';
import { ERRORS, MatchedTransactionPOJO } from './types'; import { ERRORS, MatchedTransactionPOJO } from './types';
import { ServiceError } from '../Items/ServiceError'; import { ServiceError } from '../Items/ServiceError';
@@ -22,18 +22,24 @@ export const sortClosestMatchTransactions = (
}; };
export const sumMatchTranasctions = (transactions: Array<any>) => { export const sumMatchTranasctions = (transactions: Array<any>) => {
return transactions.reduce( const total = transactions.reduce(
(total, item) => (sum, item) => {
total + const amount = parseFloat(item.amount) || 0;
(item.transactionNormal === 'debit' ? 1 : -1) * parseFloat(item.amount), const multiplier = item.transactionNormal === 'debit' ? 1 : -1;
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>
) => { ) => {
return sumBy(uncategorizedTransactions, 'amount'); const total = 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,7 +100,10 @@ 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.
if (totalUncategorizedTransactions !== totalMatchedTranasctions) { // Use tolerance-based comparison to handle floating-point precision issues
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,24 +12,30 @@ export class MatchTransactionsTypes {
private static registry: MatchTransactionsTypesRegistry; private static registry: MatchTransactionsTypesRegistry;
/** /**
* Consttuctor method. * Constructor 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: GetMatchedTransactionsByInvoices }, { type: 'SaleInvoice', service: this.getMatchedInvoicesService },
{ type: 'Bill', service: GetMatchedTransactionsByBills }, { type: 'Bill', service: this.getMatchedBillsService },
{ type: 'Expense', service: GetMatchedTransactionsByExpenses }, { type: 'Expense', service: this.getMatchedExpensesService },
{ {
type: 'ManualJournal', type: 'ManualJournal',
service: GetMatchedTransactionsByManualJournals, service: this.getMatchedManualJournalsService,
}, },
{ {
type: 'CashflowTransaction', type: 'CashflowTransaction',
service: GetMatchedTransactionsByCashflow, service: this.getMatchedCashflowService,
}, },
]; ];
} }
@@ -50,14 +56,13 @@ export class MatchTransactionsTypes {
* Boots all the registered importables. * Boots all the registered importables.
*/ */
public boot() { public boot() {
if (!MatchTransactionsTypes.registry) { const instance = MatchTransactionsTypesRegistry.getInstance();
const instance = MatchTransactionsTypesRegistry.getInstance();
this.registered.forEach((registered) => { // Always register services to ensure they're available
// const serviceInstanace = Container.get(registered.service); this.registered.forEach((registered) => {
// instance.register(registered.type, serviceInstanace); instance.register(registered.type, registered.service);
}); });
MatchTransactionsTypes.registry = instance;
} MatchTransactionsTypes.registry = instance;
} }
} }

View File

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

View File

@@ -0,0 +1,72 @@
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,7 +2,10 @@
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,6 +104,12 @@ 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: 'The payment number should be unique.', importHint: 'bill_payment.field.payment_number_hint',
}, },
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: 'Matches the account name or code.', importHint: 'account.field.account_hint',
}, },
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: 'Matches the bill number.', importHint: 'bill_payment.field.bill_hint',
}, },
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: 'Branch', name: 'invoice.field.branch',
fieldType: 'relation', fieldType: 'relation',
relationModel: 'Branch', relationModel: 'Branch',
relationImportMatch: ['name', 'code'], relationImportMatch: ['name', 'code'],

View File

@@ -13,6 +13,8 @@ export class BillPaymentTransformer extends Transformer {
'formattedPaymentDate', 'formattedPaymentDate',
'formattedCreatedAt', 'formattedCreatedAt',
'formattedAmount', 'formattedAmount',
'formattedTotal',
'formattedSubtotal',
'entries', 'entries',
'attachments', 'attachments',
]; ];
@@ -47,6 +49,29 @@ 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 No.', name: 'bill.field.bill_number',
fieldType: 'text', fieldType: 'text',
required: true, required: true,
}, },
referenceNo: { referenceNo: {
name: 'Reference No.', name: 'bill.field.reference_no',
fieldType: 'text', fieldType: 'text',
}, },
billDate: { billDate: {
name: 'Date', name: 'bill.field.bill_date',
fieldType: 'date', fieldType: 'date',
required: true, required: true,
}, },
dueDate: { dueDate: {
name: 'Due Date', name: 'bill.field.due_date',
fieldType: 'date', fieldType: 'date',
required: true, required: true,
}, },
vendorId: { vendorId: {
name: 'Vendor', name: 'bill.field.vendor',
fieldType: 'relation', fieldType: 'relation',
relationModel: 'Contact', relationModel: 'Contact',
relationImportMatch: 'displayName', relationImportMatch: 'displayName',
required: true, required: true,
}, },
exchangeRate: { exchangeRate: {
name: 'Exchange Rate', name: 'bill.field.exchange_rate',
fieldType: 'number', fieldType: 'number',
}, },
note: { note: {
name: 'Note', name: 'bill.field.note',
fieldType: 'text', fieldType: 'text',
}, },
open: { open: {
name: 'Open', name: 'bill.field.open',
fieldType: 'boolean', fieldType: 'boolean',
}, },
entries: { entries: {
name: 'Entries', name: 'bill.field.entries',
fieldType: 'collection', fieldType: 'collection',
collectionOf: 'object', collectionOf: 'object',
collectionMinLength: 1, collectionMinLength: 1,
required: true, required: true,
fields: { fields: {
itemId: { itemId: {
name: 'Item', name: 'bill.field.item',
fieldType: 'relation', fieldType: 'relation',
relationModel: 'Item', relationModel: 'Item',
relationImportMatch: ['name', 'code'], relationImportMatch: ['name', 'code'],
required: true, required: true,
importHint: 'Matches the item name or code.', importHint: 'bill.field.item_hint',
}, },
rate: { rate: {
name: 'Rate', name: 'bill.field.rate',
fieldType: 'number', fieldType: 'number',
required: true, required: true,
}, },
quantity: { quantity: {
name: 'Quantity', name: 'bill.field.quantity',
fieldType: 'number', fieldType: 'number',
required: true, required: true,
}, },
description: { description: {
name: 'Line Description', name: 'bill.field.description',
fieldType: 'text', fieldType: 'text',
}, },
}, },
}, },
branchId: { branchId: {
name: 'Branch', name: 'invoice.field.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: 'Warehouse', name: 'invoice.field.warehouse',
fieldType: 'relation', fieldType: 'relation',
relationModel: 'Warehouse', relationModel: 'Warehouse',
relationImportMatch: ['name', 'code'], relationImportMatch: ['name', 'code'],

View File

@@ -94,6 +94,7 @@ 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,
}); });
}; };
@@ -169,6 +170,7 @@ 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

@@ -164,73 +164,73 @@ export const CreditNoteMeta = {
}, },
fields2: { fields2: {
customerId: { customerId: {
name: 'Customer', name: 'credit_note.field.customer',
fieldType: 'relation', fieldType: 'relation',
relationModel: 'Contact', relationModel: 'Contact',
relationImportMatch: 'displayName', relationImportMatch: 'displayName',
required: true, required: true,
}, },
exchangeRate: { exchangeRate: {
name: 'Exchange Rate', name: 'credit_note.field.exchange_rate',
fieldType: 'number', fieldType: 'number',
}, },
creditNoteDate: { creditNoteDate: {
name: 'Credit Note Date', name: 'credit_note.field.credit_note_date',
fieldType: 'date', fieldType: 'date',
required: true, required: true,
}, },
referenceNo: { referenceNo: {
name: 'Reference No.', name: 'credit_note.field.reference_no',
fieldType: 'text', fieldType: 'text',
}, },
note: { note: {
name: 'Note', name: 'credit_note.field.note',
fieldType: 'text', fieldType: 'text',
}, },
termsConditions: { termsConditions: {
name: 'Terms & Conditions', name: 'credit_note.field.terms_conditions',
fieldType: 'text', fieldType: 'text',
}, },
creditNoteNumber: { creditNoteNumber: {
name: 'Credit Note Number', name: 'credit_note.field.credit_note_number',
fieldType: 'text', fieldType: 'text',
}, },
open: { open: {
name: 'Open', name: 'credit_note.field.open',
fieldType: 'boolean', fieldType: 'boolean',
}, },
entries: { entries: {
name: 'Entries', name: 'credit_note.field.entries',
fieldType: 'collection', fieldType: 'collection',
collectionOf: 'object', collectionOf: 'object',
collectionMinLength: 1, collectionMinLength: 1,
fields: { fields: {
itemId: { itemId: {
name: 'Item', name: 'credit_note.field.item',
fieldType: 'relation', fieldType: 'relation',
relationModel: 'Item', relationModel: 'Item',
relationImportMatch: ['name', 'code'], relationImportMatch: ['name', 'code'],
required: true, required: true,
importHint: 'Matches the item name or code.', importHint: 'invoice.field.item_hint',
}, },
rate: { rate: {
name: 'Rate', name: 'credit_note.field.rate',
fieldType: 'number', fieldType: 'number',
required: true, required: true,
}, },
quantity: { quantity: {
name: 'Quantity', name: 'credit_note.field.quantity',
fieldType: 'number', fieldType: 'number',
required: true, required: true,
}, },
description: { description: {
name: 'Description', name: 'credit_note.field.description',
fieldType: 'text', fieldType: 'text',
}, },
}, },
}, },
branchId: { branchId: {
name: 'Branch', name: 'invoice.field.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: 'Warehouse', name: 'invoice.field.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, { money: false }); return this.formatNumber(credit.amount);
}; };
/** /**
@@ -130,7 +130,7 @@ export class CreditNoteTransformer extends Transformer {
* @returns {string} * @returns {string}
*/ */
protected adjustmentFormatted = (credit): string => { protected adjustmentFormatted = (credit): string => {
return this.formatMoney(credit.adjustment, { return this.formatNumber(credit.adjustment, {
currencyCode: credit.currencyCode, currencyCode: credit.currencyCode,
excerptZero: true, excerptZero: true,
}); });
@@ -156,6 +156,7 @@ 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,
}); });
}; };
@@ -167,6 +168,7 @@ 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

@@ -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 { ManualJournal } from '../ManualJournals/models/ManualJournal'; import { Expense } from './models/Expense.model';
@Injectable() @Injectable()
@ImportableService({ name: ManualJournal.name }) @ImportableService({ name: Expense.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: 'Matches the account name or code.', importHint: 'account.field.account_hint',
}, },
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: 'Matches the account name or code.', importHint: 'account.field.account_hint',
}, },
amount: { amount: {
name: 'expense.field.amount', name: 'expense.field.amount',
@@ -187,7 +187,7 @@ export const ExpenseMeta = {
fieldType: 'boolean', fieldType: 'boolean',
}, },
branchId: { branchId: {
name: 'Branch', name: 'invoice.field.branch',
fieldType: 'relation', fieldType: 'relation',
relationModel: 'Branch', relationModel: 'Branch',
relationImportMatch: ['name', 'code'], relationImportMatch: ['name', 'code'],

View File

@@ -1,14 +1,13 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { ChromiumlyTenancy } from '../ChromiumlyTenancy/ChromiumlyTenancy.service'; import { ChromiumlyTenancy } from '../ChromiumlyTenancy/ChromiumlyTenancy.service';
import { TemplateInjectable } from '../TemplateInjectable/TemplateInjectable.service'; import { renderExportResourceTableTemplateHtml } from '@bigcapital/pdf-templates';
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.
@@ -19,21 +18,18 @@ export class ExportPdf {
* @returns * @returns
*/ */
public async pdf( public async pdf(
columns: { accessor: string }, columns: { accessor: string; name?: string; style?: string; group?: 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 = await this.templateInjectable.render( const htmlContent = renderExportResourceTableTemplateHtml({
'modules/export-resource-table', table: { rows, columns },
{ sheetTitle,
table: { rows, columns }, sheetDescription,
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,7 +172,10 @@ export class TrialBalanceSheet extends FinancialSheet {
private filterNoneTransactions = ( private filterNoneTransactions = (
accountNode: ITrialBalanceAccount accountNode: ITrialBalanceAccount
): boolean => { ): boolean => {
return false === this.repository.totalAccountsLedger.isEmpty(); const accountLedger = this.repository.totalAccountsLedger.whereAccountId(
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,9 +55,8 @@ export class ImportFileDataTransformer {
/** /**
* Aggregates parsed data based on resource metadata configuration. * Aggregates parsed data based on resource metadata configuration.
* @param {number} tenantId * @param {string} resourceName - The resource name.
* @param {string} resourceName * @param {Record<string, any>} parsedData - The parsed data to aggregate.
* @param {Record<string, any>} parsedData
* @returns {Record<string, any>[]} * @returns {Record<string, any>[]}
*/ */
public aggregateParsedValues( public aggregateParsedValues(
@@ -110,8 +109,11 @@ 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>[]> {
// const tenantModels = this.tenancy.models(tenantId); // Create a model resolver function that uses ResourceService
const _valueParser = valueParser(fields, {}, trx); const modelResolver = (modelName: string) => {
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,7 +19,8 @@ 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 {Record<string, any>} mappedDTOs * @param {ResourceMetaFieldsMap} importableFields - Already localized fields from ResourceService
* @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 = this.importFileCommon.transformParams(resource, params); const _params = await 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,6 +15,12 @@ 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

@@ -0,0 +1,287 @@
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,52 +284,54 @@ 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, tenantModels: any, trx?: Knex.Transaction) => (fields: ResourceMetaFieldsMap, modelResolver: ModelResolver, 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 = tenantModels[field.relationModel]; const RelationModel = modelResolver(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 relationQuery = RelationModel.query(trx);
const relationKeys = castArray(field?.relationImportMatch); const relationKeys = castArray(field?.relationImportMatch);
relationQuery.where(function () { relationQuery.where(function () {
relationKeys.forEach((relationKey: string) => { relationKeys.forEach((relationKey: string) => {
this.orWhereRaw('LOWER(??) = LOWER(?)', [relationKey, value]); this.orWhereRaw('LOWER(??) = LOWER(?)', [relationKey, value]);
});
}); });
}); const result = await relationQuery.first();
const result = await relationQuery.first(); _value = get(result, 'id');
_value = get(result, 'id'); } else if (field.fieldType === 'collection') {
} else if (field.fieldType === 'collection') { const ObjectFieldKey = key.includes('.') ? key.split('.')[1] : key;
const ObjectFieldKey = key.includes('.') ? key.split('.')[1] : key; const _valueParser = valueParser(fields, modelResolver);
const _valueParser = valueParser(fields, tenantModels); _value = await _valueParser(value, ObjectFieldKey, fieldKey);
_value = await _valueParser(value, ObjectFieldKey, fieldKey); }
} return _value;
return _value; };
};
/** /**
* Parses the field key and detarmines the key path. * Parses the field key and detarmines the key path.
@@ -402,12 +404,17 @@ 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.entries); existingEntry[groupOn].push(...curr[groupOn]);
} 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 * 60; // 1 minute const debounceTime = 1000 * 10; // 10 seconds
// 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,7 +2,8 @@ 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 } from 'nestjs-cls'; import { ClsService, UseCls } 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';
@@ -14,7 +15,7 @@ import { Process } from '@nestjs/bull';
interface ComputeItemCostJobPayload extends TenantJobPayload { interface ComputeItemCostJobPayload extends TenantJobPayload {
itemId: number; itemId: number;
startingDate: Date; startingDate: Date | string;
} }
@Processor({ @Processor({
name: ComputeItemCostQueue, name: ComputeItemCostQueue,
@@ -39,28 +40,34 @@ 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;
console.log(`Compute item cost for item ${itemId} started`); // Parse startingDate using moment to handle both Date and string formats
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(
startingDate, startingDateObj,
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, itemId, organizationId, userId }, { startingDate: startingDateObj, 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 computing item cost:', error); console.error(`[error] Error computing item cost for item ${itemId}:`, 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: 'Matches the account name or code.', importHint: 'account.field.account_hint',
}, },
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: 'Matches the account name or code.', importHint: 'account.field.account_hint',
}, },
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: 'Matches the account name or code.', importHint: 'account.field.account_hint',
}, },
sellDescription: { sellDescription: {
name: 'Sell Description', name: 'item.field.sell_description',
fieldType: 'text', fieldType: 'text',
}, },
purchaseDescription: { purchaseDescription: {
name: 'Purchase Description', name: 'item.field.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: 'Matches the category name.', importHint: 'item.field.category_hint',
}, },
active: { active: {
name: 'item.field.active', name: 'item.field.active',

View File

@@ -74,6 +74,9 @@ 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: 'Matches the account name or code.', importHint: 'account.field.account_hint',
}, },
paymentReceiveNo: { paymentReceiveNo: {
name: 'payment_receive.field.payment_receive_no', name: 'payment_receive.field.payment_receive_no',
fieldType: 'text', fieldType: 'text',
importHint: 'The payment number should be unique.', importHint: 'payment_receive.field.payment_no_hint',
}, },
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: 'Matches the invoice number.', importHint: 'payment_receive.field.invoice_hint',
}, },
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: 'Branch', name: 'invoice.field.branch',
fieldType: 'relation', fieldType: 'relation',
relationModel: 'Branch', relationModel: 'Branch',
relationImportMatch: ['name', 'code'], relationImportMatch: ['name', 'code'],

View File

@@ -11,6 +11,7 @@ export class PaymentReceiveTransfromer extends Transformer {
public includeAttributes = (): string[] => { public includeAttributes = (): string[] => {
return [ return [
'subtotalFormatted', 'subtotalFormatted',
'formatttedTotal',
'formattedPaymentDate', 'formattedPaymentDate',
'formattedCreatedAt', 'formattedCreatedAt',
'formattedAmount', 'formattedAmount',
@@ -45,7 +46,18 @@ 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,
}); });
}; };
@@ -66,7 +78,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, { money: false }); return this.formatNumber(payment.exchangeRate);
}; };
/** /**

View File

@@ -1,5 +1,6 @@
import { ModuleRef } from '@nestjs/core'; import { ModuleRef } from '@nestjs/core';
import { pickBy } from 'lodash'; import { pickBy, mapValues } 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';
@@ -20,7 +21,8 @@ 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.
@@ -96,7 +98,45 @@ export class ResourceService {
}; };
/** /**
* Retrieve the resource fields. * Localizes a single field by translating its name and importHint.
* @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}
*/ */
@@ -104,8 +144,11 @@ 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.filterSupportFeatures(meta.fields2); return this.localizeFields(
filteredFields as Record<string, IModelMetaField2>,
);
} }
/** /**

View File

@@ -191,52 +191,52 @@ export const SaleEstimateMeta = {
}, },
fields2: { fields2: {
customerId: { customerId: {
name: 'Customer', name: 'estimate.field.customer',
fieldType: 'relation', fieldType: 'relation',
relationModel: 'Contact', relationModel: 'Contact',
relationImportMatch: ['displayName'], relationImportMatch: ['displayName'],
required: true, required: true,
}, },
estimateDate: { estimateDate: {
name: 'Estimate Date', name: 'estimate.field.estimate_date',
fieldType: 'date', fieldType: 'date',
required: true, required: true,
}, },
expirationDate: { expirationDate: {
name: 'Expiration Date', name: 'estimate.field.expiration_date',
fieldType: 'date', fieldType: 'date',
required: true, required: true,
}, },
estimateNumber: { estimateNumber: {
name: 'Estimate No.', name: 'estimate.field.estimate_number',
fieldType: 'text', fieldType: 'text',
}, },
reference: { reference: {
name: 'Reference No.', name: 'estimate.field.reference_no',
fieldType: 'text', fieldType: 'text',
}, },
exchangeRate: { exchangeRate: {
name: 'Exchange Rate', name: 'estimate.field.exchange_rate',
fieldType: 'number', fieldType: 'number',
}, },
currencyCode: { currencyCode: {
name: 'Currency', name: 'estimate.field.currency',
fieldType: 'text', fieldType: 'text',
}, },
note: { note: {
name: 'Note', name: 'estimate.field.note',
fieldType: 'text', fieldType: 'text',
}, },
termsConditions: { termsConditions: {
name: 'Terms & Conditions', name: 'estimate.field.terms_conditions',
fieldType: 'text', fieldType: 'text',
}, },
delivered: { delivered: {
name: 'Delivered', name: 'estimate.field.delivered',
type: 'boolean', type: 'boolean',
}, },
entries: { entries: {
name: 'Entries', name: 'estimate.field.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: 'Matches the item name or code.', importHint: 'invoice.field.item_hint',
}, },
rate: { rate: {
name: 'invoice.field.rate', name: 'invoice.field.rate',
@@ -261,13 +261,13 @@ export const SaleEstimateMeta = {
required: true, required: true,
}, },
description: { description: {
name: 'Line Description', name: 'invoice.field.description',
fieldType: 'text', fieldType: 'text',
}, },
}, },
}, },
branchId: { branchId: {
name: 'Branch', name: 'invoice.field.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: 'Warehouse', name: 'invoice.field.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.formatMoney(estimate.adjustment, { return this.formatNumber(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 { ManualJournal } from '@/modules/ManualJournals/models/ManualJournal'; import { SaleInvoice } from '../models/SaleInvoice';
@Injectable() @Injectable()
@ImportableService({ name: ManualJournal.name }) @ImportableService({ name: SaleInvoice.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: 'Matches the item name or code.', importHint: 'invoice.field.item_hint',
}, },
rate: { rate: {
name: 'invoice.field.rate', name: 'invoice.field.rate',
@@ -283,7 +283,7 @@ export const SaleInvoiceMeta = {
printable: false, printable: false,
}, },
branchId: { branchId: {
name: 'Branch', name: 'invoice.field.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: 'Warehouse', name: 'invoice.field.warehouse',
fieldType: 'relation', fieldType: 'relation',
relationModel: 'Warehouse', relationModel: 'Warehouse',
relationImportMatch: ['name', 'code'], relationImportMatch: ['name', 'code'],

View File

@@ -70,6 +70,7 @@ 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
}); });
}; };
@@ -113,7 +114,6 @@ 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,6 +170,7 @@ 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
}); });
}; };
@@ -212,7 +213,7 @@ export class SaleInvoiceTransformer extends Transformer {
* @returns {string} * @returns {string}
*/ */
protected adjustmentFormatted = (invoice: SaleInvoice): string => { protected adjustmentFormatted = (invoice: SaleInvoice): string => {
return this.formatMoney(invoice.adjustment, { return this.formatNumber(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

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

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,6 +203,7 @@ 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', name: 'vendor_credit.field.vendor',
fieldType: 'relation', fieldType: 'relation',
relationModel: 'Contact', relationModel: 'Contact',
relationImportMatch: 'displayName', relationImportMatch: 'displayName',
required: true, required: true,
}, },
exchangeRate: { exchangeRate: {
name: 'Echange Rate', name: 'vendor_credit.field.exchange_rate',
fieldType: 'text', fieldType: 'text',
}, },
vendorCreditNumber: { vendorCreditNumber: {
name: 'Vendor Credit No.', name: 'vendor_credit.field.vendor_credit_number',
fieldType: 'text', fieldType: 'text',
}, },
referenceNo: { referenceNo: {
name: 'Refernece No.', name: 'vendor_credit.field.reference_no',
fieldType: 'text', fieldType: 'text',
}, },
vendorCreditDate: { vendorCreditDate: {
name: 'Vendor Credit Date', name: 'vendor_credit.field.vendor_credit_date',
fieldType: 'date', fieldType: 'date',
required: true, required: true,
}, },
note: { note: {
name: 'Note', name: 'vendor_credit.field.note',
fieldType: 'text', fieldType: 'text',
}, },
open: { open: {
name: 'Open', name: 'vendor_credit.field.open',
fieldType: 'boolean', fieldType: 'boolean',
}, },
entries: { entries: {
name: 'Entries', name: 'vendor_credit.field.entries',
fieldType: 'collection', fieldType: 'collection',
collectionOf: 'object', collectionOf: 'object',
collectionMinLength: 1, collectionMinLength: 1,
required: true, required: true,
fields: { fields: {
itemId: { itemId: {
name: 'Item Name', name: 'vendor_credit.field.item',
fieldType: 'relation', fieldType: 'relation',
relationModel: 'Item', relationModel: 'Item',
relationImportMatch: ['name', 'code'], relationImportMatch: ['name', 'code'],
required: true, required: true,
importHint: 'Matches the item name or code.', importHint: 'invoice.field.item_hint',
}, },
rate: { rate: {
name: 'Rate', name: 'vendor_credit.field.rate',
fieldType: 'number', fieldType: 'number',
required: true, required: true,
}, },
quantity: { quantity: {
name: 'Quantity', name: 'vendor_credit.field.quantity',
fieldType: 'number', fieldType: 'number',
required: true, required: true,
}, },
description: { description: {
name: 'Description', name: 'vendor_credit.field.description',
fieldType: 'text', fieldType: 'text',
}, },
}, },
}, },
branchId: { branchId: {
name: 'Branch', name: 'invoice.field.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: 'Warehouse', name: 'invoice.field.warehouse',
fieldType: 'relation', fieldType: 'relation',
relationModel: 'Warehouse', relationModel: 'Warehouse',
relationImportMatch: ['name', 'code'], relationImportMatch: ['name', 'code'],

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

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,12 +20,3 @@ 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
accounts={accounts} items={accounts}
onAccountSelected={handleAccountSelected} onItemSelect={handleAccountSelected}
selectedAccountId={initialValue} selectedValue={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 default (mapState) => { export const withFeatureCan = (mapState) => {
const featuresSelector = getDashboardFeaturesSelector(); const featuresSelector = getDashboardFeaturesSelector();
const mapStateToProps = (state, props) => { const mapStateToProps = (state, props) => {

View File

@@ -10,7 +10,16 @@ import {
TextArea, TextArea,
HTMLSelect, HTMLSelect,
} from '@blueprintjs-formik/core'; } from '@blueprintjs-formik/core';
import { MultiSelect, SuggestField } from '@blueprintjs-formik/select'; import {
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';
@@ -22,11 +31,17 @@ export {
RadioGroup as FRadioGroup, RadioGroup as FRadioGroup,
Switch as FSwitch, Switch as FSwitch,
FSelect, FSelect,
MultiSelect as FMultiSelect, FormikMultiSelect as FMultiSelect,
EditableText as FEditableText, EditableText as FEditableText,
SuggestField as FSuggest, FormikSuggest 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, getIn } from 'formik'; import { Field, FastField, getIn } from 'formik';
import { CurrencyInput } from './MoneyInputGroup'; import { CurrencyInput } from './MoneyInputGroup';
const fieldToMoneyInputGroup = ({ const fieldToMoneyInputGroup = ({
@@ -32,6 +32,7 @@ function FieldToMoneyInputGroup({ ...props }) {
return <CurrencyInput {...fieldToMoneyInputGroup(props)} />; return <CurrencyInput {...fieldToMoneyInputGroup(props)} />;
} }
export function FMoneyInputGroup({ ...props }) { export function FMoneyInputGroup({ fastField, ...props }) {
return <Field {...props} component={FieldToMoneyInputGroup} />; const FieldComponent = fastField ? FastField : Field;
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 { Select } from '@blueprintjs-formik/select'; import { FormikSelect } 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 <Select input={input} fill={true} {...props} />; return <FormikSelect 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

View File

@@ -4,7 +4,7 @@ import { includes } from 'lodash';
import { compose } from '@/utils'; import { compose } from '@/utils';
import { Redirect } from 'react-router-dom'; import { Redirect } from 'react-router-dom';
import withSubscriptions from '@/containers/Subscriptions/withSubscriptions'; import { withSubscriptions } from '@/containers/Subscriptions/withSubscriptions';
/** /**
* Ensures the given subscription type is active or redirect to the given route. * Ensures the given subscription type is active or redirect to the given route.

View File

@@ -4,7 +4,7 @@ import { includes } from 'lodash';
import { compose } from '@/utils'; import { compose } from '@/utils';
import { Redirect } from 'react-router-dom'; import { Redirect } from 'react-router-dom';
import withSubscriptions from '@/containers/Subscriptions/withSubscriptionss'; import { withSubscriptions } from '@/containers/Subscriptions/withSubscriptionss';
/** /**
* Ensures the given subscription type is active or redirect to the given route. * Ensures the given subscription type is active or redirect to the given route.

View File

@@ -4,7 +4,7 @@ import { includes } from 'lodash';
import { compose } from '@/utils'; import { compose } from '@/utils';
import { Redirect } from 'react-router-dom'; import { Redirect } from 'react-router-dom';
import withSubscriptions from '@/containers/Subscriptions/withSubscriptionss'; import { withSubscriptions } from '@/containers/Subscriptions/withSubscriptionss';
/** /**
* Ensures the given subscription type is active or redirect to the given route. * Ensures the given subscription type is active or redirect to the given route.

View File

@@ -2,7 +2,7 @@
import React from 'react'; import React from 'react';
import * as R from 'ramda'; import * as R from 'ramda';
import { FMultiSelect } from '@/components/Forms'; import { FMultiSelect } from '@/components/Forms';
import withDialogActions from '@/containers/Dialog/withDialogActions'; import { withDialogActions } from '@/containers/Dialog/withDialogActions';
/** /**
* Items multi-select. * Items multi-select.

View File

@@ -9,7 +9,7 @@ import { CLASSES } from '@/constants/classes';
import { FormattedMessage as T } from '@/components'; import { FormattedMessage as T } from '@/components';
import withDrawerActions from '@/containers/Drawer/withDrawerActions'; import { withDrawerActions } from '@/containers/Drawer/withDrawerActions';
import { DRAWERS } from '@/constants/drawers'; import { DRAWERS } from '@/constants/drawers';

View File

@@ -11,7 +11,7 @@ import PreferencesContentRoute from '@/components/Preferences/PreferencesContent
import DashboardErrorBoundary from '@/components/Dashboard/DashboardErrorBoundary'; import DashboardErrorBoundary from '@/components/Dashboard/DashboardErrorBoundary';
import PreferencesSidebar from '@/components/Preferences/PreferencesSidebar'; import PreferencesSidebar from '@/components/Preferences/PreferencesSidebar';
import withDashboardActions from '@/containers/Dashboard/withDashboardActions'; import { withDashboardActions } from '@/containers/Dashboard/withDashboardActions';
import '@/style/pages/Preferences/Page.scss'; import '@/style/pages/Preferences/Page.scss';

View File

@@ -10,7 +10,7 @@ import CurrenciesActions from '@/containers/Preferences/Currencies/CurrenciesAct
import WarehousesActions from '@/containers/Preferences/Warehouses/WarehousesActions'; import WarehousesActions from '@/containers/Preferences/Warehouses/WarehousesActions';
import BranchesActions from '@/containers/Preferences/Branches/BranchesActions'; import BranchesActions from '@/containers/Preferences/Branches/BranchesActions';
import ApiKeysActions from '@/containers/Preferences/ApiKeys/ApiKeysActions'; import ApiKeysActions from '@/containers/Preferences/ApiKeys/ApiKeysActions';
import withDashboard from '@/containers/Dashboard/withDashboard'; import { withDashboard } from '@/containers/Dashboard/withDashboard';
import { compose } from '@/utils'; import { compose } from '@/utils';

View File

@@ -24,11 +24,19 @@ export const TD = styled.td`
export const TRDarkSingleLine = styled(TR)` export const TRDarkSingleLine = styled(TR)`
${TD} { ${TD} {
border-bottom: 1px solid #000; border-bottom: 1px solid #000;
.bp4-dark & {
border-bottom-color: var(--color-dark-gray5);
}
} }
`; `;
export const TRDarkDoubleLines = styled(TR)` export const TRDarkDoubleLines = styled(TR)`
${TD} { ${TD} {
border-bottom: 3px double #000; border-bottom: 3px double #000;
.bp4-dark & {
border-bottom-color: var(--color-dark-gray5);
}
} }
`; `;

View File

@@ -3,7 +3,7 @@ import * as R from 'ramda';
import intl from 'react-intl-universal'; import intl from 'react-intl-universal';
import { FSelect } from '@/components'; import { FSelect } 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 { MenuItem } from '@blueprintjs/core'; import { MenuItem } from '@blueprintjs/core';
// Create new account renderer. // Create new account renderer.

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 VendorDrawerLinkComponent({ function VendorDrawerLinkComponent({

View File

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

View File

@@ -171,6 +171,15 @@ export const financialReportMenus = [
subject: AbilitySubject.Report, subject: AbilitySubject.Report,
ability: ReportsAction.READ_INVENTORY_ITEM_DETAILS, ability: ReportsAction.READ_INVENTORY_ITEM_DETAILS,
}, },
{
title: <T id={'inventory_valuation'} />,
desc: (
<T id={'summerize_your_transactions_for_each_inventory_item'} />
),
link: '/financial-reports/inventory-valuation',
subject: AbilitySubject.Report,
ability: ReportsAction.READ_INVENTORY_VALUATION_SUMMARY,
},
], ],
}, },
{ {

View File

@@ -15,7 +15,7 @@ export const TABLES = {
EXPENSES: 'expenses', EXPENSES: 'expenses',
CASHFLOW_ACCOUNTS: 'cashflow_accounts', CASHFLOW_ACCOUNTS: 'cashflow_accounts',
CASHFLOW_Transactions: 'cashflow_transactions', CASHFLOW_Transactions: 'cashflow_transactions',
UNCATEGORIZED_CASHFLOW_TRANSACTION: 'UNCATEGORIZED_CASHFLOW_TRANSACTION', UNCATEGORIZED_BANK_TRANSACTION: 'UNCATEGORIZED_BANK_TRANSACTION',
CREDIT_NOTES: 'credit_notes', CREDIT_NOTES: 'credit_notes',
VENDOR_CREDITS: 'vendor_credits', VENDOR_CREDITS: 'vendor_credits',
WAREHOUSE_TRANSFERS: 'warehouse_transfers', WAREHOUSE_TRANSFERS: 'warehouse_transfers',

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