Compare commits

...

65 Commits

Author SHA1 Message Date
Ahmed Bouhuolia
6d24474162 Merge pull request #663 from bigcapitalhq/fix-uncategorize-bank-transaction
fix: Un-categorize bank transactions
2024-09-07 13:27:11 +02:00
Ahmed Bouhuolia
5962b990c4 fix: Uncategorize bank transactions 2024-09-07 13:26:02 +02:00
Ahmed Bouhuolia
9f21f649f0 Merge pull request #662 from bigcapitalhq/refactor-expense-gl
refactor: The expense G/L writer
2024-09-04 21:10:51 +02:00
Ahmed Bouhuolia
d3d2112b8a refactor: The expense G/L writer 2024-09-04 21:10:13 +02:00
Ahmed Bouhuolia
3795322a65 Merge pull request #659 from bigcapitalhq/format-money-table-columns
feat: Tabular number of all money columns
2024-09-04 18:48:38 +02:00
Ahmed Bouhuolia
fe5cd5a8ea feat: Mark more columns as money columns 2024-09-04 18:46:42 +02:00
Ahmed Bouhuolia
c032a5db16 Merge pull request #661 from bigcapitalhq/fix-payment-made-full-amount
fix: Payment made filling the form full amount field
2024-09-04 17:35:54 +02:00
Ahmed Bouhuolia
3fcb6fefde fix: Payment made filling the form full amount field 2024-09-04 17:34:19 +02:00
Ahmed Bouhuolia
16f5cb713d Merge pull request #660 from bigcapitalhq/fix-cast-array-rule-ids
fix: Array cast of recognize function rule ids
2024-09-04 16:49:32 +02:00
Ahmed Bouhuolia
f7a7925028 fix: Array cast of recognize function rule ids 2024-09-04 16:48:34 +02:00
Ahmed Bouhuolia
66fb0c9fa3 feat: Tabular number of all money columns 2024-09-04 14:58:31 +02:00
Ahmed Bouhuolia
85acc85f17 Merge pull request #655 from bigcapitalhq/ui-tweaks
feat: Datatable UI improvements
2024-09-04 14:02:43 +02:00
Ahmed Bouhuolia
c76ce09191 feat: optimize the style of bank account transactions tables 2024-09-04 13:59:13 +02:00
Ahmed Bouhuolia
e3532098b2 Merge branch 'develop' into ui-tweaks 2024-09-04 09:53:55 +02:00
Ahmed Bouhuolia
b6783eb22e Merge pull request #658 from bigcapitalhq/banking-layout-breaking
feat: Bank pages layout breaking
2024-09-04 09:52:35 +02:00
Ahmed Bouhuolia
4ef00ab122 feat: Bank pages layout breaking 2024-09-04 09:51:35 +02:00
Ahmed Bouhuolia
3dd827417a fix: remove duplicated event types 2024-09-03 17:31:00 +02:00
Ahmed Bouhuolia
cbacd02aa2 Merge pull request #656 from bigcapitalhq/add-help-dropdown-menu
feat: Add help dropdown menu
2024-09-03 17:28:12 +02:00
Ahmed Bouhuolia
3b7e0fb78a Merge pull request #657 from bigcapitalhq/suspense-lazy-banking-pages
fix: Suspense the lazy loaded components in banking pages
2024-09-03 17:26:02 +02:00
Ahmed Bouhuolia
9add716395 fix: Suspense the lazy loaded components in banking pages 2024-09-03 17:24:11 +02:00
Ahmed Bouhuolia
083ea28a1f feat: Add help dropdown menu 2024-09-03 16:41:20 +02:00
Ahmed Bouhuolia
1b51742c36 feat: Datatable UI improvements 2024-09-03 16:39:13 +02:00
Ahmed Bouhuolia
0c6f23e770 Merge pull request #654 from bigcapitalhq/expense-credit-card
fix: Expense cannot accept credit card as payment account
2024-09-03 12:23:03 +02:00
Ahmed Bouhuolia
37a8ca4e97 fix: Expense cannot accept credit card as payment account 2024-09-03 12:22:07 +02:00
Ahmed Bouhuolia
795303c3a8 Merge pull request #653 from bigcapitalhq/tracking-more-events
feat: Tracking more Posthog events
2024-09-03 11:28:27 +02:00
Ahmed Bouhuolia
63ba3f0898 Merge branch 'develop' into tracking-more-events 2024-09-03 11:28:05 +02:00
Ahmed Bouhuolia
62594efa00 feat: Tracking more Posthog events 2024-09-03 11:26:24 +02:00
Ahmed Bouhuolia
9ac0dcbdc3 Merge pull request #651 from bigcapitalhq/bank-transactions-events
feat: Track banking service events
2024-09-02 17:13:49 +02:00
Ahmed Bouhuolia
df588dc4ce Merge branch 'develop' into bank-transactions-events 2024-09-02 17:13:43 +02:00
Ahmed Bouhuolia
d54f14a87a feat: Track banking service events 2024-09-02 17:12:37 +02:00
Ahmed Bouhuolia
a4d4be54c1 Merge pull request #650 from bigcapitalhq/cover-more-tracking-events
feat: Cover more tracking events.
2024-09-02 15:20:11 +02:00
Ahmed Bouhuolia
ddd17e74b5 feat: Cover more tracking events. 2024-09-02 15:19:01 +02:00
Ahmed Bouhuolia
81c0761fbe Merge pull request #649 from bigcapitalhq/import-multi-branches-expenses
fix: Integrate multiple branches with expense resource
2024-09-02 15:02:53 +02:00
Ahmed Bouhuolia
0812e3087e fix: Integrate multiple branches with expense resource 2024-09-02 15:02:02 +02:00
Ahmed Bouhuolia
0dd05493b2 Merge pull request #645 from bigcapitalhq/multi-branches-warehoues-in-importing
feat: integrate multiple branches and warehouses to resource importing
2024-09-02 14:46:08 +02:00
Ahmed Bouhuolia
bfb3909d26 feat: integrate multiple branches and warehouses with import 2024-09-02 14:42:05 +02:00
Ahmed Bouhuolia
266902026e Merge pull request #648 from bigcapitalhq/fix-bank-transactions-infinity-scrolling
fix: Bank transactions infinity scrolling
2024-09-02 10:41:54 +02:00
Ahmed Bouhuolia
791c4a4e9e fix: Bank transactions infinity scrolling 2024-09-02 10:41:16 +02:00
Ahmed Bouhuolia
2cfa9123b8 fix: Syntax error 2024-09-01 23:32:54 +02:00
Ahmed Bouhuolia
bb9614aafb Merge pull request #646 from bigcapitalhq/events-tracking
feat(server): Events tracking using Posthog
2024-09-01 23:06:38 +02:00
Ahmed Bouhuolia
302564a56e fix: remove the un-used file 2024-09-01 23:02:41 +02:00
Ahmed Bouhuolia
dcfc231d4d feat(server): Events tracking using Posthog 2024-09-01 23:01:25 +02:00
Ahmed Bouhuolia
64080ed678 Merge pull request #644 from bigcapitalhq/set-default-index-entries
fix: Set default index to transaction entries
2024-09-01 15:10:40 +02:00
Ahmed Bouhuolia
263935d91e Merge pull request #643 from bigcapitalhq/import-bugs
fix: Import bugs
2024-09-01 15:02:31 +02:00
Ahmed Bouhuolia
7f5ffb8da1 fix: Add the missing columns to the payment received and made models 2024-09-01 15:00:51 +02:00
Ahmed Bouhuolia
f07d25edbe fix: Set default index to transaction entries 2024-09-01 13:32:47 +02:00
Ahmed Bouhuolia
7c07d6b5ff fix: The unimported functions 2024-09-01 11:16:50 +02:00
Ahmed Bouhuolia
e433f4ad68 fix: Item resource columns labels 2024-09-01 11:07:55 +02:00
Ahmed Bouhuolia
a79b9caff6 Merge pull request #641 from bigcapitalhq/fix-getting-sheet-columns
fix: Getting the sheet columns in import sheet
2024-08-30 17:56:53 +02:00
Ahmed Bouhuolia
2227cead66 Merge pull request #624 from bigcapitalhq/subscription-middleware
fix: Subscription middleware
2024-08-30 17:56:02 +02:00
Ahmed Bouhuolia
410c4ea3e2 fix: Subscription active detarminer 2024-08-30 17:52:53 +02:00
Ahmed Bouhuolia
f92acbcbe0 fix: Getting the sheet columns in import sheet 2024-08-30 17:03:16 +02:00
Ahmed Bouhuolia
c986585cd9 Merge pull request #640 from bigcapitalhq/fix-typo-one-click-demo
fix: Typo one-click demo page
2024-08-30 00:15:29 +02:00
Ahmed Bouhuolia
250f0a30ef fix: Typo one-click demo page 2024-08-30 00:14:59 +02:00
Ahmed Bouhuolia
84b5e1adc1 Merge pull request #639 from bigcapitalhq/add-customer-type-to-customers
fix: Add customer type to customers resource
2024-08-29 22:50:20 +02:00
Ahmed Bouhuolia
2ab28370db fix: Add customer type to customers resource 2024-08-29 22:49:49 +02:00
Ahmed Bouhuolia
a88a525326 Merge pull request #638 from bigcapitalhq/use-standard-date-format-export
fix: use standard ISO 8601 format for exported data
2024-08-29 22:40:59 +02:00
Ahmed Bouhuolia
fce8e2c5a4 fix: use standard ISO 8601 format for exported data 2024-08-29 22:39:51 +02:00
Ahmed Bouhuolia
095608266c Merge pull request #632 from bigcapitalhq/split-lazy-loading
feat: Optimize loading perf. by splitting big chunks and lazy loading them
2024-08-29 21:27:40 +02:00
Ahmed Bouhuolia
0ec8aaa330 fix: delete unwanted files 2024-08-29 21:27:20 +02:00
Ahmed Bouhuolia
9fcb3ef77d feat: Add fallback spinner to authentication lazy-loaded pages 2024-08-29 21:19:36 +02:00
Ahmed Bouhuolia
dc61c57daf fix: Add spinner to preferences lazy loaded pages 2024-08-29 21:03:27 +02:00
Ahmed Bouhuolia
af284f3f6d feat: split the preferences pages 2024-08-29 20:49:08 +02:00
Ahmed Bouhuolia
c9fe6d9b37 feat: Optimize loading perf. by spliting big chunks and lazy loading them 2024-08-26 22:51:40 +02:00
Ahmed Bouhuolia
ee2d8d3065 fix: subscription middleare 2024-08-25 12:42:42 +02:00
186 changed files with 3075 additions and 871 deletions

View File

@@ -89,3 +89,7 @@ S3_ACCESS_KEY_ID=
S3_SECRET_ACCESS_KEY= S3_SECRET_ACCESS_KEY=
S3_ENDPOINT= S3_ENDPOINT=
S3_BUCKET= S3_BUCKET=
# PostHog
POSTHOG_API_KEY=
POSTHOG_HOST=

View File

@@ -99,6 +99,7 @@
"objection-unique": "^1.2.2", "objection-unique": "^1.2.2",
"plaid": "^10.3.0", "plaid": "^10.3.0",
"pluralize": "^8.0.0", "pluralize": "^8.0.0",
"posthog-node": "^4.2.0",
"pug": "^3.0.2", "pug": "^3.0.2",
"puppeteer": "^10.2.0", "puppeteer": "^10.2.0",
"qim": "0.0.52", "qim": "0.0.52",

View File

@@ -1,6 +1,8 @@
import { Container } from 'typedi'; import { Container } from 'typedi';
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from 'express';
const SupportedMethods = ['POST', 'PUT'];
export default (subscriptionSlug = 'main') => export default (subscriptionSlug = 'main') =>
async (req: Request, res: Response, next: NextFunction) => { async (req: Request, res: Response, next: NextFunction) => {
const { tenant, tenantId } = req; const { tenant, tenantId } = req;
@@ -19,8 +21,10 @@ export default (subscriptionSlug = 'main') =>
errors: [{ type: 'TENANT.HAS.NO.SUBSCRIPTION' }], errors: [{ type: 'TENANT.HAS.NO.SUBSCRIPTION' }],
}); });
} }
// Validate in case the subscription is inactive. const isMethodSupported = SupportedMethods.includes(req.method);
else if (subscription.inactive()) { const isSubscriptionInactive = subscription.inactive();
if (isMethodSupported && isSubscriptionInactive) {
return res.boom.badRequest(null, { return res.boom.badRequest(null, {
errors: [{ type: 'ORGANIZATION.SUBSCRIPTION.INACTIVE' }], errors: [{ type: 'ORGANIZATION.SUBSCRIPTION.INACTIVE' }],
}); });

View File

@@ -253,4 +253,12 @@ module.exports = {
enable: parseBoolean(process.env.ONE_CLICK_DEMO_ACCOUNTS, false), enable: parseBoolean(process.env.ONE_CLICK_DEMO_ACCOUNTS, false),
demoUrl: process.env.ONE_CLICK_DEMO_ACCOUNTS_URL || '', demoUrl: process.env.ONE_CLICK_DEMO_ACCOUNTS_URL || '',
}, },
/**
* PostHog
*/
posthog: {
apiKey: process.env.POSTHOG_API_KEY,
host: process.env.POSTHOG_HOST
}
}; };

View File

@@ -0,0 +1,79 @@
export const SALE_INVOICE_CREATED = 'Sale invoice created';
export const SALE_INVOICE_EDITED = 'Sale invoice d';
export const SALE_INVOICE_DELETED = 'Sale invoice deleted';
export const SALE_INVOICE_MAIL_DELIVERED = 'Sale invoice mail delivered';
export const SALE_ESTIMATE_CREATED = 'Sale estimate created';
export const SALE_ESTIMATE_EDITED = 'Sale estimate edited';
export const SALE_ESTIMATE_DELETED = 'Sale estimate deleted';
export const PAYMENT_RECEIVED_CREATED = 'Payment received created';
export const PAYMENT_RECEIVED_EDITED = 'payment received edited';
export const PAYMENT_RECEIVED_DELETED = 'Payment received deleted';
export const BILL_CREATED = 'Bill created';
export const BILL_EDITED = 'Bill edited';
export const BILL_DELETED = 'Bill deleted';
export const PAYMENT_MADE_CREATED = 'Payment made created';
export const PAYMENT_MADE_EDITED = 'Payment made edited';
export const PAYMENT_MADE_DELETED = 'Payment made deleted';
export const EXPENSE_CREATED = 'Expense created';
export const EXPENSE_EDITED = 'Expense edited';
export const EXPENSE_DELETED = 'Expense deleted';
export const ACCOUNT_CREATED = 'Account created';
export const ACCOUNT_EDITED = 'Account Edited';
export const ACCOUNT_DELETED = 'Account deleted';
export const ITEM_EVENT_CREATED = 'Item created';
export const ITEM_EVENT_EDITED = 'Item edited';
export const ITEM_EVENT_DELETED = 'Item deleted';
export const AUTH_SIGNED_UP = 'Auth Signed-up';
export const AUTH_RESET_PASSWORD = 'Auth reset password';
export const SUBSCRIPTION_CANCELLED = 'Subscription cancelled';
export const SUBSCRIPTION_RESUMED = 'Subscription resumed';
export const SUBSCRIPTION_PLAN_CHANGED = 'Subscription plan changed';
export const CUSTOMER_CREATED = 'Customer created';
export const CUSTOMER_EDITED = 'Customer edited';
export const CUSTOMER_DELETED = 'Customer deleted';
export const VENDOR_CREATED = 'Vendor created';
export const VENDOR_EDITED = 'Vendor edited';
export const VENDOR_DELETED = 'Vendor deleted';
export const TRANSACTIONS_LOCKING_LOCKED = 'Transactions locking locked';
export const TRANSACTIONS_LOCKING_LOCKING_CANCELLED =
'Transactions locking cancelled';
export const TRANSACTIONS_LOCKING_PARTIALLY_UNLOCKED =
'Transactions locking partially unlocked';
export const TRANSACTIONS_LOCKING_PARTIALLY_UNLOCK_CANCELLED =
'Transactions locking partially unlock cancelled';
export const BANK_TRANSACTION_MATCHED = 'Bank transaction matching deleted';
export const BANK_TRANSACTION_EXCLUDED = 'Bank transaction excluded';
export const BANK_TRANSACTION_CATEGORIZED = 'Bank transaction categorized';
export const BANK_TRANSACTION_UNCATEGORIZED = 'Bank transaction uncategorized';
export const BANK_ACCOUNT_DISCONNECTED = 'Bank account disconnected';
export const MANUAL_JOURNAL_CREATED = 'Manual journal created';
export const MANUAL_JOURNAL_EDITED = 'Manual journal edited';
export const MANUAL_JOURNAL_DELETED = 'Manual journal deleted';
export const MANUAL_JOURNAL_PUBLISHED = 'Manual journal published';
export const BANK_RULE_CREATED = 'Bank rule created';
export const BANK_RULE_EDITED = 'Bank rule edited';
export const BANK_RULE_DELETED = 'Bank rule deleted';
// # Event Groups
export const ACCOUNT_GROUP = 'Account';
export const ITEM_GROUP = 'Item';
export const AUTH_GROUP = 'Auth';
export const SALE_GROUP = 'Sale';
export const PAYMENT_GROUP = 'Payment';
export const BILL_GROUP = 'Bill';
export const EXPENSE_GROUP = 'Expense';

View File

