diff --git a/packages/server/src/modules/Accounts/Accounts.module.ts b/packages/server/src/modules/Accounts/Accounts.module.ts index 41ae6bc11..6017a3189 100644 --- a/packages/server/src/modules/Accounts/Accounts.module.ts +++ b/packages/server/src/modules/Accounts/Accounts.module.ts @@ -18,7 +18,6 @@ import { BankAccount } from '../BankingTransactions/models/BankAccount'; import { GetAccountsService } from './GetAccounts.service'; import { DynamicListModule } from '../DynamicListing/DynamicList.module'; import { AccountsExportable } from './AccountsExportable.service'; -// import { GetAccountsService } from './GetAccounts.service'; const models = [RegisterTenancyModel(BankAccount)]; @@ -39,8 +38,13 @@ const models = [RegisterTenancyModel(BankAccount)]; GetAccountTypesService, GetAccountTransactionsService, GetAccountsService, - AccountsExportable + AccountsExportable, + ], + exports: [ + AccountRepository, + CreateAccountService, + ...models, + AccountsExportable, ], - exports: [AccountRepository, CreateAccountService, ...models], }) export class AccountsModule {} diff --git a/packages/server/src/modules/Accounts/AccountsExportable.service.ts b/packages/server/src/modules/Accounts/AccountsExportable.service.ts index e05c2db0d..2931916c6 100644 --- a/packages/server/src/modules/Accounts/AccountsExportable.service.ts +++ b/packages/server/src/modules/Accounts/AccountsExportable.service.ts @@ -2,12 +2,13 @@ import { AccountsApplication } from './AccountsApplication.service'; import { Exportable } from '../Export/Exportable'; import { EXPORT_SIZE_LIMIT } from '../Export/constants'; import { IAccountsFilter, IAccountsStructureType } from './Accounts.types'; -import { Injectable } from '@nestjs/common'; +import { Global, Injectable } from '@nestjs/common'; import { ExportableService } from '../Export/decorators/ExportableModel.decorator'; import { Account } from './models/Account.model'; @Injectable() @ExportableService({ name: Account.name }) +@Global() export class AccountsExportable extends Exportable { /** * @param {AccountsApplication} accountsApplication diff --git a/packages/server/src/modules/Accounts/models/Account.meta.ts b/packages/server/src/modules/Accounts/models/Account.meta.ts new file mode 100644 index 000000000..37314928c --- /dev/null +++ b/packages/server/src/modules/Accounts/models/Account.meta.ts @@ -0,0 +1,199 @@ +import { ACCOUNT_TYPES } from "../Accounts.constants"; + +export const AccountMeta = { + defaultFilterField: 'name', + defaultSort: { + sortOrder: 'DESC', + sortField: 'name', + }, + importable: true, + exportable: true, + print: { + pageTitle: 'Chart of Accounts', + }, + fields: { + name: { + name: 'account.field.name', + column: 'name', + fieldType: 'text', + }, + description: { + name: 'account.field.description', + column: 'description', + fieldType: 'text', + }, + slug: { + name: 'account.field.slug', + column: 'slug', + fieldType: 'text', + columnable: false, + filterable: false, + }, + code: { + name: 'account.field.code', + column: 'code', + fieldType: 'text', + }, + root_type: { + name: 'account.field.root_type', + fieldType: 'enumeration', + options: [ + { key: 'asset', label: 'Asset' }, + { key: 'liability', label: 'Liability' }, + { key: 'equity', label: 'Equity' }, + { key: 'Income', label: 'Income' }, + { key: 'expense', label: 'Expense' }, + ], + filterCustomQuery: RootTypeFieldFilterQuery, + sortable: false, + }, + normal: { + name: 'account.field.normal', + fieldType: 'enumeration', + options: [ + { key: 'debit', label: 'account.field.normal.debit' }, + { key: 'credit', label: 'account.field.normal.credit' }, + ], + filterCustomQuery: NormalTypeFieldFilterQuery, + sortable: false, + }, + type: { + name: 'account.field.type', + column: 'account_type', + fieldType: 'enumeration', + options: ACCOUNT_TYPES.map((accountType) => ({ + label: accountType.label, + key: accountType.key, + })), + }, + active: { + name: 'account.field.active', + column: 'active', + fieldType: 'boolean', + filterable: false, + }, + balance: { + name: 'account.field.balance', + column: 'amount', + fieldType: 'number', + }, + currency: { + name: 'account.field.currency', + column: 'currency_code', + fieldType: 'text', + filterable: false, + }, + created_at: { + name: 'account.field.created_at', + column: 'created_at', + fieldType: 'date', + }, + }, + columns: { + name: { + name: 'account.field.name', + type: 'text', + }, + code: { + name: 'account.field.code', + type: 'text', + }, + rootType: { + name: 'account.field.root_type', + type: 'text', + accessor: 'accountRootType', + }, + accountType: { + name: 'account.field.type', + accessor: 'accountTypeLabel', + type: 'text', + }, + accountNormal: { + name: 'account.field.normal', + accessor: 'accountNormalFormatted', + }, + currencyCode: { + name: 'account.field.currency', + type: 'text', + }, + bankBalance: { + name: 'account.field.bank_balance', + accessor: 'bankBalanceFormatted', + type: 'text', + exportable: true, + }, + balance: { + name: 'account.field.balance', + accessor: 'formattedAmount', + }, + description: { + name: 'account.field.description', + type: 'text', + }, + active: { + name: 'account.field.active', + type: 'boolean', + }, + createdAt: { + name: 'account.field.created_at', + printable: false, + }, + }, + fields2: { + name: { + name: 'account.field.name', + fieldType: 'text', + unique: true, + required: true, + }, + description: { + name: 'account.field.description', + fieldType: 'text', + }, + code: { + name: 'account.field.code', + fieldType: 'text', + minLength: 3, + maxLength: 6, + unique: true, + importHint: 'Unique number to identify the account.', + }, + accountType: { + name: 'account.field.type', + fieldType: 'enumeration', + options: ACCOUNT_TYPES.map((accountType) => ({ + label: accountType.label, + key: accountType.key, + })), + required: true, + }, + active: { + name: 'account.field.active', + fieldType: 'boolean', + }, + currencyCode: { + name: 'account.field.currency', + fieldType: 'text', + }, + parentAccountId: { + name: 'account.field.parent_account', + fieldType: 'relation', + relationModel: 'Account', + relationImportMatch: ['name', 'code'], + }, + }, +}; + +/** + * Filter query of root type field . + */ +function RootTypeFieldFilterQuery(query, role) { + query.modify('filterByRootType', role.value); +} + +/** + * Filter query of normal field . + */ +function NormalTypeFieldFilterQuery(query, role) { + query.modify('filterByAccountNormal', role.value); +} \ No newline at end of file diff --git a/packages/server/src/modules/Accounts/models/Account.model.ts b/packages/server/src/modules/Accounts/models/Account.model.ts index 40f127ec5..e7429581b 100644 --- a/packages/server/src/modules/Accounts/models/Account.model.ts +++ b/packages/server/src/modules/Accounts/models/Account.model.ts @@ -1,27 +1,27 @@ /* eslint-disable global-require */ -// import { mixin, Model } from 'objection'; import { castArray } from 'lodash'; +import { Model } from 'objection'; import DependencyGraph from '@/libs/dependency-graph'; import { ACCOUNT_TYPES, getAccountsSupportsMultiCurrency, } from '@/constants/accounts'; -import { TenantModel } from '@/modules/System/models/TenantModel'; // import { SearchableModel } from '@/modules/Search/SearchableMdel'; // import { CustomViewBaseModel } from '@/modules/CustomViews/CustomViewBaseModel'; // import { ModelSettings } from '@/modules/Settings/ModelSettings'; import { AccountTypesUtils } from '@/libs/accounts-utils/AccountTypesUtils'; -import { Model } from 'objection'; import { PlaidItem } from '@/modules/BankingPlaid/models/PlaidItem'; import { TenantBaseModel } from '@/modules/System/models/TenantBaseModel'; import { flatToNestedArray } from '@/utils/flat-to-nested-array'; import { ExportableModel } from '../../Export/decorators/ExportableModel.decorator'; +import { AccountMeta } from './Account.meta'; +import { InjectModelMeta } from '@/modules/Tenancy/TenancyModels/decorators/InjectModelMeta.decorator'; // import AccountSettings from './Account.Settings'; // import { DEFAULT_VIEWS } from '@/modules/Accounts/constants'; // import { buildFilterQuery, buildSortColumnQuery } from '@/lib/ViewRolesBuilder'; -// import { flatToNestedArray } from 'utils'; @ExportableModel() +@InjectModelMeta(AccountMeta) export class Account extends TenantBaseModel { public name!: string; public slug!: string; diff --git a/packages/server/src/modules/App/App.module.ts b/packages/server/src/modules/App/App.module.ts index 0e0d0dc1c..28791abb3 100644 --- a/packages/server/src/modules/App/App.module.ts +++ b/packages/server/src/modules/App/App.module.ts @@ -80,6 +80,7 @@ import { LoopsModule } from '../Loops/Loops.module'; import { AttachmentsModule } from '../Attachments/Attachment.module'; import { S3Module } from '../S3/S3.module'; import { ExportModule } from '../Export/Export.module'; +import { ImportModule } from '../Import/Import.module'; @Module({ imports: [ @@ -194,7 +195,8 @@ import { ExportModule } from '../Export/Export.module'; LoopsModule, AttachmentsModule, S3Module, - ExportModule + ExportModule, + ImportModule ], controllers: [AppController], providers: [ diff --git a/packages/server/src/modules/BillPayments/BillPayments.module.ts b/packages/server/src/modules/BillPayments/BillPayments.module.ts index e9f01f6e7..9bd5f345f 100644 --- a/packages/server/src/modules/BillPayments/BillPayments.module.ts +++ b/packages/server/src/modules/BillPayments/BillPayments.module.ts @@ -17,6 +17,7 @@ import { BillPaymentGLEntriesSubscriber } from './subscribers/BillPaymentGLEntri import { LedgerModule } from '../Ledger/Ledger.module'; import { AccountsModule } from '../Accounts/Accounts.module'; import { BillPaymentsExportable } from './queries/BillPaymentsExportable'; +import { GetBillPayments } from '../Bills/queries/GetBillPayments'; @Module({ imports: [LedgerModule, AccountsModule], @@ -35,6 +36,7 @@ import { BillPaymentsExportable } from './queries/BillPaymentsExportable'; TenancyContext, BillPaymentGLEntries, BillPaymentGLEntriesSubscriber, + GetBillPayments, BillPaymentsExportable ], exports: [BillPaymentValidators, CreateBillPaymentService], diff --git a/packages/server/src/modules/Bills/models/Bill.meta.ts b/packages/server/src/modules/Bills/models/Bill.meta.ts new file mode 100644 index 000000000..b5313dbd9 --- /dev/null +++ b/packages/server/src/modules/Bills/models/Bill.meta.ts @@ -0,0 +1,286 @@ +import { Features } from "@/common/types/Features"; + +export const BillMeta = { + defaultFilterField: 'vendor', + defaultSort: { + sortOrder: 'DESC', + sortField: 'bill_date', + }, + importable: true, + exportFlattenOn: 'entries', + exportable: true, + importAggregator: 'group', + importAggregateOn: 'entries', + importAggregateBy: 'billNumber', + print: { + pageTitle: 'Bills', + }, + fields: { + vendor: { + name: 'bill.field.vendor', + column: 'vendor_id', + fieldType: 'relation', + + relationType: 'enumeration', + relationKey: 'vendor', + + relationEntityLabel: 'display_name', + relationEntityKey: 'id', + }, + bill_number: { + name: 'bill.field.bill_number', + column: 'bill_number', + columnable: true, + fieldType: 'text', + }, + bill_date: { + name: 'bill.field.bill_date', + column: 'bill_date', + columnable: true, + fieldType: 'date', + }, + due_date: { + name: 'bill.field.due_date', + column: 'due_date', + columnable: true, + fieldType: 'date', + }, + reference_no: { + name: 'bill.field.reference_no', + column: 'reference_no', + columnable: true, + fieldType: 'text', + }, + status: { + name: 'bill.field.status', + fieldType: 'enumeration', + columnable: true, + options: [ + { label: 'bill.field.status.paid', key: 'paid' }, + { label: 'bill.field.status.partially-paid', key: 'partially-paid' }, + { label: 'bill.field.status.overdue', key: 'overdue' }, + { label: 'bill.field.status.unpaid', key: 'unpaid' }, + { label: 'bill.field.status.opened', key: 'opened' }, + { label: 'bill.field.status.draft', key: 'draft' }, + ], + filterCustomQuery: StatusFieldFilterQuery, + sortCustomQuery: StatusFieldSortQuery, + }, + amount: { + name: 'bill.field.amount', + column: 'amount', + fieldType: 'number', + }, + payment_amount: { + name: 'bill.field.payment_amount', + column: 'payment_amount', + fieldType: 'number', + }, + note: { + name: 'bill.field.note', + column: 'note', + fieldType: 'text', + }, + created_at: { + name: 'bill.field.created_at', + column: 'created_at', + fieldType: 'date', + }, + }, + columns: { + billDate: { + name: 'Date', + accessor: 'formattedBillDate', + }, + billNumber: { + name: 'Bill No.', + type: 'text', + }, + referenceNo: { + name: 'Reference No.', + type: 'text', + }, + dueDate: { + name: 'Due Date', + type: 'date', + accessor: 'formattedDueDate', + }, + vendorId: { + name: 'Vendor', + accessor: 'vendor.displayName', + type: 'text', + }, + amount: { + name: 'Amount', + accessor: 'formattedAmount', + }, + exchangeRate: { + name: 'Exchange Rate', + type: 'number', + printable: false, + }, + currencyCode: { + name: 'Currency Code', + type: 'text', + printable: false, + }, + dueAmount: { + name: 'Due Amount', + accessor: 'formattedDueAmount', + }, + paidAmount: { + name: 'Paid Amount', + accessor: 'formattedPaymentAmount', + }, + note: { + name: 'Note', + type: 'text', + printable: false, + }, + open: { + name: 'Open', + type: 'boolean', + printable: false, + }, + entries: { + name: 'Entries', + accessor: 'entries', + type: 'collection', + collectionOf: 'object', + columns: { + itemName: { + name: 'Item Name', + accessor: 'item.name', + }, + rate: { + name: 'Item Rate', + accessor: 'rateFormatted', + }, + quantity: { + name: 'Item Quantity', + accessor: 'quantityFormatted', + }, + description: { + name: 'Item Description', + }, + amount: { + name: 'Item Amount', + accessor: 'totalFormatted', + }, + }, + }, + branch: { + name: 'Branch', + type: 'text', + accessor: 'branch.name', + features: [Features.BRANCHES], + }, + warehouse: { + name: 'Warehouse', + type: 'text', + accessor: 'warehouse.name', + features: [Features.BRANCHES], + }, + }, + fields2: { + billNumber: { + name: 'Bill No.', + fieldType: 'text', + required: true, + }, + referenceNo: { + name: 'Reference No.', + fieldType: 'text', + }, + billDate: { + name: 'Date', + fieldType: 'date', + required: true, + }, + dueDate: { + name: 'Due Date', + fieldType: 'date', + required: true, + }, + vendorId: { + name: 'Vendor', + fieldType: 'relation', + relationModel: 'Contact', + relationImportMatch: 'displayName', + required: true, + }, + exchangeRate: { + name: 'Exchange Rate', + fieldType: 'number', + }, + note: { + name: 'Note', + fieldType: 'text', + }, + open: { + name: 'Open', + fieldType: 'boolean', + }, + entries: { + name: 'Entries', + fieldType: 'collection', + collectionOf: 'object', + collectionMinLength: 1, + required: true, + fields: { + itemId: { + name: 'Item', + fieldType: 'relation', + relationModel: 'Item', + relationImportMatch: ['name', 'code'], + required: true, + importHint: 'Matches the item name or code.', + }, + rate: { + name: 'Rate', + fieldType: 'number', + required: true, + }, + quantity: { + name: 'Quantity', + fieldType: 'number', + required: true, + }, + description: { + name: 'Line Description', + 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, + }, + }, +}; + +/** + * Status field filter custom query. + */ +function StatusFieldFilterQuery(query, role) { + query.modify('statusFilter', role.value); +} + +/** + * Status field sort custom query. + */ +function StatusFieldSortQuery(query, role) { + query.modify('sortByStatus', role.order); +} \ No newline at end of file diff --git a/packages/server/src/modules/Bills/models/Bill.ts b/packages/server/src/modules/Bills/models/Bill.ts index 41da17fc3..9c665bf6f 100644 --- a/packages/server/src/modules/Bills/models/Bill.ts +++ b/packages/server/src/modules/Bills/models/Bill.ts @@ -1,4 +1,5 @@ -import { Model, raw, mixin } from 'objection'; +import type { Knex } from 'knex'; +import { Model, raw } from 'objection'; import { castArray, difference, defaultTo } from 'lodash'; import * as moment from 'moment'; import * as R from 'ramda'; @@ -12,11 +13,13 @@ import { BaseModel, PaginationQueryBuilderType } from '@/models/Model'; import { ItemEntry } from '@/modules/TransactionItemEntry/models/ItemEntry'; import { BillLandedCost } from '@/modules/BillLandedCosts/models/BillLandedCost'; import { DiscountType } from '@/common/types/Discount'; -import type { Knex, QueryBuilder } from 'knex'; import { TenantBaseModel } from '@/modules/System/models/TenantBaseModel'; import { ExportableModel } from '@/modules/Export/decorators/ExportableModel.decorator'; +import { InjectModelMeta } from '@/modules/Tenancy/TenancyModels/decorators/InjectModelMeta.decorator'; +import { BillMeta } from './Bill.meta'; @ExportableModel() +@InjectModelMeta(BillMeta) export class Bill extends TenantBaseModel { public amount: number; public paymentAmount: number; diff --git a/packages/server/src/modules/Branches/models/Branch.meta.ts b/packages/server/src/modules/Branches/models/Branch.meta.ts new file mode 100644 index 000000000..0361d10bd --- /dev/null +++ b/packages/server/src/modules/Branches/models/Branch.meta.ts @@ -0,0 +1,11 @@ +export const BranchMeta = { + fields2: { + name: { + name: 'Name', + fieldType: 'text', + required: true, + }, + }, + columns: {}, + fields: {} +}; \ No newline at end of file diff --git a/packages/server/src/modules/Branches/models/Branch.model.ts b/packages/server/src/modules/Branches/models/Branch.model.ts index 20f3aa593..d09d30845 100644 --- a/packages/server/src/modules/Branches/models/Branch.model.ts +++ b/packages/server/src/modules/Branches/models/Branch.model.ts @@ -3,7 +3,10 @@ // import BranchMetadata from './Branch.settings'; // import ModelSetting from './ModelSetting'; import { BaseModel } from '@/models/Model'; +import { InjectModelMeta } from '@/modules/Tenancy/TenancyModels/decorators/InjectModelMeta.decorator'; +import { BranchMeta } from './Branch.meta'; +@InjectModelMeta(BranchMeta) export class Branch extends BaseModel{ name!: string; code!: string; diff --git a/packages/server/src/modules/Customers/Customers.module.ts b/packages/server/src/modules/Customers/Customers.module.ts index 7531ae9b6..434b7893a 100644 --- a/packages/server/src/modules/Customers/Customers.module.ts +++ b/packages/server/src/modules/Customers/Customers.module.ts @@ -14,9 +14,11 @@ import { CustomersApplication } from './CustomersApplication.service'; import { DeleteCustomer } from './commands/DeleteCustomer.service'; import { CustomersExportable } from './CustomersExportable'; import { CustomersImportable } from './CustomersImportable'; +import { GetCustomers } from './queries/GetCustomers.service'; +import { DynamicListModule } from '../DynamicListing/DynamicList.module'; @Module({ - imports: [TenancyDatabaseModule], + imports: [TenancyDatabaseModule, DynamicListModule], controllers: [CustomersController], providers: [ ActivateCustomer, @@ -33,7 +35,8 @@ import { CustomersImportable } from './CustomersImportable'; TransformerInjectable, GetCustomerService, CustomersExportable, - CustomersImportable + CustomersImportable, + GetCustomers ], }) export class CustomersModule {} diff --git a/packages/server/src/modules/Customers/models/Customer.meta.ts b/packages/server/src/modules/Customers/models/Customer.meta.ts new file mode 100644 index 000000000..4651a0ac4 --- /dev/null +++ b/packages/server/src/modules/Customers/models/Customer.meta.ts @@ -0,0 +1,422 @@ +export const CustomerMeta = { + importable: true, + exportable: true, + defaultFilterField: 'displayName', + defaultSort: { + sortOrder: 'DESC', + sortField: 'created_at', + }, + print: { + pageTitle: 'Customers', + }, + fields: { + first_name: { + name: 'vendor.field.first_name', + column: 'first_name', + fieldType: 'text', + }, + last_name: { + name: 'vendor.field.last_name', + column: 'last_name', + fieldType: 'text', + }, + display_name: { + name: 'vendor.field.display_name', + column: 'display_name', + fieldType: 'text', + }, + email: { + name: 'vendor.field.email', + column: 'email', + fieldType: 'text', + }, + work_phone: { + name: 'vendor.field.work_phone', + column: 'work_phone', + fieldType: 'text', + }, + personal_phone: { + name: 'vendor.field.personal_pone', + column: 'personal_phone', + fieldType: 'text', + }, + company_name: { + name: 'vendor.field.company_name', + column: 'company_name', + fieldType: 'text', + }, + website: { + name: 'vendor.field.website', + column: 'website', + fieldType: 'text', + }, + created_at: { + name: 'vendor.field.created_at', + column: 'created_at', + fieldType: 'date', + }, + balance: { + name: 'vendor.field.balance', + column: 'balance', + fieldType: 'number', + }, + opening_balance: { + name: 'vendor.field.opening_balance', + column: 'opening_balance', + fieldType: 'number', + }, + opening_balance_at: { + name: 'vendor.field.opening_balance_at', + column: 'opening_balance_at', + fieldType: 'date', + }, + currency_code: { + name: 'vendor.field.currency', + column: 'currency_code', + fieldType: 'text', + }, + status: { + name: 'vendor.field.status', + type: 'enumeration', + options: [ + { key: 'overdue', label: 'vendor.field.status.overdue' }, + { key: 'unpaid', label: 'vendor.field.status.unpaid' }, + ], + filterCustomQuery: (query, role) => { + switch (role.value) { + case 'overdue': + query.modify('overdue'); + break; + case 'unpaid': + query.modify('unpaid'); + break; + } + }, + }, + }, + columns: { + customerType: { + name: 'Customer Type', + type: 'text', + accessor: 'formattedCustomerType', + }, + firstName: { + name: 'vendor.field.first_name', + type: 'text', + }, + lastName: { + name: 'vendor.field.last_name', + type: 'text', + }, + displayName: { + name: 'vendor.field.display_name', + type: 'text', + }, + email: { + name: 'vendor.field.email', + type: 'text', + }, + workPhone: { + name: 'vendor.field.work_phone', + type: 'text', + }, + personalPhone: { + name: 'vendor.field.personal_phone', + type: 'text', + }, + companyName: { + name: 'vendor.field.company_name', + type: 'text', + }, + website: { + name: 'vendor.field.website', + type: 'text', + }, + balance: { + name: 'vendor.field.balance', + type: 'number', + accessor: 'formattedBalance', + }, + openingBalance: { + name: 'vendor.field.opening_balance', + type: 'number', + printable: false, + }, + openingBalanceAt: { + name: 'vendor.field.opening_balance_at', + type: 'date', + printable: false, + accessor: 'formattedOpeningBalanceAt' + }, + currencyCode: { + name: 'vendor.field.currency', + type: 'text', + printable: false, + }, + status: { + name: 'vendor.field.status', + printable: false, + }, + note: { + name: 'vendor.field.note', + printable: false, + }, + // Billing Address + billingAddress1: { + name: 'Billing Address 1', + column: 'billing_address1', + type: 'text', + printable: false, + }, + billingAddress2: { + name: 'Billing Address 2', + column: 'billing_address2', + type: 'text', + printable: false, + }, + billingAddressCity: { + name: 'Billing Address City', + column: 'billing_address_city', + type: 'text', + printable: false, + }, + billingAddressCountry: { + name: 'Billing Address Country', + column: 'billing_address_country', + type: 'text', + printable: false, + }, + billingAddressPostcode: { + name: 'Billing Address Postcode', + column: 'billing_address_postcode', + type: 'text', + printable: false, + }, + billingAddressState: { + name: 'Billing Address State', + column: 'billing_address_state', + type: 'text', + printable: false, + }, + billingAddressPhone: { + name: 'Billing Address Phone', + column: 'billing_address_phone', + type: 'text', + printable: false, + }, + // Shipping Address + shippingAddress1: { + name: 'Shipping Address 1', + column: 'shipping_address1', + type: 'text', + printable: false, + }, + shippingAddress2: { + name: 'Shipping Address 2', + column: 'shipping_address2', + type: 'text', + printable: false, + }, + shippingAddressCity: { + name: 'Shipping Address City', + column: 'shipping_address_city', + type: 'text', + printable: false, + }, + shippingAddressCountry: { + name: 'Shipping Address Country', + column: 'shipping_address_country', + type: 'text', + printable: false, + }, + shippingAddressPostcode: { + name: 'Shipping Address Postcode', + column: 'shipping_address_postcode', + type: 'text', + printable: false, + }, + shippingAddressPhone: { + name: 'Shipping Address Phone', + column: 'shipping_address_phone', + type: 'text', + printable: false, + }, + shippingAddressState: { + name: 'Shipping Address State', + column: 'shipping_address_state', + type: 'text', + printable: false, + }, + createdAt: { + name: 'vendor.field.created_at', + type: 'date', + printable: false, + }, + }, + fields2: { + customerType: { + name: 'Customer Type', + fieldType: 'enumeration', + options: [ + { key: 'business', label: 'Business' }, + { key: 'individual', label: 'Individual' }, + ], + required: true, + }, + firstName: { + name: 'customer.field.first_name', + column: 'first_name', + fieldType: 'text', + }, + lastName: { + name: 'customer.field.last_name', + column: 'last_name', + fieldType: 'text', + }, + displayName: { + name: 'customer.field.display_name', + column: 'display_name', + fieldType: 'text', + required: true, + }, + email: { + name: 'customer.field.email', + column: 'email', + fieldType: 'text', + }, + workPhone: { + name: 'customer.field.work_phone', + column: 'work_phone', + fieldType: 'text', + }, + personalPhone: { + name: 'customer.field.personal_phone', + column: 'personal_phone', + fieldType: 'text', + }, + companyName: { + name: 'customer.field.company_name', + column: 'company_name', + fieldType: 'text', + }, + website: { + name: 'customer.field.website', + column: 'website', + fieldType: 'url', + }, + openingBalance: { + name: 'customer.field.opening_balance', + column: 'opening_balance', + fieldType: 'number', + }, + openingBalanceAt: { + name: 'customer.field.opening_balance_at', + column: 'opening_balance_at', + filterable: false, + fieldType: 'date', + }, + openingBalanceExchangeRate: { + name: 'Opening Balance Ex. Rate', + column: 'opening_balance_exchange_rate', + fieldType: 'number', + }, + currencyCode: { + name: 'customer.field.currency', + column: 'currency_code', + fieldType: 'text', + }, + note: { + name: 'Note', + column: 'note', + fieldType: 'text', + }, + active: { + name: 'Active', + column: 'active', + fieldType: 'boolean', + }, + // Billing Address + billingAddress1: { + name: 'Billing Address 1', + column: 'billing_address1', + fieldType: 'text', + }, + billingAddress2: { + name: 'Billing Address 2', + column: 'billing_address2', + fieldType: 'text', + }, + billingAddressCity: { + name: 'Billing Address City', + column: 'billing_address_city', + fieldType: 'text', + }, + billingAddressCountry: { + name: 'Billing Address Country', + column: 'billing_address_country', + fieldType: 'text', + }, + billingAddressPostcode: { + name: 'Billing Address Postcode', + column: 'billing_address_postcode', + fieldType: 'text', + }, + billingAddressState: { + name: 'Billing Address State', + column: 'billing_address_state', + fieldType: 'text', + }, + billingAddressPhone: { + name: 'Billing Address Phone', + column: 'billing_address_phone', + fieldType: 'text', + }, + // Shipping Address + shippingAddress1: { + name: 'Shipping Address 1', + column: 'shipping_address1', + fieldType: 'text', + }, + shippingAddress2: { + name: 'Shipping Address 2', + column: 'shipping_address2', + fieldType: 'text', + }, + shippingAddressCity: { + name: 'Shipping Address City', + column: 'shipping_address_city', + fieldType: 'text', + }, + shippingAddressCountry: { + name: 'Shipping Address Country', + column: 'shipping_address_country', + fieldType: 'text', + }, + shippingAddressPostcode: { + name: 'Shipping Address Postcode', + column: 'shipping_address_postcode', + fieldType: 'text', + }, + shippingAddressPhone: { + name: 'Shipping Address Phone', + column: 'shipping_address_phone', + fieldType: 'text', + }, + shippingAddressState: { + name: 'Shipping Address State', + column: 'shipping_address_state', + fieldType: 'text', + }, + }, +}; + +function statusFieldFilterQuery(query, role) { + switch (role.value) { + case 'overdue': + query.modify('overdue'); + break; + case 'unpaid': + query.modify('unpaid'); + break; + } +} \ No newline at end of file diff --git a/packages/server/src/modules/Customers/models/Customer.ts b/packages/server/src/modules/Customers/models/Customer.ts index 1e720625a..ee86a5b81 100644 --- a/packages/server/src/modules/Customers/models/Customer.ts +++ b/packages/server/src/modules/Customers/models/Customer.ts @@ -1,6 +1,9 @@ import { TenantBaseModel } from '@/modules/System/models/TenantBaseModel'; +import { InjectModelMeta } from '@/modules/Tenancy/TenancyModels/decorators/InjectModelMeta.decorator'; +import { CustomerMeta } from './Customer.meta'; +@InjectModelMeta(CustomerMeta) export class Customer extends TenantBaseModel{ contactService: string; contactType: string; diff --git a/packages/server/src/modules/Export/Export.module.ts b/packages/server/src/modules/Export/Export.module.ts index ca47b42cc..59e73e771 100644 --- a/packages/server/src/modules/Export/Export.module.ts +++ b/packages/server/src/modules/Export/Export.module.ts @@ -4,9 +4,34 @@ import { ExportResourceService } from './ExportService'; import { ExportPdf } from './ExportPdf'; import { ExportAls } from './ExportAls'; import { ExportApplication } from './ExportApplication'; +import { ResourceModule } from '../Resource/Resource.module'; +import { RegisterTenancyModel } from '../Tenancy/TenancyModels/Tenancy.module'; +import { ImportModel } from '../Import/models/Import'; +import { ExportableResources } from './ExportResources'; +import { ExportableRegistry } from './ExportRegistery'; +import { TemplateInjectableModule } from '../TemplateInjectable/TemplateInjectable.module'; +import { ChromiumlyTenancyModule } from '../ChromiumlyTenancy/ChromiumlyTenancy.module'; +import { AccountsModule } from '../Accounts/Accounts.module'; + +const models = [RegisterTenancyModel(ImportModel)]; @Module({ - providers: [ExportResourceService, ExportPdf, ExportAls, ExportApplication], + imports: [ + ...models, + ResourceModule, + TemplateInjectableModule, + ChromiumlyTenancyModule, + AccountsModule + ], + providers: [ + ExportResourceService, + ExportPdf, + ExportAls, + ExportApplication, + ExportableResources, + ExportableRegistry + ], + exports: [...models], controllers: [ExportController], }) export class ExportModule {} diff --git a/packages/server/src/modules/Export/Export.utils.ts b/packages/server/src/modules/Export/Export.utils.ts index 97b31125e..c902b6fe6 100644 --- a/packages/server/src/modules/Export/Export.utils.ts +++ b/packages/server/src/modules/Export/Export.utils.ts @@ -3,6 +3,7 @@ import { ExportFormat } from './common'; export const convertAcceptFormatToFormat = (accept: string): ExportFormat => { switch (accept) { + default: case ACCEPT_TYPE.APPLICATION_CSV: return ExportFormat.Csv; case ACCEPT_TYPE.APPLICATION_PDF: diff --git a/packages/server/src/modules/Export/ExportResources.ts b/packages/server/src/modules/Export/ExportResources.ts deleted file mode 100644 index 660b02829..000000000 --- a/packages/server/src/modules/Export/ExportResources.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { Injectable } from "@nestjs/common"; -import { ExportableRegistry } from "./ExportRegistery"; -import { AccountsExportable } from "../Accounts/AccountsExportable.service"; - -@Injectable() -export class ExportableResources { - constructor( - private readonly exportRegistry: ExportableRegistry, - ) { - this.boot(); - } - - /** - * Importable instances. - */ - private importables = [ - // { resource: 'Account', exportable: AccountsExportable }, - // { resource: 'Item', exportable: ItemsExportable }, - // { resource: 'ItemCategory', exportable: ItemCategoriesExportable }, - // { resource: 'Customer', exportable: CustomersExportable }, - // { resource: 'Vendor', exportable: VendorsExportable }, - // { resource: 'Expense', exportable: ExpensesExportable }, - // { resource: 'SaleInvoice', exportable: SaleInvoicesExportable }, - // { resource: 'SaleEstimate', exportable: SaleEstimatesExportable }, - // { resource: 'SaleReceipt', exportable: SaleReceiptsExportable }, - // { resource: 'Bill', exportable: BillsExportable }, - // { resource: 'PaymentReceive', exportable: PaymentsReceivedExportable }, - // { resource: 'BillPayment', exportable: BillPaymentExportable }, - // { resource: 'ManualJournal', exportable: ManualJournalsExportable }, - // { resource: 'CreditNote', exportable: CreditNotesExportable }, - // { resource: 'VendorCredit', exportable: VendorCreditsExportable }, - // { resource: 'TaxRate', exportable: TaxRatesExportable }, - ]; - - /** - * - */ - public get registry() { - return ExportableResources.registry; - } - - /** - * Boots all the registered importables. - */ - public boot() { - if (!ExportableResources.registry) { - const instance = ExportableRegistry.getInstance(); - - this.importables.forEach((importable) => { - const importableInstance = Container.get(importable.exportable); - instance.registerExportable(importable.resource, importableInstance); - }); - ExportableResources.registry = instance; - } - } -} diff --git a/packages/server/src/modules/Export/ExportService.ts b/packages/server/src/modules/Export/ExportService.ts index 0dc465d28..6e8e704e4 100644 --- a/packages/server/src/modules/Export/ExportService.ts +++ b/packages/server/src/modules/Export/ExportService.ts @@ -1,5 +1,5 @@ import { Injectable } from '@nestjs/common'; -import xlsx from 'xlsx'; +import * as xlsx from 'xlsx'; import * as R from 'ramda'; import { get } from 'lodash'; import { sanitizeResourceName } from '../Import/_utils'; @@ -10,6 +10,9 @@ import { ExportPdf } from './ExportPdf'; import { ExportAls } from './ExportAls'; import { IModelMeta, IModelMetaColumn } from '@/interfaces/Model'; import { ServiceError } from '../Items/ServiceError'; +import { ResourceService } from '../Resource/ResourceService'; +import { getExportableService } from './decorators/ExportableModel.decorator'; +import { ContextIdFactory, ModuleRef } from '@nestjs/core'; @Injectable() export class ExportResourceService { @@ -18,6 +21,7 @@ export class ExportResourceService { private readonly exportPdf: ExportPdf, private readonly exportableResources: ExportableResources, private readonly resourceService: ResourceService, + private readonly moduleRef: ModuleRef, ) {} /** @@ -27,11 +31,9 @@ export class ExportResourceService { */ public async export( resourceName: string, - format: ExportFormat = ExportFormat.Csv + format: ExportFormat = ExportFormat.Csv, ) { - return this.exportAls.run(() => - this.exportAlsRun(resourceName, format) - ); + return this.exportAls.run(() => this.exportAlsRun(resourceName, format)); } /** @@ -41,18 +43,19 @@ export class ExportResourceService { */ public async exportAlsRun( resourceName: string, - format: ExportFormat = ExportFormat.Csv + format: ExportFormat = ExportFormat.Csv, ) { const resource = sanitizeResourceName(resourceName); const resourceMeta = this.getResourceMeta(resource); - const resourceColumns = this.resourceService.getResourceColumns( - resource - ); + + const resourceColumns = this.resourceService.getResourceColumns(resource); this.validateResourceMeta(resourceMeta); const data = await this.getExportableData(resource); const transformed = this.transformExportedData(resource, data); + console.log(format); + // Returns the csv, xlsx format. if (format === ExportFormat.Csv || format === ExportFormat.Xlsx) { const exportableColumns = this.getExportableColumns(resourceColumns); @@ -66,7 +69,7 @@ export class ExportResourceService { return this.exportPdf.pdf( printableColumns, transformed, - resourceMeta?.print?.pageTitle + resourceMeta?.print?.pageTitle, ); } } @@ -102,14 +105,14 @@ export class ExportResourceService { */ private transformExportedData( resource: string, - data: Array> + data: Array>, ): Array> { const resourceMeta = this.getResourceMeta(resource); return R.when>, Array>>( R.always(Boolean(resourceMeta.exportFlattenOn)), (data) => flatDataCollections(data, resourceMeta.exportFlattenOn), - data + data, ); } /** @@ -119,10 +122,14 @@ export class ExportResourceService { * @returns A promise that resolves to the exportable data. */ private async getExportableData(resource: string) { - const exportable = - this.exportableResources.registry.getExportable(resource); - - return exportable.exportable({}); + const exportable = getExportableService(resource); + const contextId = ContextIdFactory.create(); + const exportableInstance = await this.moduleRef.resolve( + exportable, + contextId, + { strict: false }, + ); + return exportableInstance.exportable({}); } /** @@ -133,7 +140,7 @@ export class ExportResourceService { private getExportableColumns(resourceColumns: any) { const processColumns = ( columns: { [key: string]: IModelMetaColumn }, - parent = '' + parent = '', ) => { return Object.entries(columns) .filter(([_, value]) => value.exportable !== false) @@ -159,9 +166,10 @@ export class ExportResourceService { private getPrintableColumns(resourceMeta: IModelMeta) { const processColumns = ( columns: { [key: string]: IModelMetaColumn }, - parent = '' + parent = '', ) => { return Object.entries(columns) + // @ts-expect-error .filter(([_, value]) => value.printable !== false) .flatMap(([key, value]) => { if (value.type === 'collection' && value.collectionOf === 'object') { @@ -191,7 +199,7 @@ export class ExportResourceService { private createWorkbook(data: any[], exportableColumns: any[]) { const workbook = xlsx.utils.book_new(); const worksheetData = data.map((item) => - exportableColumns.map((col) => get(item, getDataAccessor(col))) + exportableColumns.map((col) => get(item, getDataAccessor(col))), ); worksheetData.unshift(exportableColumns.map((col) => col.name)); diff --git a/packages/server/src/modules/Export/dtos/ExportQuery.dto.ts b/packages/server/src/modules/Export/dtos/ExportQuery.dto.ts index 04d1eae67..da78d0869 100644 --- a/packages/server/src/modules/Export/dtos/ExportQuery.dto.ts +++ b/packages/server/src/modules/Export/dtos/ExportQuery.dto.ts @@ -4,8 +4,4 @@ export class ExportQuery { @IsString() @IsNotEmpty() resource: string; - - @IsString() - @IsNotEmpty() - format: string; } diff --git a/packages/server/src/modules/Import/Import.module.ts b/packages/server/src/modules/Import/Import.module.ts index 953f6ce4f..032e34508 100644 --- a/packages/server/src/modules/Import/Import.module.ts +++ b/packages/server/src/modules/Import/Import.module.ts @@ -12,11 +12,17 @@ import { ImportFileMapping } from './ImportFileMapping'; import { ImportFileDataValidator } from './ImportFileDataValidator'; import { ImportFileDataTransformer } from './ImportFileDataTransformer'; import { ImportFileCommon } from './ImportFileCommon'; +import { ImportableResources } from './ImportableResources'; +import { ResourceModule } from '../Resource/Resource.module'; +import { TenancyModule } from '../Tenancy/Tenancy.module'; +import { AccountsModule } from '../Accounts/Accounts.module'; @Module({ + imports: [ResourceModule, TenancyModule, AccountsModule], providers: [ ImportAls, ImportSampleService, + ImportableResources, ImportResourceApplication, ImportDeleteExpiredFiles, ImportFileUploadService, @@ -27,7 +33,7 @@ import { ImportFileCommon } from './ImportFileCommon'; ImportFileMapping, ImportFileDataValidator, ImportFileDataTransformer, - ImportFileCommon + ImportFileCommon, ], exports: [ImportAls], }) diff --git a/packages/server/src/modules/Import/ImportableResources.ts b/packages/server/src/modules/Import/ImportableResources.ts index 2a9bf0d35..a16c4306a 100644 --- a/packages/server/src/modules/Import/ImportableResources.ts +++ b/packages/server/src/modules/Import/ImportableResources.ts @@ -35,7 +35,7 @@ export class ImportableResources { // resource: 'UncategorizedCashflowTransaction', // importable: UncategorizedTransactionsImportable, // }, - // { resource: 'Customer', importable: CustomersImportable }, + // { resource: 'Customer', importable: CustomersImportable }, // { resource: 'Vendor', importable: VendorsImportable }, // { resource: 'Item', importable: ItemsImportable }, // { resource: 'ItemCategory', importable: ItemCategoriesImportable }, diff --git a/packages/server/src/modules/Import/_utils.ts b/packages/server/src/modules/Import/_utils.ts index 33f270705..63d58e25e 100644 --- a/packages/server/src/modules/Import/_utils.ts +++ b/packages/server/src/modules/Import/_utils.ts @@ -2,8 +2,8 @@ import * as Yup from 'yup'; import * as moment from 'moment'; import * as R from 'ramda'; import { Knex } from 'knex'; -import fs from 'fs/promises'; -import path from 'path'; +import * as fs from 'fs/promises'; +import * as path from 'path'; import { defaultTo, upperFirst, @@ -18,7 +18,7 @@ import { split, last, } from 'lodash'; -import pluralize from 'pluralize'; +import * as pluralize from 'pluralize'; import { ResourceMetaFieldsMap } from './interfaces'; import { multiNumberParse } from '@/utils/multi-number-parse'; import { ServiceError } from '../Items/ServiceError'; @@ -70,13 +70,13 @@ export const convertFieldsToYupValidation = (fields: ResourceMetaFieldsMap) => { if (!isUndefined(field.minLength)) { fieldSchema = fieldSchema.min( field.minLength, - `Minimum length is ${field.minLength} characters` + `Minimum length is ${field.minLength} characters`, ); } if (!isUndefined(field.maxLength)) { fieldSchema = fieldSchema.max( field.maxLength, - `Maximum length is ${field.maxLength} characters` + `Maximum length is ${field.maxLength} characters`, ); } } else if (field.fieldType === 'number') { @@ -106,7 +106,7 @@ export const convertFieldsToYupValidation = (fields: ResourceMetaFieldsMap) => { return true; } return moment(val, 'YYYY-MM-DD', true).isValid(); - } + }, ); } else if (field.fieldType === 'url') { fieldSchema = fieldSchema.url(); @@ -150,12 +150,12 @@ const parseFieldName = (fieldName: string, field: IModelMetaField) => { */ export const getUnmappedSheetColumns = (columns, mapping) => { return columns.filter( - (column) => !mapping.some((map) => map.from === column) + (column) => !mapping.some((map) => map.from === column), ); }; export const sanitizeResourceName = (resourceName: string) => { - return upperFirst(camelCase(pluralize.singular(resourceName))); + return upperFirst(camelCase(pluralize(resourceName, 1))); }; export const getSheetColumns = (sheetData: unknown[]) => { @@ -171,11 +171,11 @@ export const getSheetColumns = (sheetData: unknown[]) => { */ export const getUniqueImportableValue = ( importableFields: { [key: string]: IModelMetaField2 }, - objectDTO: Record + objectDTO: Record, ) => { const uniqueImportableValue = pickBy( importableFields, - (field) => field.unique + (field) => field.unique, ); const uniqueImportableKeys = Object.keys(uniqueImportableValue); const uniqueImportableKey = first(uniqueImportableKeys); @@ -255,7 +255,7 @@ export const getResourceColumns = (resourceColumns: { (group: string) => ([fieldKey, { name, importHint, required, order, ...field }]: [ string, - IModelMetaField2 + IModelMetaField2, ]) => { const extra: Record = {}; const key = fieldKey; @@ -300,7 +300,7 @@ export const valueParser = // Parses the enumeration value. } else if (field.fieldType === 'enumeration') { const option = get(field, 'options', []).find( - (option) => option.label?.toLowerCase() === value?.toLowerCase() + (option) => option.label?.toLowerCase() === value?.toLowerCase(), ); _value = get(option, 'key'); // Parses the numeric value. @@ -356,7 +356,7 @@ export const parseKey = R.curry( } } return _key; - } + }, ); /** @@ -399,11 +399,11 @@ export const getFieldKey = (input: string) => { export function aggregate( input: Array, comparatorAttr: string, - groupOn: string + groupOn: string, ): Array> { return input.reduce((acc, curr) => { const existingEntry = acc.find( - (entry) => entry[comparatorAttr] === curr[comparatorAttr] + (entry) => entry[comparatorAttr] === curr[comparatorAttr], ); if (existingEntry) { diff --git a/packages/server/src/modules/ItemCategories/models/ItemCategory.meta.ts b/packages/server/src/modules/ItemCategories/models/ItemCategory.meta.ts new file mode 100644 index 000000000..8d14fc9d1 --- /dev/null +++ b/packages/server/src/modules/ItemCategories/models/ItemCategory.meta.ts @@ -0,0 +1,64 @@ + + +export const ItemCategoryMeta = { + defaultFilterField: 'name', + defaultSort: { + sortField: 'name', + sortOrder: 'DESC', + }, + importable: true, + exportable: true, + fields: { + name: { + name: 'item_category.field.name', + column: 'name', + fieldType: 'text', + }, + description: { + name: 'item_category.field.description', + column: 'description', + fieldType: 'text', + }, + count: { + name: 'item_category.field.count', + column: 'count', + fieldType: 'number', + virtualColumn: true, + }, + created_at: { + name: 'item_category.field.created_at', + column: 'created_at', + columnType: 'date', + }, + }, + columns: { + name: { + name: 'item_category.field.name', + type: 'text', + }, + description: { + name: 'item_category.field.description', + type: 'text', + }, + count: { + name: 'item_category.field.count', + type: 'text', + }, + createdAt: { + name: 'item_category.field.created_at', + type: 'text', + }, + }, + fields2: { + name: { + name: 'item_category.field.name', + column: 'name', + fieldType: 'text', + }, + description: { + name: 'item_category.field.description', + column: 'description', + fieldType: 'text', + }, + }, +}; \ No newline at end of file diff --git a/packages/server/src/modules/ItemCategories/models/ItemCategory.model.ts b/packages/server/src/modules/ItemCategories/models/ItemCategory.model.ts index 8fbfafbbb..a75803403 100644 --- a/packages/server/src/modules/ItemCategories/models/ItemCategory.model.ts +++ b/packages/server/src/modules/ItemCategories/models/ItemCategory.model.ts @@ -1,8 +1,11 @@ +import { Model } from 'objection'; import { ExportableModel } from '@/modules/Export/decorators/ExportableModel.decorator'; import { TenantBaseModel } from '@/modules/System/models/TenantBaseModel'; -import { Model } from 'objection'; +import { InjectModelMeta } from '@/modules/Tenancy/TenancyModels/decorators/InjectModelMeta.decorator'; +import { ItemCategoryMeta } from './ItemCategory.meta'; @ExportableModel() +@InjectModelMeta(ItemCategoryMeta) export class ItemCategory extends TenantBaseModel { name!: string; description!: string; diff --git a/packages/server/src/modules/Items/models/Item.meta.ts b/packages/server/src/modules/Items/models/Item.meta.ts new file mode 100644 index 000000000..d2aefb23e --- /dev/null +++ b/packages/server/src/modules/Items/models/Item.meta.ts @@ -0,0 +1,309 @@ +export const ItemMeta = { + importable: true, + exportable: true, + defaultFilterField: 'name', + defaultSort: { + sortField: 'name', + sortOrder: 'DESC', + }, + print: { + pageTitle: 'Items', + }, + fields: { + type: { + name: 'item.field.type', + column: 'type', + fieldType: 'enumeration', + options: [ + { key: 'inventory', label: 'item.field.type.inventory' }, + { key: 'service', label: 'item.field.type.service' }, + { key: 'non-inventory', label: 'item.field.type.non-inventory' }, + ], + }, + name: { + name: 'item.field.name', + column: 'name', + fieldType: 'text', + }, + code: { + name: 'item.field.code', + column: 'code', + fieldType: 'text', + }, + sellable: { + name: 'item.field.sellable', + column: 'sellable', + fieldType: 'boolean', + }, + purchasable: { + name: 'item.field.purchasable', + column: 'purchasable', + fieldType: 'boolean', + }, + sell_price: { + name: 'item.field.sell_price', + column: 'sell_price', + fieldType: 'number', + }, + cost_price: { + name: 'item.field.cost_price', + column: 'cost_price', + fieldType: 'number', + }, + cost_account: { + name: 'item.field.cost_account', + column: 'cost_account_id', + fieldType: 'relation', + + relationType: 'enumeration', + relationKey: 'costAccount', + + relationEntityLabel: 'name', + relationEntityKey: 'slug', + }, + sell_account: { + name: 'item.field.sell_account', + column: 'sell_account_id', + fieldType: 'relation', + + relationType: 'enumeration', + relationKey: 'sellAccount', + + relationEntityLabel: 'name', + relationEntityKey: 'slug', + }, + inventory_account: { + name: 'item.field.inventory_account', + column: 'inventory_account_id', + + relationType: 'enumeration', + relationKey: 'inventoryAccount', + + relationEntityLabel: 'name', + relationEntityKey: 'slug', + }, + sell_description: { + name: 'Sell description', + column: 'sell_description', + fieldType: 'text', + }, + purchase_description: { + name: 'Purchase description', + column: 'purchase_description', + fieldType: 'text', + }, + quantity_on_hand: { + name: 'item.field.quantity_on_hand', + column: 'quantity_on_hand', + fieldType: 'number', + }, + note: { + name: 'item.field.note', + column: 'note', + fieldType: 'text', + }, + category: { + name: 'item.field.category', + column: 'category_id', + + relationType: 'enumeration', + relationKey: 'category', + + relationEntityLabel: 'name', + relationEntityKey: 'id', + }, + active: { + name: 'item.field.active', + column: 'active', + fieldType: 'boolean', + filterable: false, + }, + created_at: { + name: 'item.field.created_at', + column: 'created_at', + columnType: 'date', + fieldType: 'date', + }, + }, + columns: { + type: { + name: 'item.field.type', + type: 'text', + exportable: true, + accessor: 'typeFormatted', + }, + name: { + name: 'item.field.name', + type: 'text', + exportable: true, + }, + code: { + name: 'item.field.code', + type: 'text', + exportable: true, + }, + sellable: { + name: 'item.field.sellable', + type: 'boolean', + exportable: true, + printable: false, + }, + purchasable: { + name: 'item.field.purchasable', + type: 'boolean', + exportable: true, + printable: false, + }, + sellPrice: { + name: 'item.field.sell_price', + type: 'number', + exportable: true, + }, + costPrice: { + name: 'item.field.cost_price', + type: 'number', + exportable: true, + }, + costAccount: { + name: 'item.field.cost_account', + type: 'text', + accessor: 'costAccount.name', + exportable: true, + printable: false, + }, + sellAccount: { + name: 'item.field.sell_account', + type: 'text', + accessor: 'sellAccount.name', + exportable: true, + printable: false, + }, + inventoryAccount: { + name: 'item.field.inventory_account', + type: 'text', + accessor: 'inventoryAccount.name', + exportable: true, + }, + sellDescription: { + name: 'Sell description', + type: 'text', + exportable: true, + printable: false, + }, + purchaseDescription: { + name: 'Purchase description', + type: 'text', + exportable: true, + printable: false, + }, + quantityOnHand: { + name: 'item.field.quantity_on_hand', + type: 'number', + exportable: true, + }, + note: { + name: 'item.field.note', + type: 'text', + exportable: true, + }, + category: { + name: 'item.field.category', + type: 'text', + accessor: 'category.name', + exportable: true, + }, + active: { + name: 'item.field.active', + fieldType: 'boolean', + exportable: true, + printable: false, + }, + createdAt: { + name: 'item.field.created_at', + type: 'date', + exportable: true, + printable: false, + }, + }, + fields2: { + type: { + name: 'item.field.type', + fieldType: 'enumeration', + options: [ + { key: 'inventory', label: 'item.field.type.inventory' }, + { key: 'service', label: 'item.field.type.service' }, + { key: 'non-inventory', label: 'item.field.type.non-inventory' }, + ], + required: true, + }, + name: { + name: 'item.field.name', + fieldType: 'text', + required: true, + }, + code: { + name: 'item.field.code', + fieldType: 'text', + }, + sellable: { + name: 'item.field.sellable', + fieldType: 'boolean', + }, + purchasable: { + name: 'item.field.purchasable', + fieldType: 'boolean', + }, + sellPrice: { + name: 'item.field.sell_price', + fieldType: 'number', + }, + costPrice: { + name: 'item.field.cost_price', + fieldType: 'number', + }, + costAccountId: { + name: 'item.field.cost_account', + fieldType: 'relation', + relationModel: 'Account', + relationImportMatch: ['name', 'code'], + importHint: 'Matches the account name or code.', + }, + sellAccountId: { + name: 'item.field.sell_account', + fieldType: 'relation', + relationModel: 'Account', + relationImportMatch: ['name', 'code'], + importHint: 'Matches the account name or code.', + }, + inventoryAccountId: { + name: 'item.field.inventory_account', + fieldType: 'relation', + relationModel: 'Account', + relationImportMatch: ['name', 'code'], + importHint: 'Matches the account name or code.', + }, + sellDescription: { + name: 'Sell Description', + fieldType: 'text', + }, + purchaseDescription: { + name: 'Purchase Description', + fieldType: 'text', + }, + note: { + name: 'item.field.note', + fieldType: 'text', + }, + categoryId: { + name: 'item.field.category', + fieldType: 'relation', + relationModel: 'ItemCategory', + relationImportMatch: ['name'], + importHint: 'Matches the category name.', + }, + active: { + name: 'item.field.active', + fieldType: 'boolean', + }, + }, +}; \ No newline at end of file diff --git a/packages/server/src/modules/Items/models/Item.ts b/packages/server/src/modules/Items/models/Item.ts index 3b7f33cd4..39227a0e0 100644 --- a/packages/server/src/modules/Items/models/Item.ts +++ b/packages/server/src/modules/Items/models/Item.ts @@ -2,8 +2,11 @@ import { Warehouse } from '@/modules/Warehouses/models/Warehouse.model'; import { TenantBaseModel } from '@/modules/System/models/TenantBaseModel'; import { Model } from 'objection'; import { ExportableModel } from '@/modules/Export/decorators/ExportableModel.decorator'; +import { InjectModelMeta } from '@/modules/Tenancy/TenancyModels/decorators/InjectModelMeta.decorator'; +import { ItemMeta } from './Item.meta'; @ExportableModel() +@InjectModelMeta(ItemMeta) export class Item extends TenantBaseModel { public readonly quantityOnHand: number; public readonly name: string; diff --git a/packages/server/src/modules/ManualJournals/ManualJournals.module.ts b/packages/server/src/modules/ManualJournals/ManualJournals.module.ts index 1ca42131c..2b2a8c773 100644 --- a/packages/server/src/modules/ManualJournals/ManualJournals.module.ts +++ b/packages/server/src/modules/ManualJournals/ManualJournals.module.ts @@ -17,9 +17,11 @@ import { ManualJournalGLEntries } from './commands/ManualJournalGLEntries'; import { LedgerModule } from '../Ledger/Ledger.module'; import { ManualJournalsExportable } from './commands/ManualJournalExportable'; import { ManualJournalImportable } from './commands/ManualJournalsImport'; +import { GetManualJournals } from './queries/GetManualJournals.service'; +import { DynamicListModule } from '../DynamicListing/DynamicList.module'; @Module({ - imports: [BranchesModule, LedgerModule], + imports: [BranchesModule, LedgerModule, DynamicListModule], controllers: [ManualJournalsController], providers: [ TenancyContext, @@ -34,6 +36,7 @@ import { ManualJournalImportable } from './commands/ManualJournalsImport'; AutoIncrementOrdersService, ManualJournalsApplication, GetManualJournal, + GetManualJournals, ManualJournalGLEntries, ManualJournalWriteGLSubscriber, ManualJournalsExportable, diff --git a/packages/server/src/modules/ManualJournals/models/ManualJournal.meta.ts b/packages/server/src/modules/ManualJournals/models/ManualJournal.meta.ts new file mode 100644 index 000000000..352dccd26 --- /dev/null +++ b/packages/server/src/modules/ManualJournals/models/ManualJournal.meta.ts @@ -0,0 +1,230 @@ +export const ManualJournalMeta = { + defaultFilterField: 'date', + defaultSort: { + sortOrder: 'DESC', + sortField: 'name', + }, + importable: true, + exportFlattenOn: 'entries', + + exportable: true, + importAggregator: 'group', + importAggregateOn: 'entries', + importAggregateBy: 'journalNumber', + + print: { + pageTitle: 'Manual Journals', + }, + + fields: { + date: { + name: 'manual_journal.field.date', + column: 'date', + fieldType: 'date', + }, + journal_number: { + name: 'manual_journal.field.journal_number', + column: 'journal_number', + fieldType: 'text', + }, + reference: { + name: 'manual_journal.field.reference', + column: 'reference', + fieldType: 'text', + }, + journal_type: { + name: 'manual_journal.field.journal_type', + column: 'journal_type', + fieldType: 'text', + }, + amount: { + name: 'manual_journal.field.amount', + column: 'amount', + fieldType: 'number', + }, + description: { + name: 'manual_journal.field.description', + column: 'description', + fieldType: 'text', + }, + status: { + name: 'manual_journal.field.status', + column: 'status', + fieldType: 'enumeration', + options: [ + { key: 'draft', label: 'Draft' }, + { key: 'published', label: 'published' }, + ], + filterCustomQuery: StatusFieldFilterQuery, + sortCustomQuery: StatusFieldSortQuery, + }, + created_at: { + name: 'manual_journal.field.created_at', + column: 'created_at', + fieldType: 'date', + }, + }, + columns: { + date: { + name: 'manual_journal.field.date', + type: 'date', + accessor: 'formattedDate', + }, + journalNumber: { + name: 'manual_journal.field.journal_number', + type: 'text', + }, + reference: { + name: 'manual_journal.field.reference', + type: 'text', + }, + journalType: { + name: 'manual_journal.field.journal_type', + type: 'text', + }, + amount: { + name: 'Amount', + accessor: 'formattedAmount', + }, + currencyCode: { + name: 'manual_journal.field.currency', + type: 'text', + printable: false, + }, + exchangeRate: { + name: 'manual_journal.field.exchange_rate', + type: 'number', + printable: false, + }, + description: { + name: 'manual_journal.field.description', + type: 'text', + }, + entries: { + name: 'Entries', + type: 'collection', + collectionOf: 'object', + columns: { + credit: { + name: 'Credit', + type: 'text', + }, + debit: { + name: 'Debit', + type: 'text', + }, + account: { + name: 'Account', + accessor: 'account.name', + }, + contact: { + name: 'Contact', + accessor: 'contact.displayName', + }, + note: { + name: 'Note', + }, + }, + publish: { + name: 'Publish', + type: 'boolean', + printable: false, + }, + publishedAt: { + name: 'Published At', + printable: false, + }, + }, + createdAt: { + name: 'Created At', + accessor: 'formattedCreatedAt', + printable: false, + }, + }, + fields2: { + date: { + name: 'manual_journal.field.date', + fieldType: 'date', + required: true, + }, + journalNumber: { + name: 'manual_journal.field.journal_number', + fieldType: 'text', + required: true, + }, + reference: { + name: 'manual_journal.field.reference', + fieldType: 'text', + }, + journalType: { + name: 'manual_journal.field.journal_type', + fieldType: 'text', + }, + currencyCode: { + name: 'manual_journal.field.currency', + fieldType: 'text', + }, + exchange_rate: { + name: 'manual_journal.field.exchange_rate', + fieldType: 'number', + }, + description: { + name: 'manual_journal.field.description', + fieldType: 'text', + }, + entries: { + name: 'Entries', + fieldType: 'collection', + collectionOf: 'object', + collectionMinLength: 2, + required: true, + fields: { + credit: { + name: 'Credit', + fieldType: 'number', + required: true, + }, + debit: { + name: 'Debit', + fieldType: 'number', + required: true, + }, + accountId: { + name: 'Account', + fieldType: 'relation', + relationModel: 'Account', + relationImportMatch: ['name', 'code'], + required: true, + }, + contact: { + name: 'Contact', + fieldType: 'relation', + relationModel: 'Contact', + relationImportMatch: 'displayName', + }, + note: { + name: 'Note', + fieldType: 'text', + }, + }, + }, + publish: { + name: 'Publish', + fieldType: 'boolean', + }, + }, +}; + +/** + * Status field sorting custom query. + */ +function StatusFieldSortQuery(query, role) { + return query.modify('sortByStatus', role.order); +} + +/** + * Status field filter custom query. + */ +function StatusFieldFilterQuery(query, role) { + query.modify('filterByStatus', role.value); +} \ No newline at end of file diff --git a/packages/server/src/modules/ManualJournals/models/ManualJournal.ts b/packages/server/src/modules/ManualJournals/models/ManualJournal.ts index e9e1b1077..a8c0e78ef 100644 --- a/packages/server/src/modules/ManualJournals/models/ManualJournal.ts +++ b/packages/server/src/modules/ManualJournals/models/ManualJournal.ts @@ -10,8 +10,11 @@ import { ManualJournalEntry } from './ManualJournalEntry'; import { Document } from '@/modules/ChromiumlyTenancy/models/Document'; import { TenantBaseModel } from '@/modules/System/models/TenantBaseModel'; import { ExportableModel } from '@/modules/Export/decorators/ExportableModel.decorator'; +import { InjectModelMeta } from '@/modules/Tenancy/TenancyModels/decorators/InjectModelMeta.decorator'; +import { ManualJournalMeta } from './ManualJournal.meta'; @ExportableModel() +@InjectModelMeta(ManualJournalMeta) export class ManualJournal extends TenantBaseModel { date: Date; journalNumber: string; diff --git a/packages/server/src/modules/Resource/Resource.module.ts b/packages/server/src/modules/Resource/Resource.module.ts index e4e3ae54f..297d27414 100644 --- a/packages/server/src/modules/Resource/Resource.module.ts +++ b/packages/server/src/modules/Resource/Resource.module.ts @@ -1,7 +1,12 @@ import { Module } from '@nestjs/common'; import { ResourceService } from './ResourceService'; +import { BranchesModule } from '../Branches/Branches.module'; +import { WarehousesModule } from '../Warehouses/Warehouses.module'; +import { AccountsExportable } from '../Accounts/AccountsExportable.service'; +import { AccountsModule } from '../Accounts/Accounts.module'; @Module({ + imports: [BranchesModule, WarehousesModule, AccountsModule], providers: [ResourceService], exports: [ResourceService], }) diff --git a/packages/server/src/modules/Resource/ResourceService.ts b/packages/server/src/modules/Resource/ResourceService.ts index a59fbb522..f45474648 100644 --- a/packages/server/src/modules/Resource/ResourceService.ts +++ b/packages/server/src/modules/Resource/ResourceService.ts @@ -28,7 +28,7 @@ export class ResourceService { */ public getResourceModel(inputModelName: string) { const modelName = resourceToModelName(inputModelName); - const resourceModel = this.moduleRef.get(modelName); + const resourceModel = this.moduleRef.get(modelName, { strict: false }); if (!resourceModel) { throw new ServiceError(ERRORS.RESOURCE_MODEL_NOT_FOUND); @@ -46,7 +46,7 @@ export class ResourceService { const resourceModel = this.getResourceModel(modelName); // Retrieve the resource meta. - const resourceMeta = resourceModel.getMeta(metakey); + const resourceMeta = resourceModel().getMeta(metakey); // Localization the fields names. return resourceMeta; diff --git a/packages/server/src/modules/Resource/_utils.ts b/packages/server/src/modules/Resource/_utils.ts index bd88a6a1c..b934433ea 100644 --- a/packages/server/src/modules/Resource/_utils.ts +++ b/packages/server/src/modules/Resource/_utils.ts @@ -1,5 +1,5 @@ import { camelCase, upperFirst } from 'lodash'; -import pluralize from 'pluralize'; +import * as pluralize from 'pluralize'; export const resourceToModelName = (resourceName: string): string => { return upperFirst(camelCase(pluralize.singular(resourceName))); diff --git a/packages/server/src/modules/SaleInvoices/models/SaleInvoice.meta.ts b/packages/server/src/modules/SaleInvoices/models/SaleInvoice.meta.ts new file mode 100644 index 000000000..8d48e5e21 --- /dev/null +++ b/packages/server/src/modules/SaleInvoices/models/SaleInvoice.meta.ts @@ -0,0 +1,316 @@ +import { Features } from "@/common/types/Features"; + +export const SaleInvoiceMeta = { + defaultFilterField: 'customer', + defaultSort: { + sortOrder: 'DESC', + sortField: 'created_at', + }, + exportable: true, + exportFlattenOn: 'entries', + + importable: true, + importAggregator: 'group', + importAggregateOn: 'entries', + importAggregateBy: 'invoiceNo', + + print: { + pageTitle: 'Sale invoices', + }, + fields: { + customer: { + name: 'invoice.field.customer', + column: 'customer_id', + fieldType: 'relation', + + relationType: 'enumeration', + relationKey: 'customer', + + relationEntityLabel: 'display_name', + relationEntityKey: 'id', + }, + invoice_date: { + name: 'invoice.field.invoice_date', + column: 'invoice_date', + fieldType: 'date', + }, + due_date: { + name: 'invoice.field.due_date', + column: 'due_date', + fieldType: 'date', + }, + invoice_no: { + name: 'invoice.field.invoice_no', + column: 'invoice_no', + fieldType: 'text', + }, + reference_no: { + name: 'invoice.field.reference_no', + column: 'reference_no', + fieldType: 'text', + }, + invoice_message: { + name: 'invoice.field.invoice_message', + column: 'invoice_message', + fieldType: 'text', + }, + terms_conditions: { + name: 'invoice.field.terms_conditions', + column: 'terms_conditions', + fieldType: 'text', + }, + amount: { + name: 'invoice.field.amount', + column: 'balance', + fieldType: 'number', + }, + payment_amount: { + name: 'invoice.field.payment_amount', + column: 'payment_amount', + fieldType: 'number', + }, + due_amount: { + // calculated. + name: 'invoice.field.due_amount', + column: 'due_amount', + fieldType: 'number', + virtualColumn: true, + }, + status: { + name: 'invoice.field.status', + fieldType: 'enumeration', + options: [ + { key: 'draft', label: 'invoice.field.status.draft' }, + { key: 'delivered', label: 'invoice.field.status.delivered' }, + { key: 'unpaid', label: 'invoice.field.status.unpaid' }, + { key: 'overdue', label: 'invoice.field.status.overdue' }, + { key: 'partially-paid', label: 'invoice.field.status.partially-paid' }, + { key: 'paid', label: 'invoice.field.status.paid' }, + ], + filterCustomQuery: StatusFieldFilterQuery, + sortCustomQuery: StatusFieldSortQuery, + }, + created_at: { + name: 'invoice.field.created_at', + column: 'created_at', + fieldType: 'date', + }, + }, + columns: { + invoiceDate: { + name: 'invoice.field.invoice_date', + type: 'date', + accessor: 'invoiceDateFormatted', + }, + dueDate: { + name: 'invoice.field.due_date', + type: 'date', + accessor: 'dueDateFormatted', + }, + referenceNo: { + name: 'invoice.field.reference_no', + type: 'text', + }, + invoiceNo: { + name: 'invoice.field.invoice_no', + type: 'text', + }, + customer: { + name: 'invoice.field.customer', + type: 'text', + accessor: 'customer.displayName', + }, + amount: { + name: 'invoice.field.amount', + type: 'text', + accessor: 'balanceAmountFormatted', + }, + exchangeRate: { + name: 'invoice.field.exchange_rate', + type: 'number', + printable: false, + }, + currencyCode: { + name: 'invoice.field.currency', + type: 'text', + printable: false, + }, + paidAmount: { + name: 'Paid Amount', + accessor: 'paymentAmountFormatted', + }, + dueAmount: { + name: 'Due Amount', + accessor: 'dueAmountFormatted', + }, + invoiceMessage: { + name: 'invoice.field.invoice_message', + type: 'text', + printable: false, + }, + termsConditions: { + name: 'invoice.field.terms_conditions', + type: 'text', + printable: false, + }, + delivered: { + name: 'invoice.field.delivered', + type: 'boolean', + printable: false, + accessor: 'isDelivered', + }, + entries: { + name: 'Entries', + accessor: 'entries', + type: 'collection', + collectionOf: 'object', + columns: { + itemName: { + name: 'Item Name', + accessor: 'item.name', + }, + rate: { + name: 'Item Rate', + accessor: 'rateFormatted', + }, + quantity: { + name: 'Item Quantity', + accessor: 'quantityFormatted', + }, + description: { + name: 'Item Description', + printable: false, + }, + amount: { + name: 'Item Amount', + accessor: 'totalFormatted', + }, + }, + }, + branch: { + name: 'Branch', + type: 'text', + accessor: 'branch.name', + features: [Features.BRANCHES], + }, + warehouse: { + name: 'Warehouse', + type: 'text', + accessor: 'warehouse.name', + features: [Features.BRANCHES], + }, + }, + fields2: { + invoiceDate: { + name: 'invoice.field.invoice_date', + fieldType: 'date', + required: true, + }, + dueDate: { + name: 'invoice.field.due_date', + fieldType: 'date', + required: true, + }, + referenceNo: { + name: 'invoice.field.reference_no', + fieldType: 'text', + }, + invoiceNo: { + name: 'invoice.field.invoice_no', + fieldType: 'text', + }, + customerId: { + name: 'invoice.field.customer', + fieldType: 'relation', + relationModel: 'Contact', + relationImportMatch: 'displayName', + required: true, + }, + exchangeRate: { + name: 'invoice.field.exchange_rate', + fieldType: 'number', + printable: false, + }, + currencyCode: { + name: 'invoice.field.currency', + fieldType: 'text', + printable: false, + }, + invoiceMessage: { + name: 'invoice.field.invoice_message', + fieldType: 'text', + printable: false, + }, + termsConditions: { + name: 'invoice.field.terms_conditions', + fieldType: 'text', + printable: false, + }, + entries: { + name: 'invoice.field.entries', + fieldType: 'collection', + collectionOf: 'object', + collectionMinLength: 1, + required: true, + fields: { + itemId: { + name: 'invoice.field.item_name', + fieldType: 'relation', + relationModel: 'Item', + relationImportMatch: ['name', 'code'], + required: true, + importHint: 'Matches the item name or code.', + }, + rate: { + name: 'invoice.field.rate', + fieldType: 'number', + required: true, + }, + quantity: { + name: 'invoice.field.quantity', + fieldType: 'number', + required: true, + }, + description: { + name: 'invoice.field.description', + fieldType: 'text', + }, + }, + }, + delivered: { + name: 'invoice.field.delivered', + fieldType: 'boolean', + 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, + }, + }, +}; + +/** + * Status field filter custom query. + */ +function StatusFieldFilterQuery(query, role) { + query.modify('statusFilter', role.value); +} + +/** + * Status field sort custom query. + */ +function StatusFieldSortQuery(query, role) { + query.modify('sortByStatus', role.order); +} \ No newline at end of file diff --git a/packages/server/src/modules/SaleInvoices/models/SaleInvoice.ts b/packages/server/src/modules/SaleInvoices/models/SaleInvoice.ts index c7d490eb8..ad3bbdd63 100644 --- a/packages/server/src/modules/SaleInvoices/models/SaleInvoice.ts +++ b/packages/server/src/modules/SaleInvoices/models/SaleInvoice.ts @@ -11,13 +11,15 @@ import { DiscountType } from '@/common/types/Discount'; import { Account } from '@/modules/Accounts/models/Account.model'; import { ISearchRole } from '@/modules/DynamicListing/DynamicFilter/DynamicFilter.types'; import { TenantBaseModel } from '@/modules/System/models/TenantBaseModel'; -import { PaymentIntegrationTransactionLink } from '../SaleInvoice.types'; import { TransactionPaymentServiceEntry } from '@/modules/PaymentServices/models/TransactionPaymentServiceEntry.model'; import { InjectAttachable } from '@/modules/Attachments/decorators/InjectAttachable.decorator'; import { ExportableModel } from '@/modules/Export/decorators/ExportableModel.decorator'; +import { InjectModelMeta } from '@/modules/Tenancy/TenancyModels/decorators/InjectModelMeta.decorator'; +import { SaleInvoiceMeta } from './SaleInvoice.meta'; @InjectAttachable() @ExportableModel() +@InjectModelMeta(SaleInvoiceMeta) export class SaleInvoice extends TenantBaseModel{ public taxAmountWithheld: number; public balance: number; diff --git a/packages/server/src/modules/Tenancy/TenancyModels/Tenancy.module.ts b/packages/server/src/modules/Tenancy/TenancyModels/Tenancy.module.ts index 66f3e1b40..66fa82478 100644 --- a/packages/server/src/modules/Tenancy/TenancyModels/Tenancy.module.ts +++ b/packages/server/src/modules/Tenancy/TenancyModels/Tenancy.module.ts @@ -82,6 +82,7 @@ const models = [ TenantUser, ]; + /** * Decorator factory that registers a model with the tenancy system. * @param model The model class to register diff --git a/packages/server/src/modules/Tenancy/TenancyModels/decorators/InjectModelMeta.decorator.ts b/packages/server/src/modules/Tenancy/TenancyModels/decorators/InjectModelMeta.decorator.ts new file mode 100644 index 000000000..9c0793be2 --- /dev/null +++ b/packages/server/src/modules/Tenancy/TenancyModels/decorators/InjectModelMeta.decorator.ts @@ -0,0 +1,18 @@ + +/** + * Decorator function that adds metadata to the model class. + * @param value - The metadata value to be added to the model. + * @returns A class decorator function. + */ +export function InjectModelMeta(value: any) { + return function(target: any) { + // Define a static getter for 'meta' on the target class + Object.defineProperty(target, 'meta', { + get: function() { + return value; + }, + enumerable: true, + configurable: true + }); + }; +} \ No newline at end of file diff --git a/packages/server/src/modules/Vendors/models/Vendor.meta.ts b/packages/server/src/modules/Vendors/models/Vendor.meta.ts new file mode 100644 index 000000000..90411934f --- /dev/null +++ b/packages/server/src/modules/Vendors/models/Vendor.meta.ts @@ -0,0 +1,407 @@ +export const VendorMeta = { + defaultFilterField: 'displayName', + defaultSort: { + sortOrder: 'DESC', + sortField: 'created_at', + }, + importable: true, + exportable: true, + fields: { + first_name: { + name: 'vendor.field.first_name', + column: 'first_name', + fieldType: 'text', + }, + last_name: { + name: 'vendor.field.last_name', + column: 'last_name', + fieldType: 'text', + }, + display_name: { + name: 'vendor.field.display_name', + column: 'display_name', + fieldType: 'text', + }, + email: { + name: 'vendor.field.email', + column: 'email', + fieldType: 'text', + }, + work_phone: { + name: 'vendor.field.work_phone', + column: 'work_phone', + fieldType: 'text', + }, + personal_phone: { + name: 'vendor.field.personal_phone', + column: 'personal_phone', + fieldType: 'text', + }, + company_name: { + name: 'vendor.field.company_name', + column: 'company_name', + fieldType: 'text', + }, + website: { + name: 'vendor.field.website', + column: 'website', + fieldType: 'text', + }, + created_at: { + name: 'vendor.field.created_at', + column: 'created_at', + fieldType: 'date', + }, + balance: { + name: 'vendor.field.balance', + column: 'balance', + fieldType: 'number', + }, + opening_balance: { + name: 'vendor.field.opening_balance', + column: 'opening_balance', + fieldType: 'number', + }, + opening_balance_at: { + name: 'vendor.field.opening_balance_at', + column: 'opening_balance_at', + fieldType: 'date', + }, + currency_code: { + name: 'vendor.field.currency', + column: 'currency_code', + fieldType: 'text', + }, + status: { + name: 'vendor.field.status', + type: 'enumeration', + options: [ + { key: 'overdue', label: 'vendor.field.status.overdue' }, + { key: 'unpaid', label: 'vendor.field.status.unpaid' }, + ], + filterCustomQuery: (query, role) => { + switch (role.value) { + case 'overdue': + query.modify('overdue'); + break; + case 'unpaid': + query.modify('unpaid'); + break; + } + }, + }, + }, + columns: { + firstName: { + name: 'vendor.field.first_name', + type: 'text', + }, + lastName: { + name: 'vendor.field.last_name', + type: 'text', + }, + displayName: { + name: 'vendor.field.display_name', + type: 'text', + }, + email: { + name: 'vendor.field.email', + type: 'text', + }, + workPhone: { + name: 'vendor.field.work_phone', + type: 'text', + }, + personalPhone: { + name: 'vendor.field.personal_phone', + type: 'text', + }, + companyName: { + name: 'vendor.field.company_name', + type: 'text', + }, + website: { + name: 'vendor.field.website', + type: 'text', + }, + balance: { + name: 'vendor.field.balance', + type: 'number', + }, + openingBalance: { + name: 'vendor.field.opening_balance', + type: 'number', + printable: false + }, + openingBalanceAt: { + name: 'vendor.field.opening_balance_at', + type: 'date', + printable: false + }, + currencyCode: { + name: 'vendor.field.currency', + type: 'text', + printable: false + }, + status: { + name: 'vendor.field.status', + printable: false + }, + note: { + name: 'vendor.field.note', + type: 'text', + printable: false + }, + // Billing Address + billingAddress1: { + name: 'Billing Address 1', + column: 'billing_address1', + type: 'text', + exportable: true, + printable: false + }, + billingAddress2: { + name: 'Billing Address 2', + column: 'billing_address2', + type: 'text', + exportable: true, + printable: false + }, + billingAddressCity: { + name: 'Billing Address City', + column: 'billing_address_city', + type: 'text', + exportable: true, + printable: false + }, + billingAddressCountry: { + name: 'Billing Address Country', + column: 'billing_address_country', + type: 'text', + exportable: true, + printable: false + }, + billingAddressPostcode: { + name: 'Billing Address Postcode', + column: 'billing_address_postcode', + type: 'text', + exportable: true, + printable: false + }, + billingAddressState: { + name: 'Billing Address State', + column: 'billing_address_state', + type: 'text', + exportable: true, + printable: false + }, + billingAddressPhone: { + name: 'Billing Address Phone', + column: 'billing_address_phone', + type: 'text', + exportable: true, + printable: false + }, + // Shipping Address + shippingAddress1: { + name: 'Shipping Address 1', + column: 'shipping_address1', + type: 'text', + exportable: true, + printable: false + }, + shippingAddress2: { + name: 'Shipping Address 2', + column: 'shipping_address2', + type: 'text', + exportable: true, + printable: false + }, + shippingAddressCity: { + name: 'Shipping Address City', + column: 'shipping_address_city', + type: 'text', + exportable: true, + printable: false + }, + shippingAddressCountry: { + name: 'Shipping Address Country', + column: 'shipping_address_country', + type: 'text', + exportable: true, + printable: false + }, + shippingAddressPostcode: { + name: 'Shipping Address Postcode', + column: 'shipping_address_postcode', + type: 'text', + exportable: true, + printable: false + }, + shippingAddressState: { + name: 'Shipping Address State', + column: 'shipping_address_state', + type: 'text', + exportable: true, + printable: false + }, + shippingAddressPhone: { + name: 'Shipping Address Phone', + column: 'shipping_address_phone', + type: 'text', + exportable: true, + printable: false + }, + createdAt: { + name: 'vendor.field.created_at', + type: 'date', + exportable: true, + printable: false + }, + }, + fields2: { + firstName: { + name: 'vendor.field.first_name', + column: 'first_name', + fieldType: 'text', + }, + lastName: { + name: 'vendor.field.last_name', + column: 'last_name', + fieldType: 'text', + }, + displayName: { + name: 'vendor.field.display_name', + column: 'display_name', + fieldType: 'text', + required: true, + }, + email: { + name: 'vendor.field.email', + column: 'email', + fieldType: 'text', + }, + workPhone: { + name: 'vendor.field.work_phone', + column: 'work_phone', + fieldType: 'text', + }, + personalPhone: { + name: 'vendor.field.personal_phone', + column: 'personal_phone', + fieldType: 'text', + }, + companyName: { + name: 'vendor.field.company_name', + column: 'company_name', + fieldType: 'text', + }, + website: { + name: 'vendor.field.website', + column: 'website', + fieldType: 'text', + }, + openingBalance: { + name: 'vendor.field.opening_balance', + column: 'opening_balance', + fieldType: 'number', + }, + openingBalanceAt: { + name: 'vendor.field.opening_balance_at', + column: 'opening_balance_at', + fieldType: 'date', + }, + openingBalanceExchangeRate: { + name: 'Opening Balance Ex. Rate', + column: 'opening_balance_exchange_rate', + fieldType: 'number', + }, + currencyCode: { + name: 'vendor.field.currency', + column: 'currency_code', + fieldType: 'text', + }, + note: { + name: 'Note', + column: 'note', + fieldType: 'text', + }, + active: { + name: 'Active', + column: 'active', + fieldType: 'boolean', + }, + // Billing Address + billingAddress1: { + name: 'Billing Address 1', + column: 'billing_address1', + fieldType: 'text', + }, + billingAddress2: { + name: 'Billing Address 2', + column: 'billing_address2', + fieldType: 'text', + }, + billingAddressCity: { + name: 'Billing Address City', + column: 'billing_address_city', + fieldType: 'text', + }, + billingAddressCountry: { + name: 'Billing Address Country', + column: 'billing_address_country', + fieldType: 'text', + }, + billingAddressPostcode: { + name: 'Billing Address Postcode', + column: 'billing_address_postcode', + fieldType: 'text', + }, + billingAddressState: { + name: 'Billing Address State', + column: 'billing_address_state', + fieldType: 'text', + }, + billingAddressPhone: { + name: 'Billing Address Phone', + column: 'billing_address_phone', + fieldType: 'text', + }, + // Shipping Address + shippingAddress1: { + name: 'Shipping Address 1', + column: 'shipping_address1', + fieldType: 'text', + }, + shippingAddress2: { + name: 'Shipping Address 2', + column: 'shipping_address2', + fieldType: 'text', + }, + shippingAddressCity: { + name: 'Shipping Address City', + column: 'shipping_address_city', + fieldType: 'text', + }, + shippingAddressCountry: { + name: 'Shipping Address Country', + column: 'shipping_address_country', + fieldType: 'text', + }, + shippingAddressPostcode: { + name: 'Shipping Address Postcode', + column: 'shipping_address_postcode', + fieldType: 'text', + }, + shippingAddressState: { + name: 'Shipping Address State', + column: 'shipping_address_state', + fieldType: 'text', + }, + shippingAddressPhone: { + name: 'Shipping Address Phone', + column: 'shipping_address_phone', + fieldType: 'text', + }, + }, +}; \ No newline at end of file diff --git a/packages/server/src/modules/Vendors/models/Vendor.ts b/packages/server/src/modules/Vendors/models/Vendor.ts index 194912df4..cc26bac3b 100644 --- a/packages/server/src/modules/Vendors/models/Vendor.ts +++ b/packages/server/src/modules/Vendors/models/Vendor.ts @@ -9,6 +9,8 @@ import { Model, mixin } from 'objection'; import { BaseModel } from '@/models/Model'; import { TenantBaseModel } from '@/modules/System/models/TenantBaseModel'; import { ExportableModel } from '@/modules/Export/decorators/ExportableModel.decorator'; +import { InjectModelMeta } from '@/modules/Tenancy/TenancyModels/decorators/InjectModelMeta.decorator'; +import { VendorMeta } from './Vendor.meta'; // class VendorQueryBuilder extends PaginationQueryBuilder { // constructor(...args) { @@ -23,6 +25,7 @@ import { ExportableModel } from '@/modules/Export/decorators/ExportableModel.dec // } @ExportableModel() +@InjectModelMeta(VendorMeta) export class Vendor extends TenantBaseModel { contactService: string; contactType: string; diff --git a/packages/server/src/modules/Warehouses/Warehouses.module.ts b/packages/server/src/modules/Warehouses/Warehouses.module.ts index b141e5a59..aa899f2dc 100644 --- a/packages/server/src/modules/Warehouses/Warehouses.module.ts +++ b/packages/server/src/modules/Warehouses/Warehouses.module.ts @@ -90,6 +90,6 @@ const models = [RegisterTenancyModel(Warehouse)]; InventoryTransactionsWarehouses, ValidateWarehouseExistance ], - exports: [WarehouseTransactionDTOTransform, ...models], + exports: [WarehousesSettings, WarehouseTransactionDTOTransform, ...models], }) export class WarehousesModule {}