1
0

Merge pull request #396 from bigcapitalhq/aggregate-rows-import

feat: Aggregate rows import
This commit is contained in:
Ahmed Bouhuolia
2024-04-07 23:55:06 +02:00
committed by GitHub
97 changed files with 3389 additions and 759 deletions
+39 -12
View File
@@ -244,6 +244,7 @@
"account.field.active": "Active",
"account.field.currency": "Currency",
"account.field.balance": "Balance",
"account.field.parent_account": "Parent Account",
"account.field.created_at": "Created at",
"item.field.type": "Item Type",
"item.field.type.inventory": "Inventory",
@@ -277,8 +278,14 @@
"invoice.field.invoice_message": "Invoice message",
"invoice.field.terms_conditions": "Terms & conditions",
"invoice.field.amount": "Amount",
"invoice.field.exchange_rate": "Exchange Rate",
"invoice.field.payment_amount": "Payment amount",
"invoice.field.due_amount": "Due amount",
"invoice.field.delivered": "Delivered",
"invoice.field.item_name": "Item Name",
"invoice.field.rate": "Rate",
"invoice.field.quantity": "Quantity",
"invoice.field.description": "Description",
"invoice.field.status": "Status",
"invoice.field.status.paid": "Paid",
"invoice.field.status.partially-paid": "Partially paid",
@@ -287,6 +294,7 @@
"invoice.field.status.delivered": "Delivered",
"invoice.field.status.draft": "Draft",
"invoice.field.created_at": "Created at",
"invoice.field.currency": "Currency",
"estimate.field.amount": "Amount",
"estimate.field.estimate_number": "Estimate number",
"estimate.field.customer": "Customer",
@@ -301,22 +309,31 @@
"estimate.field.status.approved": "Approved",
"estimate.field.status.draft": "Draft",
"estimate.field.created_at": "Created at",
"payment_receive.field.customer": "Customer",
"payment_receive.field.payment_date": "Payment date",
"payment_receive.field.amount": "Amount",
"payment_receive.field.reference_no": "Reference No.",
"payment_receive.field.deposit_account": "Deposit account",
"payment_receive.field.payment_receive_no": "Payment receive No.",
"payment_receive.field.statement": "Statement",
"payment_receive.field.created_at": "Created at",
"payment_receive.field.customer": "Customer",
"payment_receive.field.exchange_rate": "Exchange Rate",
"payment_receive.field.payment_date": "Payment Date",
"payment_receive.field.reference_no": "Reference No.",
"payment_receive.field.deposit_account": "Deposit Account",
"payment_receive.field.entries": "Entries",
"payment_receive.field.invoice": "Invoice",
"payment_receive.field.entries.payment_amount": "Payment Amount",
"bill_payment.field.vendor": "Vendor",
"bill_payment.field.amount": "Amount",
"bill_payment.field.due_amount": "Due amount",
"bill_payment.field.payment_account": "Payment account",
"bill_payment.field.payment_number": "Payment number",
"bill_payment.field.payment_date": "Payment date",
"bill_payment.field.due_amount": "Due Amount",
"bill_payment.field.payment_account": "Payment Account",
"bill_payment.field.payment_number": "Payment No.",
"bill_payment.field.payment_date": "Payment Date",
"bill_payment.field.reference_no": "Reference No.",
"bill_payment.field.description": "Description",
"bill_payment.field.exchange_rate": "Exchange Rate",
"bill_payment.field.statement": "Statement",
"bill_payment.field.entries.bill": "Bill No.",
"bill_payment.field.entries.payment_amount": "Payment Amount",
"bill_payment.field.reference": "Reference No.",
"bill_payment.field.created_at": "Created at",
"bill.field.vendor": "Vendor",
"bill.field.bill_number": "Bill number",
@@ -344,22 +361,30 @@
"inventory_adjustment.field.description": "Description",
"inventory_adjustment.field.published_at": "Published at",
"inventory_adjustment.field.created_at": "Created at",
"expense.field.payment_date": "Payment date",
"expense.field.payment_account": "Payment account",
"expense.field.payment_date": "Payment Date",
"expense.field.payment_account": "Payment Account",
"expense.field.amount": "Amount",
"expense.field.currency_code": "Currency",
"expense.field.exchange_rate": "Exchange Rate",
"expense.field.reference_no": "Reference No.",
"expense.field.description": "Description",
"expense.field.line_description": "Line Description",
"expense.field.published": "Published",
"expense.field.categories": "Categories",
"expense.field.expense_account": "Expense Account",
"expense.field.publish": "Publish",
"expense.field.status": "Status",
"expense.field.status.draft": "Draft",
"expense.field.status.published": "Published",
"expense.field.created_at": "Created at",
"manual_journal.field.date": "Date",
"manual_journal.field.journal_number": "Journal number",
"manual_journal.field.journal_number": "Journal No.",
"manual_journal.field.reference": "Reference No.",
"manual_journal.field.journal_type": "Journal type",
"manual_journal.field.journal_type": "Journal Type",
"manual_journal.field.amount": "Amount",
"manual_journal.field.description": "Description",
"manual_journal.field.currency": "Currency",
"manual_journal.field.exchange_rate": "Exchange Rate",
"manual_journal.field.status": "Status",
"manual_journal.field.created_at": "Created at",
"receipt.field.amount": "Amount",
@@ -412,6 +437,8 @@
"vendor.field.status.unpaid": "Unpaid",
"Invoice write-off": "Invoice write-off",
"transaction_type.credit_note": "Credit note",
"transaction_type.refund_credit_note": "Refund credit note",
"transaction_type.vendor_credit": "Vendor credit",
@@ -349,7 +349,7 @@ export default class AccountsController extends BaseController {
// Filter query.
const filter = {
sortOrder: 'desc',
columnSortBy: 'createdAt',
columnSortBy: 'created_at',
inactiveMode: false,
structure: IAccountsStructureType.Tree,
...this.matchedQueryData(req),
@@ -289,7 +289,7 @@ export default class CustomersController extends ContactsController {
const filter = {
inactiveMode: false,
sortOrder: 'desc',
columnSortBy: 'createdAt',
columnSortBy: 'created_at',
page: 1,
pageSize: 12,
...this.matchedQueryData(req),
@@ -272,7 +272,7 @@ export default class VendorsController extends ContactsController {
const vendorsFilter: IVendorsFilter = {
inactiveMode: false,
sortOrder: 'desc',
columnSortBy: 'createdAt',
columnSortBy: 'created_at',
page: 1,
pageSize: 12,
...this.matchedQueryData(req),
@@ -37,6 +37,7 @@ export class ImportController extends BaseController {
[
param('import_id').exists().isString(),
body('mapping').exists().isArray({ min: 1 }),
body('mapping.*.group').optional(),
body('mapping.*.from').exists(),
body('mapping.*.to').exists(),
],
@@ -344,7 +344,7 @@ export default class ItemsController extends BaseController {
const filter = {
sortOrder: 'DESC',
columnSortBy: 'createdAt',
columnSortBy: 'created_at',
page: 1,
pageSize: 12,
inactiveMode: false,
+60
View File
@@ -69,6 +69,7 @@ export type IModelMetaField = IModelMetaFieldCommon &
| IModelMetaFieldUrl
| IModelMetaEnumerationField
| IModelMetaRelationField
| IModelMetaCollectionField
);
export interface IModelMetaEnumerationOption {
@@ -92,12 +93,71 @@ export interface IModelMetaRelationEnumerationField {
relationEntityKey: string;
}
export interface IModelMetaFieldWithFields {
fields: IModelMetaFieldCommon2 &
(
| IModelMetaFieldText
| IModelMetaFieldNumber
| IModelMetaFieldBoolean
| IModelMetaFieldDate
| IModelMetaFieldUrl
| IModelMetaEnumerationField
| IModelMetaRelationField
);
}
interface IModelMetaCollectionObjectField extends IModelMetaFieldWithFields {
collectionOf: 'object';
}
export interface IModelMetaCollectionFieldCommon {
fieldType: 'collection';
collectionMinLength?: number;
collectionMaxLength?: number;
}
export type IModelMetaCollectionField = IModelMetaCollectionFieldCommon &
IModelMetaCollectionObjectField;
export type IModelMetaRelationField = IModelMetaRelationFieldCommon &
IModelMetaRelationEnumerationField;
export interface IModelMeta {
defaultFilterField: string;
defaultSort: IModelMetaDefaultSort;
importable?: boolean;
importAggregator?: string;
importAggregateOn?: string;
importAggregateBy?: string;
fields: { [key: string]: IModelMetaField };
}
// ----
export interface IModelMetaFieldCommon2 {
name: string;
required?: boolean;
importHint?: string;
order?: number;
unique?: number;
}
export interface IModelMetaRelationField2 {
fieldType: 'relation';
relationModel: string;
importableRelationLabel: string | string[];
}
export type IModelMetaField2 = IModelMetaFieldCommon2 &
(
| IModelMetaFieldText
| IModelMetaFieldNumber
| IModelMetaFieldBoolean
| IModelMetaFieldDate
| IModelMetaFieldUrl
| IModelMetaEnumerationField
| IModelMetaRelationField2
| IModelMetaCollectionField
);
+47 -38
View File
@@ -12,18 +12,11 @@ export default {
name: 'account.field.name',
column: 'name',
fieldType: 'text',
unique: true,
required: true,
importable: true,
exportable: true,
order: 1,
},
description: {
name: 'account.field.description',
column: 'description',
fieldType: 'text',
importable: true,
exportable: true,
},
slug: {
name: 'account.field.slug',
@@ -31,20 +24,13 @@ export default {
fieldType: 'text',
columnable: false,
filterable: false,
importable: false,
},
code: {
name: 'account.field.code',
column: 'code',
fieldType: 'text',
exportable: true,
importable: true,
minLength: 3,
maxLength: 6,
unique: true,
importHint: 'Unique number to identify the account.',
},
rootType: {
root_type: {
name: 'account.field.root_type',
fieldType: 'enumeration',
options: [
@@ -56,7 +42,6 @@ export default {
],
filterCustomQuery: RootTypeFieldFilterQuery,
sortable: false,
importable: false,
},
normal: {
name: 'account.field.normal',
@@ -67,9 +52,8 @@ export default {
],
filterCustomQuery: NormalTypeFieldFilterQuery,
sortable: false,
importable: false,
},
accountType: {
type: {
name: 'account.field.type',
column: 'account_type',
fieldType: 'enumeration',
@@ -77,46 +61,71 @@ export default {
label: accountType.label,
key: accountType.key,
})),
required: true,
importable: true,
exportable: true,
order: 2,
},
active: {
name: 'account.field.active',
column: 'active',
fieldType: 'boolean',
filterable: false,
exportable: true,
importable: true,
},
balance: {
name: 'account.field.balance',
column: 'amount',
fieldType: 'number',
importable: false,
},
currencyCode: {
currency: {
name: 'account.field.currency',
column: 'currency_code',
fieldType: 'text',
filterable: false,
importable: true,
exportable: true,
},
parentAccount: {
name: 'account.field.parent_account',
column: 'parent_account_id',
fieldType: 'relation',
to: { model: 'Account', to: 'id' },
importable: false,
},
createdAt: {
created_at: {
name: 'account.field.created_at',
column: 'created_at',
fieldType: 'date',
importable: false,
exportable: true,
},
},
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'],
},
},
};
+74 -1
View File
@@ -1,10 +1,13 @@
export default {
defaultFilterField: 'vendor',
defaultSort: {
sortOrder: 'DESC',
sortField: 'bill_date',
},
importable: true,
importAggregator: 'group',
importAggregateOn: 'entries',
importAggregateBy: 'billNumber',
fields: {
vendor: {
name: 'bill.field.vendor',
@@ -77,6 +80,76 @@ export default {
fieldType: 'date',
},
},
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,
},
rate: {
name: 'Rate',
fieldType: 'number',
required: true,
},
quantity: {
name: 'Quantity',
fieldType: 'number',
required: true,
},
description: {
name: 'Line Description',
fieldType: 'text',
},
},
},
},
};
/**
@@ -4,6 +4,10 @@ export default {
sortOrder: 'DESC',
sortField: 'bill_date',
},
importable: true,
importAggregator: 'group',
importAggregateOn: 'entries',
importAggregateBy: 'paymentNumber',
fields: {
vendor: {
name: 'bill_payment.field.vendor',
@@ -33,7 +37,7 @@ export default {
relationType: 'enumeration',
relationKey: 'paymentAccount',
relationEntityLabel: 'name',
relationEntityKey: 'slug',
},
@@ -63,4 +67,64 @@ export default {
fieldType: 'date',
},
},
fields2: {
vendorId: {
name: 'bill_payment.field.vendor',
fieldType: 'relation',
relationModel: 'Contact',
relationImportMatch: ['displayName'],
required: true,
},
payment_date: {
name: 'bill_payment.field.payment_date',
fieldType: 'date',
required: true,
},
paymentNumber: {
name: 'bill_payment.field.payment_number',
fieldType: 'text',
unique: true,
},
paymentAccountId: {
name: 'bill_payment.field.payment_account',
fieldType: 'relation',
relationModel: 'Account',
relationImportMatch: ['name', 'code'],
required: true,
},
exchangeRate: {
name: 'bill_payment.field.exchange_rate',
fieldType: 'number',
},
statement: {
name: 'bill_payment.field.statement',
fieldType: 'text',
},
reference: {
name: 'bill_payment.field.reference',
fieldType: 'text',
},
entries: {
name: 'bill_payment.field.entries',
column: 'entries',
fieldType: 'collection',
collectionOf: 'object',
collectionMinLength: 1,
required: true,
fields: {
billId: {
name: 'bill_payment.field.entries.bill',
fieldType: 'relation',
relationModel: 'Bill',
relationImportMatch: 'billNumber',
required: true,
},
paymentAmount: {
name: 'bill_payment.field.entries.payment_amount',
fieldType: 'number',
required: true,
},
},
},
},
};
@@ -12,6 +12,10 @@ export default {
sortOrder: 'DESC',
sortField: 'name',
},
importable: true,
importAggregator: 'group',
importAggregateOn: 'entries',
importAggregateBy: 'creditNoteNumber',
fields: {
customer: {
name: 'credit_note.field.customer',
@@ -77,4 +81,71 @@ export default {
fieldType: 'date',
},
},
fields2: {
customerId: {
name: 'Customer',
fieldType: 'relation',
relationModel: 'Contact',
relationImportMatch: 'displayName',
required: true,
},
exchangeRate: {
name: 'Exchange Rate',
fieldType: 'number',
},
creditNoteDate: {
name: 'Credit Note Date',
fieldType: 'date',
required: true,
},
referenceNo: {
name: 'Reference No.',
fieldType: 'text',
},
note: {
name: 'Note',
fieldType: 'text',
},
termsConditions: {
name: 'Terms & Conditions',
fieldType: 'text',
},
creditNoteNumber: {
name: 'Credit Note Number',
fieldType: 'text',
},
open: {
name: 'Open',
fieldType: 'boolean',
},
entries: {
name: 'Entries',
fieldType: 'collection',
collectionOf: 'object',
collectionMinLength: 1,
fields: {
itemId: {
name: 'Item',
fieldType: 'relation',
relationModel: 'Item',
relationImportMatch: ['name', 'code'],
required: true,
},
rate: {
name: 'Rate',
fieldType: 'number',
required: true,
},
quantity: {
name: 'Quantity',
fieldType: 'number',
required: true,
},
description: {
name: 'Description',
fieldType: 'text',
},
},
},
},
};
+86 -53
View File
@@ -3,213 +3,246 @@ export default {
defaultFilterField: 'displayName',
defaultSort: {
sortOrder: 'DESC',
sortField: 'createdAt',
sortField: 'created_at',
},
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;
}
},
},
},
fields2: {
customerType: {
name: 'Customer Type',
column: 'contact_type',
fieldType: 'enumeration',
options: [
{ key: 'business', label: 'Business' },
{ key: 'individual', label: 'Individual' },
],
importable: true,
required: true,
},
firstName: {
name: 'customer.field.first_name',
column: 'first_name',
fieldType: 'text',
importable: true,
},
lastName: {
name: 'customer.field.last_name',
column: 'last_name',
fieldType: 'text',
importable: true,
},
displayName: {
name: 'customer.field.display_name',
column: 'display_name',
fieldType: 'text',
required: true,
importable: true,
},
email: {
name: 'customer.field.email',
column: 'email',
fieldType: 'text',
importable: true,
},
workPhone: {
name: 'customer.field.work_phone',
column: 'work_phone',
fieldType: 'text',
importable: true,
},
personalPhone: {
name: 'customer.field.personal_phone',
column: 'personal_phone',
fieldType: 'text',
importable: true,
},
companyName: {
name: 'customer.field.company_name',
column: 'company_name',
fieldType: 'text',
importable: true,
},
website: {
name: 'customer.field.website',
column: 'website',
fieldType: 'url',
importable: true,
},
balance: {
name: 'customer.field.balance',
column: 'balance',
fieldType: 'number',
},
openingBalance: {
name: 'customer.field.opening_balance',
column: 'opening_balance',
fieldType: 'number',
importable: true,
},
openingBalanceAt: {
name: 'customer.field.opening_balance_at',
column: 'opening_balance_at',
filterable: false,
fieldType: 'date',
importable: true,
},
openingBalanceExchangeRate: {
name: 'Opening Balance Ex. Rate',
column: 'opening_balance_exchange_rate',
fieldType: 'number',
importable: true,
},
currencyCode: {
name: 'customer.field.currency',
column: 'currency_code',
fieldType: 'text',
importable: true,
},
note: {
name: 'Note',
column: 'note',
fieldType: 'text',
importable: true,
},
active: {
name: 'Active',
column: 'active',
fieldType: 'boolean',
importable: true,
},
status: {
name: 'customer.field.status',
fieldType: 'enumeration',
options: [
{ key: 'active', label: 'customer.field.status.active' },
{ key: 'inactive', label: 'customer.field.status.inactive' },
{ key: 'overdue', label: 'customer.field.status.overdue' },
{ key: 'unpaid', label: 'customer.field.status.unpaid' },
],
filterCustomQuery: statusFieldFilterQuery,
},
// Billing Address
billingAddress1: {
name: 'Billing Address 1',
column: 'billing_address1',
fieldType: 'text',
importable: true,
},
billingAddress2: {
name: 'Billing Address 2',
column: 'billing_address2',
fieldType: 'text',
importable: true,
},
billingAddressCity: {
name: 'Billing Address City',
column: 'billing_address_city',
fieldType: 'text',
importable: true,
},
billingAddressCountry: {
name: 'Billing Address Country',
column: 'billing_address_country',
fieldType: 'text',
importable: true,
},
billingAddressPostcode: {
name: 'Billing Address Postcode',
column: 'billing_address_postcode',
fieldType: 'text',
importable: true,
},
billingAddressState: {
name: 'Billing Address State',
column: 'billing_address_state',
fieldType: 'text',
importable: true,
},
billingAddressPhone: {
name: 'Billing Address Phone',
column: 'billing_address_phone',
fieldType: 'text',
importable: true,
},
// Shipping Address
shippingAddress1: {
name: 'Shipping Address 1',
column: 'shipping_address1',
fieldType: 'text',
importable: true,
},
shippingAddress2: {
name: 'Shipping Address 2',
column: 'shipping_address2',
fieldType: 'text',
importable: true,
},
shippingAddressCity: {
name: 'Shipping Address City',
column: 'shipping_address_city',
fieldType: 'text',
importable: true,
},
shippingAddressCountry: {
name: 'Shipping Address Country',
column: 'shipping_address_country',
fieldType: 'text',
importable: true,
},
shippingAddressPostcode: {
name: 'Shipping Address Postcode',
column: 'shipping_address_postcode',
fieldType: 'text',
importable: true,
},
shippingAddressPhone: {
name: 'Shipping Address Phone',
column: 'shipping_address_phone',
fieldType: 'text',
importable: true,
},
shippingAddressState: {
name: 'Shipping Address State',
column: 'shipping_address_state',
fieldType: 'text',
importable: true,
},
//
createdAt: {
name: 'customer.field.created_at',
column: 'created_at',
fieldType: 'date',
},
},
};
+66 -8
View File
@@ -7,13 +7,14 @@ export default {
sortOrder: 'DESC',
sortField: 'name',
},
importable: true,
fields: {
'payment_date': {
payment_date: {
name: 'expense.field.payment_date',
column: 'payment_date',
fieldType: 'date',
},
'payment_account': {
payment_account: {
name: 'expense.field.payment_account',
column: 'payment_account_id',
fieldType: 'relation',
@@ -24,27 +25,27 @@ export default {
relationEntityLabel: 'name',
relationEntityKey: 'slug',
},
'amount': {
amount: {
name: 'expense.field.amount',
column: 'total_amount',
fieldType: 'number',
},
'reference_no': {
reference_no: {
name: 'expense.field.reference_no',
column: 'reference_no',
fieldType: 'text',
},
'description': {
description: {
name: 'expense.field.description',
column: 'description',
fieldType: 'text',
},
'published': {
published: {
name: 'expense.field.published',
column: 'published_at',
fieldType: 'date',
},
'status': {
status: {
name: 'expense.field.status',
fieldType: 'enumeration',
options: [
@@ -54,12 +55,69 @@ export default {
filterCustomQuery: StatusFieldFilterQuery,
sortCustomQuery: StatusFieldSortQuery,
},
'created_at': {
created_at: {
name: 'expense.field.created_at',
column: 'created_at',
fieldType: 'date',
},
},
fields2: {
paymentAccountId: {
name: 'expense.field.payment_account',
fieldType: 'relation',
relationModel: 'Account',
relationImportMatch: ['name', 'code'],
required: true,
},
referenceNo: {
name: 'expense.field.reference_no',
fieldType: 'text',
},
paymentDate: {
name: 'expense.field.payment_date',
fieldType: 'date',
required: true,
},
currencyCode: {
name: 'expense.field.currency_code',
fieldType: 'text',
},
exchangeRate: {
name: 'expense.field.exchange_rate',
fieldType: 'number',
},
description: {
name: 'expense.field.description',
fieldType: 'text',
},
categories: {
name: 'expense.field.categories',
fieldType: 'collection',
collectionOf: 'object',
fields: {
expenseAccountId: {
name: 'expense.field.expense_account',
fieldType: 'relation',
relationModel: 'Account',
relationImportMatch: ['name', 'code'],
required: true,
},
amount: {
name: 'expense.field.amount',
fieldType: 'number',
required: true,
},
description: {
name: 'expense.field.line_description',
fieldType: 'text',
},
},
},
publish: {
name: 'expense.field.publish',
fieldType: 'boolean',
},
},
};
function StatusFieldFilterQuery(query, role) {
+130 -52
View File
@@ -15,54 +15,39 @@ export default {
{ key: 'service', label: 'item.field.type.service' },
{ key: 'non-inventory', label: 'item.field.type.non-inventory' },
],
importable: true,
required: true,
},
name: {
name: 'item.field.name',
column: 'name',
fieldType: 'text',
importable: true,
required: true,
unique: true,
},
code: {
name: 'item.field.code',
column: 'code',
fieldType: 'text',
importable: true,
},
sellable: {
name: 'item.field.sellable',
column: 'sellable',
fieldType: 'boolean',
importable: true,
required: true,
},
purchasable: {
name: 'item.field.purchasable',
column: 'purchasable',
fieldType: 'boolean',
importable: true,
required: true,
},
sellPrice: {
name: 'item.field.sell_price',
sell_price: {
name: 'item.field.cost_price',
column: 'sell_price',
fieldType: 'number',
importable: true,
required: true,
},
costPrice: {
name: 'item.field.cost_price',
cost_price: {
name: 'item.field.cost_account',
column: 'cost_price',
fieldType: 'number',
importable: true,
required: true,
},
costAccount: {
name: 'item.field.cost_account',
cost_account: {
name: 'item.field.sell_account',
column: 'cost_account_id',
fieldType: 'relation',
@@ -71,14 +56,9 @@ export default {
relationEntityLabel: 'name',
relationEntityKey: 'slug',
dataTransferObjectKey: 'costAccountId',
importableRelationLabel: ['name', 'code'],
importable: true,
required: true,
},
sellAccount: {
name: 'item.field.sell_account',
sell_account: {
name: 'item.field.sell_description',
column: 'sell_account_id',
fieldType: 'relation',
@@ -87,41 +67,28 @@ export default {
relationEntityLabel: 'name',
relationEntityKey: 'slug',
importableRelationLabel: ['name', 'code'],
importable: true,
required: true,
},
inventoryAccount: {
inventory_account: {
name: 'item.field.inventory_account',
column: 'inventory_account_id',
fieldType: 'relation',
relationType: 'enumeration',
relationKey: 'inventoryAccount',
relationEntityLabel: 'name',
relationEntityKey: 'slug',
importableRelationLabel: ['name', 'code'],
importable: true,
required: true,
},
sellDescription: {
sell_description: {
name: 'Sell description',
column: 'sell_description',
fieldType: 'text',
importable: true,
},
purchaseDescription: {
purchase_description: {
name: 'Purchase description',
column: 'purchase_description',
fieldType: 'text',
importable: true,
},
quantityOnHand: {
quantity_on_hand: {
name: 'item.field.quantity_on_hand',
column: 'quantity_on_hand',
fieldType: 'number',
@@ -130,29 +97,140 @@ export default {
name: 'item.field.note',
column: 'note',
fieldType: 'text',
importable: true,
},
category: {
name: 'item.field.category',
column: 'category_id',
fieldType: 'relation',
relationType: 'enumeration',
relationKey: 'category',
relationEntityLabel: 'name',
relationEntityKey: 'id',
importableRelationLabel: 'name',
importable: true,
},
active: {
name: 'item.field.active',
column: 'active',
fieldType: 'boolean',
importable: true,
filterable: false,
},
createdAt: {
created_at: {
name: 'item.field.created_at',
column: 'created_at',
columnType: 'date',
fieldType: 'date',
},
},
fields2: {
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.cost_price',
column: 'sell_price',
fieldType: 'number',
},
cost_price: {
name: 'item.field.cost_account',
column: 'cost_price',
fieldType: 'number',
},
cost_account: {
name: 'item.field.sell_account',
column: 'cost_account_id',
fieldType: 'relation',
relationType: 'enumeration',
relationKey: 'costAccount',
relationEntityLabel: 'name',
relationEntityKey: 'slug',
},
sell_account: {
name: 'item.field.sell_description',
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',
@@ -10,13 +10,11 @@ export default {
name: 'item_category.field.name',
column: 'name',
fieldType: 'text',
importable: true,
},
description: {
name: 'item_category.field.description',
column: 'description',
fieldType: 'text',
importable: true,
},
count: {
name: 'item_category.field.count',
@@ -30,4 +28,16 @@ export default {
columnType: 'date',
},
},
fields2: {
name: {
name: 'item_category.field.name',
column: 'name',
fieldType: 'text',
},
description: {
name: 'item_category.field.description',
column: 'description',
fieldType: 'text',
},
},
};
@@ -4,54 +4,130 @@ export default {
sortOrder: 'DESC',
sortField: 'name',
},
importable: true,
importAggregator: 'group',
importAggregateOn: 'entries',
importAggregateBy: 'journalNumber',
fields: {
'date': {
date: {
name: 'manual_journal.field.date',
column: 'date',
fieldType: 'date',
},
'journal_number': {
journal_number: {
name: 'manual_journal.field.journal_number',
column: 'journal_number',
fieldType: 'text',
},
'reference': {
reference: {
name: 'manual_journal.field.reference',
column: 'reference',
fieldType: 'text',
},
'journal_type': {
journal_type: {
name: 'manual_journal.field.journal_type',
column: 'journal_type',
fieldType: 'text',
},
'amount': {
amount: {
name: 'manual_journal.field.amount',
column: 'amount',
fieldType: 'number',
},
'description': {
description: {
name: 'manual_journal.field.description',
column: 'description',
fieldType: 'text',
},
'status': {
status: {
name: 'manual_journal.field.status',
column: 'status',
fieldType: 'enumeration',
options: [
{ key: 'draft', label: 'Draft' },
{ key: 'published', label: 'published' }
{ key: 'published', label: 'published' },
],
filterCustomQuery: StatusFieldFilterQuery,
sortCustomQuery: StatusFieldSortQuery,
},
'created_at': {
created_at: {
name: 'manual_journal.field.created_at',
column: 'created_at',
fieldType: 'date',
},
},
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',
},
},
};
/**
@@ -64,6 +140,6 @@ function StatusFieldSortQuery(query, role) {
/**
* Status field filter custom query.
*/
function StatusFieldFilterQuery(query, role) {
function StatusFieldFilterQuery(query, role) {
query.modify('filterByStatus', role.value);
}
+25 -4
View File
@@ -1,33 +1,54 @@
import { get } from 'lodash';
import { IModelMeta, IModelMetaField, IModelMetaDefaultSort } from '@/interfaces';
import {
IModelMeta,
IModelMetaField,
IModelMetaDefaultSort,
} from '@/interfaces';
const defaultModelMeta = {
fields: {},
fields2: {},
};
export default (Model) =>
class ModelSettings extends Model {
/**
*
* @returns {IModelMeta}
*/
static get meta(): IModelMeta {
throw new Error('');
}
/**
* Parsed meta merged with default emta.
* @returns {IModelMeta}
*/
static get parsedMeta(): IModelMeta {
return {
...defaultModelMeta,
...this.meta,
};
}
/**
* Retrieve specific model field meta of the given field key.
* @param {string} key
* @returns {IModelMetaField}
*/
public static getField(key: string, attribute?:string): IModelMetaField {
public static getField(key: string, attribute?: string): IModelMetaField {
const field = get(this.meta.fields, key);
return attribute ? get(field, attribute) : field;
}
/**
* Retrieve the specific model meta.
* Retrieves the specific model meta.
* @param {string} key
* @returns
*/
public static getMeta(key?: string) {
return key ? get(this.meta, key): this.meta;
return key ? get(this.parsedMeta, key) : this.parsedMeta;
}
/**
@@ -1,5 +1,8 @@
export default {
importable: true,
importAggregator: 'group',
importAggregateOn: 'entries',
importAggregateBy: 'paymentReceiveNo',
fields: {
customer: {
name: 'payment_receive.field.customer',
@@ -54,4 +57,62 @@ export default {
fieldDate: 'date',
},
},
fields2: {
customerId: {
name: 'payment_receive.field.customer',
fieldType: 'relation',
relationModel: 'Contact',
relationImportMatch: ['displayName'],
required: true,
},
exchangeRate: {
name: 'payment_receive.field.exchange_rate',
fieldType: 'number',
},
paymentDate: {
name: 'payment_receive.field.payment_date',
fieldType: 'date',
required: true,
},
referenceNo: {
name: 'payment_receive.field.reference_no',
fieldType: 'text',
},
depositAccountId: {
name: 'payment_receive.field.deposit_account',
fieldType: 'relation',
relationModel: 'Account',
relationImportMatch: ['name', 'code'],
required: true,
},
paymentReceiveNo: {
name: 'payment_receive.field.payment_receive_no',
fieldType: 'text',
},
statement: {
name: 'payment_receive.field.statement',
fieldType: 'text',
},
entries: {
name: 'payment_receive.field.entries',
fieldType: 'collection',
collectionOf: 'object',
collectionMinLength: 1,
required: true,
fields: {
invoiceId: {
name: 'payment_receive.field.invoice',
fieldType: 'relation',
relationModel: 'SaleInvoice',
relationImportMatch: 'invoiceNo',
required: true,
},
paymentAmount: {
name: 'payment_receive.field.entries.payment_amount',
fieldType: 'number',
required: true,
},
},
},
},
};
@@ -4,18 +4,22 @@ export default {
sortOrder: 'DESC',
sortField: 'estimate_date',
},
importable: true,
importAggregator: 'group',
importAggregateOn: 'entries',
importAggregateBy: 'estimateNumber',
fields: {
'amount': {
amount: {
name: 'estimate.field.amount',
column: 'amount',
fieldType: 'number',
},
'estimate_number': {
estimate_number: {
name: 'estimate.field.estimate_number',
column: 'estimate_number',
fieldType: 'text',
},
'customer': {
customer: {
name: 'estimate.field.customer',
column: 'customer_id',
fieldType: 'relation',
@@ -26,32 +30,32 @@ export default {
relationEntityLabel: 'display_name',
relationEntityKey: 'id',
},
'estimate_date': {
estimate_date: {
name: 'estimate.field.estimate_date',
column: 'estimate_date',
fieldType: 'date',
},
'expiration_date': {
expiration_date: {
name: 'estimate.field.expiration_date',
column: 'expiration_date',
fieldType: 'date',
},
'reference_no': {
reference_no: {
name: 'estimate.field.reference_no',
column: 'reference',
fieldType: 'text',
},
'note': {
note: {
name: 'estimate.field.note',
column: 'note',
fieldType: 'text',
},
'terms_conditions': {
terms_conditions: {
name: 'estimate.field.terms_conditions',
column: 'terms_conditions',
fieldType: 'text',
},
'status': {
status: {
name: 'estimate.field.status',
fieldType: 'enumeration',
options: [
@@ -63,12 +67,89 @@ export default {
filterCustomQuery: StatusFieldFilterQuery,
sortCustomQuery: StatusFieldSortQuery,
},
'created_at': {
created_at: {
name: 'estimate.field.created_at',
column: 'created_at',
columnType: 'date',
},
},
fields2: {
customerId: {
name: 'Customer',
fieldType: 'relation',
relationModel: 'Contact',
relationImportMatch: ['displayName'],
required: true,
},
estimateDate: {
name: 'Estimate Date',
fieldType: 'date',
required: true,
},
expirationDate: {
name: 'Expiration Date',
fieldType: 'date',
required: true,
},
estimateNumber: {
name: 'Estimate No.',
fieldType: 'text',
},
reference: {
name: 'Reference No.',
fieldType: 'text',
},
exchangeRate: {
name: 'Exchange Rate',
fieldType: 'number',
},
currencyCode: {
name: 'Currency',
fieldType: 'text',
},
note: {
name: 'Note',
fieldType: 'text',
},
termsConditions: {
name: 'Terms & Conditions',
fieldType: 'text',
},
delivered: {
name: 'Delivered',
type: 'boolean',
},
entries: {
name: '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,
},
rate: {
name: 'invoice.field.rate',
fieldType: 'number',
required: true,
},
quantity: {
name: 'invoice.field.quantity',
fieldType: 'number',
required: true,
},
description: {
name: 'Line Description',
fieldType: 'text',
},
},
},
},
};
function StatusFieldSortQuery(query, role) {
@@ -4,6 +4,10 @@ export default {
sortOrder: 'DESC',
sortField: 'created_at',
},
importable: true,
importAggregator: 'group',
importAggregateOn: 'entries',
importAggregateBy: 'invoiceNo',
fields: {
customer: {
name: 'invoice.field.customer',
@@ -83,6 +87,83 @@ export default {
fieldType: 'date',
},
},
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',
},
currencyCode: {
name: 'invoice.field.currency',
fieldType: 'text',
},
invoiceMessage: {
name: 'invoice.field.invoice_message',
fieldType: 'text',
},
termsConditions: {
name: 'invoice.field.terms_conditions',
fieldType: 'text',
},
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,
},
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',
},
},
};
/**
@@ -4,13 +4,17 @@ export default {
sortOrder: 'DESC',
sortField: 'created_at',
},
importable: true,
importAggregator: 'group',
importAggregateOn: 'entries',
importAggregateBy: 'receiptNumber',
fields: {
'amount': {
amount: {
name: 'receipt.field.amount',
column: 'amount',
fieldType: 'number',
},
'deposit_account': {
deposit_account: {
column: 'deposit_account_id',
name: 'receipt.field.deposit_account',
fieldType: 'relation',
@@ -21,7 +25,7 @@ export default {
relationEntityLabel: 'name',
relationEntityKey: 'slug',
},
'customer': {
customer: {
name: 'receipt.field.customer',
column: 'customer_id',
fieldType: 'relation',
@@ -32,38 +36,37 @@ export default {
relationEntityLabel: 'display_name',
relationEntityKey: 'id',
},
'receipt_date': {
receipt_date: {
name: 'receipt.field.receipt_date',
column: 'receipt_date',
fieldType: 'date',
},
'receipt_number': {
receipt_number: {
name: 'receipt.field.receipt_number',
column: 'receipt_number',
fieldType: 'text',
},
'reference_no': {
reference_no: {
name: 'receipt.field.reference_no',
column: 'reference_no',
fieldType: 'text',
},
'receipt_message': {
receipt_message: {
name: 'receipt.field.receipt_message',
column: 'receipt_message',
fieldType: 'text',
},
'statement': {
statement: {
name: 'receipt.field.statement',
column: 'statement',
fieldType: 'text',
},
'created_at': {
created_at: {
name: 'receipt.field.created_at',
column: 'created_at',
fieldType: 'date',
},
'status': {
status: {
name: 'receipt.field.status',
fieldType: 'enumeration',
options: [
@@ -74,6 +77,81 @@ export default {
sortCustomQuery: StatusFieldSortQuery,
},
},
fields2: {
receiptDate: {
name: 'Receipt Date',
fieldType: 'date',
required: true,
},
customerId: {
name: 'Customer',
fieldType: 'relation',
relationModel: 'Contact',
relationImportMatch: 'displayName',
required: true,
},
depositAccountId: {
name: 'Deposit Account',
fieldType: 'relation',
relationModel: 'Account',
relationImportMatch: ['name', 'code'],
required: true,
},
exchangeRate: {
name: 'Exchange Rate',
fieldType: 'number',
},
receiptNumber: {
name: 'Receipt Number',
fieldType: 'text',
},
referenceNo: {
name: 'Reference No.',
fieldType: 'text',
},
closed: {
name: 'Closed',
fieldType: 'boolean',
},
entries: {
name: '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,
},
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',
},
},
},
statement: {
name: 'Statement',
fieldType: 'text',
},
receiptMessage: {
name: 'Receipt Message',
fieldType: 'text',
},
},
};
function StatusFieldFilterQuery(query, role) {
@@ -2,7 +2,7 @@ export default {
defaultFilterField: 'createdAt',
defaultSort: {
sortOrder: 'DESC',
sortField: 'createdAt',
sortField: 'created_at',
},
importable: true,
fields: {
@@ -10,33 +10,27 @@ export default {
name: 'Date',
column: 'date',
fieldType: 'date',
importable: true,
required: true,
},
payee: {
name: 'Payee',
column: 'payee',
fieldType: 'text',
importable: true,
},
description: {
name: 'Description',
column: 'description',
fieldType: 'text',
importable: true,
},
referenceNo: {
name: 'Reference No.',
column: 'reference_no',
fieldType: 'text',
importable: true,
},
amount: {
name: 'Amount',
column: 'Amount',
fieldType: 'numeric',
required: true,
importable: true,
},
account: {
name: 'Account',
@@ -51,4 +45,32 @@ export default {
importable: false,
},
},
fields2: {
date: {
name: 'Date',
fieldType: 'date',
required: true,
},
payee: {
name: 'Payee',
column: 'payee',
fieldType: 'text',
},
description: {
name: 'Description',
column: 'description',
fieldType: 'text',
},
referenceNo: {
name: 'Reference No.',
column: 'reference_no',
fieldType: 'text',
},
amount: {
name: 'Amount',
column: 'Amount',
fieldType: 'numeric',
required: true,
},
},
};
+89 -60
View File
@@ -2,99 +2,74 @@ export default {
defaultFilterField: 'displayName',
defaultSort: {
sortOrder: 'DESC',
sortField: 'createdAt',
sortField: 'created_at',
},
importable: true,
fields: {
firstName: {
first_name: {
name: 'vendor.field.first_name',
column: 'first_name',
fieldType: 'text',
importable: true,
},
lastName: {
last_name: {
name: 'vendor.field.last_name',
column: 'last_name',
fieldType: 'text',
importable: true,
},
displayName: {
display_name: {
name: 'vendor.field.display_name',
column: 'display_name',
fieldType: 'text',
required: true,
importable: true,
},
email: {
name: 'vendor.field.email',
column: 'email',
fieldType: 'text',
importable: true,
},
workPhone: {
work_phone: {
name: 'vendor.field.work_phone',
column: 'work_phone',
fieldType: 'text',
importable: true,
},
personalPhone: {
name: 'vendor.field.personal_phone',
personal_phone: {
name: 'vendor.field.personal_pone',
column: 'personal_phone',
fieldType: 'text',
importable: true,
},
companyName: {
company_name: {
name: 'vendor.field.company_name',
column: 'company_name',
fieldType: 'text',
importable: true,
},
website: {
name: 'vendor.field.website',
column: 'website',
fieldType: 'text',
importable: true,
},
created_at: {
name: 'vendor.field.created_at',
column: 'created_at',
fieldType: 'date',
},
balance: {
name: 'vendor.field.balance',
column: 'balance',
fieldType: 'number',
},
openingBalance: {
opening_balance: {
name: 'vendor.field.opening_balance',
column: 'opening_balance',
fieldType: 'number',
importable: true,
},
openingBalanceAt: {
opening_balance_at: {
name: 'vendor.field.opening_balance_at',
column: 'opening_balance_at',
fieldType: 'date',
importable: true,
},
openingBalanceExchangeRate: {
name: 'Opening Balance Ex. Rate',
column: 'opening_balance_exchange_rate',
fieldType: 'number',
importable: true,
},
currencyCode: {
currency_code: {
name: 'vendor.field.currency',
column: 'currency_code',
fieldType: 'text',
importable: true,
},
note: {
name: 'Note',
column: 'note',
fieldType: 'text',
importable: true,
},
active: {
name: 'Active',
column: 'active',
fieldType: 'boolean',
importable: true,
},
status: {
name: 'vendor.field.status',
@@ -114,96 +89,150 @@ export default {
}
},
},
},
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',
importable: true,
},
billingAddress2: {
name: 'Billing Address 2',
column: 'billing_address2',
fieldType: 'text',
importable: true,
},
billingAddressCity: {
name: 'Billing Address City',
column: 'billing_address_city',
fieldType: 'text',
importable: true,
},
billingAddressCountry: {
name: 'Billing Address Country',
column: 'billing_address_country',
fieldType: 'text',
importable: true,
},
billingAddressPostcode: {
name: 'Billing Address Postcode',
column: 'billing_address_postcode',
fieldType: 'text',
importable: true,
},
billingAddressState: {
name: 'Billing Address State',
column: 'billing_address_state',
fieldType: 'text',
importable: true,
},
billingAddressPhone: {
name: 'Billing Address Phone',
column: 'billing_address_phone',
fieldType: 'text',
importable: true,
},
// Shipping Address
shippingAddress1: {
name: 'Shipping Address 1',
column: 'shipping_address1',
fieldType: 'text',
importable: true,
},
shippingAddress2: {
name: 'Shipping Address 2',
column: 'shipping_address2',
fieldType: 'text',
importable: true,
},
shippingAddressCity: {
name: 'Shipping Address City',
column: 'shipping_address_city',
fieldType: 'text',
importable: true,
},
shippingAddressCountry: {
name: 'Shipping Address Country',
column: 'shipping_address_country',
fieldType: 'text',
importable: true,
},
shippingAddressPostcode: {
name: 'Shipping Address Postcode',
column: 'shipping_address_postcode',
fieldType: 'text',
importable: true,
},
shippingAddressState: {
name: 'Shipping Address State',
column: 'shipping_address_state',
fieldType: 'text',
importable: true,
},
shippingAddressPhone: {
name: 'Shipping Address Phone',
column: 'shipping_address_phone',
fieldType: 'text',
importable: true,
},
createdAt: {
name: 'vendor.field.created_at',
column: 'created_at',
fieldType: 'date',
},
},
};
@@ -12,6 +12,10 @@ export default {
sortOrder: 'DESC',
sortField: 'name',
},
importable: true,
importAggregator: 'group',
importAggregateOn: 'entries',
importAggregateBy: 'vendorCreditNumber',
fields: {
vendor: {
name: 'vendor_credit.field.vendor',
@@ -72,4 +76,68 @@ export default {
fieldType: 'date',
},
},
fields2: {
vendorId: {
name: 'Vendor',
fieldType: 'relation',
relationModel: 'Contact',
relationImportMatch: 'displayName',
required: true,
},
exchangeRate: {
name: 'Echange Rate',
fieldType: 'text',
},
vendorCreditNumber: {
name: 'Vendor Credit No.',
fieldType: 'text',
},
referenceNo: {
name: 'Refernece No.',
fieldType: 'text',
},
vendorCreditDate: {
name: 'Vendor Credit Date',
fieldType: 'date',
required: true,
},
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 Name',
fieldType: 'relation',
relationModel: 'Item',
relationImportMatch: ['name', 'code'],
required: true,
},
rate: {
name: 'Rate',
fieldType: 'number',
required: true,
},
quantity: {
name: 'Quantity',
fieldType: 'number',
required: true,
},
description: {
name: 'Description',
fieldType: 'text',
},
},
},
},
};
@@ -12,7 +12,7 @@ export const VendorsSampleData = [
"Opening Balance At": "2022-02-02",
"Opening Balance Ex. Rate": 2,
"Currency": "LYD",
"Active": "F",
"Active": "T",
"Note": "Doloribus autem optio temporibus dolores mollitia sit.",
"Billing Address 1": "862 Jessika Well",
"Billing Address 2": "1091 Dorthy Mount",
@@ -42,7 +42,7 @@ export const VendorsSampleData = [
"Opening Balance At": "2022-02-02",
"Opening Balance Ex. Rate": 2,
"Currency": "LYD",
"Active": "F",
"Active": "T",
"Note": "Doloribus dolore dolor dicta vitae in fugit nisi quibusdam.",
"Billing Address 1": "532 Simonis Spring",
"Billing Address 2": "3122 Nicolas Inlet",
@@ -72,7 +72,7 @@ export const VendorsSampleData = [
"Opening Balance At": "2022-02-02",
"Opening Balance Ex. Rate": 2,
"Currency": "LYD",
"Active": "F",
"Active": "T",
"Note": "Vero quibusdam rem fugit aperiam est modi.",
"Billing Address 1": "214 Sauer Villages",
"Billing Address 2": "30687 Kacey Square",
@@ -102,7 +102,7 @@ export const VendorsSampleData = [
"Opening Balance At": "2022-02-02",
"Opening Balance Ex. Rate": 2,
"Currency": "LYD",
"Active": "F",
"Active": "T",
"Note": "Quis cumque molestias rerum.",
"Billing Address 1": "22590 Cathy Harbor",
"Billing Address 2": "24493 Brycen Brooks",
@@ -34,7 +34,7 @@ export default class CreateCreditNote extends BaseCreditNotes {
public newCreditNote = async (
tenantId: number,
creditNoteDTO: ICreditNoteNewDTO,
authorizedUser: ISystemUser
trx?: Knex.Transaction
) => {
const { CreditNote, Contact } = this.tenancy.models(tenantId);
@@ -66,28 +66,32 @@ export default class CreateCreditNote extends BaseCreditNotes {
customer.currencyCode
);
// Creates a new credit card transactions under unit-of-work envirement.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onCreditNoteCreating` event.
await this.eventPublisher.emitAsync(events.creditNote.onCreating, {
tenantId,
creditNoteDTO,
trx,
} as ICreditNoteCreatingPayload);
return this.uow.withTransaction(
tenantId,
async (trx: Knex.Transaction) => {
// Triggers `onCreditNoteCreating` event.
await this.eventPublisher.emitAsync(events.creditNote.onCreating, {
tenantId,
creditNoteDTO,
trx,
} as ICreditNoteCreatingPayload);
// Upsert the credit note graph.
const creditNote = await CreditNote.query(trx).upsertGraph({
...creditNoteModel,
});
// Triggers `onCreditNoteCreated` event.
await this.eventPublisher.emitAsync(events.creditNote.onCreated, {
tenantId,
creditNoteDTO,
creditNote,
creditNoteId: creditNote.id,
trx,
} as ICreditNoteCreatedPayload);
// Upsert the credit note graph.
const creditNote = await CreditNote.query(trx).upsertGraph({
...creditNoteModel,
});
// Triggers `onCreditNoteCreated` event.
await this.eventPublisher.emitAsync(events.creditNote.onCreated, {
tenantId,
creditNoteDTO,
creditNote,
creditNoteId: creditNote.id,
trx,
} as ICreditNoteCreatedPayload);
return creditNote;
});
return creditNote;
},
trx
);
};
}
@@ -0,0 +1,44 @@
import { Inject, Service } from 'typedi';
import { Knex } from 'knex';
import { ICreditNoteNewDTO } from '@/interfaces';
import { Importable } from '../Import/Importable';
import CreateCreditNote from './CreateCreditNote';
@Service()
export class CreditNotesImportable extends Importable {
@Inject()
private createCreditNoteImportable: CreateCreditNote;
/**
* Importing to account service.
* @param {number} tenantId
* @param {IAccountCreateDTO} createAccountDTO
* @returns
*/
public importable(
tenantId: number,
createAccountDTO: ICreditNoteNewDTO,
trx?: Knex.Transaction
) {
return this.createCreditNoteImportable.newCreditNote(
tenantId,
createAccountDTO,
trx
);
}
/**
* Concurrrency controlling of the importing process.
* @returns {number}
*/
public get concurrency() {
return 1;
}
/**
* Retrieves the sample data that used to download accounts sample sheet.
*/
public sampleData(): any[] {
return [];
}
}
@@ -88,7 +88,8 @@ export class CreateExpense {
public newExpense = async (
tenantId: number,
expenseDTO: IExpenseCreateDTO,
authorizedUser: ISystemUser
authorizedUser: ISystemUser,
trx?: Knex.Transaction
): Promise<IExpense> => {
const { Expense } = await this.tenancy.models(tenantId);
@@ -103,28 +104,32 @@ export class CreateExpense {
);
// Writes the expense transaction with associated transactions under
// unit-of-work envirement.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onExpenseCreating` event.
await this.eventPublisher.emitAsync(events.expenses.onCreating, {
trx,
tenantId,
expenseDTO,
} as IExpenseCreatingPayload);
return this.uow.withTransaction(
tenantId,
async (trx: Knex.Transaction) => {
// Triggers `onExpenseCreating` event.
await this.eventPublisher.emitAsync(events.expenses.onCreating, {
trx,
tenantId,
expenseDTO,
} as IExpenseCreatingPayload);
// Creates a new expense transaction graph.
const expense: IExpense = await Expense.query(trx).upsertGraph(
expenseObj
);
// Triggers `onExpenseCreated` event.
await this.eventPublisher.emitAsync(events.expenses.onCreated, {
tenantId,
expenseId: expense.id,
authorizedUser,
expense,
trx,
} as IExpenseCreatedPayload);
// Creates a new expense transaction graph.
const expense: IExpense = await Expense.query(trx).upsertGraph(
expenseObj
);
// Triggers `onExpenseCreated` event.
await this.eventPublisher.emitAsync(events.expenses.onCreated, {
tenantId,
expenseId: expense.id,
authorizedUser,
expense,
trx,
} as IExpenseCreatedPayload);
return expense;
});
return expense;
},
trx
);
};
}
@@ -0,0 +1,46 @@
import { Inject, Service } from 'typedi';
import { Knex } from 'knex';
import { IExpenseCreateDTO } from '@/interfaces';
import { Importable } from '../Import/Importable';
import { CreateExpense } from './CRUD/CreateExpense';
import { ExpensesSampleData } from './constants';
@Service()
export class ExpensesImportable extends Importable {
@Inject()
private createExpenseService: CreateExpense;
/**
* Importing to account service.
* @param {number} tenantId
* @param {IAccountCreateDTO} createAccountDTO
* @returns
*/
public importable(
tenantId: number,
createAccountDTO: IExpenseCreateDTO,
trx?: Knex.Transaction
) {
return this.createExpenseService.newExpense(
tenantId,
createAccountDTO,
{},
trx
);
}
/**
* Concurrrency controlling of the importing process.
* @returns {number}
*/
public get concurrency() {
return 1;
}
/**
* Retrieves the sample data that used to download accounts sample sheet.
*/
public sampleData(): any[] {
return ExpensesSampleData;
}
}
@@ -36,3 +36,43 @@ export const ERRORS = {
EXPENSE_ALREADY_PUBLISHED: 'expense_already_published',
EXPENSE_HAS_ASSOCIATED_LANDED_COST: 'EXPENSE_HAS_ASSOCIATED_LANDED_COST',
};
export const ExpensesSampleData = [
{
'Payment Date': '2024-03-01',
'Reference No.': 'REF-1',
'Payment Account': 'Petty Cash',
Description: 'Vel et dolorem architecto veniam.',
'Currency Code': '',
'Exchange Rate': '',
'Expense Account': 'Utilities Expense',
Amount: 9000,
'Line Description': 'Voluptates voluptas corporis vel.',
Publish: 'T',
},
{
'Payment Date': '2024-03-02',
'Reference No.': 'REF-2',
'Payment Account': 'Petty Cash',
Description: 'Id est molestias.',
'Currency Code': '',
'Exchange Rate': '',
'Expense Account': 'Utilities Expense',
Amount: 9000,
'Line Description': 'Eos voluptatem cumque et voluptate reiciendis.',
Publish: 'T',
},
{
'Payment Date': '2024-03-03',
'Reference No.': 'REF-3',
'Payment Account': 'Petty Cash',
Description: 'Quam cupiditate at nihil dicta dignissimos non fugit illo.',
'Currency Code': '',
'Exchange Rate': '',
'Expense Account': 'Utilities Expense',
Amount: 9000,
'Line Description':
'Hic alias rerum sed commodi dolores sint animi perferendis.',
Publish: 'T',
},
];
@@ -71,7 +71,7 @@ export class ImportFileCommon {
parsedData: Record<string, any>[],
trx?: Knex.Transaction
): Promise<[ImportOperSuccess[], ImportOperError[]]> {
const importableFields = this.resource.getResourceImportableFields(
const resourceFields = this.resource.getResourceFields2(
tenantId,
importFile.resource
);
@@ -90,7 +90,7 @@ export class ImportFileCommon {
};
const transformedDTO = importable.transform(objectDTO, context);
const rowNumber = index + 1;
const uniqueValue = getUniqueImportableValue(importableFields, objectDTO);
const uniqueValue = getUniqueImportableValue(resourceFields, objectDTO);
const errorContext = {
rowNumber,
uniqueValue,
@@ -98,7 +98,7 @@ export class ImportFileCommon {
try {
// Validate the DTO object before passing it to the service layer.
await this.importFileValidator.validateData(
importableFields,
resourceFields,
transformedDTO
);
try {
@@ -1,63 +1,91 @@
import { Inject, Service } from 'typedi';
import * as R from 'ramda';
import bluebird from 'bluebird';
import { isUndefined, get, chain, toArray, pickBy, castArray } from 'lodash';
import { isUndefined, pickBy, set } from 'lodash';
import { Knex } from 'knex';
import { ImportMappingAttr, ResourceMetaFieldsMap } from './interfaces';
import { trimObject, parseBoolean } from './_utils';
import { Account, Item } from '@/models';
import {
valueParser,
parseKey,
getFieldKey,
aggregate,
sanitizeSheetData,
} from './_utils';
import ResourceService from '../Resource/ResourceService';
import { multiNumberParse } from '@/utils/multi-number-parse';
import HasTenancyService from '../Tenancy/TenancyService';
const CurrencyParsingDTOs = 10;
const getMapToPath = (to: string, group = '') =>
group ? `${group}.${to}` : to;
@Service()
export class ImportFileDataTransformer {
@Inject()
private resource: ResourceService;
@Inject()
private tenancy: HasTenancyService;
/**
* Parses the given sheet data before passing to the service layer.
* based on the mapped fields and the each field type .
* based on the mapped fields and the each field type.
* @param {number} tenantId -
* @param {}
*/
public async parseSheetData(
tenantId: number,
importFile: any,
importableFields: any,
importableFields: ResourceMetaFieldsMap,
data: Record<string, unknown>[],
trx?: Knex.Transaction
) {
): Promise<Record<string, any>[]> {
// Sanitize the sheet data.
const sanitizedData = this.sanitizeSheetData(data);
const sanitizedData = sanitizeSheetData(data);
// Map the sheet columns key with the given map.
const mappedDTOs = this.mapSheetColumns(
sanitizedData,
importFile.mappingParsed
);
const resourceModel = this.resource.getResourceModel(
tenantId,
importFile.resource
);
// Parse the mapped sheet values.
return this.parseExcelValues(
const parsedValues = await this.parseExcelValues(
tenantId,
importableFields,
mappedDTOs,
resourceModel,
trx
);
const aggregateValues = this.aggregateParsedValues(
tenantId,
importFile.resource,
parsedValues
);
return aggregateValues;
}
/**
* Sanitizes the data in the imported sheet by trimming object keys.
* @param json - The JSON data representing the imported sheet.
* @returns {string[][]} - The sanitized data with trimmed object keys.
* Aggregates parsed data based on resource metadata configuration.
* @param {number} tenantId
* @param {string} resourceName
* @param {Record<string, any>} parsedData
* @returns {Record<string, any>[]}
*/
public sanitizeSheetData(json) {
return R.compose(R.map(trimObject))(json);
}
public aggregateParsedValues = (
tenantId: number,
resourceName: string,
parsedData: Record<string, any>[]
): Record<string, any>[] => {
let _value = parsedData;
const meta = this.resource.getResourceMeta(tenantId, resourceName);
if (meta.importAggregator === 'group') {
_value = aggregate(
_value,
meta.importAggregateBy,
meta.importAggregateOn
);
}
return _value;
};
/**
* Maps the columns of the imported data based on the provided mapping attributes.
@@ -74,7 +102,8 @@ export class ImportFileDataTransformer {
map
.filter((mapping) => !isUndefined(item[mapping.from]))
.forEach((mapping) => {
newItem[mapping.to] = item[mapping.from];
const toPath = getMapToPath(mapping.to, mapping.group);
newItem[toPath] = item[mapping.from];
});
return newItem;
});
@@ -87,78 +116,32 @@ export class ImportFileDataTransformer {
* @returns {Record<string, any>}
*/
public async parseExcelValues(
tenantId: number,
fields: ResourceMetaFieldsMap,
valueDTOs: Record<string, any>[],
resourceModel: any,
trx?: Knex.Transaction
): Promise<Record<string, any>> {
// Prases the given object value based on the field key type.
const parser = async (value, key) => {
let _value = value;
const field = fields[key];
): Promise<Record<string, any>[]> {
const tenantModels = this.tenancy.models(tenantId);
const _valueParser = valueParser(fields, tenantModels, trx);
const _keyParser = parseKey(fields);
// Parses the boolean value.
if (fields[key].fieldType === 'boolean') {
_value = parseBoolean(value);
// Parses the enumeration value.
} else if (field.fieldType === 'enumeration') {
const field = fields[key];
const option = get(field, 'options', []).find(
(option) => option.label === value
);
_value = get(option, 'key');
// Parses the numeric value.
} else if (fields[key].fieldType === 'number') {
_value = multiNumberParse(value);
// Parses the relation value.
} else if (field.fieldType === 'relation') {
const relationModel = resourceModel.relationMappings[field.relationKey];
const RelationModel = relationModel?.modelClass;
if (!relationModel || !RelationModel) {
throw new Error(`The relation model of ${key} field is not exist.`);
}
const relationQuery = RelationModel.query(trx);
const relationKeys = field?.importableRelationLabel
? castArray(field?.importableRelationLabel)
: castArray(field?.relationEntityLabel);
relationQuery.where(function () {
relationKeys.forEach((relationKey: string) => {
this.orWhereRaw('LOWER(??) = LOWER(?)', [relationKey, value]);
});
});
const result = await relationQuery.first();
_value = get(result, 'id');
}
return _value;
};
const parseKey = (key: string) => {
const field = fields[key];
let _objectTransferObjectKey = key;
if (field.fieldType === 'relation') {
_objectTransferObjectKey = `${key}Id`;
}
return _objectTransferObjectKey;
};
const parseAsync = async (valueDTO) => {
// Remove the undefined fields.
// Clean up the undefined keys that not exist in resource fields.
const _valueDTO = pickBy(
valueDTO,
(value, key) => !isUndefined(fields[key])
(value, key) => !isUndefined(fields[getFieldKey(key)])
);
// Keys of mapped values. key structure: `group.key` or `key`.
const keys = Object.keys(_valueDTO);
// Map the object values.
return bluebird.reduce(
keys,
async (acc, key) => {
const parsedValue = await parser(_valueDTO[key], key);
const parsedKey = await parseKey(key);
acc[parsedKey] = parsedValue;
const parsedValue = await _valueParser(_valueDTO[key], key);
const parsedKey = await _keyParser(key);
set(acc, parsedKey, parsedValue);
return acc;
},
{}
@@ -1,4 +1,4 @@
import { fromPairs } from 'lodash';
import { fromPairs, isUndefined } from 'lodash';
import { Inject, Service } from 'typedi';
import HasTenancyService from '../Tenancy/TenancyService';
import {
@@ -69,7 +69,7 @@ export class ImportFileMapping {
importFile: any,
maps: ImportMappingAttr[]
) {
const fields = this.resource.getResourceImportableFields(
const fields = this.resource.getResourceFields2(
tenantId,
importFile.resource
);
@@ -78,11 +78,20 @@ export class ImportFileMapping {
);
const invalid = [];
// is not empty, is not undefined or map.group
maps.forEach((map) => {
if (
'undefined' === typeof fields[map.to] ||
'undefined' === typeof columnsMap[map.from]
) {
let _invalid = true;
if (!map.group && fields[map.to]) {
_invalid = false;
}
if (map.group && fields[map.group] && fields[map.group]?.fields[map.to]) {
_invalid = false;
}
if (columnsMap[map.from]) {
_invalid = false;
}
if (_invalid) {
invalid.push(map);
}
});
@@ -105,10 +114,14 @@ export class ImportFileMapping {
} else {
fromMap[map.from] = true;
}
if (toMap[map.to]) {
const toPath = !isUndefined(map?.group)
? `${map.group}.${map.to}`
: map.to;
if (toMap[toPath]) {
throw new ServiceError(ERRORS.DUPLICATED_TO_MAP_ATTR);
} else {
toMap[map.to] = true;
toMap[toPath] = true;
}
});
}
@@ -128,6 +141,7 @@ export class ImportFileMapping {
tenantId,
resource
);
// @todo Validate date type of the nested fields.
maps.forEach((map) => {
if (
typeof fields[map.to] !== 'undefined' &&
@@ -53,11 +53,10 @@ export class ImportFileProcess {
const sheetData = this.importCommon.parseXlsxSheet(buffer);
const header = getSheetColumns(sheetData);
const importableFields = this.resource.getResourceImportableFields(
const resourceFields = this.resource.getResourceFields2(
tenantId,
importFile.resource
);
// Runs the importing operation with ability to return errors that will happen.
const [successedImport, failedImport, allData] =
await this.uow.withTransaction(
@@ -67,7 +66,7 @@ export class ImportFileProcess {
const parsedData = await this.importParser.parseSheetData(
tenantId,
importFile,
importableFields,
resourceFields,
sheetData,
trx
);
@@ -1,8 +1,11 @@
import { Inject, Service } from 'typedi';
import HasTenancyService from '../Tenancy/TenancyService';
import { sanitizeResourceName, validateSheetEmpty } from './_utils';
import {
getResourceColumns,
sanitizeResourceName,
validateSheetEmpty,
} from './_utils';
import ResourceService from '../Resource/ResourceService';
import { IModelMetaField } from '@/interfaces';
import { ImportFileCommon } from './ImportFileCommon';
import { ImportFileDataValidator } from './ImportFileDataValidator';
import { ImportFileUploadPOJO } from './interfaces';
@@ -77,11 +80,11 @@ export class ImportFileUploadService {
columns: coumnsStringified,
params: paramsStringified,
});
const resourceColumnsMap = this.resourceService.getResourceImportableFields(
const resourceColumnsMap = this.resourceService.getResourceFields2(
tenantId,
resource
);
const resourceColumns = this.getResourceColumns(resourceColumnsMap);
const resourceColumns = getResourceColumns(resourceColumnsMap);
return {
import: {
@@ -92,23 +95,4 @@ export class ImportFileUploadService {
resourceColumns,
};
}
getResourceColumns(resourceColumns: { [key: string]: IModelMetaField }) {
return Object.entries(resourceColumns)
.map(
([key, { name, importHint, required, order }]: [
string,
IModelMetaField
]) => ({
key,
name,
required,
hint: importHint,
order,
})
)
.sort((a, b) =>
a.order && b.order ? a.order - b.order : a.order ? -1 : b.order ? 1 : 0
);
}
}
@@ -5,7 +5,7 @@ export class ImportableRegistry {
private static instance: ImportableRegistry;
private importables: Record<string, Importable>;
private constructor() {
constructor() {
this.importables = {};
}
@@ -6,6 +6,16 @@ import { CustomersImportable } from '../Contacts/Customers/CustomersImportable';
import { VendorsImportable } from '../Contacts/Vendors/VendorsImportable';
import { ItemsImportable } from '../Items/ItemsImportable';
import { ItemCategoriesImportable } from '../ItemCategories/ItemCategoriesImportable';
import { ManualJournalImportable } from '../ManualJournals/ManualJournalsImport';
import { BillsImportable } from '../Purchases/Bills/BillsImportable';
import { ExpensesImportable } from '../Expenses/ExpensesImportable';
import { SaleInvoicesImportable } from '../Sales/Invoices/SaleInvoicesImportable';
import { SaleEstimatesImportable } from '../Sales/Estimates/SaleEstimatesImportable';
import { BillPaymentsImportable } from '../Purchases/BillPayments/BillPaymentsImportable';
import { VendorCreditsImportable } from '../Purchases/VendorCredits/VendorCreditsImportable';
import { PaymentReceivesImportable } from '../Sales/PaymentReceives/PaymentReceivesImportable';
import { CreditNotesImportable } from '../CreditNotes/CreditNotesImportable';
import { SaleReceiptsImportable } from '../Sales/Receipts/SaleReceiptsImportable';
@Service()
export class ImportableResources {
@@ -28,6 +38,16 @@ export class ImportableResources {
{ resource: 'Vendor', importable: VendorsImportable },
{ resource: 'Item', importable: ItemsImportable },
{ resource: 'ItemCategory', importable: ItemCategoriesImportable },
{ resource: 'ManualJournal', importable: ManualJournalImportable },
{ resource: 'Bill', importable: BillsImportable },
{ resource: 'Expense', importable: ExpensesImportable },
{ resource: 'SaleInvoice', importable: SaleInvoicesImportable },
{ resource: 'SaleEstimate', importable: SaleEstimatesImportable },
{ resource: 'BillPayment', importable: BillPaymentsImportable },
{ resource: 'PaymentReceive', importable: PaymentReceivesImportable },
{ resource: 'VendorCredit', importable: VendorCreditsImportable },
{ resource: 'CreditNote', importable: CreditNotesImportable },
{ resource: 'SaleReceipt', importable: SaleReceiptsImportable }
];
public get registry() {
+248 -9
View File
@@ -1,5 +1,7 @@
import * as Yup from 'yup';
import moment from 'moment';
import * as R from 'ramda';
import { Knex } from 'knex';
import {
defaultTo,
upperFirst,
@@ -8,11 +10,17 @@ import {
isUndefined,
pickBy,
isEmpty,
castArray,
get,
head,
split,
last,
} from 'lodash';
import pluralize from 'pluralize';
import { ResourceMetaFieldsMap } from './interfaces';
import { IModelMetaField } from '@/interfaces';
import { IModelMetaField, IModelMetaField2 } from '@/interfaces';
import { ServiceError } from '@/exceptions';
import { multiNumberParse } from '@/utils/multi-number-parse';
export const ERRORS = {
RESOURCE_NOT_IMPORTABLE: 'RESOURCE_NOT_IMPORTABLE',
@@ -25,7 +33,12 @@ export const ERRORS = {
IMPORTED_SHEET_EMPTY: 'IMPORTED_SHEET_EMPTY',
};
export function trimObject(obj) {
/**
* Trimms the imported object string values before parsing.
* @param {Record<string, string | number>} obj
* @returns {<Record<string, string | number>}
*/
export function trimObject(obj: Record<string, string | number>) {
return Object.entries(obj).reduce((acc, [key, value]) => {
// Trim the key
const trimmedKey = key.trim();
@@ -38,8 +51,14 @@ export function trimObject(obj) {
}, {});
}
/**
* Generates the Yup validation schema based on the given resource fields.
* @param {ResourceMetaFieldsMap} fields
* @returns {Yup}
*/
export const convertFieldsToYupValidation = (fields: ResourceMetaFieldsMap) => {
const yupSchema = {};
Object.keys(fields).forEach((fieldName: string) => {
const field = fields[fieldName] as IModelMetaField;
let fieldSchema;
@@ -89,6 +108,17 @@ export const convertFieldsToYupValidation = (fields: ResourceMetaFieldsMap) => {
);
} else if (field.fieldType === 'url') {
fieldSchema = fieldSchema.url();
} else if (field.fieldType === 'collection') {
const nestedFieldShema = convertFieldsToYupValidation(field.fields);
fieldSchema = Yup.array().label(field.name);
if (!isUndefined(field.collectionMaxLength)) {
fieldSchema = fieldSchema.max(field.collectionMaxLength);
}
if (!isUndefined(field.collectionMinLength)) {
fieldSchema = fieldSchema.min(field.collectionMinLength);
}
fieldSchema = fieldSchema.of(nestedFieldShema);
}
if (field.required) {
fieldSchema = fieldSchema.required();
@@ -103,15 +133,18 @@ export const convertFieldsToYupValidation = (fields: ResourceMetaFieldsMap) => {
const parseFieldName = (fieldName: string, field: IModelMetaField) => {
let _key = fieldName;
if (field.fieldType === 'relation') {
_key = `${fieldName}Id`;
}
if (field.dataTransferObjectKey) {
_key = field.dataTransferObjectKey;
}
return _key;
};
/**
* Retrieves the unmapped sheet columns.
* @param columns
* @param mapping
* @returns
*/
export const getUnmappedSheetColumns = (columns, mapping) => {
return columns.filter(
(column) => !mapping.some((map) => map.from === column)
@@ -134,7 +167,7 @@ export const getSheetColumns = (sheetData: unknown[]) => {
* @returns {string}
*/
export const getUniqueImportableValue = (
importableFields: { [key: string]: IModelMetaField },
importableFields: { [key: string]: IModelMetaField2 },
objectDTO: Record<string, any>
) => {
const uniqueImportableValue = pickBy(
@@ -155,15 +188,15 @@ export const validateSheetEmpty = (sheetData: Array<any>) => {
if (isEmpty(sheetData)) {
throw new ServiceError(ERRORS.IMPORTED_SHEET_EMPTY);
}
}
};
const booleanValuesRepresentingTrue: string[] = ['true', 'yes', 'y', 't', '1'];
const booleanValuesRepresentingFalse: string[] = ['false', 'no', 'n', 'f', '0'];
/**
* Parses the given string value to boolean.
* @param {string} value
* @returns {string|null}
* @param {string} value
* @returns {string|null}
*/
export const parseBoolean = (value: string): boolean | null => {
const normalizeValue = (value: string): string =>
@@ -182,3 +215,209 @@ export const parseBoolean = (value: string): boolean | null => {
}
return null;
};
export const transformInputToGroupedFields = (input) => {
const output = [];
// Group for non-nested fields
const mainGroup = {
groupLabel: '',
groupKey: '',
fields: [],
};
input.forEach((item) => {
if (!item.fields) {
// If the item does not have nested fields, add it to the main group
mainGroup.fields.push(item);
} else {
// If the item has nested fields, create a new group for these fields
output.push({
groupLabel: item.name,
groupKey: item.key,
fields: item.fields,
});
}
});
// Add the main group to the output if it contains any fields
if (mainGroup.fields.length > 0) {
output.unshift(mainGroup); // Add the main group at the beginning
}
return output;
};
export const getResourceColumns = (resourceColumns: {
[key: string]: IModelMetaField2;
}) => {
const mapColumn =
(group: string) =>
([fieldKey, { name, importHint, required, order, ...field }]: [
string,
IModelMetaField2
]) => {
const extra: Record<string, any> = {};
const key = fieldKey;
if (group) {
extra.group = group;
}
if (field.fieldType === 'collection') {
extra.fields = mapColumns(field.fields, key);
}
return {
key,
name,
required,
hint: importHint,
order,
...extra,
};
};
const sortColumn = (a, b) =>
a.order && b.order ? a.order - b.order : a.order ? -1 : b.order ? 1 : 0;
const mapColumns = (columns, parentKey = '') =>
Object.entries(columns).map(mapColumn(parentKey)).sort(sortColumn);
return R.compose(transformInputToGroupedFields, mapColumns)(resourceColumns);
};
// Prases the given object value based on the field key type.
export const valueParser =
(fields: ResourceMetaFieldsMap, tenantModels: any, trx?: Knex.Transaction) =>
async (value: any, key: string, group = '') => {
let _value = value;
const fieldKey = key.includes('.') ? key.split('.')[0] : key;
const field = group ? fields[group]?.fields[fieldKey] : fields[fieldKey];
// Parses the boolean value.
if (field.fieldType === 'boolean') {
_value = parseBoolean(value);
// Parses the enumeration value.
} else if (field.fieldType === 'enumeration') {
const option = get(field, 'options', []).find(
(option) => option.label === value
);
_value = get(option, 'key');
// Parses the numeric value.
} else if (field.fieldType === 'number') {
_value = multiNumberParse(value);
// Parses the relation value.
} else if (field.fieldType === 'relation') {
const RelationModel = tenantModels[field.relationModel];
if (!RelationModel) {
throw new Error(`The relation model of ${key} field is not exist.`);
}
const relationQuery = RelationModel.query(trx);
const relationKeys = castArray(field?.relationImportMatch);
relationQuery.where(function () {
relationKeys.forEach((relationKey: string) => {
this.orWhereRaw('LOWER(??) = LOWER(?)', [relationKey, value]);
});
});
const result = await relationQuery.first();
_value = get(result, 'id');
} else if (field.fieldType === 'collection') {
const ObjectFieldKey = key.includes('.') ? key.split('.')[1] : key;
const _valueParser = valueParser(fields, tenantModels);
_value = await _valueParser(value, ObjectFieldKey, fieldKey);
}
return _value;
};
/**
* Parses the field key and detarmines the key path.
* @param {{ [key: string]: IModelMetaField2 }} fields
* @param {string} key - Mapped key path. formats: `group.key` or `key`.
* @returns {string}
*/
export const parseKey = R.curry(
(fields: { [key: string]: IModelMetaField2 }, key: string) => {
const fieldKey = getFieldKey(key);
const field = fields[fieldKey];
let _key = key;
if (field.fieldType === 'collection') {
if (field.collectionOf === 'object') {
const nestedFieldKey = last(key.split('.'));
_key = `${fieldKey}[0].${nestedFieldKey}`;
} else if (
field.collectionOf === 'string' ||
field.collectionOf ||
'numberic'
) {
_key = `${fieldKey}`;
}
}
console.log(_key);
return _key;
}
);
/**
* Retrieves the field root key, for instance: I -> entries.itemId O -> entries.
* @param {string} input
* @returns {string}
*/
export const getFieldKey = (input: string) => {
const keys = split(input, '.');
const firstKey = head(keys).split('[')[0]; // Split by "[" in case of array notation
return firstKey;
};
/**
{ * Aggregates the input array of objects based on a comparator attribute and groups the entries.
* This function is useful for combining multiple entries into a single entry based on a specific attribute,
* while aggregating other attributes into an array.}
*
* @param {Array} input - The array of objects to be aggregated.
* @param {string} comparatorAttr - The attribute of the objects used for comparison to aggregate.
* @param {string} groupOn - The attribute of the objects where the grouped entries will be pushed.
* @returns {Array} - The aggregated array of objects.
*
* @example
* // Example input:
* const input = [
* { id: 1, name: 'John', entries: ['entry1'] },
* { id: 2, name: 'Jane', entries: ['entry2'] },
* { id: 1, name: 'John', entries: ['entry3'] },
* ];
* const comparatorAttr = 'id';
* const groupOn = 'entries';
*
* // Example output:
* const output = [
* { id: 1, name: 'John', entries: ['entry1', 'entry3'] },
* { id: 2, name: 'Jane', entries: ['entry2'] },
* ];
*/
export function aggregate(
input: Array<any>,
comparatorAttr: string,
groupOn: string
): Array<Record<string, any>> {
return input.reduce((acc, curr) => {
const existingEntry = acc.find(
(entry) => entry[comparatorAttr] === curr[comparatorAttr]
);
if (existingEntry) {
existingEntry[groupOn].push(...curr.entries);
} else {
acc.push({ ...curr });
}
return acc;
}, []);
}
/**
* Sanitizes the data in the imported sheet by trimming object keys.
* @param json - The JSON data representing the imported sheet.
* @returns {string[][]} - The sanitized data with trimmed object keys.
*/
export const sanitizeSheetData = (json) => {
return R.compose(R.map(trimObject))(json);
};
@@ -1,9 +1,10 @@
import { IModelMetaField } from '@/interfaces';
import { IModelMetaField, IModelMetaField2 } from '@/interfaces';
import Import from '@/models/Import';
export interface ImportMappingAttr {
from: string;
to: string;
group?: string;
dateFormat?: string;
}
@@ -13,7 +14,7 @@ export interface ImportValidationError {
constraints: Record<string, string>;
}
export type ResourceMetaFieldsMap = { [key: string]: IModelMetaField };
export type ResourceMetaFieldsMap = { [key: string]: IModelMetaField2 };
export interface ImportInsertError {
rowNumber: number;
@@ -61,16 +62,15 @@ export interface ImportOperError {
error: ImportInsertError[];
index: number;
}
export interface ImportableContext {
import: Import,
import: Import;
rowIndex: number;
}
export const ImportDateFormats = [
'yyyy-MM-dd',
'dd.MM.yy',
'MM/dd/yy',
'dd/MMM/yyyy'
]
'dd/MMM/yyyy',
];
@@ -0,0 +1 @@
export class ImportDeleteExpiredFilesJobs {}
@@ -102,7 +102,10 @@ export default class ItemCategoriesService implements IItemCategoriesService {
}
});
if (foundItemCategory) {
throw new ServiceError(ERRORS.CATEGORY_NAME_EXISTS);
throw new ServiceError(
ERRORS.CATEGORY_NAME_EXISTS,
'The item category name is already exist.'
);
}
}
@@ -21,7 +21,6 @@ export class ItemsImportable extends Importable {
createDTO: IItemCreateDTO,
trx?: Knex.Transaction<any, any[]>
): Promise<void> {
console.log(createDTO, tenantId, 'XX');
await this.createItemService.createItem(tenantId, createDTO, trx);
}
@@ -82,7 +82,10 @@ export class CommandManualJournalValidators {
}
});
if (journals.length > 0) {
throw new ServiceError(ERRORS.JOURNAL_NUMBER_EXISTS);
throw new ServiceError(
ERRORS.JOURNAL_NUMBER_EXISTS,
'The journal number is already exist.'
);
}
}
@@ -73,9 +73,7 @@ export class CreateManualJournalService {
return R.compose(
// Omits the `branchId` from entries if multiply branches feature not active.
this.branchesDTOTransformer.transformDTO(tenantId)
)(
initialDTO
);
)(initialDTO);
}
/**
@@ -133,7 +131,8 @@ export class CreateManualJournalService {
public makeJournalEntries = async (
tenantId: number,
manualJournalDTO: IManualJournalDTO,
authorizedUser: ISystemUser
authorizedUser: ISystemUser,
trx?: Knex.Transaction
): Promise<{ manualJournal: IManualJournal }> => {
const { ManualJournal } = this.tenancy.models(tenantId);
@@ -156,27 +155,31 @@ export class CreateManualJournalService {
);
// Creates a manual journal transactions with associated transactions
// under unit-of-work envirement.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onManualJournalCreating` event.
await this.eventPublisher.emitAsync(events.manualJournals.onCreating, {
tenantId,
manualJournalDTO,
trx,
} as IManualJournalCreatingPayload);
return this.uow.withTransaction(
tenantId,
async (trx: Knex.Transaction) => {
// Triggers `onManualJournalCreating` event.
await this.eventPublisher.emitAsync(events.manualJournals.onCreating, {
tenantId,
manualJournalDTO,
trx,
} as IManualJournalCreatingPayload);
// Upsert the manual journal object.
const manualJournal = await ManualJournal.query(trx).upsertGraph({
...manualJournalObj,
});
// Triggers `onManualJournalCreated` event.
await this.eventPublisher.emitAsync(events.manualJournals.onCreated, {
tenantId,
manualJournal,
manualJournalId: manualJournal.id,
trx,
} as IManualJournalEventCreatedPayload);
// Upsert the manual journal object.
const manualJournal = await ManualJournal.query(trx).upsertGraph({
...manualJournalObj,
});
// Triggers `onManualJournalCreated` event.
await this.eventPublisher.emitAsync(events.manualJournals.onCreated, {
tenantId,
manualJournal,
manualJournalId: manualJournal.id,
trx,
} as IManualJournalEventCreatedPayload);
return { manualJournal };
});
return { manualJournal };
},
trx
);
};
}
@@ -0,0 +1,60 @@
import { Inject } from 'typedi';
import { Knex } from 'knex';
import * as Yup from 'yup';
import { Importable } from '../Import/Importable';
import { CreateManualJournalService } from './CreateManualJournal';
import { IManualJournalDTO } from '@/interfaces';
import { ImportableContext } from '../Import/interfaces';
import { ManualJournalsSampleData } from './constants';
export class ManualJournalImportable extends Importable {
@Inject()
private createManualJournalService: CreateManualJournalService;
/**
* Importing to account service.
* @param {number} tenantId
* @param {IAccountCreateDTO} createAccountDTO
* @returns
*/
public importable(
tenantId: number,
createJournalDTO: IManualJournalDTO,
trx?: Knex.Transaction
) {
return this.createManualJournalService.makeJournalEntries(
tenantId,
createJournalDTO,
{},
trx
);
}
/**
* Transformes the DTO before passing it to importable and validation.
* @param {Record<string, any>} createDTO
* @param {ImportableContext} context
* @returns {Record<string, any>}
*/
public transform(createDTO: Record<string, any>, context: ImportableContext) {
return createDTO;
}
/**
* Params validation schema.
* @returns {ValidationSchema[]}
*/
public paramsValidationSchema() {
return Yup.object().shape({
autoIncrement: Yup.boolean(),
});
}
/**
* Retrieves the sample data of manual journals that used to download sample sheet.
* @returns {Record<string, any>}
*/
public sampleData(): Record<string, any>[] {
return ManualJournalsSampleData;
}
}
@@ -29,3 +29,36 @@ export const CONTACTS_CONFIG = [
];
export const DEFAULT_VIEWS = [];
export const ManualJournalsSampleData = [
{
Date: '2024-02-02',
'Journal No': 'J-100022',
'Reference No.': 'REF-10000',
'Currency Code': '',
'Exchange Rate': '',
'Journal Type': '',
Description: 'Animi quasi qui itaque aut possimus illum est magnam enim.',
Credit: 1000,
Debit: 0,
Note: 'Qui reprehenderit voluptate.',
Account: 'Bank Account',
Contact: '',
Publish: 'T',
},
{
Date: '2024-02-02',
'Journal No': 'J-100022',
'Reference No.': 'REF-10000',
'Currency Code': '',
'Exchange Rate': '',
'Journal Type': '',
Description: 'In assumenda dicta autem non est corrupti non et.',
Credit: 0,
Debit: 1000,
Note: 'Omnis tempora qui fugiat neque dolor voluptatem aut repudiandae nihil.',
Account: 'Bank Account',
Contact: '',
Publish: 'T',
},
];
@@ -0,0 +1,45 @@
import { Inject, Service } from 'typedi';
import { Knex } from 'knex';
import { IBillPaymentDTO } from '@/interfaces';
import { CreateBillPayment } from './CreateBillPayment';
import { Importable } from '@/services/Import/Importable';
import { BillsPaymentsSampleData } from './constants';
@Service()
export class BillPaymentsImportable extends Importable {
@Inject()
private createBillPaymentService: CreateBillPayment;
/**
* Importing to account service.
* @param {number} tenantId
* @param {IAccountCreateDTO} createAccountDTO
* @returns
*/
public importable(
tenantId: number,
billPaymentDTO: IBillPaymentDTO,
trx?: Knex.Transaction
) {
return this.createBillPaymentService.createBillPayment(
tenantId,
billPaymentDTO,
trx
);
}
/**
* Concurrrency controlling of the importing process.
* @returns {number}
*/
public get concurrency() {
return 1;
}
/**
* Retrieves the sample data that used to download accounts sample sheet.
*/
public sampleData(): any[] {
return BillsPaymentsSampleData;
}
}
@@ -48,7 +48,8 @@ export class CreateBillPayment {
*/
public async createBillPayment(
tenantId: number,
billPaymentDTO: IBillPaymentDTO
billPaymentDTO: IBillPaymentDTO,
trx?: Knex.Transaction
): Promise<IBillPayment> {
const { BillPayment, Contact } = this.tenancy.models(tenantId);
@@ -97,28 +98,32 @@ export class CreateBillPayment {
);
// Writes bill payment transacation with associated transactions
// under unit-of-work envirement.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onBillPaymentCreating` event.
await this.eventPublisher.emitAsync(events.billPayment.onCreating, {
tenantId,
billPaymentDTO,
trx,
} as IBillPaymentCreatingPayload);
return this.uow.withTransaction(
tenantId,
async (trx: Knex.Transaction) => {
// Triggers `onBillPaymentCreating` event.
await this.eventPublisher.emitAsync(events.billPayment.onCreating, {
tenantId,
billPaymentDTO,
trx,
} as IBillPaymentCreatingPayload);
// Writes the bill payment graph to the storage.
const billPayment = await BillPayment.query(trx).insertGraphAndFetch({
...billPaymentObj,
});
// Writes the bill payment graph to the storage.
const billPayment = await BillPayment.query(trx).insertGraphAndFetch({
...billPaymentObj,
});
// Triggers `onBillPaymentCreated` event.
await this.eventPublisher.emitAsync(events.billPayment.onCreated, {
tenantId,
billPayment,
billPaymentId: billPayment.id,
trx,
} as IBillPaymentEventCreatedPayload);
// Triggers `onBillPaymentCreated` event.
await this.eventPublisher.emitAsync(events.billPayment.onCreated, {
tenantId,
billPayment,
billPaymentId: billPayment.id,
trx,
} as IBillPaymentEventCreatedPayload);
return billPayment;
});
return billPayment;
},
trx
);
}
}
@@ -15,3 +15,36 @@ export const ERRORS = {
};
export const DEFAULT_VIEWS = [];
export const BillsPaymentsSampleData = [
{
'Payment Date': '2024-03-01',
Vendor: 'Gabriel Kovacek',
'Payment No.': 'P-10001',
'Reference No.': 'REF-1',
'Payment Account': 'Petty Cash',
Statement: 'Vel et dolorem architecto veniam.',
'Bill No': 'B-120',
'Payment Amount': 100,
},
{
'Payment Date': '2024-03-02',
Vendor: 'Gabriel Kovacek',
'Payment No.': 'P-10002',
'Reference No.': 'REF-2',
'Payment Account': 'Petty Cash',
Statement: 'Id est molestias.',
'Bill No': 'B-121',
'Payment Amount': 100,
},
{
'Payment Date': '2024-03-03',
Vendor: 'Gabriel Kovacek',
'Payment No.': 'P-10003',
'Reference No.': 'REF-3',
'Payment Account': 'Petty Cash',
Statement: 'Quam cupiditate at nihil dicta dignissimos non fugit illo.',
'Bill No': 'B-122',
'Payment Amount': 100,
},
];
@@ -0,0 +1,46 @@
import { Inject, Service } from 'typedi';
import { Knex } from 'knex';
import { Importable } from '@/services/Import/Importable';
import { CreateBill } from './CreateBill';
import { IBillDTO } from '@/interfaces';
import { BillsSampleData } from './constants';
@Service()
export class BillsImportable extends Importable {
@Inject()
private createBillService: CreateBill;
/**
* Importing to account service.
* @param {number} tenantId
* @param {IAccountCreateDTO} createAccountDTO
* @returns
*/
public importable(
tenantId: number,
createAccountDTO: IBillDTO,
trx?: Knex.Transaction
) {
return this.createBillService.createBill(
tenantId,
createAccountDTO,
{},
trx
);
}
/**
* Concurrrency controlling of the importing process.
* @returns {number}
*/
public get concurrency() {
return 1;
}
/**
* Retrieves the sample data that used to download accounts sample sheet.
*/
public sampleData(): any[] {
return BillsSampleData;
}
}
@@ -23,12 +23,12 @@ export class BillsValidators {
/**
* Validates the bill amount is bigger than paid amount.
* @param {number} billAmount
* @param {number} paidAmount
* @param {number} billAmount
* @param {number} paidAmount
*/
public validateBillAmountBiggerPaidAmount(
billAmount: number,
paidAmount: number,
paidAmount: number
) {
if (billAmount < paidAmount) {
throw new ServiceError(ERRORS.BILL_AMOUNT_SMALLER_THAN_PAID_AMOUNT);
@@ -53,7 +53,10 @@ export class BillsValidators {
});
if (foundBills.length > 0) {
throw new ServiceError(ERRORS.BILL_NUMBER_EXISTS);
throw new ServiceError(
ERRORS.BILL_NUMBER_EXISTS,
'The bill number is not unique.'
);
}
}
@@ -53,7 +53,8 @@ export class CreateBill {
public async createBill(
tenantId: number,
billDTO: IBillDTO,
authorizedUser: ISystemUser
authorizedUser: ISystemUser,
trx?: Knex.Transaction
): Promise<IBill> {
const { Bill, Contact } = this.tenancy.models(tenantId);
@@ -91,26 +92,30 @@ export class CreateBill {
authorizedUser
);
// Write new bill transaction with associated transactions under UOW env.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onBillCreating` event.
await this.eventPublisher.emitAsync(events.bill.onCreating, {
trx,
billDTO,
tenantId,
} as IBillCreatingPayload);
return this.uow.withTransaction(
tenantId,
async (trx: Knex.Transaction) => {
// Triggers `onBillCreating` event.
await this.eventPublisher.emitAsync(events.bill.onCreating, {
trx,
billDTO,
tenantId,
} as IBillCreatingPayload);
// Inserts the bill graph object to the storage.
const bill = await Bill.query(trx).upsertGraph(billObj);
// Inserts the bill graph object to the storage.
const bill = await Bill.query(trx).upsertGraph(billObj);
// Triggers `onBillCreated` event.
await this.eventPublisher.emitAsync(events.bill.onCreated, {
tenantId,
bill,
billId: bill.id,
trx,
} as IBillCreatedPayload);
// Triggers `onBillCreated` event.
await this.eventPublisher.emitAsync(events.bill.onCreated, {
tenantId,
bill,
billId: bill.id,
trx,
} as IBillCreatedPayload);
return bill;
});
return bill;
},
trx
);
}
}
@@ -75,3 +75,49 @@ export const DEFAULT_VIEWS = [
columns: DEFAULT_VIEW_COLUMNS,
},
];
export const BillsSampleData = [
{
'Bill No.': 'B-101',
'Reference No.': 'REF0',
Date: '2024-01-01',
'Due Date': '2024-03-01',
Vendor: 'Gabriel Kovacek',
'Exchange Rate': 1,
Note: 'Vel in sit sint.',
Open: 'T',
Item: 'VonRueden, Ruecker and Hettinger',
Quantity: 100,
Rate: 100,
'Line Description': 'Id a vel quis vel aut.',
},
{
'Bill No.': 'B-102',
'Reference No.': 'REF0',
Date: '2024-01-01',
'Due Date': '2024-03-01',
Vendor: 'Gabriel Kovacek',
'Exchange Rate': 1,
Note: 'Quia ut dolorem qui sint velit.',
Open: 'T',
Item: 'Thompson - Reichert',
Quantity: 200,
Rate: 50,
'Line Description':
'Nesciunt in adipisci quia ab reiciendis nam sed saepe consequatur.',
},
{
'Bill No.': 'B-103',
'Reference No.': 'REF0',
Date: '2024-01-01',
'Due Date': '2024-03-01',
Vendor: 'Gabriel Kovacek',
'Exchange Rate': 1,
Note: 'Dolore aut voluptatem minus pariatur alias pariatur.',
Open: 'T',
Item: 'VonRueden, Ruecker and Hettinger',
Quantity: 100,
Rate: 100,
'Line Description': 'Quam eligendi provident.',
},
];
@@ -33,7 +33,8 @@ export default class CreateVendorCredit extends BaseVendorCredit {
*/
public newVendorCredit = async (
tenantId: number,
vendorCreditCreateDTO: IVendorCreditCreateDTO
vendorCreditCreateDTO: IVendorCreditCreateDTO,
trx?: Knex.Transaction
) => {
const { VendorCredit, Vendor } = this.tenancy.models(tenantId);
@@ -59,27 +60,31 @@ export default class CreateVendorCredit extends BaseVendorCredit {
vendor.currencyCode
);
// Saves the vendor credit transactions under UOW envirement.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onVendorCreditCreating` event.
await this.eventPublisher.emitAsync(events.vendorCredit.onCreating, {
tenantId,
vendorCreditCreateDTO,
trx,
} as IVendorCreditCreatingPayload);
return this.uow.withTransaction(
tenantId,
async (trx: Knex.Transaction) => {
// Triggers `onVendorCreditCreating` event.
await this.eventPublisher.emitAsync(events.vendorCredit.onCreating, {
tenantId,
vendorCreditCreateDTO,
trx,
} as IVendorCreditCreatingPayload);
// Saves the vendor credit graph.
const vendorCredit = await VendorCredit.query(trx).upsertGraphAndFetch({
...vendorCreditModel,
});
// Triggers `onVendorCreditCreated` event.
await this.eventPublisher.emitAsync(events.vendorCredit.onCreated, {
tenantId,
vendorCredit,
vendorCreditCreateDTO,
trx,
} as IVendorCreditCreatedPayload);
// Saves the vendor credit graph.
const vendorCredit = await VendorCredit.query(trx).upsertGraphAndFetch({
...vendorCreditModel,
});
// Triggers `onVendorCreditCreated` event.
await this.eventPublisher.emitAsync(events.vendorCredit.onCreated, {
tenantId,
vendorCredit,
vendorCreditCreateDTO,
trx,
} as IVendorCreditCreatedPayload);
return vendorCredit;
});
return vendorCredit;
},
trx
);
};
}
@@ -0,0 +1,45 @@
import { Inject, Service } from 'typedi';
import { Knex } from 'knex';
import { Importable } from '@/services/Import/Importable';
import CreateVendorCredit from './CreateVendorCredit';
import { IVendorCreditCreateDTO } from '@/interfaces';
import { VendorCreditsSampleData } from './constants';
@Service()
export class VendorCreditsImportable extends Importable {
@Inject()
private createVendorCreditService: CreateVendorCredit;
/**
* Importing to account service.
* @param {number} tenantId
* @param {IAccountCreateDTO} createAccountDTO
* @returns
*/
public importable(
tenantId: number,
createPaymentDTO: IVendorCreditCreateDTO,
trx?: Knex.Transaction
) {
return this.createVendorCreditService.newVendorCredit(
tenantId,
createPaymentDTO,
trx
);
}
/**
* Concurrrency controlling of the importing process.
* @returns {number}
*/
public get concurrency() {
return 1;
}
/**
* Retrieves the sample data that used to download accounts sample sheet.
*/
public sampleData(): any[] {
return VendorCreditsSampleData;
}
}
@@ -1,11 +1,14 @@
export const ERRORS = {
VENDOR_CREDIT_NOT_FOUND: 'VENDOR_CREDIT_NOT_FOUND',
VENDOR_CREDIT_ALREADY_OPENED: 'VENDOR_CREDIT_ALREADY_OPENED',
VENDOR_CREDIT_HAS_NO_REMAINING_AMOUNT: 'VENDOR_CREDIT_HAS_NO_REMAINING_AMOUNT',
VENDOR_CREDIT_APPLY_TO_BILLS_NOT_FOUND: 'VENDOR_CREDIT_APPLY_TO_BILLS_NOT_FOUND',
VENDOR_CREDIT_HAS_NO_REMAINING_AMOUNT:
'VENDOR_CREDIT_HAS_NO_REMAINING_AMOUNT',
VENDOR_CREDIT_APPLY_TO_BILLS_NOT_FOUND:
'VENDOR_CREDIT_APPLY_TO_BILLS_NOT_FOUND',
BILLS_HAS_NO_REMAINING_AMOUNT: 'BILLS_HAS_NO_REMAINING_AMOUNT',
VENDOR_CREDIT_HAS_REFUND_TRANSACTIONS: 'VENDOR_CREDIT_HAS_REFUND_TRANSACTIONS',
VENDOR_CREDIT_HAS_APPLIED_BILLS: 'VENDOR_CREDIT_HAS_APPLIED_BILLS'
VENDOR_CREDIT_HAS_REFUND_TRANSACTIONS:
'VENDOR_CREDIT_HAS_REFUND_TRANSACTIONS',
VENDOR_CREDIT_HAS_APPLIED_BILLS: 'VENDOR_CREDIT_HAS_APPLIED_BILLS',
};
export const DEFAULT_VIEW_COLUMNS = [];
@@ -62,3 +65,18 @@ export const DEFAULT_VIEWS = [
columns: DEFAULT_VIEW_COLUMNS,
},
];
export const VendorCreditsSampleData = [
{
Vendor: 'Randall Kohler VENDOR',
'Vendor Credit Date': '2024-01-01',
'Vendor Credit No.': 'VC-0001',
'Reference No.': 'REF-00001',
'Exchange Rate': '',
Note: 'Note',
Open: 'T',
'Item Name': 'Hettinger, Schumm and Bartoletti',
Quantity: 100,
Rate: 100,
},
];
@@ -2,7 +2,7 @@ import { Service, Inject } from 'typedi';
import { camelCase, upperFirst, pickBy } from 'lodash';
import * as qim from 'qim';
import pluralize from 'pluralize';
import { IModelMeta, IModelMetaField } from '@/interfaces';
import { IModelMeta, IModelMetaField, IModelMetaField2 } from '@/interfaces';
import TenancyService from '@/services/Tenancy/TenancyService';
import { ServiceError } from '@/exceptions';
import I18nService from '@/services/I18n/I18nService';
@@ -74,11 +74,20 @@ export default class ResourceService {
return meta.fields;
}
public getResourceFields2(
tenantId: number,
modelName: string
): { [key: string]: IModelMetaField2 } {
const meta = this.getResourceMeta(tenantId, modelName);
return meta.fields2;
}
/**
*
* @param {number} tenantId
* @param {string} modelName
* @returns
*
* @param {number} tenantId
* @param {string} modelName
* @returns
*/
public getResourceImportableFields(
tenantId: number,
@@ -96,9 +105,14 @@ export default class ResourceService {
const $enumerationType = (field) =>
field.fieldType === 'enumeration' ? field : undefined;
const $hasFields = (field) => 'undefined' !== typeof field.fields ? field : undefined;
const naviagations = [
['fields', qim.$each, 'name'],
['fields', qim.$each, $enumerationType, 'options', qim.$each, 'label'],
['fields2', qim.$each, 'name'],
['fields2', qim.$each, $enumerationType, 'options', qim.$each, 'label'],
['fields2', qim.$each, $hasFields, 'fields', qim.$each, 'name'],
];
return this.i18nService.i18nApply(naviagations, meta, tenantId);
}
@@ -43,7 +43,8 @@ export class CreateSaleEstimate {
*/
public async createEstimate(
tenantId: number,
estimateDTO: ISaleEstimateDTO
estimateDTO: ISaleEstimateDTO,
trx?: Knex.Transaction
): Promise<ISaleEstimate> {
const { SaleEstimate, Contact } = this.tenancy.models(tenantId);
@@ -75,28 +76,32 @@ export class CreateSaleEstimate {
estimateDTO.entries
);
// Creates a sale estimate transaction with associated transactions as UOW.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onSaleEstimateCreating` event.
await this.eventPublisher.emitAsync(events.saleEstimate.onCreating, {
estimateDTO,
tenantId,
trx,
} as ISaleEstimateCreatingPayload);
return this.uow.withTransaction(
tenantId,
async (trx: Knex.Transaction) => {
// Triggers `onSaleEstimateCreating` event.
await this.eventPublisher.emitAsync(events.saleEstimate.onCreating, {
estimateDTO,
tenantId,
trx,
} as ISaleEstimateCreatingPayload);
// Upsert the sale estimate graph to the storage.
const saleEstimate = await SaleEstimate.query(trx).upsertGraphAndFetch({
...estimateObj,
});
// Triggers `onSaleEstimateCreated` event.
await this.eventPublisher.emitAsync(events.saleEstimate.onCreated, {
tenantId,
saleEstimate,
saleEstimateId: saleEstimate.id,
saleEstimateDTO: estimateDTO,
trx,
} as ISaleEstimateCreatedPayload);
// Upsert the sale estimate graph to the storage.
const saleEstimate = await SaleEstimate.query(trx).upsertGraphAndFetch({
...estimateObj,
});
// Triggers `onSaleEstimateCreated` event.
await this.eventPublisher.emitAsync(events.saleEstimate.onCreated, {
tenantId,
saleEstimate,
saleEstimateId: saleEstimate.id,
saleEstimateDTO: estimateDTO,
trx,
} as ISaleEstimateCreatedPayload);
return saleEstimate;
});
return saleEstimate;
},
trx
);
}
}
@@ -41,7 +41,10 @@ export class SaleEstimateValidators {
}
});
if (foundSaleEstimate) {
throw new ServiceError(ERRORS.SALE_ESTIMATE_NUMBER_EXISTANCE);
throw new ServiceError(
ERRORS.SALE_ESTIMATE_NUMBER_EXISTANCE,
'The given sale estimate is not unique.'
);
}
}
@@ -0,0 +1,45 @@
import { Inject, Service } from 'typedi';
import { Knex } from 'knex';
import { ISaleEstimateDTO } from '@/interfaces';
import { CreateSaleEstimate } from './CreateSaleEstimate';
import { Importable } from '@/services/Import/Importable';
import { SaleEstimatesSampleData } from './constants';
@Service()
export class SaleEstimatesImportable extends Importable {
@Inject()
private createEstimateService: CreateSaleEstimate;
/**
* Importing to account service.
* @param {number} tenantId
* @param {IAccountCreateDTO} createAccountDTO
* @returns
*/
public importable(
tenantId: number,
createEstimateDTO: ISaleEstimateDTO,
trx?: Knex.Transaction
) {
return this.createEstimateService.createEstimate(
tenantId,
createEstimateDTO,
trx
);
}
/**
* Concurrrency controlling of the importing process.
* @returns {number}
*/
public get concurrency() {
return 1;
}
/**
* Retrieves the sample data that used to download accounts sample sheet.
*/
public sampleData(): any[] {
return SaleEstimatesSampleData;
}
}
@@ -122,3 +122,54 @@ export const DEFAULT_VIEWS = [
columns: DEFAULT_VIEW_COLUMNS,
},
];
export const SaleEstimatesSampleData = [
{
Customer: 'Ambrose Olson',
'Estimate Date': '2024-01-01',
'Expiration Date': '2025-01-01',
'Estimate No.': 'EST-0001',
'Reference No.': 'REF-0001',
Currency: '',
'Exchange Rate': '',
Note: 'Vel autem quis aut ab.',
'Terms & Conditions': 'Provident illo architecto sit iste in.',
Delivered: 'T',
'Item Name': 'Hettinger, Schumm and Bartoletti',
Quantity: 1000,
Rate: 20,
'Line Description': 'Rem esse doloremque praesentium harum maiores.',
},
{
Customer: 'Ambrose Olson',
'Estimate Date': '2024-01-02',
'Expiration Date': '2025-01-02',
'Estimate No.': 'EST-0002',
'Reference No.': 'REF-0002',
Currency: '',
'Exchange Rate': '',
Note: 'Tempora voluptas odio deleniti rerum vitae consequatur nihil quis sunt.',
'Terms & Conditions': 'Ut eum incidunt quibusdam rerum vero.',
Delivered: 'T',
'Item Name': 'Hettinger, Schumm and Bartoletti',
Quantity: 1000,
Rate: 20,
'Line Description': 'Qui voluptate aliquam maxime aliquam.',
},
{
Customer: 'Ambrose Olson',
'Estimate Date': '2024-01-03',
'Expiration Date': '2025-01-03',
'Estimate No.': 'EST-0003',
'Reference No.': 'REF-0003',
Currency: '',
'Exchange Rate': '',
Note: 'Quia voluptatem delectus doloremque.',
'Terms & Conditions': 'Facilis porro vitae ratione.',
Delivered: 'T',
'Item Name': 'Hettinger, Schumm and Bartoletti',
Quantity: 1000,
Rate: 20,
'Line Description': 'Qui suscipit ducimus qui qui.',
},
];
@@ -51,7 +51,8 @@ export class CreateSaleInvoice {
public createSaleInvoice = async (
tenantId: number,
saleInvoiceDTO: ISaleInvoiceCreateDTO,
authorizedUser: ITenantUser
authorizedUser: ITenantUser,
trx?: Knex.Transaction
): Promise<ISaleInvoice> => {
const { SaleInvoice, SaleEstimate, Contact } =
this.tenancy.models(tenantId);
@@ -96,33 +97,37 @@ export class CreateSaleInvoice {
);
}
// Creates a new sale invoice and associated transactions under unit of work env.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onSaleInvoiceCreating` event.
await this.eventPublisher.emitAsync(events.saleInvoice.onCreating, {
saleInvoiceDTO,
tenantId,
trx,
} as ISaleInvoiceCreatingPaylaod);
return this.uow.withTransaction(
tenantId,
async (trx: Knex.Transaction) => {
// Triggers `onSaleInvoiceCreating` event.
await this.eventPublisher.emitAsync(events.saleInvoice.onCreating, {
saleInvoiceDTO,
tenantId,
trx,
} as ISaleInvoiceCreatingPaylaod);
// Create sale invoice graph to the storage.
const saleInvoice = await SaleInvoice.query(trx).upsertGraph(
saleInvoiceObj
);
const eventPayload: ISaleInvoiceCreatedPayload = {
tenantId,
saleInvoice,
saleInvoiceDTO,
saleInvoiceId: saleInvoice.id,
authorizedUser,
trx,
};
// Triggers the event `onSaleInvoiceCreated`.
await this.eventPublisher.emitAsync(
events.saleInvoice.onCreated,
eventPayload
);
return saleInvoice;
});
// Create sale invoice graph to the storage.
const saleInvoice = await SaleInvoice.query(trx).upsertGraph(
saleInvoiceObj
);
const eventPayload: ISaleInvoiceCreatedPayload = {
tenantId,
saleInvoice,
saleInvoiceDTO,
saleInvoiceId: saleInvoice.id,
authorizedUser,
trx,
};
// Triggers the event `onSaleInvoiceCreated`.
await this.eventPublisher.emitAsync(
events.saleInvoice.onCreated,
eventPayload
);
return saleInvoice;
},
trx
);
};
/**
@@ -0,0 +1,46 @@
import { Inject, Service } from 'typedi';
import { Knex } from 'knex';
import { ISaleInvoiceCreateDTO } from '@/interfaces';
import { CreateSaleInvoice } from './CreateSaleInvoice';
import { Importable } from '@/services/Import/Importable';
import { SaleInvoicesSampleData } from './constants';
@Service()
export class SaleInvoicesImportable extends Importable {
@Inject()
private createInvoiceService: CreateSaleInvoice;
/**
* Importing to account service.
* @param {number} tenantId
* @param {IAccountCreateDTO} createAccountDTO
* @returns
*/
public importable(
tenantId: number,
createAccountDTO: ISaleInvoiceCreateDTO,
trx?: Knex.Transaction
) {
return this.createInvoiceService.createSaleInvoice(
tenantId,
createAccountDTO,
{},
trx
);
}
/**
* Concurrrency controlling of the importing process.
* @returns {number}
*/
public get concurrency() {
return 1;
}
/**
* Retrieves the sample data that used to download accounts sample sheet.
*/
public sampleData(): any[] {
return SaleInvoicesSampleData;
}
}
@@ -109,3 +109,52 @@ export const DEFAULT_VIEWS = [
columns: DEFAULT_VIEW_COLUMNS,
},
];
export const SaleInvoicesSampleData = [
{
'Invoice No.': 'B-101',
'Reference No.': 'REF0',
'Invoice Date': '2024-01-01',
'Due Date': '2024-03-01',
Customer: 'Harley Veum',
'Exchange Rate': 1,
'Invoice Message': 'Aspernatur doloremque amet quia aut.',
'Terms & Conditions': 'Quia illum aut dolores.',
Delivered: 'T',
Item: 'VonRueden, Ruecker and Hettinger',
Quantity: 100,
Rate: 100,
Description: 'Description',
},
{
'Invoice No.': 'B-102',
'Reference No.': 'REF0',
'Invoice Date': '2024-01-01',
'Due Date': '2024-03-01',
Customer: 'Harley Veum',
'Exchange Rate': 1,
'Invoice Message': 'Est omnis enim vel.',
'Terms & Conditions': 'Iusto et sint nobis sit.',
Delivered: 'T',
Item: 'Thompson - Reichert',
Quantity: 200,
Rate: 50,
Description: 'Description',
},
{
'Invoice No.': 'B-103',
'Reference No.': 'REF0',
'Invoice Date': '2024-01-01',
'Due Date': '2024-03-01',
Customer: 'Harley Veum',
'Exchange Rate': 1,
'Invoice Message':
'Repudiandae voluptatibus repellat minima voluptatem rerum veniam.',
'Terms & Conditions': 'Id quod inventore ex rerum velit sed.',
Delivered: 'T',
Item: 'VonRueden, Ruecker and Hettinger',
Quantity: 100,
Rate: 100,
Description: 'Description',
},
];
@@ -42,7 +42,8 @@ export class CreatePaymentReceive {
public async createPaymentReceive(
tenantId: number,
paymentReceiveDTO: IPaymentReceiveCreateDTO,
authorizedUser: ISystemUser
authorizedUser: ISystemUser,
trx?: Knex.Transaction
) {
const { PaymentReceive, Contact } = this.tenancy.models(tenantId);
@@ -88,31 +89,35 @@ export class CreatePaymentReceive {
tenantMeta.baseCurrency
);
// Creates a payment receive transaction under UOW envirment.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onPaymentReceiveCreating` event.
await this.eventPublisher.emitAsync(events.paymentReceive.onCreating, {
trx,
paymentReceiveDTO,
tenantId,
} as IPaymentReceiveCreatingPayload);
return this.uow.withTransaction(
tenantId,
async (trx: Knex.Transaction) => {
// Triggers `onPaymentReceiveCreating` event.
await this.eventPublisher.emitAsync(events.paymentReceive.onCreating, {
trx,
paymentReceiveDTO,
tenantId,
} as IPaymentReceiveCreatingPayload);
// Inserts the payment receive transaction.
const paymentReceive = await PaymentReceive.query(
trx
).insertGraphAndFetch({
...paymentReceiveObj,
});
// Triggers `onPaymentReceiveCreated` event.
await this.eventPublisher.emitAsync(events.paymentReceive.onCreated, {
tenantId,
paymentReceive,
paymentReceiveId: paymentReceive.id,
authorizedUser,
trx,
} as IPaymentReceiveCreatedPayload);
// Inserts the payment receive transaction.
const paymentReceive = await PaymentReceive.query(
trx
).insertGraphAndFetch({
...paymentReceiveObj,
});
// Triggers `onPaymentReceiveCreated` event.
await this.eventPublisher.emitAsync(events.paymentReceive.onCreated, {
tenantId,
paymentReceive,
paymentReceiveId: paymentReceive.id,
authorizedUser,
trx,
} as IPaymentReceiveCreatedPayload);
return paymentReceive;
});
return paymentReceive;
},
trx
);
}
/**
@@ -0,0 +1,46 @@
import { Inject, Service } from 'typedi';
import { Knex } from 'knex';
import { IPaymentReceiveCreateDTO } from '@/interfaces';
import { Importable } from '@/services/Import/Importable';
import { CreatePaymentReceive } from './CreatePaymentReceive';
import { PaymentsReceiveSampleData } from './constants';
@Service()
export class PaymentReceivesImportable extends Importable {
@Inject()
private createPaymentReceiveService: CreatePaymentReceive;
/**
* Importing to account service.
* @param {number} tenantId
* @param {IAccountCreateDTO} createAccountDTO
* @returns
*/
public importable(
tenantId: number,
createPaymentDTO: IPaymentReceiveCreateDTO,
trx?: Knex.Transaction
) {
return this.createPaymentReceiveService.createPaymentReceive(
tenantId,
createPaymentDTO,
{},
trx
);
}
/**
* Concurrrency controlling of the importing process.
* @returns {number}
*/
public get concurrency() {
return 1;
}
/**
* Retrieves the sample data that used to download accounts sample sheet.
*/
public sampleData(): any[] {
return PaymentsReceiveSampleData;
}
}
@@ -31,3 +31,17 @@ export const ERRORS = {
};
export const DEFAULT_VIEWS = [];
export const PaymentsReceiveSampleData = [
{
Customer: 'Randall Kohler',
'Payment Date': '2024-10-10',
'Payment Receive No.': 'PAY-0001',
'Reference No.': 'REF-0001',
'Deposit Account': 'Petty Cash',
'Exchange Rate': '',
Statement: 'Totam optio quisquam qui.',
Invoice: 'INV-00001',
'Payment Amount': 850,
},
];
@@ -4,6 +4,7 @@ import {
ISaleReceipt,
ISaleReceiptCreatedPayload,
ISaleReceiptCreatingPayload,
ISaleReceiptDTO,
} from '@/interfaces';
import events from '@/subscribers/events';
import TenancyService from '@/services/Tenancy/TenancyService';
@@ -41,7 +42,8 @@ export class CreateSaleReceipt {
*/
public async createSaleReceipt(
tenantId: number,
saleReceiptDTO: any
saleReceiptDTO: ISaleReceiptDTO,
trx?: Knex.Transaction
): Promise<ISaleReceipt> {
const { SaleReceipt, Contact } = this.tenancy.models(tenantId);
@@ -80,27 +82,31 @@ export class CreateSaleReceipt {
);
}
// Creates a sale receipt transaction and associated transactions under UOW env.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onSaleReceiptCreating` event.
await this.eventPublisher.emitAsync(events.saleReceipt.onCreating, {
saleReceiptDTO,
tenantId,
trx,
} as ISaleReceiptCreatingPayload);
return this.uow.withTransaction(
tenantId,
async (trx: Knex.Transaction) => {
// Triggers `onSaleReceiptCreating` event.
await this.eventPublisher.emitAsync(events.saleReceipt.onCreating, {
saleReceiptDTO,
tenantId,
trx,
} as ISaleReceiptCreatingPayload);
// Inserts the sale receipt graph to the storage.
const saleReceipt = await SaleReceipt.query().upsertGraph({
...saleReceiptObj,
});
// Triggers `onSaleReceiptCreated` event.
await this.eventPublisher.emitAsync(events.saleReceipt.onCreated, {
tenantId,
saleReceipt,
saleReceiptId: saleReceipt.id,
trx,
} as ISaleReceiptCreatedPayload);
// Inserts the sale receipt graph to the storage.
const saleReceipt = await SaleReceipt.query().upsertGraph({
...saleReceiptObj,
});
// Triggers `onSaleReceiptCreated` event.
await this.eventPublisher.emitAsync(events.saleReceipt.onCreated, {
tenantId,
saleReceipt,
saleReceiptId: saleReceipt.id,
trx,
} as ISaleReceiptCreatedPayload);
return saleReceipt;
});
return saleReceipt;
},
trx
);
}
}
@@ -0,0 +1,45 @@
import { Inject, Service } from 'typedi';
import { Knex } from 'knex';
import { IAccountCreateDTO, ISaleReceiptDTO } from '@/interfaces';
import { CreateSaleReceipt } from './CreateSaleReceipt';
import { Importable } from '@/services/Import/Importable';
import { SaleReceiptsSampleData } from './constants';
@Service()
export class SaleReceiptsImportable extends Importable {
@Inject()
private createReceiptService: CreateSaleReceipt;
/**
* Importing to sale receipts service.
* @param {number} tenantId
* @param {IAccountCreateDTO} createAccountDTO
* @returns
*/
public importable(
tenantId: number,
createAccountDTO: ISaleReceiptDTO,
trx?: Knex.Transaction
) {
return this.createReceiptService.createSaleReceipt(
tenantId,
createAccountDTO,
trx
);
}
/**
* Concurrrency controlling of the importing process.
* @returns {number}
*/
public get concurrency() {
return 1;
}
/**
* Retrieves the sample data that used to download accounts sample sheet.
*/
public sampleData(): any[] {
return SaleReceiptsSampleData;
}
}
@@ -46,3 +46,23 @@ export const DEFAULT_VIEWS = [
columns: DEFAULT_VIEW_COLUMNS,
},
];
export const SaleReceiptsSampleData = [
{
"Receipt Date": "2023-01-01",
"Customer": "Randall Kohler",
"Deposit Account": "Petty Cash",
"Exchange Rate": "",
"Receipt Number": "REC-00001",
"Reference No.": "REF-0001",
"Statement": "Delectus unde aut soluta et accusamus placeat.",
"Receipt Message": "Vitae asperiores dicta.",
"Closed": "T",
"Item": "Schmitt Group",
"Quantity": 100,
"Rate": 200,
"Line Description": "Distinctio distinctio sit veritatis consequatur iste quod veritatis."
}
]
@@ -72,6 +72,10 @@ function ManualJournalActionsBar({
const handleRefreshBtnClick = () => {
refresh();
};
// Handle import button click.
const handleImportBtnClick = () => {
history.push('/manual-journals/import');
}
// Handle table row size change.
const handleTableRowSizeChange = (size) => {
@@ -130,6 +134,7 @@ function ManualJournalActionsBar({
className={Classes.MINIMAL}
icon={<Icon icon="file-import-16" iconSize={16} />}
text={<T id={'import'} />}
onClick={handleImportBtnClick}
/>
<Button
className={Classes.MINIMAL}
@@ -0,0 +1,25 @@
// @ts-nocheck
import { DashboardInsider } from '@/components';
import { ImportView } from '../Import/ImportView';
import { useHistory } from 'react-router-dom';
export default function ManualJournalsImport() {
const history = useHistory();
const handleCancelBtnClick = () => {
history.push('/manual-journals');
};
const handleImportSuccess = () => {
history.push('/manual-journals');
};
return (
<DashboardInsider name={'import-manual-journals'}>
<ImportView
resource={'manual-journals'}
onCancelClick={handleCancelBtnClick}
onImportSuccess={handleImportSuccess}
/>
</DashboardInsider>
);
}
@@ -0,0 +1,25 @@
// @ts-nocheck
import { useHistory } from 'react-router-dom';
import { DashboardInsider } from '@/components';
import { ImportView } from '../Import/ImportView';
export default function ExpensesImport() {
const history = useHistory();
const handleCancelBtnClick = () => {
history.push('/expenses');
};
const handleImportSuccess = () => {
history.push('/expenses');
};
return (
<DashboardInsider name={'import-expenses'}>
<ImportView
resource={'expenses'}
onCancelClick={handleCancelBtnClick}
onImportSuccess={handleImportSuccess}
/>
</DashboardInsider>
);
}
@@ -79,6 +79,11 @@ function ExpensesActionsBar({
refresh();
};
// Handle the import button click.
const handleImportBtnClick = () => {
history.push('/expenses/import');
}
// Handle table row size change.
const handleTableRowSizeChange = (size) => {
addSetting('expenses', 'tableSize', size);
@@ -135,6 +140,7 @@ function ExpensesActionsBar({
className={Classes.MINIMAL}
icon={<Icon icon="file-import-16" iconSize={16} />}
text={<T id={'import'} />}
onClick={handleImportBtnClick}
/>
<Button
className={Classes.MINIMAL}
@@ -1,18 +1,19 @@
import { useMemo } from 'react';
import { useCallback, useMemo } from 'react';
import clsx from 'classnames';
import { Button, Intent, Position } from '@blueprintjs/core';
import { useFormikContext } from 'formik';
import { FSelect, Group, Hint } from '@/components';
import { Box, FSelect, Group, Hint } from '@/components';
import { ImportFileMappingForm } from './ImportFileMappingForm';
import { EntityColumn, useImportFileContext } from './ImportFileProvider';
import { EntityColumnField, useImportFileContext } from './ImportFileProvider';
import { CLASSES } from '@/constants';
import { ImportFileContainer } from './ImportFileContainer';
import { ImportStepperStep } from './_types';
import { ImportFileMapBootProvider } from './ImportFileMappingBoot';
import styles from './ImportFileMapping.module.scss';
import { getFieldKey } from './_utils';
export function ImportFileMapping() {
const { importId } = useImportFileContext();
const { importId, entityColumns } = useImportFileContext();
return (
<ImportFileMapBootProvider importId={importId}>
@@ -23,56 +24,98 @@ export function ImportFileMapping() {
Bigcapital fields.
</p>
<table className={clsx('bp4-html-table', styles.table)}>
<thead>
<tr>
<th className={styles.label}>Bigcapital Fields</th>
<th className={styles.field}>Sheet Column Headers</th>
</tr>
</thead>
<tbody>
<ImportFileMappingFields />
</tbody>
</table>
{entityColumns.map((entityColumn, index) => (
<ImportFileMappingGroup
groupKey={entityColumn.groupKey}
groupName={entityColumn.groupName}
fields={entityColumn.fields}
/>
))}
</ImportFileContainer>
<ImportFileMappingFloatingActions />
</ImportFileMappingForm>
</ImportFileMapBootProvider>
);
}
function ImportFileMappingFields() {
const { entityColumns, sheetColumns } = useImportFileContext();
interface ImportFileMappingGroupProps {
groupKey: string;
groupName: string;
fields: any;
}
/**
* Mapping fields group
* @returns {React.ReactNode}
*/
function ImportFileMappingGroup({
groupKey,
groupName,
fields,
}: ImportFileMappingGroupProps) {
return (
<Box>
{groupName && <h3>{groupName}</h3>}
<table className={clsx('bp4-html-table', styles.table)}>
<thead>
<tr>
<th className={styles.label}>Bigcapital Fields</th>
<th className={styles.field}>Sheet Column Headers</th>
</tr>
</thead>
<tbody>
<ImportFileMappingFields fields={fields} />
</tbody>
</table>
</Box>
);
}
interface ImportFileMappingFieldsProps {
fields: EntityColumnField[];
}
/**
* Import mapping fields.
* @returns {React.ReactNode}
*/
function ImportFileMappingFields({ fields }: ImportFileMappingFieldsProps) {
const { sheetColumns } = useImportFileContext();
const items = useMemo(
() => sheetColumns.map((column) => ({ value: column, text: column })),
[sheetColumns],
);
const columnMapper = (column: EntityColumn, index: number) => (
<tr key={index}>
<td className={styles.label}>
{column.name}{' '}
{column.required && <span className={styles.requiredSign}>*</span>}
</td>
<td className={styles.field}>
<Group spacing={4}>
<FSelect
name={column.key}
items={items}
popoverProps={{ minimal: true }}
minimal={true}
fill={true}
/>
{column.hint && (
<Hint content={column.hint} position={Position.BOTTOM} />
)}
</Group>
</td>
</tr>
const columnMapper = useCallback(
(column: EntityColumnField, index: number) => (
<tr key={index}>
<td className={styles.label}>
{column.name}{' '}
{column.required && <span className={styles.requiredSign}>*</span>}
</td>
<td className={styles.field}>
<Group spacing={4}>
<FSelect
name={getFieldKey(column.key, column.group)}
items={items}
popoverProps={{ minimal: true }}
minimal={true}
fill={true}
/>
{column.hint && (
<Hint content={column.hint} position={Position.BOTTOM} />
)}
</Group>
</td>
</tr>
),
[items],
);
const columns = useMemo(
() => fields.map(columnMapper),
[columnMapper, fields],
);
const columns = entityColumns.map(columnMapper);
return <>{columns}</>;
}
@@ -3,17 +3,12 @@ import { Intent } from '@blueprintjs/core';
import { useImportFileMapping } from '@/hooks/query/import';
import { Form, Formik, FormikHelpers } from 'formik';
import { useImportFileContext } from './ImportFileProvider';
import { useMemo } from 'react';
import { isEmpty, lowerCase } from 'lodash';
import { AppToaster } from '@/components';
import { useImportFileMapBootContext } from './ImportFileMappingBoot';
import { transformToForm } from '@/utils';
interface ImportFileMappingFormProps {
children: React.ReactNode;
}
type ImportFileMappingFormValues = Record<string, string | null>;
import { ImportFileMappingFormProps } from './_types';
import {
transformValueToReq,
useImportFileMappingInitialValues,
} from './_utils';
export function ImportFileMappingForm({
children,
@@ -52,50 +47,3 @@ export function ImportFileMappingForm({
</Formik>
);
}
const transformValueToReq = (value: ImportFileMappingFormValues) => {
const mapping = Object.keys(value)
.filter((key) => !isEmpty(value[key]))
.map((key) => ({ from: value[key], to: key }));
return { mapping };
};
const transformResToFormValues = (value: { from: string; to: string }[]) => {
return value?.reduce((acc, map) => {
acc[map.to] = map.from;
return acc;
}, {});
};
const useImportFileMappingInitialValues = () => {
const { importFile } = useImportFileMapBootContext();
const { entityColumns, sheetColumns } = useImportFileContext();
const initialResValues = useMemo(
() => transformResToFormValues(importFile?.map || []),
[importFile?.map],
);
const initialValues = useMemo(
() =>
entityColumns.reduce((acc, { key, name }) => {
const _name = lowerCase(name);
const _matched = sheetColumns.find(
(column) => lowerCase(column) === _name,
);
// Match the default column name the same field name
// if matched one of sheet columns has the same field name.
acc[key] = _matched ? _matched : '';
return acc;
}, {}),
[entityColumns, sheetColumns],
);
return useMemo(
() => ({
...transformToForm(initialResValues, initialValues),
...initialValues,
}),
[initialValues, initialResValues],
);
};
@@ -7,12 +7,19 @@ import React, {
useState,
} from 'react';
export type EntityColumn = {
export type EntityColumnField = {
key: string;
name: string;
required?: boolean;
hint?: string;
group?: string;
};
export interface EntityColumn {
groupKey: string;
groupName: string;
fields: EntityColumnField[];
}
export type SheetColumn = string;
export type SheetMap = { from: string; to: string };
@@ -7,6 +7,7 @@ import * as Yup from 'yup';
import { useImportFileContext } from './ImportFileProvider';
import { ImportAlert, ImportStepperStep } from './_types';
import { useAlertsManager } from './AlertsManager';
import { transformToCamelCase } from '@/utils';
const initialValues = {
file: null,
@@ -55,9 +56,11 @@ export function ImportFileUploadForm({
uploadImportFile(formData)
.then(({ data }) => {
setImportId(data.import.import_id);
setSheetColumns(data.sheet_columns);
setEntityColumns(data.resource_columns);
const _data = transformToCamelCase(data);
setImportId(_data.import.importId);
setSheetColumns(_data.sheetColumns);
setEntityColumns(_data.resourceColumns);
setStep(ImportStepperStep.Mapping);
setSubmitting(false);
})
@@ -5,5 +5,11 @@ export enum ImportStepperStep {
}
export enum ImportAlert {
IMPORTED_SHEET_EMPTY = 'IMPORTED_SHEET_EMPTY'
}
IMPORTED_SHEET_EMPTY = 'IMPORTED_SHEET_EMPTY',
}
export interface ImportFileMappingFormProps {
children: React.ReactNode;
}
export type ImportFileMappingFormValues = Record<string, string | null>;
@@ -0,0 +1,114 @@
// @ts-nocheck
import { useMemo } from 'react';
import {
chain,
isEmpty,
lowerCase,
head,
last,
set,
get,
assign,
} from 'lodash';
import {
EntityColumn,
SheetColumn,
useImportFileContext,
} from './ImportFileProvider';
import { useImportFileMapBootContext } from './ImportFileMappingBoot';
import { deepdash, transformToForm } from '@/utils';
import { ImportFileMappingFormValues } from './_types';
export const getFieldKey = (key: string, group = '') => {
return group ? `${group}.${key}` : key;
};
type ImportFileMappingRes = { from: string; to: string; group: string }[];
/**
* Transformes the mapping form values to request.
* @param {ImportFileMappingFormValues} value
* @returns {ImportFileMappingRes[]}
*/
export const transformValueToReq = (
value: ImportFileMappingFormValues,
): { mapping: ImportFileMappingRes[] } => {
const mapping = chain(value)
.thru(deepdash.index)
.pickBy((_value, key) => !isEmpty(get(value, key)))
.map((from, key) => ({
from,
to: key.includes('.') ? last(key.split('.')) : key,
group: key.includes('.') ? head(key.split('.')) : '',
}))
.value();
return { mapping };
};
/**
* Transformes the mapping response to form values.
* @param {{ from: string; to: string, group: string }[]} value
* @returns {Record<string, object | string>}
*/
export const transformResToFormValues = (
value: { from: string; to: string , group: string }[],
): Record<string, object | string> => {
return value?.reduce((acc, map) => {
const path = map?.group ? `${map.group}.${map.to}` : map.to;
set(acc, path, map.from);
return acc;
}, {});
};
/**
* Retrieves the initial values of mapping form.
* @param {EntityColumn[]} entityColumns
* @param {SheetColumn[]} sheetColumns
*/
const getInitialDefaultValues = (
entityColumns: EntityColumn[],
sheetColumns: SheetColumn[],
) => {
return entityColumns.reduce((acc, { fields, groupKey }) => {
fields.forEach(({ key, name }) => {
const _name = lowerCase(name);
const _matched = sheetColumns.find(
(column) => lowerCase(column) === _name,
);
const _key = groupKey ? `${groupKey}.${key}` : key;
const _value = _matched ? _matched : '';
set(acc, _key, _value);
});
return acc;
}, {});
};
/**
* Retrieves the initial values of mapping form.
* @returns {Record<string, any>}
*/
export const useImportFileMappingInitialValues = () => {
const { importFile } = useImportFileMapBootContext();
const { entityColumns, sheetColumns } = useImportFileContext();
const initialResValues = useMemo(
() => transformResToFormValues(importFile?.map || []),
[importFile?.map],
);
// Retrieves the initial default values.
const initialDefaultValues = useMemo(
() => getInitialDefaultValues(entityColumns, sheetColumns),
[entityColumns, sheetColumns],
);
return useMemo<Record<string, any>>(
() =>
assign(
initialDefaultValues,
transformToForm(initialResValues, initialDefaultValues),
),
[initialDefaultValues, initialResValues],
);
};
@@ -0,0 +1,25 @@
// @ts-nocheck
import { useHistory } from 'react-router-dom';
import { DashboardInsider } from '@/components';
import { ImportView } from '@/containers/Import';
export default function BillsImport() {
const history = useHistory();
const handleCancelBtnClick = () => {
history.push('/bills');
};
const handleImportSuccess = () => {
history.push('/bills');
};
return (
<DashboardInsider name={'import-bills'}>
<ImportView
resource={'bills'}
onCancelClick={handleCancelBtnClick}
onImportSuccess={handleImportSuccess}
/>
</DashboardInsider>
);
}
@@ -78,6 +78,11 @@ function BillActionsBar({
addSetting('bills', 'tableSize', size);
};
// Handle the import button click.
const handleImportBtnClick = () => {
history.push('/bills/import');
}
return (
<DashboardActionsBar>
<NavbarGroup>
@@ -130,6 +135,7 @@ function BillActionsBar({
className={Classes.MINIMAL}
icon={<Icon icon={'file-import-16'} />}
text={<T id={'import'} />}
onClick={handleImportBtnClick}
/>
<Button
className={Classes.MINIMAL}
@@ -75,6 +75,11 @@ function VendorsCreditNoteActionsBar({
addSetting('vendorCredit', 'tableSize', size);
};
// Handle import button click.
const handleImportBtnClick = () => {
history.push('/vendor-credits/import')
}
return (
<DashboardActionsBar>
<NavbarGroup>
@@ -117,6 +122,7 @@ function VendorsCreditNoteActionsBar({
className={Classes.MINIMAL}
icon={<Icon icon={'file-import-16'} />}
text={<T id={'import'} />}
onClick={handleImportBtnClick}
/>
<Button
className={Classes.MINIMAL}
@@ -0,0 +1,25 @@
// @ts-nocheck
import { DashboardInsider } from '@/components';
import { ImportView } from '@/containers/Import';
import { useHistory } from 'react-router-dom';
export default function VendorCreditsImport() {
const history = useHistory();
const handleCancelBtnClick = () => {
history.push('/vendor-credits');
};
const handleImportSuccess = () => {
history.push('/vendor-credits');
};
return (
<DashboardInsider name={'import-vendor-credit'}>
<ImportView
resource={'vendor_credit'}
onCancelClick={handleCancelBtnClick}
onImportSuccess={handleImportSuccess}
/>
</DashboardInsider>
);
}
@@ -0,0 +1,25 @@
// @ts-nocheck
import { useHistory } from 'react-router-dom';
import { DashboardInsider } from '@/components';
import { ImportView } from '@/containers/Import';
export default function PaymentMadesImport() {
const history = useHistory();
const handleCancelBtnClick = () => {
history.push('/payment-mades');
};
const handleImportSuccess = () => {
history.push('/payment-mades');
};
return (
<DashboardInsider name={'import-payment-mades'}>
<ImportView
resource={'bill_payment'}
onCancelClick={handleCancelBtnClick}
onImportSuccess={handleImportSuccess}
/>
</DashboardInsider>
);
}
@@ -78,6 +78,11 @@ function PaymentMadeActionsBar({
addSetting('billPayments', 'tableSize', size);
};
// Handle the import button click.
const handleImportBtnClick = () => {
history.push('/payment-mades/import');
}
return (
<DashboardActionsBar>
<NavbarGroup>
@@ -128,6 +133,7 @@ function PaymentMadeActionsBar({
className={Classes.MINIMAL}
icon={<Icon icon={'file-import-16'} />}
text={<T id={'import'} />}
onClick={handleImportBtnClick}
/>
<Button
className={Classes.MINIMAL}
@@ -0,0 +1,25 @@
// @ts-nocheck
import { DashboardInsider } from '@/components';
import { ImportView } from '@/containers/Import';
import { useHistory } from 'react-router-dom';
export default function CreditNotesImport() {
const history = useHistory();
const handleCancelBtnClick = () => {
history.push('/credit-notes');
};
const handleImportSuccess = () => {
history.push('/credit-notes');
};
return (
<DashboardInsider name={'import-credit-notes'}>
<ImportView
resource={'credit_note'}
onCancelClick={handleCancelBtnClick}
onImportSuccess={handleImportSuccess}
/>
</DashboardInsider>
);
}
@@ -69,6 +69,11 @@ function CreditNotesActionsBar({
addSetting('creditNote', 'tableSize', size);
};
// Handle import button click.
const handleImportBtnClick = () => {
history.push('/credit-notes/import');
};
return (
<DashboardActionsBar>
<NavbarGroup>
@@ -111,6 +116,7 @@ function CreditNotesActionsBar({
className={Classes.MINIMAL}
icon={<Icon icon={'file-import-16'} />}
text={<T id={'import'} />}
onClick={handleImportBtnClick}
/>
<Button
className={Classes.MINIMAL}
@@ -0,0 +1,25 @@
// @ts-nocheck
import { useHistory } from 'react-router-dom';
import { DashboardInsider } from '@/components';
import { ImportView } from '@/containers/Import';
export default function EstimatesImport() {
const history = useHistory();
const handleCancelBtnClick = () => {
history.push('/estimates');
};
const handleImportSuccess = () => {
history.push('/estimates');
};
return (
<DashboardInsider name={'import-accounts'}>
<ImportView
resource={'sale_estimate'}
onCancelClick={handleCancelBtnClick}
onImportSuccess={handleImportSuccess}
/>
</DashboardInsider>
);
}
@@ -77,6 +77,11 @@ function EstimateActionsBar({
addSetting('salesEstimates', 'tableSize', size);
};
// Handle the import button click.
const handleImportBtnClick = () => {
history.push('/estimates/import');
}
return (
<DashboardActionsBar>
<NavbarGroup>
@@ -130,6 +135,7 @@ function EstimateActionsBar({
className={Classes.MINIMAL}
icon={<Icon icon={'file-import-16'} />}
text={<T id={'import'} />}
onClick={handleImportBtnClick}
/>
<Button
className={Classes.MINIMAL}
@@ -0,0 +1,25 @@
// @ts-nocheck
import { useHistory } from 'react-router-dom';
import { DashboardInsider } from '@/components';
import { ImportView } from '@/containers/Import';
export default function InvoicesImport() {
const history = useHistory();
const handleCancelBtnClick = () => {
history.push('/invoices');
};
const handleImportSuccess = () => {
history.push('/invoices');
};
return (
<DashboardInsider name={'import-invoices'}>
<ImportView
resource={'sale_invoice'}
onCancelClick={handleCancelBtnClick}
onImportSuccess={handleImportSuccess}
/>
</DashboardInsider>
);
}
@@ -74,6 +74,11 @@ function InvoiceActionsBar({
addSetting('salesInvoices', 'tableSize', size);
};
// Handle the import button click.
const handleImportBtnClick = () => {
history.push('/invoices/import');
};
return (
<DashboardActionsBar>
<NavbarGroup>
@@ -124,6 +129,7 @@ function InvoiceActionsBar({
className={Classes.MINIMAL}
icon={<Icon icon={'file-import-16'} />}
text={<T id={'import'} />}
onClick={handleImportBtnClick}
/>
<Button
className={Classes.MINIMAL}
@@ -0,0 +1,25 @@
// @ts-nocheck
import { useHistory } from 'react-router-dom';
import { DashboardInsider } from '@/components';
import { ImportView } from '@/containers/Import';
export default function PaymentsReceiveImport() {
const history = useHistory();
const handleCancelBtnClick = () => {
history.push('/payment-receives');
};
const handleImportSuccess = () => {
history.push('/payment-receives');
};
return (
<DashboardInsider name={'import-accounts'}>
<ImportView
resource={'payment_receive'}
onCancelClick={handleCancelBtnClick}
onImportSuccess={handleImportSuccess}
/>
</DashboardInsider>
);
}
@@ -26,7 +26,10 @@ import withPaymentReceives from './withPaymentReceives';
import withPaymentReceivesActions from './withPaymentReceivesActions';
import withSettings from '@/containers/Settings/withSettings';
import withSettingsActions from '@/containers/Settings/withSettingsActions';
import { PaymentReceiveAction, AbilitySubject } from '@/constants/abilityOption';
import {
PaymentReceiveAction,
AbilitySubject,
} from '@/constants/abilityOption';
import { usePaymentReceivesListContext } from './PaymentReceiptsListProvider';
import { useRefreshPaymentReceive } from '@/hooks/query/paymentReceives';
import { compose } from '@/utils';
@@ -75,6 +78,10 @@ function PaymentReceiveActionsBar({
const handleTableRowSizeChange = (size) => {
addSetting('paymentReceives', 'tableSize', size);
};
// Handle the import button click.
const handleImportBtnClick = () => {
history.push('/payment-receives/import');
};
return (
<DashboardActionsBar>
@@ -126,6 +133,7 @@ function PaymentReceiveActionsBar({
className={Classes.MINIMAL}
icon={<Icon icon={'file-import-16'} />}
text={<T id={'import'} />}
onClick={handleImportBtnClick}
/>
<Button
className={Classes.MINIMAL}
@@ -81,6 +81,11 @@ function ReceiptActionsBar({
addSetting('salesReceipts', 'tableSize', size);
};
// Handle the import button click.
const handleImportBtnClick = () => {
history.push('/receipts/import');
};
return (
<DashboardActionsBar>
<NavbarGroup>
@@ -134,6 +139,7 @@ function ReceiptActionsBar({
className={Classes.MINIMAL}
icon={<Icon icon={'file-import-16'} />}
text={<T id={'import'} />}
onClick={handleImportBtnClick}
/>
<Button
className={Classes.MINIMAL}
@@ -0,0 +1,25 @@
// @ts-nocheck
import { useHistory } from 'react-router-dom';
import { ImportView } from '@/containers/Import';
import { DashboardInsider } from '@/components';
export default function ReceiptsImport() {
const history = useHistory();
const handleCancelBtnClick = () => {
history.push('/accounts');
};
const handleImportSuccess = () => {
history.push('/accounts');
};
return (
<DashboardInsider name={'import-accounts'}>
<ImportView
resource={'sale_receipt'}
onCancelClick={handleCancelBtnClick}
onImportSuccess={handleImportSuccess}
/>
</DashboardInsider>
);
}
+114 -2
View File
@@ -51,6 +51,17 @@ export const getDashboardRoutes = () => [
defaultSearchResource: RESOURCES_TYPES.MANUAL_JOURNAL,
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
{
path: `/manual-journals/import`,
component: lazy(
() => import('@/containers/Accounting/ManualJournalsImport'),
),
breadcrumb: intl.get('edit'),
pageTitle: 'Manual Journals Import',
backLink: true,
defaultSearchResource: RESOURCES_TYPES.MANUAL_JOURNAL,
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
{
path: `/manual-journals`,
component: lazy(
@@ -495,6 +506,16 @@ export const getDashboardRoutes = () => [
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
// Expenses.
{
path: `/expenses/import`,
component: lazy(() => import('@/containers/Expenses/ExpensesImport')),
breadcrumb: 'Expenses Import',
hotkey: 'ctrl+shift+x',
pageTitle: 'Expenses Import',
sidebarExpand: false,
backLink: true,
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
{
path: `/expenses/new`,
component: lazy(
@@ -645,6 +666,18 @@ export const getDashboardRoutes = () => [
},
// Estimates
{
path: `/estimates/import`,
component: lazy(
() => import('@/containers/Sales/Estimates/EstimatesImport'),
),
name: 'estimate-edit',
breadcrumb: 'Estimates Import',
pageTitle: 'Estimates Import',
backLink: true,
defaultSearchResource: RESOURCES_TYPES.ESTIMATE,
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
{
path: `/estimates/:id/edit`,
component: lazy(
@@ -703,6 +736,16 @@ export const getDashboardRoutes = () => [
},
// Invoices.
{
path: `/invoices/import`,
component: lazy(() => import('@/containers/Sales/Invoices/InvoicesImport')),
name: 'invoice-edit',
breadcrumb: 'Invoices Import',
pageTitle: 'Invoices Import',
backLink: true,
defaultSearchResource: RESOURCES_TYPES.INVOICE,
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
{
path: `/invoices/:id/edit`,
component: lazy(
@@ -741,8 +784,19 @@ export const getDashboardRoutes = () => [
defaultSearchResource: RESOURCES_TYPES.INVOICE,
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
// Sales Receipts.
{
path: `/receipts/import`,
component: lazy(
() => import('@/containers/Sales/Receipts/SaleReceiptsImport'),
),
name: 'receipt-import',
breadcrumb: 'Receipts Import',
pageTitle: 'Receipts Import',
backLink: true,
defaultSearchResource: RESOURCES_TYPES.RECEIPT,
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
{
path: `/receipts/:id/edit`,
component: lazy(
@@ -783,6 +837,18 @@ export const getDashboardRoutes = () => [
},
// Sales Credit notes.
{
path: `/credit-notes/import`,
component: lazy(
() => import('@/containers/Sales/CreditNotes/CreditNotesImport'),
),
name: 'credit-note-import',
breadcrumb: 'Credit Notes Import',
pageTitle: 'Credit Notes Import',
backLink: true,
defaultSearchResource: RESOURCES_TYPES.CREDIT_NOTE,
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
{
path: `/credit-notes/:id/edit`,
component: lazy(
@@ -844,8 +910,19 @@ export const getDashboardRoutes = () => [
defaultSearchResource: RESOURCES_TYPES.CREDIT_NOTE,
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
// Payment receives
{
path: `/payment-receives/import`,
component: lazy(
() => import('@/containers/Sales/PaymentReceives/PaymentReceivesImport'),
),
name: 'payment-receive-import',
breadcrumb: 'Payments Receive Import',
pageTitle: 'Payments Receive Import',
backLink: true,
defaultSearchResource: RESOURCES_TYPES.PAYMENT_RECEIVE,
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
{
path: `/payment-receives/:id/edit`,
component: lazy(
@@ -893,6 +970,16 @@ export const getDashboardRoutes = () => [
},
// Bills
{
path: `/bills/import`,
component: lazy(() => import('@/containers/Purchases/Bills/BillImport')),
name: 'bill-edit',
// breadcrumb: intl.get('edit'),
pageTitle: 'Bills Import',
backLink: true,
defaultSearchResource: RESOURCES_TYPES.BILL,
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
{
path: `/bills/:id/edit`,
component: lazy(
@@ -932,6 +1019,18 @@ export const getDashboardRoutes = () => [
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
// Purchases Credit note.
{
path: `/vendor-credits/import`,
component: lazy(
() => import('@/containers/Purchases/CreditNotes/VendorCreditsImport'),
),
name: 'vendor-credits-edit',
breadcrumb: 'Vendor Credits Import',
pageTitle: 'Vendor Credits Import',
backLink: true,
defaultSearchResource: RESOURCES_TYPES.VENDOR_CREDIT,
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
{
path: `/vendor-credits/:id/edit`,
component: lazy(
@@ -1002,6 +1101,18 @@ export const getDashboardRoutes = () => [
subscriptionInactive: [SUBSCRIPTION_TYPE.MAIN],
},
// Payment modes.
{
path: `/payment-mades/import`,
component: lazy(
() => import('@/containers/Purchases/PaymentMades/PaymentMadesImport'),
),
name: 'payment-made-edit',
breadcrumb: intl.get('edit'),
pageTitle: 'Bills Payments Import',
backLink: true,
defaultSearchResource: RESOURCES_TYPES.PAYMENT_MADE,
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
{
path: `/payment-mades/:id/edit`,
component: lazy(
@@ -1071,6 +1182,7 @@ export const getDashboardRoutes = () => [
),
),
backLink: true,
sidebarExpand: false,
pageTitle: 'Bank Transactions Import',
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
defaultSearchResource: RESOURCES_TYPES.ACCOUNT,