@@ -99,6 +99,7 @@ export interface IBillsFilter extends IDynamicListFilterDTO {
stringifiedFilterRoles?: string; stringifiedFilterRoles?: string;
page: number; page: number;
pageSize: number; pageSize: number;
filterQuery?: (q: any) => void;
} }
export interface IBillsService { export interface IBillsService {

View File

@@ -146,6 +146,7 @@ export interface ICashflowTransactionUncategorizedPayload {
tenantId: number; tenantId: number;
uncategorizedTransactionId: number; uncategorizedTransactionId: number;
uncategorizedTransactions: Array<IUncategorizedCashflowTransaction>; uncategorizedTransactions: Array<IUncategorizedCashflowTransaction>;
oldMainUncategorizedTransaction: IUncategorizedCashflowTransaction;
oldUncategorizedTransactions: Array<IUncategorizedCashflowTransaction>; oldUncategorizedTransactions: Array<IUncategorizedCashflowTransaction>;
trx: Knex.Transaction; trx: Knex.Transaction;
} }

View File

@@ -241,6 +241,7 @@ export interface ICustomerEventCreatingPayload {
trx: Knex.Transaction; trx: Knex.Transaction;
} }
export interface ICustomerEventEditedPayload { export interface ICustomerEventEditedPayload {
tenantId: number
customerId: number; customerId: number;
customer: ICustomer; customer: ICustomer;
trx: Knex.Transaction; trx: Knex.Transaction;

View File

@@ -1,10 +1,10 @@
import { Knex } from 'knex'; import { Knex } from 'knex';
import { IDynamicListFilter, IItemEntry, IVendorCredit } from '@/interfaces'; import { IDynamicListFilter, IItemEntry } from '@/interfaces';
import { ILedgerEntry } from './Ledger'; import { ILedgerEntry } from './Ledger';
import { AttachmentLinkDTO } from './Attachments'; import { AttachmentLinkDTO } from './Attachments';
export interface ICreditNoteEntryNewDTO { export interface ICreditNoteEntryNewDTO {
index: number; index?: number;
itemId: number; itemId: number;
rate: number; rate: number;
quantity: number; quantity: number;
@@ -22,7 +22,7 @@ export interface ICreditNoteNewDTO {
entries: ICreditNoteEntryNewDTO[]; entries: ICreditNoteEntryNewDTO[];
branchId?: number; branchId?: number;
warehouseId?: number; warehouseId?: number;
attachments?: AttachmentLinkDTO[] attachments?: AttachmentLinkDTO[];
} }
export interface ICreditNoteEditDTO { export interface ICreditNoteEditDTO {
@@ -35,7 +35,7 @@ export interface ICreditNoteEditDTO {
entries: ICreditNoteEntryNewDTO[]; entries: ICreditNoteEntryNewDTO[];
branchId?: number; branchId?: number;
warehouseId?: number; warehouseId?: number;
attachments?: AttachmentLinkDTO[] attachments?: AttachmentLinkDTO[];
} }
export interface ICreditNoteEntry extends IItemEntry {} export interface ICreditNoteEntry extends IItemEntry {}
@@ -61,7 +61,7 @@ export interface ICreditNote {
localAmount?: number; localAmount?: number;
branchId?: number; branchId?: number;
warehouseId: number; warehouseId: number;
createdAt?: Date, createdAt?: Date;
} }
export enum CreditNoteAction { export enum CreditNoteAction {
@@ -130,7 +130,9 @@ export interface ICreditNoteOpenedPayload {
trx: Knex.Transaction; trx: Knex.Transaction;
} }
export interface ICreditNotesQueryDTO {} export interface ICreditNotesQueryDTO {
filterQuery?: (query: any) => void;
}
export interface ICreditNotesQueryDTO extends IDynamicListFilter { export interface ICreditNotesQueryDTO extends IDynamicListFilter {
page: number; page: number;

View File

@@ -17,6 +17,7 @@ export interface IExpensesFilter {
columnSortBy: string; columnSortBy: string;
sortOrder: string; sortOrder: string;
viewSlug?: string; viewSlug?: string;
filterQuery?: (query: any) => void;
} }
export interface IExpense { export interface IExpense {

View File

@@ -48,6 +48,7 @@ export interface IItemEntry {
export interface IItemEntryDTO { export interface IItemEntryDTO {
id?: number; id?: number;
index?: number;
itemId: number; itemId: number;
landedCost?: boolean; landedCost?: boolean;
warehouseId?: number; warehouseId?: number;

View File

@@ -151,6 +151,7 @@ export interface IModelMetaFieldCommon2 {
importHint?: string; importHint?: string;
order?: number; order?: number;
unique?: number; unique?: number;
features?: Array<any>
} }
export interface IModelMetaRelationField2 { export interface IModelMetaRelationField2 {

View File

@@ -66,7 +66,7 @@ export interface IPaymentReceivedEntry {
export interface IPaymentReceivedEntryDTO { export interface IPaymentReceivedEntryDTO {
id?: number; id?: number;
index: number; index?: number;
paymentReceiveId?: number; paymentReceiveId?: number;
invoiceId: number; invoiceId: number;
paymentAmount: number; paymentAmount: number;
@@ -74,6 +74,7 @@ export interface IPaymentReceivedEntryDTO {
export interface IPaymentsReceivedFilter extends IDynamicListFilterDTO { export interface IPaymentsReceivedFilter extends IDynamicListFilterDTO {
stringifiedFilterRoles?: string; stringifiedFilterRoles?: string;
filterQuery?: (trx: Knex.Transaction) => void;
} }
export interface IPaymentReceivePageEntry { export interface IPaymentReceivePageEntry {

View File

@@ -44,6 +44,7 @@ export interface ISaleEstimateDTO {
export interface ISalesEstimatesFilter extends IDynamicListFilterDTO { export interface ISalesEstimatesFilter extends IDynamicListFilterDTO {
stringifiedFilterRoles?: string; stringifiedFilterRoles?: string;
filterQuery?: (q: any) => void;
} }
export interface ISalesEstimatesService { export interface ISalesEstimatesService {

View File

@@ -79,6 +79,7 @@ export interface ISalesInvoicesFilter extends IDynamicListFilter {
page: number; page: number;
pageSize: number; pageSize: number;
searchKeyword?: string; searchKeyword?: string;
filterQuery?: (q: Knex.QueryBuilder) => void;
} }
export interface ISalesInvoicesService { export interface ISalesInvoicesService {

View File

@@ -29,7 +29,9 @@ export interface ISaleReceipt {
entries?: IItemEntry[]; entries?: IItemEntry[];
} }
export interface ISalesReceiptsFilter {} export interface ISalesReceiptsFilter {
filterQuery?: (query: any) => void;
}
export interface ISaleReceiptDTO { export interface ISaleReceiptDTO {
customerId: number; customerId: number;

View File

@@ -106,6 +106,7 @@ export interface IVendorCreditsQueryDTO extends IDynamicListFilter {
page: number; page: number;
pageSize: number; pageSize: number;
searchKeyword?: string; searchKeyword?: string;
filterQuery?: (q: any) => void;
} }
export interface IVendorCreditEditingPayload { export interface IVendorCreditEditingPayload {

View File

@@ -2,6 +2,7 @@ import moment from 'moment';
import * as R from 'ramda'; import * as R from 'ramda';
import { includes, isFunction, isObject, isUndefined, omit } from 'lodash'; import { includes, isFunction, isObject, isUndefined, omit } from 'lodash';
import { formatNumber, sortObjectKeysAlphabetically } from 'utils'; import { formatNumber, sortObjectKeysAlphabetically } from 'utils';
import { EXPORT_DTE_FORMAT } from '@/services/Export/constants';
export class Transformer { export class Transformer {
public context: any; public context: any;
@@ -156,22 +157,34 @@ export class Transformer {
} }
/** /**
* * Format date.
* @param date * @param {} date
* @returns * @param {string} format -
* @returns {}
*/ */
protected formatDate(date) { protected formatDate(date, format?: string) {
return date ? moment(date).format(this.dateFormat) : ''; // Use the export date format if the async operation is in exporting,
// otherwise use the given or default format.
const _format = this.context.exportAls.isExport
? EXPORT_DTE_FORMAT
: format || this.dateFormat;
return date ? moment(date).format(_format) : '';
} }
protected formatDateFromNow(date){ /**
*
* @param date
* @returns {}
*/
protected formatDateFromNow(date) {
return date ? moment(date).fromNow(true) : ''; return date ? moment(date).fromNow(true) : '';
} }
/** /**
* *
* @param number * @param number
* @returns * @returns {}
*/ */
protected formatNumber(number, props?) { protected formatNumber(number, props?) {
return formatNumber(number, { money: false, ...props }); return formatNumber(number, { money: false, ...props });
@@ -181,7 +194,7 @@ export class Transformer {
* *
* @param money * @param money
* @param options * @param options
* @returns * @returns {}
*/ */
protected formatMoney(money, options?) { protected formatMoney(money, options?) {
return formatNumber(money, { return formatNumber(money, {

View File

@@ -3,12 +3,17 @@ import { isNull } from 'lodash';
import HasTenancyService from '@/services/Tenancy/TenancyService'; import HasTenancyService from '@/services/Tenancy/TenancyService';
import { TenantMetadata } from '@/system/models'; import { TenantMetadata } from '@/system/models';
import { Transformer } from './Transformer'; import { Transformer } from './Transformer';
import { ImportAls } from '@/services/Import/ImportALS';
import { ExportAls } from '@/services/Export/ExportAls';
@Service() @Service()
export class TransformerInjectable { export class TransformerInjectable {
@Inject() @Inject()
private tenancy: HasTenancyService; private tenancy: HasTenancyService;
@Inject()
private exportAls: ExportAls;
/** /**
* Retrieves the application context of all tenant transformers. * Retrieves the application context of all tenant transformers.
* @param {number} tenantId * @param {number} tenantId
@@ -17,10 +22,12 @@ export class TransformerInjectable {
async getApplicationContext(tenantId: number) { async getApplicationContext(tenantId: number) {
const i18n = this.tenancy.i18n(tenantId); const i18n = this.tenancy.i18n(tenantId);
const organization = await TenantMetadata.query().findOne({ tenantId }); const organization = await TenantMetadata.query().findOne({ tenantId });
const exportAls = this.exportAls;
return { return {
organization, organization,
i18n, i18n,
exportAls,
}; };
} }

View File

@@ -118,6 +118,7 @@ import { LoopsEventsSubscriber } from '@/services/Loops/LoopsEventsSubscriber';
import { DeleteUncategorizedTransactionsOnAccountDeleting } from '@/services/Banking/BankAccounts/events/DeleteUncategorizedTransactionsOnAccountDeleting'; import { DeleteUncategorizedTransactionsOnAccountDeleting } from '@/services/Banking/BankAccounts/events/DeleteUncategorizedTransactionsOnAccountDeleting';
import { SeedInitialDemoAccountDataOnOrgBuild } from '@/services/OneClickDemo/events/SeedInitialDemoAccountData'; import { SeedInitialDemoAccountDataOnOrgBuild } from '@/services/OneClickDemo/events/SeedInitialDemoAccountData';
import { TriggerInvalidateCacheOnSubscriptionChange } from '@/services/Subscription/events/TriggerInvalidateCacheOnSubscriptionChange'; import { TriggerInvalidateCacheOnSubscriptionChange } from '@/services/Subscription/events/TriggerInvalidateCacheOnSubscriptionChange';
import { EventsTrackerListeners } from '@/services/EventsTracker/events/events';
export default () => { export default () => {
return new EventPublisher(); return new EventPublisher();
@@ -289,5 +290,7 @@ export const susbcribers = () => {
// Demo Account // Demo Account
SeedInitialDemoAccountDataOnOrgBuild, SeedInitialDemoAccountDataOnOrgBuild,
...EventsTrackerListeners
]; ];
}; };

View File

@@ -1,3 +1,5 @@
import { Features } from '@/interfaces';
export default { export default {
defaultFilterField: 'vendor', defaultFilterField: 'vendor',
defaultSort: { defaultSort: {
@@ -167,6 +169,18 @@ export default {
}, },
}, },
}, },
branch: {
name: 'Branch',
type: 'text',
accessor: 'branch.name',
features: [Features.BRANCHES],
},
warehouse: {
name: 'Warehouse',
type: 'text',
accessor: 'warehouse.name',
features: [Features.BRANCHES],
},
}, },
fields2: { fields2: {
billNumber: { billNumber: {
@@ -238,6 +252,22 @@ export default {
}, },
}, },
}, },
branchId: {
name: 'Branch',
fieldType: 'relation',
relationModel: 'Branch',
relationImportMatch: ['name', 'code'],
features: [Features.BRANCHES],
required: true,
},
warehouseId: {
name: 'Warehouse',
fieldType: 'relation',
relationModel: 'Warehouse',
relationImportMatch: ['name', 'code'],
features: [Features.WAREHOUSES],
required: true,
},
}, },
}; };

View File

@@ -402,6 +402,7 @@ export default class Bill extends mixin(TenantModel, [
const ItemEntry = require('models/ItemEntry'); const ItemEntry = require('models/ItemEntry');
const BillLandedCost = require('models/BillLandedCost'); const BillLandedCost = require('models/BillLandedCost');
const Branch = require('models/Branch'); const Branch = require('models/Branch');
const Warehouse = require('models/Warehouse');
const TaxRateTransaction = require('models/TaxRateTransaction'); const TaxRateTransaction = require('models/TaxRateTransaction');
const Document = require('models/Document'); const Document = require('models/Document');
const { MatchedBankTransaction } = require('models/MatchedBankTransaction'); const { MatchedBankTransaction } = require('models/MatchedBankTransaction');
@@ -453,6 +454,18 @@ export default class Bill extends mixin(TenantModel, [
}, },
}, },
/**
* Bill may has associated warehouse.
*/
warehouse: {
relation: Model.BelongsToOneRelation,
modelClass: Warehouse.default,
join: {
from: 'bills.warehouseId',
to: 'warehouses.id',
},
},
/** /**
* Bill may has associated tax rate transactions. * Bill may has associated tax rate transactions.
*/ */

View File

@@ -1,3 +1,5 @@
import { Features } from '@/interfaces';
export default { export default {
defaultFilterField: 'vendor', defaultFilterField: 'vendor',
defaultSort: { defaultSort: {
@@ -5,6 +7,8 @@ export default {
sortField: 'bill_date', sortField: 'bill_date',
}, },
exportable: true, exportable: true,
exportFlattenOn: 'entries',
importable: true, importable: true,
importAggregator: 'group', importAggregator: 'group',
importAggregateOn: 'entries', importAggregateOn: 'entries',
@@ -77,7 +81,7 @@ export default {
paymentDate: { paymentDate: {
name: 'bill_payment.field.payment_date', name: 'bill_payment.field.payment_date',
type: 'date', type: 'date',
accessor: 'formattedPaymentDate' accessor: 'formattedPaymentDate',
}, },
paymentNumber: { paymentNumber: {
name: 'bill_payment.field.payment_number', name: 'bill_payment.field.payment_number',
@@ -111,6 +115,40 @@ export default {
name: 'bill_payment.field.reference', name: 'bill_payment.field.reference',
type: 'text', type: 'text',
}, },
entries: {
name: 'Entries',
accessor: 'entries',
type: 'collection',
collectionOf: 'object',
columns: {
date: {
name: 'Bill date',
accessor: 'bill.formattedBillDate',
},
billNo: {
name: 'Bill No.',
accessor: 'bill.billNo',
},
billRefNo: {
name: 'Bill Reference No.',
accessor: 'bill.referenceNo',
},
billAmount: {
name: 'Bill Amount',
accessor: 'bill.totalFormatted',
},
paidAmount: {
name: 'Paid Amount',
accessor: 'paymentAmountFormatted',
},
},
},
branch: {
name: 'Branch',
type: 'text',
accessor: 'branch.name',
features: [Features.BRANCHES],
},
}, },
fields2: { fields2: {
vendorId: { vendorId: {
@@ -174,5 +212,13 @@ export default {
}, },
}, },
}, },
branchId: {
name: 'Branch',
fieldType: 'relation',
relationModel: 'Branch',
relationImportMatch: ['name', 'code'],
features: [Features.BRANCHES],
required: true,
},
}, },
}; };

View File

@@ -0,0 +1,11 @@
export default {
fields2: {
name: {
name: 'Name',
fieldType: 'text',
required: true,
},
},
columns: {},
fields: {}
};

View File

@@ -1,7 +1,9 @@
import { Model } from 'objection'; import { Model, mixin } from 'objection';
import TenantModel from 'models/TenantModel'; import TenantModel from 'models/TenantModel';
import BranchMetadata from './Branch.settings';
import ModelSetting from './ModelSetting';
export default class Branch extends TenantModel { export default class Branch extends mixin(TenantModel, [ModelSetting]) {
/** /**
* Table name. * Table name.
*/ */
@@ -169,4 +171,11 @@ export default class Branch extends TenantModel {
}, },
}; };
} }
/**
* Model settings.
*/
static get meta() {
return BranchMetadata;
}
} }

View File

@@ -1,3 +1,5 @@
import { Features } from '@/interfaces';
function StatusFieldFilterQuery(query, role) { function StatusFieldFilterQuery(query, role) {
query.modify('filterByStatus', role.value); query.modify('filterByStatus', role.value);
} }
@@ -100,7 +102,7 @@ export default {
}, },
creditNoteDate: { creditNoteDate: {
name: 'Credit Note Date', name: 'Credit Note Date',
accessor: 'formattedCreditNoteDate' accessor: 'formattedCreditNoteDate',
}, },
referenceNo: { referenceNo: {
name: 'Reference No.', name: 'Reference No.',
@@ -147,6 +149,18 @@ export default {
}, },
}, },
}, },
branch: {
name: 'Branch',
type: 'text',
accessor: 'branch.name',
features: [Features.BRANCHES],
},
warehouse: {
name: 'Warehouse',
type: 'text',
accessor: 'warehouse.name',
features: [Features.BRANCHES],
},
}, },
fields2: { fields2: {
customerId: { customerId: {
@@ -215,5 +229,21 @@ export default {
}, },
}, },
}, },
branchId: {
name: 'Branch',
fieldType: 'relation',
relationModel: 'Branch',
relationImportMatch: ['name', 'code'],
features: [Features.BRANCHES],
required: true,
},
warehouseId: {
name: 'Warehouse',
fieldType: 'relation',
relationModel: 'Warehouse',
relationImportMatch: ['name', 'code'],
features: [Features.WAREHOUSES],
required: true,
},
}, },
}; };

View File

@@ -175,6 +175,7 @@ export default class CreditNote extends mixin(TenantModel, [
const Customer = require('models/Customer'); const Customer = require('models/Customer');
const Branch = require('models/Branch'); const Branch = require('models/Branch');
const Document = require('models/Document'); const Document = require('models/Document');
const Warehouse = require('models/Warehouse');
return { return {
/** /**
@@ -235,6 +236,18 @@ export default class CreditNote extends mixin(TenantModel, [
}, },
}, },
/**
* Credit note may has associated warehouse.
*/
warehouse: {
relation: Model.BelongsToOneRelation,
modelClass: Warehouse.default,
join: {
from: 'credit_notes.warehouseId',
to: 'warehouses.id',
},
},
/** /**
* Credit note may has many attached attachments. * Credit note may has many attached attachments.
*/ */

View File

@@ -95,6 +95,11 @@ export default {
}, },
}, },
columns: { columns: {
customerType: {
name: 'Customer Type',
type: 'text',
accessor: 'formattedCustomerType',
},
firstName: { firstName: {
name: 'vendor.field.first_name', name: 'vendor.field.first_name',
type: 'text', type: 'text',
@@ -135,116 +140,117 @@ export default {
openingBalance: { openingBalance: {
name: 'vendor.field.opening_balance', name: 'vendor.field.opening_balance',
type: 'number', type: 'number',
printable: false printable: false,
}, },
openingBalanceAt: { openingBalanceAt: {
name: 'vendor.field.opening_balance_at', name: 'vendor.field.opening_balance_at',
type: 'date', type: 'date',
printable: false printable: false,
accessor: 'formattedOpeningBalanceAt'
}, },
currencyCode: { currencyCode: {
name: 'vendor.field.currency', name: 'vendor.field.currency',
type: 'text', type: 'text',
printable: false printable: false,
}, },
status: { status: {
name: 'vendor.field.status', name: 'vendor.field.status',
printable: false printable: false,
}, },
note: { note: {
name: 'vendor.field.note', name: 'vendor.field.note',
printable: false printable: false,
}, },
// Billing Address // Billing Address
billingAddress1: { billingAddress1: {
name: 'Billing Address 1', name: 'Billing Address 1',
column: 'billing_address1', column: 'billing_address1',
type: 'text', type: 'text',
printable: false printable: false,
}, },
billingAddress2: { billingAddress2: {
name: 'Billing Address 2', name: 'Billing Address 2',
column: 'billing_address2', column: 'billing_address2',
type: 'text', type: 'text',
printable: false printable: false,
}, },
billingAddressCity: { billingAddressCity: {
name: 'Billing Address City', name: 'Billing Address City',
column: 'billing_address_city', column: 'billing_address_city',
type: 'text', type: 'text',
printable: false printable: false,
}, },
billingAddressCountry: { billingAddressCountry: {
name: 'Billing Address Country', name: 'Billing Address Country',
column: 'billing_address_country', column: 'billing_address_country',
type: 'text', type: 'text',
printable: false printable: false,
}, },
billingAddressPostcode: { billingAddressPostcode: {
name: 'Billing Address Postcode', name: 'Billing Address Postcode',
column: 'billing_address_postcode', column: 'billing_address_postcode',
type: 'text', type: 'text',
printable: false printable: false,
}, },
billingAddressState: { billingAddressState: {
name: 'Billing Address State', name: 'Billing Address State',
column: 'billing_address_state', column: 'billing_address_state',
type: 'text', type: 'text',
printable: false printable: false,
}, },
billingAddressPhone: { billingAddressPhone: {
name: 'Billing Address Phone', name: 'Billing Address Phone',
column: 'billing_address_phone', column: 'billing_address_phone',
type: 'text', type: 'text',
printable: false printable: false,
}, },
// Shipping Address // Shipping Address
shippingAddress1: { shippingAddress1: {
name: 'Shipping Address 1', name: 'Shipping Address 1',
column: 'shipping_address1', column: 'shipping_address1',
type: 'text', type: 'text',
printable: false printable: false,
}, },
shippingAddress2: { shippingAddress2: {
name: 'Shipping Address 2', name: 'Shipping Address 2',
column: 'shipping_address2', column: 'shipping_address2',
type: 'text', type: 'text',
printable: false printable: false,
}, },
shippingAddressCity: { shippingAddressCity: {
name: 'Shipping Address City', name: 'Shipping Address City',
column: 'shipping_address_city', column: 'shipping_address_city',
type: 'text', type: 'text',
printable: false printable: false,
}, },
shippingAddressCountry: { shippingAddressCountry: {
name: 'Shipping Address Country', name: 'Shipping Address Country',
column: 'shipping_address_country', column: 'shipping_address_country',
type: 'text', type: 'text',
printable: false printable: false,
}, },
shippingAddressPostcode: { shippingAddressPostcode: {
name: 'Shipping Address Postcode', name: 'Shipping Address Postcode',
column: 'shipping_address_postcode', column: 'shipping_address_postcode',
type: 'text', type: 'text',
printable: false printable: false,
}, },
shippingAddressPhone: { shippingAddressPhone: {
name: 'Shipping Address Phone', name: 'Shipping Address Phone',
column: 'shipping_address_phone', column: 'shipping_address_phone',
type: 'text', type: 'text',
printable: false printable: false,
}, },
shippingAddressState: { shippingAddressState: {
name: 'Shipping Address State', name: 'Shipping Address State',
column: 'shipping_address_state', column: 'shipping_address_state',
type: 'text', type: 'text',
printable: false printable: false,
}, },
createdAt: { createdAt: {
name: 'vendor.field.created_at', name: 'vendor.field.created_at',
type: 'date', type: 'date',
printable: false printable: false,
}, },
}, },
fields2: { fields2: {

View File

@@ -1,3 +1,5 @@
import { Features } from '@/interfaces';
/** /**
* Expense - Settings. * Expense - Settings.
*/ */
@@ -119,6 +121,12 @@ export default {
type: 'boolean', type: 'boolean',
printable: false, printable: false,
}, },
branch: {
name: 'Branch',
type: 'text',
accessor: 'branch.name',
features: [Features.BRANCHES],
},
}, },
fields2: { fields2: {
paymentAccountId: { paymentAccountId: {
@@ -178,6 +186,14 @@ export default {
name: 'expense.field.publish', name: 'expense.field.publish',
fieldType: 'boolean', fieldType: 'boolean',
}, },
branchId: {
name: 'Branch',
fieldType: 'relation',
relationModel: 'Branch',
relationImportMatch: ['name', 'code'],
features: [Features.BRANCHES],
required: true,
},
}, },
}; };

View File

@@ -41,17 +41,17 @@ export default {
fieldType: 'boolean', fieldType: 'boolean',
}, },
sell_price: { sell_price: {
name: 'item.field.cost_price', name: 'item.field.sell_price',
column: 'sell_price', column: 'sell_price',
fieldType: 'number', fieldType: 'number',
}, },
cost_price: { cost_price: {
name: 'item.field.cost_account', name: 'item.field.cost_price',
column: 'cost_price', column: 'cost_price',
fieldType: 'number', fieldType: 'number',
}, },
cost_account: { cost_account: {
name: 'item.field.sell_account', name: 'item.field.cost_account',
column: 'cost_account_id', column: 'cost_account_id',
fieldType: 'relation', fieldType: 'relation',
@@ -62,7 +62,7 @@ export default {
relationEntityKey: 'slug', relationEntityKey: 'slug',
}, },
sell_account: { sell_account: {
name: 'item.field.sell_description', name: 'item.field.sell_account',
column: 'sell_account_id', column: 'sell_account_id',
fieldType: 'relation', fieldType: 'relation',
@@ -155,24 +155,24 @@ export default {
printable: false, printable: false,
}, },
sellPrice: { sellPrice: {
name: 'item.field.cost_price', name: 'item.field.sell_price',
type: 'number', type: 'number',
exportable: true, exportable: true,
}, },
costPrice: { costPrice: {
name: 'item.field.cost_account', name: 'item.field.cost_price',
type: 'number', type: 'number',
exportable: true, exportable: true,
}, },
costAccount: { costAccount: {
name: 'item.field.sell_account', name: 'item.field.cost_account',
type: 'text', type: 'text',
accessor: 'costAccount.name', accessor: 'costAccount.name',
exportable: true, exportable: true,
printable: false, printable: false,
}, },
sellAccount: { sellAccount: {
name: 'item.field.sell_description', name: 'item.field.sell_account',
type: 'text', type: 'text',
accessor: 'sellAccount.name', accessor: 'sellAccount.name',
exportable: true, exportable: true,

View File

@@ -1,6 +1,11 @@
import { Features } from '@/interfaces';
export default { export default {
importable: true, importable: true,
exportable: true, exportable: true,
exportFlattenOn: 'entries',
importAggregator: 'group', importAggregator: 'group',
importAggregateOn: 'entries', importAggregateOn: 'entries',
importAggregateBy: 'paymentReceiveNo', importAggregateBy: 'paymentReceiveNo',
@@ -72,7 +77,7 @@ export default {
amount: { amount: {
name: 'payment_receive.field.amount', name: 'payment_receive.field.amount',
type: 'number', type: 'number',
accessor: 'formattedAmount' accessor: 'formattedAmount',
}, },
referenceNo: { referenceNo: {
name: 'payment_receive.field.reference_no', name: 'payment_receive.field.reference_no',
@@ -92,11 +97,45 @@ export default {
type: 'text', type: 'text',
printable: false, printable: false,
}, },
entries: {
name: 'Entries',
accessor: 'entries',
type: 'collection',
collectionOf: 'object',
columns: {
date: {
name: 'Invoice date',
accessor: 'invoice.invoiceDateFormatted',
},
invoiceNo: {
name: 'Invoice No.',
accessor: 'invoice.invoiceNo',
},
invoiceRefNo: {
name: 'Invoice Reference No.',
accessor: 'invoice.referenceNo',
},
invoiceAmount: {
name: 'Invoice Amount',
accessor: 'invoice.totalFormatted',
},
paidAmount: {
name: 'Paid Amount',
accessor: 'paymentAmountFormatted',
},
},
},
created_at: { created_at: {
name: 'payment_receive.field.created_at', name: 'payment_receive.field.created_at',
type: 'date', type: 'date',
printable: false, printable: false,
}, },
branch: {
name: 'Branch',
type: 'text',
accessor: 'branch.name',
features: [Features.BRANCHES],
},
}, },
fields2: { fields2: {
customerId: { customerId: {
@@ -158,5 +197,13 @@ export default {
}, },
}, },
}, },
branchId: {
name: 'Branch',
fieldType: 'relation',
relationModel: 'Branch',
relationImportMatch: ['name', 'code'],
features: [Features.BRANCHES],
required: true,
},
}, },
}; };

View File

@@ -1,3 +1,5 @@
import { Features } from '@/interfaces';
export default { export default {
defaultFilterField: 'estimate_date', defaultFilterField: 'estimate_date',
defaultSort: { defaultSort: {
@@ -13,7 +15,7 @@ export default {
importAggregateBy: 'estimateNumber', importAggregateBy: 'estimateNumber',
print: { print: {
pageTitle: 'Sale Estimates' pageTitle: 'Sale Estimates',
}, },
fields: { fields: {
@@ -142,6 +144,7 @@ export default {
delivered: { delivered: {
name: 'Delivered', name: 'Delivered',
type: 'boolean', type: 'boolean',
accessor: 'isDelivered',
exportable: true, exportable: true,
printable: false, printable: false,
}, },
@@ -173,6 +176,18 @@ export default {
}, },
}, },
}, },
branch: {
name: 'Branch',
type: 'text',
accessor: 'branch.name',
features: [Features.BRANCHES],
},
warehouse: {
name: 'Warehouse',
type: 'text',
accessor: 'warehouse.name',
features: [Features.BRANCHES],
},
}, },
fields2: { fields2: {
customerId: { customerId: {
@@ -251,6 +266,22 @@ export default {
}, },
}, },
}, },
branchId: {
name: 'Branch',
fieldType: 'relation',
relationModel: 'Branch',
relationImportMatch: ['name', 'code'],
features: [Features.BRANCHES],
required: true,
},
warehouseId: {
name: 'Warehouse',
fieldType: 'relation',
relationModel: 'Warehouse',
relationImportMatch: ['name', 'code'],
features: [Features.WAREHOUSES],
required: true,
},
}, },
}; };

View File

@@ -182,6 +182,7 @@ export default class SaleEstimate extends mixin(TenantModel, [
const ItemEntry = require('models/ItemEntry'); const ItemEntry = require('models/ItemEntry');
const Customer = require('models/Customer'); const Customer = require('models/Customer');
const Branch = require('models/Branch'); const Branch = require('models/Branch');
const Warehouse = require('models/Warehouse');
const Document = require('models/Document'); const Document = require('models/Document');
return { return {
@@ -221,6 +222,18 @@ export default class SaleEstimate extends mixin(TenantModel, [
}, },
}, },
/**
* Sale estimate may has associated warehouse.
*/
warehouse: {
relation: Model.BelongsToOneRelation,
modelClass: Warehouse.default,
join: {
from: 'sales_estimates.warehouseId',
to: 'warehouses.id',
},
},
/** /**
* Sale estimate transaction may has many attached attachments. * Sale estimate transaction may has many attached attachments.
*/ */

View File

@@ -1,3 +1,5 @@
import { Features } from '@/interfaces';
export default { export default {
defaultFilterField: 'customer', defaultFilterField: 'customer',
defaultSort: { defaultSort: {
@@ -155,6 +157,7 @@ export default {
name: 'invoice.field.delivered', name: 'invoice.field.delivered',
type: 'boolean', type: 'boolean',
printable: false, printable: false,
accessor: 'isDelivered',
}, },
entries: { entries: {
name: 'Entries', name: 'Entries',
@@ -184,6 +187,18 @@ export default {
}, },
}, },
}, },
branch: {
name: 'Branch',
type: 'text',
accessor: 'branch.name',
features: [Features.BRANCHES],
},
warehouse: {
name: 'Warehouse',
type: 'text',
accessor: 'warehouse.name',
features: [Features.BRANCHES],
},
}, },
fields2: { fields2: {
invoiceDate: { invoiceDate: {
@@ -267,6 +282,22 @@ export default {
fieldType: 'boolean', fieldType: 'boolean',
printable: false, printable: false,
}, },
branchId: {
name: 'Branch',
fieldType: 'relation',
relationModel: 'Branch',
relationImportMatch: ['name', 'code'],
features: [Features.BRANCHES],
required: true,
},
warehouseId: {
name: 'Warehouse',
fieldType: 'relation',
relationModel: 'Warehouse',
relationImportMatch: ['name', 'code'],
features: [Features.WAREHOUSES],
required: true,
},
}, },
}; };

View File

@@ -408,6 +408,7 @@ export default class SaleInvoice extends mixin(TenantModel, [
const InventoryCostLotTracker = require('models/InventoryCostLotTracker'); const InventoryCostLotTracker = require('models/InventoryCostLotTracker');
const PaymentReceiveEntry = require('models/PaymentReceiveEntry'); const PaymentReceiveEntry = require('models/PaymentReceiveEntry');
const Branch = require('models/Branch'); const Branch = require('models/Branch');
const Warehouse = require('models/Warehouse');
const Account = require('models/Account'); const Account = require('models/Account');
const TaxRateTransaction = require('models/TaxRateTransaction'); const TaxRateTransaction = require('models/TaxRateTransaction');
const Document = require('models/Document'); const Document = require('models/Document');
@@ -499,6 +500,18 @@ export default class SaleInvoice extends mixin(TenantModel, [
}, },
}, },
/**
* Invoice may has associated warehouse.
*/
warehouse: {
relation: Model.BelongsToOneRelation,
modelClass: Warehouse.default,
join: {
from: 'sales_invoices.warehouseId',
to: 'warehouses.id',
}
},
/** /**
* Invoice may has associated written-off expense account. * Invoice may has associated written-off expense account.
*/ */

View File

@@ -1,3 +1,5 @@
import { Features } from '@/interfaces';
export default { export default {
defaultFilterField: 'receipt_date', defaultFilterField: 'receipt_date',
defaultSort: { defaultSort: {
@@ -169,6 +171,18 @@ export default {
type: 'date', type: 'date',
printable: false, printable: false,
}, },
branch: {
name: 'Branch',
type: 'text',
accessor: 'branch.name',
features: [Features.BRANCHES],
},
warehouse: {
name: 'Warehouse',
type: 'text',
accessor: 'warehouse.name',
features: [Features.BRANCHES],
},
}, },
fields2: { fields2: {
receiptDate: { receiptDate: {
@@ -245,6 +259,22 @@ export default {
name: 'Receipt Message', name: 'Receipt Message',
fieldType: 'text', fieldType: 'text',
}, },
branchId: {
name: 'Branch',
fieldType: 'relation',
relationModel: 'Branch',
relationImportMatch: ['name', 'code'],
features: [Features.BRANCHES],
required: true,
},
warehouseId: {
name: 'Warehouse',
fieldType: 'relation',
relationModel: 'Warehouse',
relationImportMatch: ['name', 'code'],
features: [Features.WAREHOUSES],
required: true,
},
}, },
}; };

View File

@@ -109,6 +109,7 @@ export default class SaleReceipt extends mixin(TenantModel, [
const ItemEntry = require('models/ItemEntry'); const ItemEntry = require('models/ItemEntry');
const Branch = require('models/Branch'); const Branch = require('models/Branch');
const Document = require('models/Document'); const Document = require('models/Document');
const Warehouse = require('models/Warehouse');
return { return {
customer: { customer: {
@@ -169,6 +170,18 @@ export default class SaleReceipt extends mixin(TenantModel, [
}, },
}, },
/**
* Sale receipt may has associated warehouse.
*/
warehouse: {
relation: Model.BelongsToOneRelation,
modelClass: Warehouse.default,
join: {
from: 'sales_receipts.warehouseId',
to: 'warehouses.id',
},
},
/** /**
* Sale receipt transaction may has many attached attachments. * Sale receipt transaction may has many attached attachments.
*/ */

View File

@@ -1,3 +1,5 @@
import { Features } from '@/interfaces';
function StatusFieldFilterQuery(query, role) { function StatusFieldFilterQuery(query, role) {
query.modify('filterByStatus', role.value); query.modify('filterByStatus', role.value);
} }
@@ -160,6 +162,18 @@ export default {
}, },
}, },
}, },
branch: {
name: 'Branch',
type: 'text',
accessor: 'branch.name',
features: [Features.BRANCHES],
},
warehouse: {
name: 'Warehouse',
type: 'text',
accessor: 'warehouse.name',
features: [Features.BRANCHES],
},
}, },
fields2: { fields2: {
vendorId: { vendorId: {
@@ -225,5 +239,21 @@ export default {
}, },
}, },
}, },
branchId: {
name: 'Branch',
fieldType: 'relation',
relationModel: 'Branch',
relationImportMatch: ['name', 'code'],
features: [Features.BRANCHES],
required: true
},
warehouseId: {
name: 'Warehouse',
fieldType: 'relation',
relationModel: 'Warehouse',
relationImportMatch: ['name', 'code'],
features: [Features.WAREHOUSES],
required: true
},
}, },
}; };

View File

@@ -178,6 +178,7 @@ export default class VendorCredit extends mixin(TenantModel, [
const ItemEntry = require('models/ItemEntry'); const ItemEntry = require('models/ItemEntry');
const Branch = require('models/Branch'); const Branch = require('models/Branch');
const Document = require('models/Document'); const Document = require('models/Document');
const Warehouse = require('models/Warehouse');
return { return {
vendor: { vendor: {
@@ -217,6 +218,18 @@ export default class VendorCredit extends mixin(TenantModel, [
}, },
}, },
/**
* Vendor credit may has associated warehouse.
*/
warehouse: {
relation: Model.BelongsToOneRelation,
modelClass: Warehouse.default,
join: {
from: 'vendor_credits.warehouseId',
to: 'warehouses.id',
},
},
/** /**
* Vendor credit may has many attached attachments. * Vendor credit may has many attached attachments.
*/ */

View File

@@ -0,0 +1,11 @@
export default {
fields2: {
name: {
name: 'Name',
fieldType: 'text',
required: true,
},
},
columns: {},
fields: {}
};

View File

@@ -78,9 +78,9 @@ export class RecognizeTranasctionsService {
}); });
const bankRules = await BankRule.query(trx).onBuild((q) => { const bankRules = await BankRule.query(trx).onBuild((q) => {
const rulesIds = castArray(ruleId); const rulesIds = !isEmpty(ruleId) ? castArray(ruleId) : [];
if (!isEmpty(rulesIds)) { if (rulesIds?.length > 0) {
q.whereIn('id', rulesIds); q.whereIn('id', rulesIds);
} }
q.withGraphFetched('conditions'); q.withGraphFetched('conditions');

View File

@@ -33,22 +33,25 @@ export class UncategorizeCashflowTransaction {
): Promise<Array<number>> { ): Promise<Array<number>> {
const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId); const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);
const oldUncategorizedTransaction = const oldMainUncategorizedTransaction =
await UncategorizedCashflowTransaction.query() await UncategorizedCashflowTransaction.query()
.findById(uncategorizedTransactionId) .findById(uncategorizedTransactionId)
.throwIfNotFound(); .throwIfNotFound();
validateTransactionShouldBeCategorized(oldUncategorizedTransaction); validateTransactionShouldBeCategorized(oldMainUncategorizedTransaction);
const associatedUncategorizedTransactions = const associatedUncategorizedTransactions =
await UncategorizedCashflowTransaction.query() await UncategorizedCashflowTransaction.query()
.where('categorizeRefId', oldUncategorizedTransaction.categorizeRefId) .where('categorizeRefId', oldMainUncategorizedTransaction.categorizeRefId)
.where( .where(
'categorizeRefType', 'categorizeRefType',
oldUncategorizedTransaction.categorizeRefType oldMainUncategorizedTransaction.categorizeRefType
); )
// Exclude the main transaction.
.whereNot('id', uncategorizedTransactionId);
const oldUncategorizedTransactions = [ const oldUncategorizedTransactions = [
oldUncategorizedTransaction, oldMainUncategorizedTransaction,
...associatedUncategorizedTransactions, ...associatedUncategorizedTransactions,
]; ];
const oldUncategoirzedTransactionsIds = oldUncategorizedTransactions.map( const oldUncategoirzedTransactionsIds = oldUncategorizedTransactions.map(
@@ -85,6 +88,7 @@ export class UncategorizeCashflowTransaction {
{ {
tenantId, tenantId,
uncategorizedTransactionId, uncategorizedTransactionId,
oldMainUncategorizedTransaction,
uncategorizedTransactions, uncategorizedTransactions,
oldUncategorizedTransactions, oldUncategorizedTransactions,
trx, trx,

View File

@@ -22,32 +22,25 @@ export class DeleteCashflowTransactionOnUncategorize {
}; };
/** /**
* Deletes the cashflow transaction on uncategorize transaction. * Deletes the cashflow transaction once uncategorize the bank transaction.
* @param {ICashflowTransactionUncategorizedPayload} payload * @param {ICashflowTransactionUncategorizedPayload} payload
*/ */
public async deleteCashflowTransactionOnUncategorize({ public async deleteCashflowTransactionOnUncategorize({
tenantId, tenantId,
oldUncategorizedTransactions, oldMainUncategorizedTransaction,
trx, trx,
}: ICashflowTransactionUncategorizedPayload) { }: ICashflowTransactionUncategorizedPayload) {
const _oldUncategorizedTransactions = oldUncategorizedTransactions.filter( // Cannot continue if the main transaction does not reference to cashflow type.
(transaction) => transaction.categorizeRefType === 'CashflowTransaction' if (
); oldMainUncategorizedTransaction.categorizeRefType !==
'CashflowTransaction'
// Deletes the cashflow transaction. ) {
if (_oldUncategorizedTransactions.length > 0) { return;
const result = await PromisePool.withConcurrency(1)
.for(_oldUncategorizedTransactions)
.process(async (oldUncategorizedTransaction) => {
await this.deleteCashflowTransactionService.deleteCashflowTransaction(
tenantId,
oldUncategorizedTransaction.categorizeRefId,
trx
);
});
if (result.errors.length > 0) {
throw new ServiceError('SOMETHING_WRONG');
}
} }
await this.deleteCashflowTransactionService.deleteCashflowTransaction(
tenantId,
oldMainUncategorizedTransaction.categorizeRefId,
trx
);
} }
} }

View File

@@ -65,6 +65,7 @@ export class EditCustomer {
}); });
// Triggers `onCustomerEdited` event. // Triggers `onCustomerEdited` event.
await this.eventPublisher.emitAsync(events.customers.onEdited, { await this.eventPublisher.emitAsync(events.customers.onEdited, {
tenantId,
customerId, customerId,
customer, customer,
trx, trx,

View File

@@ -5,11 +5,17 @@ import * as R from 'ramda';
import { ServiceError } from '@/exceptions'; import { ServiceError } from '@/exceptions';
import HasTenancyService from '@/services/Tenancy/TenancyService'; import HasTenancyService from '@/services/Tenancy/TenancyService';
import { ERRORS } from './constants'; import { ERRORS } from './constants';
import { ICreditNote, ICreditNoteEditDTO, ICreditNoteNewDTO } from '@/interfaces'; import {
ICreditNote,
ICreditNoteEditDTO,
ICreditNoteEntryNewDTO,
ICreditNoteNewDTO,
} from '@/interfaces';
import ItemsEntriesService from '@/services/Items/ItemsEntriesService'; import ItemsEntriesService from '@/services/Items/ItemsEntriesService';
import AutoIncrementOrdersService from '@/services/Sales/AutoIncrementOrdersService'; import AutoIncrementOrdersService from '@/services/Sales/AutoIncrementOrdersService';
import { WarehouseTransactionDTOTransform } from '@/services/Warehouses/Integrations/WarehouseTransactionDTOTransform'; import { WarehouseTransactionDTOTransform } from '@/services/Warehouses/Integrations/WarehouseTransactionDTOTransform';
import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform'; import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform';
import { assocItemEntriesDefaultIndex } from '../Items/utils';
@Service() @Service()
export default class BaseCreditNotes { export default class BaseCreditNotes {
@@ -43,10 +49,17 @@ export default class BaseCreditNotes {
const amount = this.itemsEntriesService.getTotalItemsEntries( const amount = this.itemsEntriesService.getTotalItemsEntries(
creditNoteDTO.entries creditNoteDTO.entries
); );
const entries = creditNoteDTO.entries.map((entry) => ({ const entries = R.compose(
...entry, // Associate the default index to each item entry.
referenceType: 'CreditNote', assocItemEntriesDefaultIndex,
}));
// Associate the reference type to credit note entries.
R.map((entry: ICreditNoteEntryNewDTO) => ({
...entry,
referenceType: 'CreditNote',
}))
)(creditNoteDTO.entries);
// Retreive the next credit note number. // Retreive the next credit note number.
const autoNextNumber = this.getNextCreditNumber(tenantId); const autoNextNumber = this.getNextCreditNumber(tenantId);

View File

@@ -15,12 +15,17 @@ export class CreditNotesExportable extends Exportable {
* @returns {} * @returns {}
*/ */
public exportable(tenantId: number, query: ICreditNotesQueryDTO) { public exportable(tenantId: number, query: ICreditNotesQueryDTO) {
const filterQuery = (query) => {
query.withGraphFetched('branch');
query.withGraphFetched('warehouse');
};
const parsedQuery = { const parsedQuery = {
sortOrder: 'desc', sortOrder: 'desc',
columnSortBy: 'created_at', columnSortBy: 'created_at',
...query, ...query,
page: 1, page: 1,
pageSize: 12000, pageSize: 12000,
filterQuery,
} as ICreditNotesQueryDTO; } as ICreditNotesQueryDTO;
return this.getCreditNotes return this.getCreditNotes

View File

@@ -48,6 +48,7 @@ export default class ListCreditNotes extends BaseCreditNotes {
builder.withGraphFetched('entries.item'); builder.withGraphFetched('entries.item');
builder.withGraphFetched('customer'); builder.withGraphFetched('customer');
dynamicFilter.buildQuery()(builder); dynamicFilter.buildQuery()(builder);
creditNotesQuery?.filterQuery && creditNotesQuery?.filterQuery(builder);
}) })
.pagination(filter.page - 1, filter.pageSize); .pagination(filter.page - 1, filter.pageSize);

View File

@@ -0,0 +1,24 @@
import { PostHog } from 'posthog-node';
import { Service } from 'typedi';
import { EventMessage } from 'posthog-node/src/types';
import config from '@/config';
@Service()
export class PosthogService {
public posthog: PostHog;
constructor() {
if (config.posthog.apiKey && config.posthog.host) {
this.posthog = new PostHog(config.posthog.apiKey, {
host: config.posthog.host,
});
}
}
public trackEvent(event: EventMessage) {
// Cannot continue if the Posthog not configured.
if (!this.posthog) return;
this.posthog.capture(event);
}
}

View File

@@ -0,0 +1,65 @@
import { Inject, Service } from 'typedi';
import { EventSubscriber } from '@/lib/EventPublisher/EventPublisher';
import {
IAccountEventCreatedPayload,
IAccountEventEditedPayload,
IAccountEventDeletedPayload,
} from '@/interfaces';
import { PosthogService } from '../PostHog';
import events from '@/subscribers/events';
import {
ACCOUNT_CREATED,
ACCOUNT_EDITED,
ACCOUNT_DELETED,
} from '@/constants/event-tracker';
@Service()
export class AccountEventsTracker extends EventSubscriber {
@Inject()
private posthog: PosthogService;
/**
* Constructor method.
*/
public attach(bus) {
bus.subscribe(
events.accounts.onCreated,
this.handleTrackAccountCreatedEvent
);
bus.subscribe(events.accounts.onEdited, this.handleTrackEditedAccountEvent);
bus.subscribe(
events.accounts.onDeleted,
this.handleTrackDeletedAccountEvent
);
}
private handleTrackAccountCreatedEvent = ({
tenantId,
}: IAccountEventCreatedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: ACCOUNT_CREATED,
properties: {},
});
};
private handleTrackEditedAccountEvent = ({
tenantId,
}: IAccountEventEditedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: ACCOUNT_EDITED,
properties: {},
});
};
private handleTrackDeletedAccountEvent = ({
tenantId,
}: IAccountEventDeletedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: ACCOUNT_DELETED,
properties: {},
});
};
}

View File

@@ -0,0 +1,35 @@
import { Inject, Service } from 'typedi';
import { EventSubscriber } from '@/lib/EventPublisher/EventPublisher';
import { IAuthSignedUpEventPayload } from '@/interfaces';
import { PosthogService } from '../PostHog';
import { AUTH_SIGNED_UP } from '@/constants/event-tracker';
import events from '@/subscribers/events';
@Service()
export class AuthenticationEventsTracker extends EventSubscriber {
@Inject()
private posthog: PosthogService;
/**
* Constructor method.
*/
public attach(bus) {
bus.subscribe(events.auth.signUp, this.handleTrackSignUpEvent);
}
private handleTrackSignUpEvent = ({
signupDTO,
user,
tenant,
}: IAuthSignedUpEventPayload) => {
this.posthog.trackEvent({
distinctId: user.email,
event: AUTH_SIGNED_UP,
properties: {
firstName: user.firstName,
lastName: user.lastName,
email: user.email,
},
});
};
}

View File

@@ -0,0 +1,68 @@
import { Inject, Service } from 'typedi';
import { EventSubscriber } from '@/lib/EventPublisher/EventPublisher';
import {
IBankRuleEventCreatedPayload,
IBankRuleEventEditedPayload,
IBankRuleEventDeletedPayload,
} from '@/services/Banking/Rules/types'; // Updated import path for interfaces
import { PosthogService } from '../PostHog';
import events from '@/subscribers/events';
import {
BANK_RULE_CREATED,
BANK_RULE_EDITED,
BANK_RULE_DELETED,
} from '@/constants/event-tracker';
@Service()
export class BankRuleEventsTracker extends EventSubscriber {
@Inject()
private posthog: PosthogService;
/**
* Constructor method.
*/
public attach(bus) {
bus.subscribe(
events.bankRules.onCreated,
this.handleTrackBankRuleCreatedEvent
);
bus.subscribe(
events.bankRules.onEdited,
this.handleTrackEditedBankRuleEvent
);
bus.subscribe(
events.bankRules.onDeleted,
this.handleTrackDeletedBankRuleEvent
);
}
private handleTrackBankRuleCreatedEvent = ({
tenantId,
}: IBankRuleEventCreatedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: BANK_RULE_CREATED,
properties: {},
});
};
private handleTrackEditedBankRuleEvent = ({
tenantId,
}: IBankRuleEventEditedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: BANK_RULE_EDITED,
properties: {},
});
};
private handleTrackDeletedBankRuleEvent = ({
tenantId,
}: IBankRuleEventDeletedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: BANK_RULE_DELETED,
properties: {},
});
};
}

View File

@@ -0,0 +1,97 @@
import { Inject, Service } from 'typedi';
import { EventSubscriber } from '@/lib/EventPublisher/EventPublisher';
import { PosthogService } from '../PostHog';
import events from '@/subscribers/events';
import {
BANK_TRANSACTION_MATCHED,
BANK_TRANSACTION_EXCLUDED,
BANK_TRANSACTION_CATEGORIZED,
BANK_TRANSACTION_UNCATEGORIZED,
BANK_ACCOUNT_DISCONNECTED,
} from '@/constants/event-tracker';
import { IBankTransactionMatchedEventPayload } from '@/services/Banking/Matching/types';
import { IBankAccountDisconnectedEventPayload } from '@/services/Banking/BankAccounts/types';
import {
ICashflowTransactionCategorizedPayload,
ICashflowTransactionUncategorizedPayload,
} from '@/interfaces';
import { IBankTransactionExcludedEventPayload } from '@/services/Banking/Exclude/_types';
@Service()
export class BankTransactionEventsTracker extends EventSubscriber {
@Inject()
private posthog: PosthogService;
public attach(bus) {
bus.subscribe(
events.bankMatch.onMatched,
this.handleTrackBankTransactionMatchedEvent
);
bus.subscribe(
events.bankTransactions.onExcluded,
this.handleTrackBankTransactionExcludedEvent
);
bus.subscribe(
events.cashflow.onTransactionCategorized,
this.handleTrackBankTransactionCategorizedEvent
);
bus.subscribe(
events.cashflow.onTransactionUncategorized,
this.handleTrackBankTransactionUncategorizedEvent
);
bus.subscribe(
events.bankAccount.onDisconnected,
this.handleTrackBankAccountDisconnectedEvent
);
}
private handleTrackBankTransactionMatchedEvent = ({
tenantId,
}: IBankTransactionMatchedEventPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: BANK_TRANSACTION_MATCHED,
properties: {},
});
};
private handleTrackBankTransactionExcludedEvent = ({
tenantId,
}: IBankTransactionExcludedEventPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: BANK_TRANSACTION_EXCLUDED,
properties: {},
});
};
private handleTrackBankTransactionCategorizedEvent = ({
tenantId,
}: ICashflowTransactionCategorizedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: BANK_TRANSACTION_CATEGORIZED,
properties: {},
});
};
private handleTrackBankTransactionUncategorizedEvent = ({
tenantId,
}: ICashflowTransactionUncategorizedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: BANK_TRANSACTION_UNCATEGORIZED,
properties: {},
});
};
private handleTrackBankAccountDisconnectedEvent = ({
tenantId,
}: IBankAccountDisconnectedEventPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: BANK_ACCOUNT_DISCONNECTED,
properties: {},
});
};
}

View File

@@ -0,0 +1,59 @@
import { Inject, Service } from 'typedi';
import { EventSubscriber } from '@/lib/EventPublisher/EventPublisher';
import {
IBillPaymentEventCreatedPayload,
IBillPaymentEventEditedPayload,
IBillPaymentEventDeletedPayload,
} from '@/interfaces';
import { PosthogService } from '../PostHog';
import events from '@/subscribers/events';
import {
BILL_CREATED,
BILL_EDITED,
BILL_DELETED,
} from '@/constants/event-tracker';
@Service()
export class BillEventsTracker extends EventSubscriber {
@Inject()
private posthog: PosthogService;
/**
* Constructor method.
*/
public attach(bus) {
bus.subscribe(events.bill.onCreated, this.handleTrackBillCreatedEvent);
bus.subscribe(events.bill.onEdited, this.handleTrackEditedBillEvent);
bus.subscribe(events.bill.onDeleted, this.handleTrackDeletedBillEvent);
}
private handleTrackBillCreatedEvent = ({
tenantId,
}: IBillPaymentEventCreatedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: BILL_CREATED,
properties: {},
});
};
private handleTrackEditedBillEvent = ({
tenantId,
}: IBillPaymentEventEditedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: BILL_EDITED,
properties: {},
});
};
private handleTrackDeletedBillEvent = ({
tenantId,
}: IBillPaymentEventDeletedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: BILL_DELETED,
properties: {},
});
};
}

View File

@@ -0,0 +1,65 @@
import { Inject, Service } from 'typedi';
import { EventSubscriber } from '@/lib/EventPublisher/EventPublisher';
import {
ICustomerEventCreatedPayload,
ICustomerEventEditedPayload,
ICustomerEventDeletedPayload,
} from '@/interfaces';
import { PosthogService } from '../PostHog';
import events from '@/subscribers/events';
import {
CUSTOMER_CREATED,
CUSTOMER_EDITED,
CUSTOMER_DELETED,
} from '@/constants/event-tracker';
@Service()
export class CustomerEventsTracker extends EventSubscriber {
@Inject()
private posthog: PosthogService;
public attach(bus) {
bus.subscribe(
events.customers.onCreated,
this.handleTrackCustomerCreatedEvent
);
bus.subscribe(
events.customers.onEdited,
this.handleTrackEditedCustomerEvent
);
bus.subscribe(
events.customers.onDeleted,
this.handleTrackDeletedCustomerEvent
);
}
private handleTrackCustomerCreatedEvent = ({
tenantId,
}: ICustomerEventCreatedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: CUSTOMER_CREATED,
properties: {},
});
};
private handleTrackEditedCustomerEvent = ({
tenantId,
}: ICustomerEventEditedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: CUSTOMER_EDITED,
properties: {},
});
};
private handleTrackDeletedCustomerEvent = ({
tenantId,
}: ICustomerEventDeletedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: CUSTOMER_DELETED,
properties: {},
});
};
}

View File

@@ -0,0 +1,65 @@
import { Inject, Service } from 'typedi';
import { EventSubscriber } from '@/lib/EventPublisher/EventPublisher';
import {
IExpenseCreatedPayload,
IExpenseEventEditPayload,
IExpenseEventDeletePayload,
} from '@/interfaces';
import { PosthogService } from '../PostHog';
import events from '@/subscribers/events';
import {
EXPENSE_CREATED,
EXPENSE_EDITED,
EXPENSE_DELETED,
} from '@/constants/event-tracker';
@Service()
export class ExpenseEventsTracker extends EventSubscriber {
@Inject()
private posthog: PosthogService;
/**
* Constructor method.
*/
public attach(bus) {
bus.subscribe(
events.expenses.onCreated,
this.handleTrackExpenseCreatedEvent
);
bus.subscribe(events.expenses.onEdited, this.handleTrackEditedExpenseEvent);
bus.subscribe(
events.expenses.onDeleted,
this.handleTrackDeletedExpenseEvent
);
}
private handleTrackExpenseCreatedEvent = ({
tenantId,
}: IExpenseCreatedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: EXPENSE_CREATED,
properties: {},
});
};
private handleTrackEditedExpenseEvent = ({
tenantId,
}: IExpenseEventEditPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: EXPENSE_EDITED,
properties: {},
});
};
private handleTrackDeletedExpenseEvent = ({
tenantId,
}: IExpenseEventDeletePayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: EXPENSE_DELETED,
properties: {},
});
};
}

View File

@@ -0,0 +1,59 @@
import { Inject, Service } from 'typedi';
import { EventSubscriber } from '@/lib/EventPublisher/EventPublisher';
import {
IItemEventCreatedPayload,
IItemEventEditedPayload,
IItemEventDeletedPayload,
} from '@/interfaces';
import { PosthogService } from '../PostHog';
import events from '@/subscribers/events';
import {
ITEM_EVENT_CREATED,
ITEM_EVENT_EDITED,
ITEM_EVENT_DELETED,
} from '@/constants/event-tracker';
@Service()
export class ItemEventsTracker extends EventSubscriber {
@Inject()
private posthog: PosthogService;
/**
* Constructor method.
*/
public attach(bus) {
bus.subscribe(events.item.onCreated, this.handleTrackItemCreatedEvent);
bus.subscribe(events.item.onEdited, this.handleTrackEditedItemEvent);
bus.subscribe(events.item.onDeleted, this.handleTrackDeletedItemEvent);
}
private handleTrackItemCreatedEvent = ({
tenantId,
}: IItemEventCreatedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: ITEM_EVENT_CREATED,
properties: {},
});
};
private handleTrackEditedItemEvent = ({
tenantId,
}: IItemEventEditedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: ITEM_EVENT_EDITED,
properties: {},
});
};
private handleTrackDeletedItemEvent = ({
tenantId,
}: IItemEventDeletedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: ITEM_EVENT_DELETED,
properties: {},
});
};
}

View File

@@ -0,0 +1,68 @@
import { Inject, Service } from 'typedi';
import { EventSubscriber } from '@/lib/EventPublisher/EventPublisher';
import {
IManualJournalEventCreatedPayload,
IManualJournalEventEditedPayload,
IManualJournalEventDeletedPayload,
} from '@/interfaces';
import { PosthogService } from '../PostHog';
import events from '@/subscribers/events';
import {
MANUAL_JOURNAL_CREATED,
MANUAL_JOURNAL_EDITED,
MANUAL_JOURNAL_DELETED,
} from '@/constants/event-tracker';
@Service()
export class ManualJournalEventsTracker extends EventSubscriber {
@Inject()
private posthog: PosthogService;
/**
* Constructor method.
*/
public attach(bus) {
bus.subscribe(
events.manualJournals.onCreated,
this.handleTrackManualJournalCreatedEvent
);
bus.subscribe(
events.manualJournals.onEdited,
this.handleTrackEditedManualJournalEvent
);
bus.subscribe(
events.manualJournals.onDeleted,
this.handleTrackDeletedManualJournalEvent
);
}
private handleTrackManualJournalCreatedEvent = ({
tenantId,
}: IManualJournalEventCreatedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: MANUAL_JOURNAL_CREATED,
properties: {},
});
};
private handleTrackEditedManualJournalEvent = ({
tenantId,
}: IManualJournalEventEditedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: MANUAL_JOURNAL_EDITED,
properties: {},
});
};
private handleTrackDeletedManualJournalEvent = ({
tenantId,
}: IManualJournalEventDeletedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: MANUAL_JOURNAL_DELETED,
properties: {},
});
};
}

View File

@@ -0,0 +1,68 @@
import { Inject, Service } from 'typedi';
import { EventSubscriber } from '@/lib/EventPublisher/EventPublisher';
import {
IBillPaymentEventCreatedPayload,
IBillPaymentEditingPayload,
IBillPaymentEventDeletedPayload,
} from '@/interfaces';
import { PosthogService } from '../PostHog';
import events from '@/subscribers/events';
import {
PAYMENT_MADE_CREATED,
PAYMENT_MADE_EDITED,
PAYMENT_MADE_DELETED,
} from '@/constants/event-tracker';
@Service()
export class PaymentMadeEventsTracker extends EventSubscriber {
@Inject()
private posthog: PosthogService;
/**
* Constructor method.
*/
public attach(bus) {
bus.subscribe(
events.billPayment.onCreated,
this.handleTrackPaymentMadeCreatedEvent
);
bus.subscribe(
events.billPayment.onEdited,
this.handleTrackEditedPaymentMadeEvent
);
bus.subscribe(
events.billPayment.onDeleted,
this.handleTrackDeletedPaymentMadeEvent
);
}
private handleTrackPaymentMadeCreatedEvent = ({
tenantId,
}: IBillPaymentEventCreatedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: PAYMENT_MADE_CREATED,
properties: {},
});
};
private handleTrackEditedPaymentMadeEvent = ({
tenantId,
}: IBillPaymentEditingPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: PAYMENT_MADE_EDITED,
properties: {},
});
};
private handleTrackDeletedPaymentMadeEvent = ({
tenantId,
}: IBillPaymentEventDeletedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: PAYMENT_MADE_DELETED,
properties: {},
});
};
}

View File

@@ -0,0 +1,68 @@
import { Inject, Service } from 'typedi';
import { EventSubscriber } from '@/lib/EventPublisher/EventPublisher';
import {
IPaymentReceivedCreatedPayload,
IPaymentReceivedEditedPayload,
IPaymentReceivedDeletedPayload,
} from '@/interfaces';
import { PosthogService } from '../PostHog';
import events from '@/subscribers/events';
import {
PAYMENT_RECEIVED_CREATED,
PAYMENT_RECEIVED_EDITED,
PAYMENT_RECEIVED_DELETED,
} from '@/constants/event-tracker';
@Service()
export class PaymentReceivedEventsTracker extends EventSubscriber {
@Inject()
private posthog: PosthogService;
/**
* Constructor method.
*/
public attach(bus) {
bus.subscribe(
events.paymentReceive.onCreated,
this.handleTrackPaymentReceivedCreatedEvent
);
bus.subscribe(
events.paymentReceive.onEdited,
this.handleTrackEditedPaymentReceivedEvent
);
bus.subscribe(
events.paymentReceive.onDeleted,
this.handleTrackDeletedPaymentReceivedEvent
);
}
private handleTrackPaymentReceivedCreatedEvent = ({
tenantId,
}: IPaymentReceivedCreatedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: PAYMENT_RECEIVED_CREATED,
properties: {},
});
};
private handleTrackEditedPaymentReceivedEvent = ({
tenantId,
}: IPaymentReceivedEditedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: PAYMENT_RECEIVED_EDITED,
properties: {},
});
};
private handleTrackDeletedPaymentReceivedEvent = ({
tenantId,
}: IPaymentReceivedDeletedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: PAYMENT_RECEIVED_DELETED,
properties: {},
});
};
}

View File

@@ -0,0 +1,68 @@
import { Inject, Service } from 'typedi';
import { EventSubscriber } from '@/lib/EventPublisher/EventPublisher';
import {
ISaleEstimateCreatedPayload,
ISaleEstimateEditedPayload,
ISaleEstimateDeletedPayload,
} from '@/interfaces';
import { PosthogService } from '../PostHog';
import events from '@/subscribers/events';
import {
SALE_ESTIMATE_CREATED,
SALE_ESTIMATE_EDITED,
SALE_ESTIMATE_DELETED,
} from '@/constants/event-tracker';
@Service()
export class SaleEstimateEventsTracker extends EventSubscriber {
@Inject()
private posthog: PosthogService;
/**
* Constructor method.
*/
public attach(bus) {
bus.subscribe(
events.saleEstimate.onCreated,
this.handleTrackEstimateCreatedEvent
);
bus.subscribe(
events.saleEstimate.onEdited,
this.handleTrackEditedEstimateEvent
);
bus.subscribe(
events.saleEstimate.onDeleted,
this.handleTrackDeletedEstimateEvent
);
}
private handleTrackEstimateCreatedEvent = ({
tenantId,
}: ISaleEstimateCreatedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: SALE_ESTIMATE_CREATED,
properties: {},
});
};
private handleTrackEditedEstimateEvent = ({
tenantId,
}: ISaleEstimateEditedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: SALE_ESTIMATE_EDITED,
properties: {},
});
};
private handleTrackDeletedEstimateEvent = ({
tenantId,
}: ISaleEstimateDeletedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: SALE_ESTIMATE_DELETED,
properties: {},
});
};
}

View File

@@ -0,0 +1,67 @@
import { Inject, Service } from 'typedi';
import { EventSubscriber } from '@/lib/EventPublisher/EventPublisher';
import {
ISaleInvoiceCreatedPayload,
ISaleInvoiceEditedPayload,
} from '@/interfaces';
import { PosthogService } from '../PostHog';
import events from '@/subscribers/events';
import {
SALE_INVOICE_CREATED,
SALE_INVOICE_DELETED,
SALE_INVOICE_EDITED,
} from '@/constants/event-tracker';
@Service()
export class SaleInvoiceEventsTracker extends EventSubscriber {
@Inject()
private posthog: PosthogService;
/**
* Constructor method.
*/
public attach(bus) {
bus.subscribe(
events.saleInvoice.onCreated,
this.handleTrackInvoiceCreatedEvent
);
bus.subscribe(
events.saleInvoice.onEdited,
this.handleTrackEditedInvoiceEvent
);
bus.subscribe(
events.saleInvoice.onDeleted,
this.handleTrackDeletedInvoiceEvent
);
}
private handleTrackInvoiceCreatedEvent = ({
tenantId,
}: ISaleInvoiceCreatedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: SALE_INVOICE_CREATED,
properties: {},
});
};
private handleTrackEditedInvoiceEvent = ({
tenantId,
}: ISaleInvoiceEditedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: SALE_INVOICE_EDITED,
properties: {},
});
};
private handleTrackDeletedInvoiceEvent = ({
tenantId,
}: ISaleInvoiceEditedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: SALE_INVOICE_DELETED,
properties: {},
});
};
}

View File

@@ -0,0 +1,76 @@
import { Inject, Service } from 'typedi';
import { EventSubscriber } from '@/lib/EventPublisher/EventPublisher';
import { ITransactionsLockingPartialUnlocked } from '@/interfaces';
import { PosthogService } from '../PostHog';
import {
TRANSACTIONS_LOCKING_LOCKED,
TRANSACTIONS_LOCKING_LOCKING_CANCELLED,
TRANSACTIONS_LOCKING_PARTIALLY_UNLOCK_CANCELLED,
TRANSACTIONS_LOCKING_PARTIALLY_UNLOCKED,
} from '@/constants/event-tracker';
import events from '@/subscribers/events';
@Service()
export class TransactionsLockingEventsTracker extends EventSubscriber {
@Inject()
private posthog: PosthogService;
public attach(bus) {
bus.subscribe(
events.transactionsLocking.locked,
this.handleTransactionsLockingLockedEvent
);
bus.subscribe(
events.transactionsLocking.lockCanceled,
this.handleTransactionsLockingCancelledEvent
);
bus.subscribe(
events.transactionsLocking.partialUnlocked,
this.handleTransactionsLockingPartiallyUnlockedEvent
);
bus.subscribe(
events.transactionsLocking.partialUnlockCanceled,
this.handleTransactionsLockingPartiallyUnlockCancelledEvent
);
}
private handleTransactionsLockingLockedEvent = ({
tenantId,
}: ITransactionsLockingPartialUnlocked) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: TRANSACTIONS_LOCKING_LOCKED,
properties: {},
});
};
private handleTransactionsLockingCancelledEvent = ({
tenantId,
}: ITransactionsLockingPartialUnlocked) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: TRANSACTIONS_LOCKING_LOCKING_CANCELLED,
properties: {},
});
};
private handleTransactionsLockingPartiallyUnlockedEvent = ({
tenantId,
}: ITransactionsLockingPartialUnlocked) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: TRANSACTIONS_LOCKING_PARTIALLY_UNLOCKED,
properties: {},
});
};
private handleTransactionsLockingPartiallyUnlockCancelledEvent = ({
tenantId,
}: ITransactionsLockingPartialUnlocked) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: TRANSACTIONS_LOCKING_PARTIALLY_UNLOCK_CANCELLED,
properties: {},
});
};
}

View File

@@ -0,0 +1,55 @@
import { Inject, Service } from 'typedi';
import { EventSubscriber } from '@/lib/EventPublisher/EventPublisher';
import { ITransactionsLockingPartialUnlocked } from '@/interfaces';
import { PosthogService } from '../PostHog';
import {
SUBSCRIPTION_CANCELLED,
SUBSCRIPTION_PLAN_CHANGED,
SUBSCRIPTION_RESUMED,
} from '@/constants/event-tracker';
import events from '@/subscribers/events';
@Service()
export class TransactionsLockingEventsTracker extends EventSubscriber {
@Inject()
private posthog: PosthogService;
public attach(bus) {
bus.subscribe(
events.subscription.onSubscriptionResumed,
this.handleSubscriptionResumedEvent
);
bus.subscribe(
events.subscription.onSubscriptionCancelled,
this.handleSubscriptionCancelledEvent
);
bus.subscribe(
events.subscription.onSubscriptionPlanChanged,
this.handleSubscriptionPlanChangedEvent
);
}
private handleSubscriptionResumedEvent = ({ tenantId }) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: SUBSCRIPTION_RESUMED,
properties: {},
});
};
private handleSubscriptionCancelledEvent = ({ tenantId }) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: SUBSCRIPTION_CANCELLED,
properties: {},
});
};
private handleSubscriptionPlanChangedEvent = ({ tenantId }) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: SUBSCRIPTION_PLAN_CHANGED,
properties: {},
});
};
}

View File

@@ -0,0 +1,56 @@
import { Inject, Service } from 'typedi';
import { EventSubscriber } from '@/lib/EventPublisher/EventPublisher';
import {
IVendorEventCreatedPayload,
IVendorEventEditedPayload,
IVendorEventDeletedPayload,
} from '@/interfaces';
import { PosthogService } from '../PostHog';
import events from '@/subscribers/events';
import {
VENDOR_CREATED,
VENDOR_EDITED,
VENDOR_DELETED,
} from '@/constants/event-tracker';
@Service()
export class VendorEventsTracker extends EventSubscriber {
@Inject()
private posthog: PosthogService;
public attach(bus) {
bus.subscribe(events.vendors.onCreated, this.handleTrackVendorCreatedEvent);
bus.subscribe(events.vendors.onEdited, this.handleTrackEditedVendorEvent);
bus.subscribe(events.vendors.onDeleted, this.handleTrackDeletedVendorEvent);
}
private handleTrackVendorCreatedEvent = ({
tenantId,
}: IVendorEventCreatedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: VENDOR_CREATED,
properties: {},
});
};
private handleTrackEditedVendorEvent = ({
tenantId,
}: IVendorEventEditedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: VENDOR_EDITED,
properties: {},
});
};
private handleTrackDeletedVendorEvent = ({
tenantId,
}: IVendorEventDeletedPayload) => {
this.posthog.trackEvent({
distinctId: `tenant-${tenantId}`,
event: VENDOR_DELETED,
properties: {},
});
};
}

View File

@@ -0,0 +1,31 @@
import { SaleInvoiceEventsTracker } from './SaleInvoicesEventsTracker';
import { SaleEstimateEventsTracker } from './SaleEstimateEventsTracker';
import { PaymentMadeEventsTracker } from './PaymentMadeEventsTracker';
import { PaymentReceivedEventsTracker } from './PaymentReceivedEventsTracker';
import { BillEventsTracker } from './BillEventsTracker';
import { ExpenseEventsTracker } from './ExpenseEventsTracker';
import { AccountEventsTracker } from './AccountEventsTracker';
import { AuthenticationEventsTracker } from './AuthenticationEventsTracker';
import { ItemEventsTracker } from './ItemEventsTracker';
import { BankTransactionEventsTracker } from './BankTransactionEventsTracker';
import { CustomerEventsTracker } from './CustomerEventsTracker';
import { VendorEventsTracker } from './VendorEventsTracker';
import { ManualJournalEventsTracker } from './ManualJournalEventsTracker';
import { BankRuleEventsTracker } from './BankRuleEventsTracker';
export const EventsTrackerListeners = [
SaleInvoiceEventsTracker,
SaleEstimateEventsTracker,
PaymentMadeEventsTracker,
PaymentReceivedEventsTracker,
BillEventsTracker,
AccountEventsTracker,
ExpenseEventsTracker,
AuthenticationEventsTracker,
ItemEventsTracker,
BankTransactionEventsTracker,
CustomerEventsTracker,
VendorEventsTracker,
ManualJournalEventsTracker,
BankRuleEventsTracker,
];

View File

@@ -1,7 +1,7 @@
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import { sumBy, difference } from 'lodash'; import { sumBy, difference } from 'lodash';
import { ServiceError } from '@/exceptions'; import { ServiceError } from '@/exceptions';
import { ERRORS } from '../constants'; import { ERRORS, SUPPORTED_EXPENSE_PAYMENT_ACCOUNT_TYPES } from '../constants';
import { import {
IAccount, IAccount,
IExpense, IExpense,
@@ -79,7 +79,9 @@ export class CommandExpenseValidator {
* @throws {ServiceError} * @throws {ServiceError}
*/ */
public validatePaymentAccountType = (paymentAccount: number[]) => { public validatePaymentAccountType = (paymentAccount: number[]) => {
if (!paymentAccount.isParentType(ACCOUNT_PARENT_TYPE.CURRENT_ASSET)) { if (
!paymentAccount.isAccountType(SUPPORTED_EXPENSE_PAYMENT_ACCOUNT_TYPES)
) {
throw new ServiceError(ERRORS.PAYMENT_ACCOUNT_HAS_INVALID_TYPE); throw new ServiceError(ERRORS.PAYMENT_ACCOUNT_HAS_INVALID_TYPE);
} }
}; };

View File

@@ -11,6 +11,7 @@ import {
} from '@/interfaces'; } from '@/interfaces';
import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform'; import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform';
import { TenantMetadata } from '@/system/models'; import { TenantMetadata } from '@/system/models';
import { assocItemEntriesDefaultIndex } from '@/services/Items/utils';
@Service() @Service()
export class ExpenseDTOTransformer { export class ExpenseDTOTransformer {
@@ -40,8 +41,8 @@ export class ExpenseDTOTransformer {
/** /**
* Mapping expense DTO to model. * Mapping expense DTO to model.
* @param {IExpenseDTO} expenseDTO * @param {IExpenseDTO} expenseDTO
* @param {ISystemUser} authorizedUser * @param {ISystemUser} authorizedUser
* @return {IExpense} * @return {IExpense}
*/ */
private expenseDTOToModel( private expenseDTOToModel(
@@ -52,9 +53,14 @@ export class ExpenseDTOTransformer {
const landedCostAmount = this.getExpenseLandedCostAmount(expenseDTO); const landedCostAmount = this.getExpenseLandedCostAmount(expenseDTO);
const totalAmount = this.getExpenseCategoriesTotal(expenseDTO.categories); const totalAmount = this.getExpenseCategoriesTotal(expenseDTO.categories);
const categories = R.compose(
// Associate the default index to categories lines.
assocItemEntriesDefaultIndex
)(expenseDTO.categories || []);
const initialDTO = { const initialDTO = {
categories: [],
...omit(expenseDTO, ['publish', 'attachments']), ...omit(expenseDTO, ['publish', 'attachments']),
categories,
totalAmount, totalAmount,
landedCostAmount, landedCostAmount,
paymentDate: moment(expenseDTO.paymentDate).toMySqlDateTime(), paymentDate: moment(expenseDTO.paymentDate).toMySqlDateTime(),

View File

@@ -54,6 +54,7 @@ export class GetExpenses {
builder.withGraphFetched('categories.expenseAccount'); builder.withGraphFetched('categories.expenseAccount');
dynamicList.buildQuery()(builder); dynamicList.buildQuery()(builder);
filterDTO?.filterQuery && filterDTO?.filterQuery(builder);
}) })
.pagination(filter.page - 1, filter.pageSize); .pagination(filter.page - 1, filter.pageSize);

View File

@@ -0,0 +1,113 @@
import * as R from 'ramda';
import {
AccountNormal,
IExpenseCategory,
ILedger,
ILedgerEntry,
} from '@/interfaces';
import Ledger from '../Accounting/Ledger';
export class ExpenseGL {
private expense: any;
/**
* Constructor method.
*/
constructor(expense: any) {
this.expense = expense;
}
/**
* Retrieves the expense GL common entry.
* @param {IExpense} expense
* @returns {Partial<ILedgerEntry>}
*/
private getExpenseGLCommonEntry = (): Partial<ILedgerEntry> => {
return {
currencyCode: this.expense.currencyCode,
exchangeRate: this.expense.exchangeRate,
transactionType: 'Expense',
transactionId: this.expense.id,
date: this.expense.paymentDate,
userId: this.expense.userId,
debit: 0,
credit: 0,
branchId: this.expense.branchId,
};
};
/**
* Retrieves the expense GL payment entry.
* @param {IExpense} expense
* @returns {ILedgerEntry}
*/
private getExpenseGLPaymentEntry = (): ILedgerEntry => {
const commonEntry = this.getExpenseGLCommonEntry();
return {
...commonEntry,
credit: this.expense.localAmount,
accountId: this.expense.paymentAccountId,
accountNormal:
this.expense?.paymentAccount?.accountNormal === 'debit'
? AccountNormal.DEBIT
: AccountNormal.CREDIT,
index: 1,
};
};
/**
* Retrieves the expense GL category entry.
* @param {IExpense} expense -
* @param {IExpenseCategory} expenseCategory -
* @param {number} index
* @returns {ILedgerEntry}
*/
private getExpenseGLCategoryEntry = R.curry(
(category: IExpenseCategory, index: number): ILedgerEntry => {
const commonEntry = this.getExpenseGLCommonEntry();
const localAmount = category.amount * this.expense.exchangeRate;
return {
...commonEntry,
accountId: category.expenseAccountId,
accountNormal: AccountNormal.DEBIT,
debit: localAmount,
note: category.description,
index: index + 2,
projectId: category.projectId,
};
}
);
/**
* Retrieves the expense GL entries.
* @param {IExpense} expense
* @returns {ILedgerEntry[]}
*/
public getExpenseGLEntries = (): ILedgerEntry[] => {
const getCategoryEntry = this.getExpenseGLCategoryEntry();
const paymentEntry = this.getExpenseGLPaymentEntry();
const categoryEntries = this.expense.categories.map(getCategoryEntry);
return [paymentEntry, ...categoryEntries];
};
/**
* Retrieves the given expense ledger.
* @param {IExpense} expense
* @returns {ILedger}
*/
public getExpenseLedger = (): ILedger => {
const entries = this.getExpenseGLEntries();
console.log(entries, 'entries');
return new Ledger(entries);
};
}

View File

@@ -1,106 +0,0 @@
import * as R from 'ramda';
import { Service } from 'typedi';
import {
AccountNormal,
IExpense,
IExpenseCategory,
ILedger,
ILedgerEntry,
} from '@/interfaces';
import Ledger from '@/services/Accounting/Ledger';
@Service()
export class ExpenseGLEntries {
/**
* Retrieves the expense GL common entry.
* @param {IExpense} expense
* @returns
*/
private getExpenseGLCommonEntry = (expense: IExpense) => {
return {
currencyCode: expense.currencyCode,
exchangeRate: expense.exchangeRate,
transactionType: 'Expense',
transactionId: expense.id,
date: expense.paymentDate,
userId: expense.userId,
debit: 0,
credit: 0,
branchId: expense.branchId,
};
};
/**
* Retrieves the expense GL payment entry.
* @param {IExpense} expense
* @returns {ILedgerEntry}
*/
private getExpenseGLPaymentEntry = (expense: IExpense): ILedgerEntry => {
const commonEntry = this.getExpenseGLCommonEntry(expense);
return {
...commonEntry,
credit: expense.localAmount,
accountId: expense.paymentAccountId,
accountNormal: AccountNormal.DEBIT,
index: 1,
};
};
/**
* Retrieves the expense GL category entry.
* @param {IExpense} expense -
* @param {IExpenseCategory} expenseCategory -
* @param {number} index
* @returns {ILedgerEntry}
*/
private getExpenseGLCategoryEntry = R.curry(
(
expense: IExpense,
category: IExpenseCategory,
index: number
): ILedgerEntry => {
const commonEntry = this.getExpenseGLCommonEntry(expense);
const localAmount = category.amount * expense.exchangeRate;
return {
...commonEntry,
accountId: category.expenseAccountId,
accountNormal: AccountNormal.DEBIT,
debit: localAmount,
note: category.description,
index: index + 2,
projectId: category.projectId,
};
}
);
/**
* Retrieves the expense GL entries.
* @param {IExpense} expense
* @returns {ILedgerEntry[]}
*/
public getExpenseGLEntries = (expense: IExpense): ILedgerEntry[] => {
const getCategoryEntry = this.getExpenseGLCategoryEntry(expense);
const paymentEntry = this.getExpenseGLPaymentEntry(expense);
const categoryEntries = expense.categories.map(getCategoryEntry);
return [paymentEntry, ...categoryEntries];
};
/**
* Retrieves the given expense ledger.
* @param {IExpense} expense
* @returns {ILedger}
*/
public getExpenseLedger = (expense: IExpense): ILedger => {
const entries = this.getExpenseGLEntries(expense);
return new Ledger(entries);
};
}

View File

@@ -0,0 +1,45 @@
import { Knex } from 'knex';
import { Inject, Service } from 'typedi';
import { IExpense, ILedger } from '@/interfaces';
import { ExpenseGL } from './ExpenseGL';
import HasTenancyService from '../Tenancy/TenancyService';
@Service()
export class ExpenseGLEntries {
@Inject()
private tenancy: HasTenancyService;
/**
* Retrieves the expense G/L of the given id.
* @param {number} tenantId
* @param {number} expenseId
* @param {Knex.Transaction} trx
* @returns {Promise<ILedger>}
*/
public getExpenseLedgerById = async (
tenantId: number,
expenseId: number,
trx?: Knex.Transaction
): Promise<ILedger> => {
const { Expense } = await this.tenancy.models(tenantId);
const expense = await Expense.query(trx)
.findById(expenseId)
.withGraphFetched('categories')
.withGraphFetched('paymentAccount')
.throwIfNotFound();
return this.getExpenseLedger(expense);
};
/**
* Retrieves the given expense ledger.
* @param {IExpense} expense
* @returns {ILedger}
*/
public getExpenseLedger = (expense: IExpense): ILedger => {
const expenseGL = new ExpenseGL(expense);
return expenseGL.getExpenseLedger();
};
}

View File

@@ -2,7 +2,7 @@ import { Knex } from 'knex';
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import LedgerStorageService from '@/services/Accounting/LedgerStorageService'; import LedgerStorageService from '@/services/Accounting/LedgerStorageService';
import HasTenancyService from '@/services/Tenancy/TenancyService'; import HasTenancyService from '@/services/Tenancy/TenancyService';
import { ExpenseGLEntries } from './ExpenseGLEntries'; import { ExpenseGLEntries } from './ExpenseGLEntriesService';
@Service() @Service()
export class ExpenseGLEntriesStorage { export class ExpenseGLEntriesStorage {
@@ -12,9 +12,6 @@ export class ExpenseGLEntriesStorage {
@Inject() @Inject()
private ledgerStorage: LedgerStorageService; private ledgerStorage: LedgerStorageService;
@Inject()
private tenancy: HasTenancyService;
/** /**
* Writes the expense GL entries. * Writes the expense GL entries.
* @param {number} tenantId * @param {number} tenantId
@@ -26,15 +23,12 @@ export class ExpenseGLEntriesStorage {
expenseId: number, expenseId: number,
trx?: Knex.Transaction trx?: Knex.Transaction
) => { ) => {
const { Expense } = await this.tenancy.models(tenantId);
const expense = await Expense.query(trx)
.findById(expenseId)
.withGraphFetched('categories');
// Retrieves the given expense ledger. // Retrieves the given expense ledger.
const expenseLedger = this.expenseGLEntries.getExpenseLedger(expense); const expenseLedger = await this.expenseGLEntries.getExpenseLedgerById(
tenantId,
expenseId,
trx
);
// Commits the expense ledger entries. // Commits the expense ledger entries.
await this.ledgerStorage.commit(tenantId, expenseLedger, trx); await this.ledgerStorage.commit(tenantId, expenseLedger, trx);
}; };

View File

@@ -15,12 +15,16 @@ export class ExpensesExportable extends Exportable {
* @returns * @returns
*/ */
public exportable(tenantId: number, query: IExpensesFilter) { public exportable(tenantId: number, query: IExpensesFilter) {
const filterQuery = (query) => {
query.withGraphFetched('branch');
};
const parsedQuery = { const parsedQuery = {
sortOrder: 'desc', sortOrder: 'desc',
columnSortBy: 'created_at', columnSortBy: 'created_at',
...query, ...query,
page: 1, page: 1,
pageSize: EXPORT_SIZE_LIMIT, pageSize: EXPORT_SIZE_LIMIT,
filterQuery,
} as IExpensesFilter; } as IExpensesFilter;
return this.expensesApplication return this.expensesApplication

View File

@@ -1,3 +1,5 @@
import { ACCOUNT_TYPE } from '@/data/AccountTypes';
export const DEFAULT_VIEW_COLUMNS = []; export const DEFAULT_VIEW_COLUMNS = [];
export const DEFAULT_VIEWS = [ export const DEFAULT_VIEWS = [
{ {
@@ -76,3 +78,12 @@ export const ExpensesSampleData = [
Publish: 'T', Publish: 'T',
}, },
]; ];
export const SUPPORTED_EXPENSE_PAYMENT_ACCOUNT_TYPES = [
ACCOUNT_TYPE.CASH,
ACCOUNT_TYPE.BANK,
ACCOUNT_TYPE.CREDIT_CARD,
ACCOUNT_TYPE.OTHER_CURRENT_ASSET,
ACCOUNT_TYPE.NON_CURRENT_ASSET,
ACCOUNT_TYPE.FIXED_ASSET,
];

View File

@@ -0,0 +1,48 @@
import { Service } from 'typedi';
import { AsyncLocalStorage } from 'async_hooks';
@Service()
export class ExportAls {
private als: AsyncLocalStorage<Map<string, any>>;
constructor() {
this.als = new AsyncLocalStorage();
}
/**
* Runs a callback function within the context of a new AsyncLocalStorage store.
* @param callback The function to be executed within the AsyncLocalStorage context.
* @returns The result of the callback function.
*/
public run<T>(callback: () => T): T {
return this.als.run<T>(new Map(), () => {
this.markAsExport();
return callback();
});
}
/**
* Retrieves the current AsyncLocalStorage store.
* @returns The current store or undefined if not in a valid context.
*/
public getStore(): Map<string, any> | undefined {
return this.als.getStore();
}
/**
* Marks the current context as an export operation.
* @param flag Boolean flag to set or unset the export status. Defaults to true.
*/
public markAsExport(flag: boolean = true): void {
const store = this.getStore();
store?.set('isExport', flag);
}
/**
* Checks if the current context is an export operation.
* @returns {boolean} True if the context is an export operation, false otherwise.
*/
public get isExport(): boolean {
return !!this.getStore()?.get('isExport');
}
}

View File

@@ -10,6 +10,7 @@ import { Errors, ExportFormat } from './common';
import { IModelMeta, IModelMetaColumn } from '@/interfaces'; import { IModelMeta, IModelMetaColumn } from '@/interfaces';
import { flatDataCollections, getDataAccessor } from './utils'; import { flatDataCollections, getDataAccessor } from './utils';
import { ExportPdf } from './ExportPdf'; import { ExportPdf } from './ExportPdf';
import { ExportAls } from './ExportAls';
@Service() @Service()
export class ExportResourceService { export class ExportResourceService {
@@ -22,20 +23,43 @@ export class ExportResourceService {
@Inject() @Inject()
private exportPdf: ExportPdf; private exportPdf: ExportPdf;
@Inject()
private exportAls: ExportAls;
/** /**
* Exports the given resource data through csv, xlsx or pdf. *
* @param {number} tenantId - Tenant id. * @param {number} tenantId
* @param {string} resourceName - Resource name. * @param {string} resourceName
* @param {ExportFormat} format - File format. * @param {ExportFormat} format
* @returns
*/ */
public async export( public async export(
tenantId: number, tenantId: number,
resourceName: string, resourceName: string,
format: ExportFormat = ExportFormat.Csv format: ExportFormat = ExportFormat.Csv
) {
return this.exportAls.run(() =>
this.exportAlsRun(tenantId, resourceName, format)
);
}
/**
* Exports the given resource data through csv, xlsx or pdf.
* @param {number} tenantId - Tenant id.
* @param {string} resourceName - Resource name.
* @param {ExportFormat} format - File format.
*/
public async exportAlsRun(
tenantId: number,
resourceName: string,
format: ExportFormat = ExportFormat.Csv
) { ) {
const resource = sanitizeResourceName(resourceName); const resource = sanitizeResourceName(resourceName);
const resourceMeta = this.getResourceMeta(tenantId, resource); const resourceMeta = this.getResourceMeta(tenantId, resource);
const resourceColumns = this.resourceService.getResourceColumns(
tenantId,
resource
);
this.validateResourceMeta(resourceMeta); this.validateResourceMeta(resourceMeta);
const data = await this.getExportableData(tenantId, resource); const data = await this.getExportableData(tenantId, resource);
@@ -43,7 +67,7 @@ export class ExportResourceService {
// Returns the csv, xlsx format. // Returns the csv, xlsx format.
if (format === ExportFormat.Csv || format === ExportFormat.Xlsx) { if (format === ExportFormat.Csv || format === ExportFormat.Xlsx) {
const exportableColumns = this.getExportableColumns(resourceMeta); const exportableColumns = this.getExportableColumns(resourceColumns);
const workbook = this.createWorkbook(transformed, exportableColumns); const workbook = this.createWorkbook(transformed, exportableColumns);
return this.exportWorkbook(workbook, format); return this.exportWorkbook(workbook, format);
@@ -122,7 +146,7 @@ export class ExportResourceService {
* @param {IModelMeta} resourceMeta - The metadata of the resource. * @param {IModelMeta} resourceMeta - The metadata of the resource.
* @returns An array of exportable columns. * @returns An array of exportable columns.
*/ */
private getExportableColumns(resourceMeta: IModelMeta) { private getExportableColumns(resourceColumns: any) {
const processColumns = ( const processColumns = (
columns: { [key: string]: IModelMetaColumn }, columns: { [key: string]: IModelMetaColumn },
parent = '' parent = ''
@@ -145,7 +169,7 @@ export class ExportResourceService {
} }
}); });
}; };
return processColumns(resourceMeta.columns); return processColumns(resourceColumns);
} }
private getPrintableColumns(resourceMeta: IModelMeta) { private getPrintableColumns(resourceMeta: IModelMeta) {

View File

@@ -1 +1,2 @@
export const EXPORT_SIZE_LIMIT = 9999999; export const EXPORT_SIZE_LIMIT = 9999999;
export const EXPORT_DTE_FORMAT = 'YYYY-MM-DD';

View File

@@ -8,6 +8,7 @@ export class ImportAls {
constructor() { constructor() {
this.als = new AsyncLocalStorage(); this.als = new AsyncLocalStorage();
} }
/** /**
* Runs a callback function within the context of a new AsyncLocalStorage store. * Runs a callback function within the context of a new AsyncLocalStorage store.
* @param callback The function to be executed within the AsyncLocalStorage context. * @param callback The function to be executed within the AsyncLocalStorage context.
@@ -82,7 +83,7 @@ export class ImportAls {
* Checks if the current context is an import operation. * Checks if the current context is an import operation.
* @returns {boolean} True if the context is an import operation, false otherwise. * @returns {boolean} True if the context is an import operation, false otherwise.
*/ */
public isImport(): boolean { public get isImport(): boolean {
return !!this.getStore()?.get('isImport'); return !!this.getStore()?.get('isImport');
} }
@@ -90,7 +91,7 @@ export class ImportAls {
* Checks if the current context is an import commit operation. * Checks if the current context is an import commit operation.
* @returns {boolean} True if the context is an import commit operation, false otherwise. * @returns {boolean} True if the context is an import commit operation, false otherwise.
*/ */
public isImportCommit(): boolean { public get isImportCommit(): boolean {
return !!this.getStore()?.get('isImportCommit'); return !!this.getStore()?.get('isImportCommit');
} }
@@ -98,7 +99,7 @@ export class ImportAls {
* Checks if the current context is an import preview operation. * Checks if the current context is an import preview operation.
* @returns {boolean} True if the context is an import preview operation, false otherwise. * @returns {boolean} True if the context is an import preview operation, false otherwise.
*/ */
public isImportPreview(): boolean { public get isImportPreview(): boolean {
return !!this.getStore()?.get('isImportPreview'); return !!this.getStore()?.get('isImportPreview');
} }
} }

View File

@@ -1,4 +1,3 @@
import XLSX from 'xlsx';
import bluebird from 'bluebird'; import bluebird from 'bluebird';
import * as R from 'ramda'; import * as R from 'ramda';
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
@@ -27,23 +26,7 @@ export class ImportFileCommon {
@Inject() @Inject()
private resource: ResourceService; private resource: ResourceService;
/**
* Maps the columns of the imported data based on the provided mapping attributes.
* @param {Record<string, any>[]} body - The array of data objects to map.
* @param {ImportMappingAttr[]} map - The mapping attributes.
* @returns {Record<string, any>[]} - The mapped data objects.
*/
public parseXlsxSheet(buffer: Buffer): Record<string, unknown>[] {
const workbook = XLSX.read(buffer, { type: 'buffer', raw: true });
const firstSheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[firstSheetName];
return XLSX.utils.sheet_to_json(worksheet, {});
}
/** /**
* 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.
* @param {number} tenantId - * @param {number} tenantId -

View File

@@ -33,6 +33,8 @@ export class ImportFileMapping {
// Invalidate the from/to map attributes. // Invalidate the from/to map attributes.
this.validateMapsAttrs(tenantId, importFile, maps); this.validateMapsAttrs(tenantId, importFile, maps);
// @todo validate the required fields.
// Validate the diplicated relations of map attrs. // Validate the diplicated relations of map attrs.
this.validateDuplicatedMapAttrs(maps); this.validateDuplicatedMapAttrs(maps);

View File

@@ -2,18 +2,14 @@ import { Inject, Service } from 'typedi';
import { chain } from 'lodash'; import { chain } from 'lodash';
import { Knex } from 'knex'; import { Knex } from 'knex';
import { ServiceError } from '@/exceptions'; import { ServiceError } from '@/exceptions';
import { import { ERRORS, getUnmappedSheetColumns, readImportFile } from './_utils';
ERRORS,
getSheetColumns,
getUnmappedSheetColumns,
readImportFile,
} from './_utils';
import { ImportFileCommon } from './ImportFileCommon'; import { ImportFileCommon } from './ImportFileCommon';
import { ImportFileDataTransformer } from './ImportFileDataTransformer'; import { ImportFileDataTransformer } from './ImportFileDataTransformer';
import ResourceService from '../Resource/ResourceService'; import ResourceService from '../Resource/ResourceService';
import UnitOfWork from '../UnitOfWork'; import UnitOfWork from '../UnitOfWork';
import { ImportFilePreviewPOJO } from './interfaces'; import { ImportFilePreviewPOJO } from './interfaces';
import { Import } from '@/system/models'; import { Import } from '@/system/models';
import { parseSheetData } from './sheet_utils';
@Service() @Service()
export class ImportFileProcess { export class ImportFileProcess {
@@ -49,10 +45,10 @@ export class ImportFileProcess {
if (!importFile.isMapped) { if (!importFile.isMapped) {
throw new ServiceError(ERRORS.IMPORT_FILE_NOT_MAPPED); throw new ServiceError(ERRORS.IMPORT_FILE_NOT_MAPPED);
} }
// Read the imported file. // Read the imported file and parse the given buffer to get columns
// and sheet data in json format.
const buffer = await readImportFile(importFile.filename); const buffer = await readImportFile(importFile.filename);
const sheetData = this.importCommon.parseXlsxSheet(buffer); const [sheetData, sheetColumns] = parseSheetData(buffer);
const header = getSheetColumns(sheetData);
const resource = importFile.resource; const resource = importFile.resource;
const resourceFields = this.resource.getResourceFields2(tenantId, resource); const resourceFields = this.resource.getResourceFields2(tenantId, resource);
@@ -87,7 +83,7 @@ export class ImportFileProcess {
.flatten() .flatten()
.value(); .value();
const unmappedColumns = getUnmappedSheetColumns(header, mapping); const unmappedColumns = getUnmappedSheetColumns(sheetColumns, mapping);
const totalCount = allData.length; const totalCount = allData.length;
const createdCount = successedImport.length; const createdCount = successedImport.length;

View File

@@ -11,6 +11,7 @@ import { ImportFileCommon } from './ImportFileCommon';
import { ImportFileDataValidator } from './ImportFileDataValidator'; import { ImportFileDataValidator } from './ImportFileDataValidator';
import { ImportFileUploadPOJO } from './interfaces'; import { ImportFileUploadPOJO } from './interfaces';
import { Import } from '@/system/models'; import { Import } from '@/system/models';
import { parseSheetData } from './sheet_utils';
@Service() @Service()
export class ImportFileUploadService { export class ImportFileUploadService {
@@ -77,14 +78,12 @@ export class ImportFileUploadService {
const buffer = await readImportFile(filename); const buffer = await readImportFile(filename);
// Parse the buffer file to array data. // Parse the buffer file to array data.
const sheetData = this.importFileCommon.parseXlsxSheet(buffer); const [sheetData, sheetColumns] = parseSheetData(buffer);
const coumnsStringified = JSON.stringify(sheetColumns);
// Throws service error if the sheet data is empty. // Throws service error if the sheet data is empty.
validateSheetEmpty(sheetData); validateSheetEmpty(sheetData);
const sheetColumns = this.importFileCommon.parseSheetColumns(sheetData);
const coumnsStringified = JSON.stringify(sheetColumns);
try { try {
// Validates the params Yup schema. // Validates the params Yup schema.
await this.importFileCommon.validateParamsSchema(resource, params); await this.importFileCommon.validateParamsSchema(resource, params);

View File

@@ -0,0 +1,56 @@
import XLSX from 'xlsx';
import { first } from 'lodash';
/**
* Parses the given sheet buffer to worksheet.
* @param {Buffer} buffer
* @returns {XLSX.WorkSheet}
*/
export function parseFirstSheet(buffer: Buffer): XLSX.WorkSheet {
const workbook = XLSX.read(buffer, { type: 'buffer', raw: true });
const firstSheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[firstSheetName];
return worksheet;
}
/**
* Extracts the given worksheet to columns.
* @param {XLSX.WorkSheet} worksheet
* @returns {Array<string>}
*/
export function extractSheetColumns(worksheet: XLSX.WorkSheet): Array<string> {
// By default, sheet_to_json scans the first row and uses the values as headers.
// With the header: 1 option, the function exports an array of arrays of values.
const sheetCells = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
const sheetCols = first(sheetCells) as Array<string>;
return sheetCols.filter((col) => col);
}
/**
* Parses the given worksheet to json values. the keys are columns labels.
* @param {XLSX.WorkSheet} worksheet
* @returns {Array<Record<string, string>>}
*/
export function parseSheetToJson(
worksheet: XLSX.WorkSheet
): Array<Record<string, string>> {
return XLSX.utils.sheet_to_json(worksheet, {});
}
/**
* Parses the given sheet buffer then retrieves the sheet data and columns.
* @param {Buffer} buffer
*/
export function parseSheetData(
buffer: Buffer
): [Array<Record<string, string>>, string[]] {
const worksheet = parseFirstSheet(buffer);
const columns = extractSheetColumns(worksheet);
const data = parseSheetToJson(worksheet);
return [data, columns];
}

View File

@@ -0,0 +1,16 @@
import { IItemEntry } from '@/interfaces';
import { isNull, isUndefined } from 'lodash';
export function assocItemEntriesDefaultIndex<T>(
entries: Array<T & { index?: number }>
): Array<T & { index: number }> {
return entries.map((entry, index) => {
return {
index:
isUndefined(entry.index) || isNull(entry.index)
? index + 1
: entry.index,
...entry,
};
});
}

View File

@@ -18,6 +18,8 @@ import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import { CommandManualJournalValidators } from './CommandManualJournalValidators'; import { CommandManualJournalValidators } from './CommandManualJournalValidators';
import { AutoIncrementManualJournal } from './AutoIncrementManualJournal'; import { AutoIncrementManualJournal } from './AutoIncrementManualJournal';
import { ManualJournalBranchesDTOTransformer } from '@/services/Branches/Integrations/ManualJournals/ManualJournalDTOTransformer'; import { ManualJournalBranchesDTOTransformer } from '@/services/Branches/Integrations/ManualJournals/ManualJournalDTOTransformer';
import { assocItemEntriesDefaultIndex } from '../Items/utils';
@Service() @Service()
export class CreateManualJournalService { export class CreateManualJournalService {
@Inject() @Inject()
@@ -58,16 +60,22 @@ export class CreateManualJournalService {
// The manual or auto-increment journal number. // The manual or auto-increment journal number.
const journalNumber = manualJournalDTO.journalNumber || autoNextNumber; const journalNumber = manualJournalDTO.journalNumber || autoNextNumber;
const entries = R.compose(
// Associate the default index to each item entry.
assocItemEntriesDefaultIndex
)(manualJournalDTO.entries);
const initialDTO = { const initialDTO = {
...omit(manualJournalDTO, ['publish', 'attachments']), ...omit(manualJournalDTO, ['publish', 'attachments']),
...(manualJournalDTO.publish ...(manualJournalDTO.publish
? { publishedAt: moment().toMySqlDateTime() } ? { publishedAt: moment().toMySqlDateTime() }
: {}), : {}),
amount, amount,
date,
currencyCode: manualJournalDTO.currencyCode || baseCurrency, currencyCode: manualJournalDTO.currencyCode || baseCurrency,
exchangeRate: manualJournalDTO.exchangeRate || 1, exchangeRate: manualJournalDTO.exchangeRate || 1,
date,
journalNumber, journalNumber,
entries,
userId: authorizedUser.id, userId: authorizedUser.id,
}; };
return R.compose( return R.compose(

View File

@@ -14,12 +14,17 @@ export class BillPaymentExportable extends Exportable {
* @returns * @returns
*/ */
public exportable(tenantId: number, query: any) { public exportable(tenantId: number, query: any) {
const filterQuery = (builder) => {
builder.withGraphFetched('entries.bill');
builder.withGraphFetched('branch');
};
const parsedQuery = { const parsedQuery = {
sortOrder: 'desc', sortOrder: 'desc',
columnSortBy: 'created_at', columnSortBy: 'created_at',
...query, ...query,
page: 1, page: 1,
pageSize: EXPORT_SIZE_LIMIT, pageSize: EXPORT_SIZE_LIMIT,
filterQuery
} as any; } as any;
return this.billPaymentsApplication return this.billPaymentsApplication

View File

@@ -3,8 +3,8 @@ import * as R from 'ramda';
import { omit, sumBy } from 'lodash'; import { omit, sumBy } from 'lodash';
import { IBillPayment, IBillPaymentDTO, IVendor } from '@/interfaces'; import { IBillPayment, IBillPaymentDTO, IVendor } from '@/interfaces';
import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform'; import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform';
import { assocItemEntriesDefaultIndex } from '@/services/Items/utils';
import { formatDateFields } from '@/utils'; import { formatDateFields } from '@/utils';
import HasTenancyService from '@/services/Tenancy/TenancyService';
@Service() @Service()
export class CommandBillPaymentDTOTransformer { export class CommandBillPaymentDTOTransformer {
@@ -27,6 +27,12 @@ export class CommandBillPaymentDTOTransformer {
const amount = const amount =
billPaymentDTO.amount ?? sumBy(billPaymentDTO.entries, 'paymentAmount'); billPaymentDTO.amount ?? sumBy(billPaymentDTO.entries, 'paymentAmount');
// Associate the default index to each item entry.
const entries = R.compose(
// Associate the default index to payment entries.
assocItemEntriesDefaultIndex
)(billPaymentDTO.entries);
const initialDTO = { const initialDTO = {
...formatDateFields(omit(billPaymentDTO, ['attachments']), [ ...formatDateFields(omit(billPaymentDTO, ['attachments']), [
'paymentDate', 'paymentDate',
@@ -34,7 +40,7 @@ export class CommandBillPaymentDTOTransformer {
amount, amount,
currencyCode: vendor.currencyCode, currencyCode: vendor.currencyCode,
exchangeRate: billPaymentDTO.exchangeRate || 1, exchangeRate: billPaymentDTO.exchangeRate || 1,
entries: billPaymentDTO.entries, entries,
}; };
return R.compose( return R.compose(
this.branchDTOTransform.transformDTO<IBillPayment>(tenantId) this.branchDTOTransform.transformDTO<IBillPayment>(tenantId)

View File

@@ -52,6 +52,7 @@ export class GetBillPayments {
builder.withGraphFetched('paymentAccount'); builder.withGraphFetched('paymentAccount');
dynamicList.buildQuery()(builder); dynamicList.buildQuery()(builder);
filter?.filterQuery && filter?.filterQuery(builder);
}) })
.pagination(filter.page - 1, filter.pageSize); .pagination(filter.page - 1, filter.pageSize);

View File

@@ -3,7 +3,7 @@ import moment from 'moment';
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import * as R from 'ramda'; import * as R from 'ramda';
import composeAsync from 'async/compose'; import composeAsync from 'async/compose';
import { formatDateFields } from 'utils'; import { assocDepthLevelToObjectTree, formatDateFields } from 'utils';
import { import {
IBillDTO, IBillDTO,
IBill, IBill,
@@ -15,6 +15,7 @@ import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/
import { WarehouseTransactionDTOTransform } from '@/services/Warehouses/Integrations/WarehouseTransactionDTOTransform'; import { WarehouseTransactionDTOTransform } from '@/services/Warehouses/Integrations/WarehouseTransactionDTOTransform';
import HasTenancyService from '@/services/Tenancy/TenancyService'; import HasTenancyService from '@/services/Tenancy/TenancyService';
import { ItemEntriesTaxTransactions } from '@/services/TaxRates/ItemEntriesTaxTransactions'; import { ItemEntriesTaxTransactions } from '@/services/TaxRates/ItemEntriesTaxTransactions';
import { assocItemEntriesDefaultIndex } from '@/services/Items/utils';
@Service() @Service()
export class BillDTOTransformer { export class BillDTOTransformer {
@@ -54,9 +55,9 @@ export class BillDTOTransformer {
/** /**
* Converts create bill DTO to model. * Converts create bill DTO to model.
* @param {number} tenantId * @param {number} tenantId
* @param {IBillDTO} billDTO * @param {IBillDTO} billDTO
* @param {IBill} oldBill * @param {IBill} oldBill
* @returns {IBill} * @returns {IBill}
*/ */
public async billDTOToModel( public async billDTOToModel(
@@ -92,7 +93,9 @@ export class BillDTOTransformer {
const entries = R.compose( const entries = R.compose(
// Remove tax code from entries. // Remove tax code from entries.
R.map(R.omit(['taxCode'])) R.map(R.omit(['taxCode'])),
// Associate the default index to each item entry line.
assocItemEntriesDefaultIndex
)(asyncEntries); )(asyncEntries);
const initialDTO = { const initialDTO = {

View File

@@ -1,8 +1,10 @@
import { Inject, Service } from 'typedi'; import { Inject, Service } from 'typedi';
import { Knex } from 'knex';
import { IBillsFilter } from '@/interfaces'; import { IBillsFilter } from '@/interfaces';
import { Exportable } from '@/services/Export/Exportable'; import { Exportable } from '@/services/Export/Exportable';
import { BillsApplication } from './BillsApplication'; import { BillsApplication } from './BillsApplication';
import { EXPORT_SIZE_LIMIT } from '@/services/Export/constants'; import { EXPORT_SIZE_LIMIT } from '@/services/Export/constants';
import Objection from 'objection';
@Service() @Service()
export class BillsExportable extends Exportable { export class BillsExportable extends Exportable {
@@ -15,12 +17,17 @@ export class BillsExportable extends Exportable {
* @returns * @returns
*/ */
public exportable(tenantId: number, query: IBillsFilter) { public exportable(tenantId: number, query: IBillsFilter) {
const filterQuery = (query) => {
query.withGraphFetched('branch');
query.withGraphFetched('warehouse');
};
const parsedQuery = { const parsedQuery = {
sortOrder: 'desc', sortOrder: 'desc',
columnSortBy: 'created_at', columnSortBy: 'created_at',
...query, ...query,
page: 1, page: 1,
pageSize: EXPORT_SIZE_LIMIT, pageSize: EXPORT_SIZE_LIMIT,
filterQuery,
} as IBillsFilter; } as IBillsFilter;
return this.billsApplication return this.billsApplication

View File

@@ -51,6 +51,9 @@ export class GetBills {
builder.withGraphFetched('vendor'); builder.withGraphFetched('vendor');
builder.withGraphFetched('entries.item'); builder.withGraphFetched('entries.item');
dynamicFilter.buildQuery()(builder); dynamicFilter.buildQuery()(builder);
// Filter query.
filterDTO?.filterQuery && filterDTO?.filterQuery(builder);
}) })
.pagination(filter.page - 1, filter.pageSize); .pagination(filter.page - 1, filter.pageSize);

View File

@@ -9,11 +9,13 @@ import {
IVendorCredit, IVendorCredit,
IVendorCreditCreateDTO, IVendorCreditCreateDTO,
IVendorCreditEditDTO, IVendorCreditEditDTO,
IVendorCreditEntryDTO,
} from '@/interfaces'; } from '@/interfaces';
import ItemsEntriesService from '@/services/Items/ItemsEntriesService'; import ItemsEntriesService from '@/services/Items/ItemsEntriesService';
import AutoIncrementOrdersService from '@/services/Sales/AutoIncrementOrdersService'; import AutoIncrementOrdersService from '@/services/Sales/AutoIncrementOrdersService';
import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform'; import { BranchTransactionDTOTransform } from '@/services/Branches/Integrations/BranchTransactionDTOTransform';
import { WarehouseTransactionDTOTransform } from '@/services/Warehouses/Integrations/WarehouseTransactionDTOTransform'; import { WarehouseTransactionDTOTransform } from '@/services/Warehouses/Integrations/WarehouseTransactionDTOTransform';
import { assocItemEntriesDefaultIndex } from '@/services/Items/utils';
@Service() @Service()
export default class BaseVendorCredit { export default class BaseVendorCredit {
@@ -50,10 +52,18 @@ export default class BaseVendorCredit {
const amount = this.itemsEntriesService.getTotalItemsEntries( const amount = this.itemsEntriesService.getTotalItemsEntries(
vendorCreditDTO.entries vendorCreditDTO.entries
); );
const entries = vendorCreditDTO.entries.map((entry) => ({
...entry, const entries = R.compose(
referenceType: 'VendorCredit', // Associate the default index to each item entry.
})); assocItemEntriesDefaultIndex,
// Associate the reference type to item entries.
R.map((entry: IVendorCreditEntryDTO) => ({
referenceType: 'VendorCredit',
...entry,
}))
)(vendorCreditDTO.entries);
// Retreive the next vendor credit number. // Retreive the next vendor credit number.
const autoNextNumber = this.getNextCreditNumber(tenantId); const autoNextNumber = this.getNextCreditNumber(tenantId);

View File

@@ -48,6 +48,10 @@ export default class ListVendorCredits extends BaseVendorCredit {
builder.withGraphFetched('entries'); builder.withGraphFetched('entries');
builder.withGraphFetched('vendor'); builder.withGraphFetched('vendor');
dynamicFilter.buildQuery()(builder); dynamicFilter.buildQuery()(builder);
// Gives ability to inject custom query to filter results.
vendorCreditQuery?.filterQuery &&
vendorCreditQuery?.filterQuery(builder);
}) })
.pagination(filter.page - 1, filter.pageSize); .pagination(filter.page - 1, filter.pageSize);

View File

@@ -2,6 +2,7 @@ import { Inject, Service } from 'typedi';
import { IVendorCreditsQueryDTO } from '@/interfaces'; import { IVendorCreditsQueryDTO } from '@/interfaces';
import ListVendorCredits from './ListVendorCredits'; import ListVendorCredits from './ListVendorCredits';
import { Exportable } from '@/services/Export/Exportable'; import { Exportable } from '@/services/Export/Exportable';
import { QueryBuilder } from 'knex';
@Service() @Service()
export class VendorCreditsExportable extends Exportable { export class VendorCreditsExportable extends Exportable {
@@ -15,12 +16,17 @@ export class VendorCreditsExportable extends Exportable {
* @returns {} * @returns {}
*/ */
public exportable(tenantId: number, query: IVendorCreditsQueryDTO) { public exportable(tenantId: number, query: IVendorCreditsQueryDTO) {
const filterQuery = (query) => {
query.withGraphFetched('branch');
query.withGraphFetched('warehouse');
};
const parsedQuery = { const parsedQuery = {
sortOrder: 'desc', sortOrder: 'desc',
columnSortBy: 'created_at', columnSortBy: 'created_at',
...query, ...query,
page: 1, page: 1,
pageSize: 12000, pageSize: 12000,
filterQuery,
} as IVendorCreditsQueryDTO; } as IVendorCreditsQueryDTO;
return this.getVendorCredits return this.getVendorCredits

View File

@@ -1,11 +1,18 @@
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import { camelCase, upperFirst, pickBy } from 'lodash'; import { camelCase, upperFirst, pickBy, isEmpty } from 'lodash';
import * as qim from 'qim'; import * as qim from 'qim';
import pluralize from 'pluralize'; import pluralize from 'pluralize';
import { IModelMeta, IModelMetaField, IModelMetaField2 } from '@/interfaces'; import {
Features,
IModelMeta,
IModelMetaField,
IModelMetaField2,
} from '@/interfaces';
import TenancyService from '@/services/Tenancy/TenancyService'; import TenancyService from '@/services/Tenancy/TenancyService';
import { ServiceError } from '@/exceptions'; import { ServiceError } from '@/exceptions';
import I18nService from '@/services/I18n/I18nService'; import I18nService from '@/services/I18n/I18nService';
import { WarehousesSettings } from '../Warehouses/WarehousesSettings';
import { BranchesSettings } from '../Branches/BranchesSettings';
const ERRORS = { const ERRORS = {
RESOURCE_MODEL_NOT_FOUND: 'RESOURCE_MODEL_NOT_FOUND', RESOURCE_MODEL_NOT_FOUND: 'RESOURCE_MODEL_NOT_FOUND',
@@ -19,6 +26,12 @@ export default class ResourceService {
@Inject() @Inject()
i18nService: I18nService; i18nService: I18nService;
@Inject()
private branchesSettings: BranchesSettings;
@Inject()
private warehousesSettings: WarehousesSettings;
/** /**
* Transform resource to model name. * Transform resource to model name.
* @param {string} resourceName * @param {string} resourceName
@@ -74,13 +87,45 @@ export default class ResourceService {
return meta.fields; return meta.fields;
} }
public filterSupportFeatures = (
tenantId,
fields: { [key: string]: IModelMetaField2 }
) => {
const isMultiFeaturesEnabled =
this.branchesSettings.isMultiBranchesActive(tenantId);
const isMultiWarehousesEnabled =
this.warehousesSettings.isMultiWarehousesActive(tenantId);
return pickBy(fields, (field) => {
if (
!isMultiWarehousesEnabled &&
field.features?.includes(Features.WAREHOUSES)
) {
return false;
}
if (
!isMultiFeaturesEnabled &&
field.features?.includes(Features.BRANCHES)
) {
return false;
}
return true;
});
};
public getResourceFields2( public getResourceFields2(
tenantId: number, tenantId: number,
modelName: string modelName: string
): { [key: string]: IModelMetaField2 } { ): { [key: string]: IModelMetaField2 } {
const meta = this.getResourceMeta(tenantId, modelName); const meta = this.getResourceMeta(tenantId, modelName);
return meta.fields2; return this.filterSupportFeatures(tenantId, meta.fields2);
}
public getResourceColumns(tenantId: number, modelName: string) {
const meta = this.getResourceMeta(tenantId, modelName);
return this.filterSupportFeatures(tenantId, meta.columns);
} }
/** /**

View File

@@ -53,6 +53,7 @@ export class GetSaleEstimates {
builder.withGraphFetched('entries'); builder.withGraphFetched('entries');
builder.withGraphFetched('entries.item'); builder.withGraphFetched('entries.item');
dynamicFilter.buildQuery()(builder); dynamicFilter.buildQuery()(builder);
filterDTO?.filterQuery && filterDTO?.filterQuery(builder);
}) })
.pagination(filter.page - 1, filter.pageSize); .pagination(filter.page - 1, filter.pageSize);

View File

@@ -9,6 +9,7 @@ import { WarehouseTransactionDTOTransform } from '@/services/Warehouses/Integrat
import { formatDateFields } from '@/utils'; import { formatDateFields } from '@/utils';
import moment from 'moment'; import moment from 'moment';
import { SaleEstimateIncrement } from './SaleEstimateIncrement'; import { SaleEstimateIncrement } from './SaleEstimateIncrement';
import { assocItemEntriesDefaultIndex } from '@/services/Items/utils';
@Service() @Service()
export class SaleEstimateDTOTransformer { export class SaleEstimateDTOTransformer {
@@ -56,6 +57,14 @@ export class SaleEstimateDTOTransformer {
// Validate the sale estimate number require. // Validate the sale estimate number require.
this.validators.validateEstimateNoRequire(estimateNumber); this.validators.validateEstimateNoRequire(estimateNumber);
const entries = R.compose(
// Associate the reference type to item entries.
R.map((entry) => R.assoc('reference_type', 'SaleEstimate', entry)),
// Associate default index to item entries.
assocItemEntriesDefaultIndex
)(estimateDTO.entries);
const initialDTO = { const initialDTO = {
amount, amount,
...formatDateFields( ...formatDateFields(
@@ -65,10 +74,7 @@ export class SaleEstimateDTOTransformer {
currencyCode: paymentCustomer.currencyCode, currencyCode: paymentCustomer.currencyCode,
exchangeRate: estimateDTO.exchangeRate || 1, exchangeRate: estimateDTO.exchangeRate || 1,
...(estimateNumber ? { estimateNumber } : {}), ...(estimateNumber ? { estimateNumber } : {}),
entries: estimateDTO.entries.map((entry) => ({ entries,
reference_type: 'SaleEstimate',
...entry,
})),
// Avoid rewrite the deliver date in edit mode when already published. // Avoid rewrite the deliver date in edit mode when already published.
...(estimateDTO.delivered && ...(estimateDTO.delivered &&
!oldSaleEstimate?.deliveredAt && { !oldSaleEstimate?.deliveredAt && {

View File

@@ -15,12 +15,17 @@ export class SaleEstimatesExportable extends Exportable {
* @returns * @returns
*/ */
public exportable(tenantId: number, query: ISalesInvoicesFilter) { public exportable(tenantId: number, query: ISalesInvoicesFilter) {
const filterQuery = (query) => {
query.withGraphFetched('branch');
query.withGraphFetched('warehouse');
};
const parsedQuery = { const parsedQuery = {
sortOrder: 'desc', sortOrder: 'desc',
columnSortBy: 'created_at', columnSortBy: 'created_at',
...query, ...query,
page: 1, page: 1,
pageSize: EXPORT_SIZE_LIMIT, pageSize: EXPORT_SIZE_LIMIT,
filterQuery,
} as ISalesInvoicesFilter; } as ISalesInvoicesFilter;
return this.saleEstimatesApplication return this.saleEstimatesApplication

View File

@@ -17,6 +17,7 @@ import { CommandSaleInvoiceValidators } from './CommandSaleInvoiceValidators';
import { SaleInvoiceIncrement } from './SaleInvoiceIncrement'; import { SaleInvoiceIncrement } from './SaleInvoiceIncrement';
import { formatDateFields } from 'utils'; import { formatDateFields } from 'utils';
import { ItemEntriesTaxTransactions } from '@/services/TaxRates/ItemEntriesTaxTransactions'; import { ItemEntriesTaxTransactions } from '@/services/TaxRates/ItemEntriesTaxTransactions';
import { assocItemEntriesDefaultIndex } from '@/services/Items/utils';
import { ItemEntry } from '@/models'; import { ItemEntry } from '@/models';
@Service() @Service()
@@ -81,7 +82,10 @@ export class CommandSaleInvoiceDTOTransformer {
const entries = R.compose( const entries = R.compose(
// Remove tax code from entries. // Remove tax code from entries.
R.map(R.omit(['taxCode'])) R.map(R.omit(['taxCode'])),
// Associate the default index for each item entry lin.
assocItemEntriesDefaultIndex
)(asyncEntries); )(asyncEntries);
const initialDTO = { const initialDTO = {

View File

@@ -52,6 +52,7 @@ export class GetSaleInvoices {
builder.withGraphFetched('entries.item'); builder.withGraphFetched('entries.item');
builder.withGraphFetched('customer'); builder.withGraphFetched('customer');
dynamicFilter.buildQuery()(builder); dynamicFilter.buildQuery()(builder);
filterDTO?.filterQuery && filterDTO?.filterQuery(builder);
}) })
.pagination(filter.page - 1, filter.pageSize); .pagination(filter.page - 1, filter.pageSize);

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