mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-15 12:20:31 +00:00
Compare commits
49 Commits
big-150-ca
...
import-fie
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
079491823d | ||
|
|
f7a87a6e9c | ||
|
|
e0cdf42980 | ||
|
|
ee56653f4b | ||
|
|
2310b09778 | ||
|
|
0684e50ebd | ||
|
|
aaa8f39e50 | ||
|
|
af981ce630 | ||
|
|
a1f8417b5d | ||
|
|
086b060351 | ||
|
|
bbafdcd8bd | ||
|
|
dd9098bdc1 | ||
|
|
3851d34ba4 | ||
|
|
b9651f30d5 | ||
|
|
45b5fb4088 | ||
|
|
aa64bcf69b | ||
|
|
cbd867b334 | ||
|
|
1a8ca83786 | ||
|
|
80c14ba1a0 | ||
|
|
785045dbad | ||
|
|
291301c1e3 | ||
|
|
824e4e13d1 | ||
|
|
74da28b464 | ||
|
|
22a016b56e | ||
|
|
040f016273 | ||
|
|
8ab809fc71 | ||
|
|
f9e5028e0d | ||
|
|
7a3e121942 | ||
|
|
fc1d123c6b | ||
|
|
ad4e51d81d | ||
|
|
973d1832bd | ||
|
|
858e3541cb | ||
|
|
a5ab535d3b | ||
|
|
1d8cec5069 | ||
|
|
aba06991d4 | ||
|
|
ff5730d8a7 | ||
|
|
a27c877321 | ||
|
|
c5063fc5b5 | ||
|
|
ab4c0ab7a7 | ||
|
|
084d9d3d10 | ||
|
|
daa1e3a6bd | ||
|
|
4270d66928 | ||
|
|
90b4f3ef6d | ||
|
|
1fc6445123 | ||
|
|
b1d5390bfc | ||
|
|
1ba26a3b85 | ||
|
|
2c98376162 | ||
|
|
b71c79fef5 | ||
|
|
2baf407814 |
1
packages/server/.gitignore
vendored
1
packages/server/.gitignore
vendored
@@ -3,3 +3,4 @@
|
||||
stdout.log
|
||||
/dist
|
||||
/build
|
||||
/public/imports
|
||||
@@ -25,6 +25,7 @@
|
||||
"@types/i18n": "^0.8.7",
|
||||
"@types/knex": "^0.16.1",
|
||||
"@types/mathjs": "^6.0.12",
|
||||
"@types/yup": "^0.29.13",
|
||||
"accepts": "^1.3.7",
|
||||
"accounting": "^0.4.1",
|
||||
"agenda": "^4.2.1",
|
||||
@@ -53,7 +54,6 @@
|
||||
"express": "^4.17.1",
|
||||
"express-basic-auth": "^1.2.0",
|
||||
"express-boom": "^3.0.0",
|
||||
"express-fileupload": "^1.1.7-alpha.3",
|
||||
"express-oauth-server": "^2.0.0",
|
||||
"express-validator": "^6.12.2",
|
||||
"form-data": "^4.0.0",
|
||||
@@ -77,6 +77,7 @@
|
||||
"moment-timezone": "^0.5.43",
|
||||
"mongodb": "^6.1.0",
|
||||
"mongoose": "^5.10.0",
|
||||
"multer": "1.4.5-lts.1",
|
||||
"mustache": "^3.0.3",
|
||||
"mysql": "^2.17.1",
|
||||
"mysql2": "^1.6.5",
|
||||
@@ -105,7 +106,8 @@
|
||||
"typedi": "^0.8.0",
|
||||
"uniqid": "^5.2.0",
|
||||
"winston": "^3.2.1",
|
||||
"xlsx": "^0.18.5"
|
||||
"xlsx": "^0.18.5",
|
||||
"yup": "^0.28.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/lodash": "^4.14.158",
|
||||
|
||||
BIN
packages/server/public/.DS_Store
vendored
Normal file
BIN
packages/server/public/.DS_Store
vendored
Normal file
Binary file not shown.
BIN
packages/server/public/imports/.DS_Store
vendored
Normal file
BIN
packages/server/public/imports/.DS_Store
vendored
Normal file
Binary file not shown.
@@ -242,7 +242,7 @@
|
||||
"account.field.normal.credit": "دائن",
|
||||
"account.field.normal.debit": "مدين",
|
||||
"account.field.type": "نوع الحساب",
|
||||
"account.field.active": "Activity",
|
||||
"account.field.active": "Active",
|
||||
"account.field.balance": "الرصيد",
|
||||
"account.field.created_at": "أنشئت في",
|
||||
"item.field.type": "نوع الصنف",
|
||||
|
||||
@@ -241,28 +241,31 @@
|
||||
"account.field.normal.credit": "Credit",
|
||||
"account.field.normal.debit": "Debit",
|
||||
"account.field.type": "Type",
|
||||
"account.field.active": "Activity",
|
||||
"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": "Item Type",
|
||||
"item.field.type.inventory": "Inventory",
|
||||
"item.field.type.service": "Service",
|
||||
"item.field.type.non-inventory": "Non inventory",
|
||||
"item.field.name": "Name",
|
||||
"item.field.code": "Code",
|
||||
"item.field.type.non-inventory": "Non Inventory",
|
||||
"item.field.name": "Item Name",
|
||||
"item.field.code": "Item Code",
|
||||
"item.field.sellable": "Sellable",
|
||||
"item.field.purchasable": "Purchasable",
|
||||
"item.field.cost_price": "Cost price",
|
||||
"item.field.cost_account": "Cost account",
|
||||
"item.field.sell_account": "Sell account",
|
||||
"item.field.sell_description": "Sell description",
|
||||
"item.field.inventory_account": "Inventory account",
|
||||
"item.field.purchase_description": "Purchase description",
|
||||
"item.field.quantity_on_hand": "Quantity on hand",
|
||||
"item.field.cost_price": "Cost Price",
|
||||
"item.field.sell_price": "Sell Price",
|
||||
"item.field.cost_account": "Cost Account",
|
||||
"item.field.sell_account": "Sell Account",
|
||||
"item.field.sell_description": "Sell Description",
|
||||
"item.field.inventory_account": "Inventory Account",
|
||||
"item.field.purchase_description": "Purchase Description",
|
||||
"item.field.quantity_on_hand": "Quantity on Hand",
|
||||
"item.field.note": "Note",
|
||||
"item.field.category": "Category",
|
||||
"item.field.active": "Active",
|
||||
"item.field.created_at": "Created at",
|
||||
"item.field.created_at": "Created At",
|
||||
"item_category.field.name": "Name",
|
||||
"item_category.field.description": "Description",
|
||||
"item_category.field.count": "Count",
|
||||
@@ -275,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",
|
||||
@@ -285,6 +294,8 @@
|
||||
"invoice.field.status.delivered": "Delivered",
|
||||
"invoice.field.status.draft": "Draft",
|
||||
"invoice.field.created_at": "Created at",
|
||||
"invoice.field.currency": "Currency",
|
||||
"invoice.field.entries": "Entries",
|
||||
"estimate.field.amount": "Amount",
|
||||
"estimate.field.estimate_number": "Estimate number",
|
||||
"estimate.field.customer": "Customer",
|
||||
@@ -299,22 +310,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",
|
||||
@@ -342,22 +362,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",
|
||||
@@ -376,8 +404,8 @@
|
||||
"customer.field.last_name": "Last name",
|
||||
"customer.field.display_name": "Display name",
|
||||
"customer.field.email": "Email",
|
||||
"customer.field.work_phone": "Work phone",
|
||||
"customer.field.personal_phone": "Personal phone",
|
||||
"customer.field.work_phone": "Work Phone Number",
|
||||
"customer.field.personal_phone": "Personal Phone Number",
|
||||
"customer.field.company_name": "Company name",
|
||||
"customer.field.website": "Website",
|
||||
"customer.field.opening_balance_at": "Opening balance at",
|
||||
@@ -385,7 +413,7 @@
|
||||
"customer.field.created_at": "Created at",
|
||||
"customer.field.balance": "Balance",
|
||||
"customer.field.status": "Status",
|
||||
"customer.field.currency": "Curreny",
|
||||
"customer.field.currency": "Currency",
|
||||
"customer.field.status.active": "Active",
|
||||
"customer.field.status.inactive": "Inactive",
|
||||
"customer.field.status.overdue": "Overdue",
|
||||
@@ -394,8 +422,8 @@
|
||||
"vendor.field.last_name": "Last name",
|
||||
"vendor.field.display_name": "Display name",
|
||||
"vendor.field.email": "Email",
|
||||
"vendor.field.work_phone": "Work phone",
|
||||
"vendor.field.personal_phone": "Personal phone",
|
||||
"vendor.field.work_phone": "Work Phone Number",
|
||||
"vendor.field.personal_phone": "Personal Phone Number",
|
||||
"vendor.field.company_name": "Company name",
|
||||
"vendor.field.website": "Website",
|
||||
"vendor.field.opening_balance_at": "Opening balance at",
|
||||
@@ -403,13 +431,15 @@
|
||||
"vendor.field.created_at": "Created at",
|
||||
"vendor.field.balance": "Balance",
|
||||
"vendor.field.status": "Status",
|
||||
"vendor.field.currency": "Curreny",
|
||||
"vendor.field.currency": "Currency",
|
||||
"vendor.field.status.active": "Active",
|
||||
"vendor.field.status.inactive": "Inactive",
|
||||
"vendor.field.status.overdue": "Overdue",
|
||||
"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",
|
||||
|
||||
@@ -27,7 +27,7 @@ export default class AccountsController extends BaseController {
|
||||
/**
|
||||
* Router constructor method.
|
||||
*/
|
||||
router() {
|
||||
public router() {
|
||||
const router = Router();
|
||||
|
||||
router.get(
|
||||
@@ -98,7 +98,7 @@ export default class AccountsController extends BaseController {
|
||||
/**
|
||||
* Create account DTO Schema validation.
|
||||
*/
|
||||
get createAccountDTOSchema() {
|
||||
private get createAccountDTOSchema() {
|
||||
return [
|
||||
check('name')
|
||||
.exists()
|
||||
@@ -131,7 +131,7 @@ export default class AccountsController extends BaseController {
|
||||
/**
|
||||
* Account DTO Schema validation.
|
||||
*/
|
||||
get editAccountDTOSchema() {
|
||||
private get editAccountDTOSchema() {
|
||||
return [
|
||||
check('name')
|
||||
.exists()
|
||||
@@ -160,14 +160,14 @@ export default class AccountsController extends BaseController {
|
||||
];
|
||||
}
|
||||
|
||||
get accountParamSchema() {
|
||||
private get accountParamSchema() {
|
||||
return [param('id').exists().isNumeric().toInt()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Accounts list validation schema.
|
||||
*/
|
||||
get accountsListSchema() {
|
||||
private get accountsListSchema() {
|
||||
return [
|
||||
query('view_slug').optional({ nullable: true }).isString().trim(),
|
||||
query('stringified_filter_roles').optional().isJSON(),
|
||||
|
||||
@@ -93,6 +93,19 @@ export default class DeleteCashflowTransactionController extends BaseController
|
||||
],
|
||||
});
|
||||
}
|
||||
if (
|
||||
error.errorType ===
|
||||
'CANNOT_DELETE_TRANSACTION_CONVERTED_FROM_UNCATEGORIZED'
|
||||
) {
|
||||
return res.boom.badRequest(null, {
|
||||
errors: [
|
||||
{
|
||||
type: 'CANNOT_DELETE_TRANSACTION_CONVERTED_FROM_UNCATEGORIZED',
|
||||
code: 4100,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
|
||||
@@ -160,10 +160,8 @@ export default class CustomersController extends ContactsController {
|
||||
try {
|
||||
const contact = await this.customersApplication.createCustomer(
|
||||
tenantId,
|
||||
contactDTO,
|
||||
user
|
||||
contactDTO
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
id: contact.id,
|
||||
message: 'The customer has been created successfully.',
|
||||
|
||||
250
packages/server/src/api/controllers/Import/ImportController.ts
Normal file
250
packages/server/src/api/controllers/Import/ImportController.ts
Normal file
@@ -0,0 +1,250 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import { body, param, query } from 'express-validator';
|
||||
import { defaultTo } from 'lodash';
|
||||
import BaseController from '@/api/controllers/BaseController';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import { ImportResourceApplication } from '@/services/Import/ImportResourceApplication';
|
||||
import { uploadImportFile } from './_utils';
|
||||
import { parseJsonSafe } from '@/utils/parse-json-safe';
|
||||
|
||||
@Service()
|
||||
export class ImportController extends BaseController {
|
||||
@Inject()
|
||||
private importResourceApp: ImportResourceApplication;
|
||||
|
||||
/**
|
||||
* Router constructor method.
|
||||
*/
|
||||
router() {
|
||||
const router = Router();
|
||||
|
||||
router.post(
|
||||
'/file',
|
||||
uploadImportFile.single('file'),
|
||||
this.importValidationSchema,
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.fileUpload.bind(this)),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/:import_id/import',
|
||||
this.asyncMiddleware(this.import.bind(this)),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
router.post(
|
||||
'/:import_id/mapping',
|
||||
[
|
||||
param('import_id').exists().isString(),
|
||||
body('mapping').exists().isArray({ min: 1 }),
|
||||
body('mapping.*.group').optional(),
|
||||
body('mapping.*.from').exists(),
|
||||
body('mapping.*.to').exists(),
|
||||
],
|
||||
this.validationResult,
|
||||
this.asyncMiddleware(this.mapping.bind(this)),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/sample',
|
||||
[query('resource').exists(), query('format').optional()],
|
||||
this.validationResult,
|
||||
this.downloadImportSample.bind(this),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/:import_id',
|
||||
this.asyncMiddleware(this.getImportFileMeta.bind(this)),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
router.get(
|
||||
'/:import_id/preview',
|
||||
this.asyncMiddleware(this.preview.bind(this)),
|
||||
this.catchServiceErrors
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Import validation schema.
|
||||
* @returns {ValidationSchema[]}
|
||||
*/
|
||||
private get importValidationSchema() {
|
||||
return [body('resource').exists(), body('params').optional()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports xlsx/csv to the given resource type.
|
||||
* @param {Request} req -
|
||||
* @param {Response} res -
|
||||
* @param {NextFunction} next -
|
||||
*/
|
||||
private async fileUpload(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const body = this.matchedBodyData(req);
|
||||
const params = defaultTo(parseJsonSafe(body.params), {});
|
||||
|
||||
try {
|
||||
const data = await this.importResourceApp.import(
|
||||
tenantId,
|
||||
body.resource,
|
||||
req.file.filename,
|
||||
params
|
||||
);
|
||||
return res.status(200).send(data);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps the columns of the imported file.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private async mapping(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const { import_id: importId } = req.params;
|
||||
const body = this.matchedBodyData(req);
|
||||
|
||||
try {
|
||||
const mapping = await this.importResourceApp.mapping(
|
||||
tenantId,
|
||||
importId,
|
||||
body?.mapping
|
||||
);
|
||||
return res.status(200).send(mapping);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Preview the imported file before actual importing.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private async preview(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const { import_id: importId } = req.params;
|
||||
|
||||
try {
|
||||
const preview = await this.importResourceApp.preview(tenantId, importId);
|
||||
|
||||
return res.status(200).send(preview);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Importing the imported file to the application storage.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private async import(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
const { import_id: importId } = req.params;
|
||||
|
||||
try {
|
||||
const result = await this.importResourceApp.process(tenantId, importId);
|
||||
|
||||
return res.status(200).send(result);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the csv/xlsx sample sheet of the given resource name.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private async downloadImportSample(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const { tenantId } = req;
|
||||
const { format, resource } = this.matchedQueryData(req);
|
||||
|
||||
try {
|
||||
const result = this.importResourceApp.sample(tenantId, resource, format);
|
||||
|
||||
return res.status(200).send(result);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the import file meta.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
private async getImportFileMeta(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
const { tenantId } = req;
|
||||
const { import_id: importId } = req.params;
|
||||
|
||||
try {
|
||||
const result = await this.importResourceApp.importMeta(
|
||||
tenantId,
|
||||
importId
|
||||
);
|
||||
return res.status(200).send(result);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms service errors to response.
|
||||
* @param {Error}
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {ServiceError} error
|
||||
*/
|
||||
private catchServiceErrors(
|
||||
error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
if (error instanceof ServiceError) {
|
||||
if (error.errorType === 'INVALID_MAP_ATTRS') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'INVALID_MAP_ATTRS' }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'DUPLICATED_FROM_MAP_ATTR') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'DUPLICATED_FROM_MAP_ATTR' }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'DUPLICATED_TO_MAP_ATTR') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'DUPLICATED_TO_MAP_ATTR' }],
|
||||
});
|
||||
}
|
||||
if (error.errorType === 'IMPORTED_FILE_EXTENSION_INVALID') {
|
||||
return res.status(400).send({
|
||||
errors: [{ type: 'IMPORTED_FILE_EXTENSION_INVALID' }],
|
||||
});
|
||||
}
|
||||
return res.status(400).send({
|
||||
errors: [{ type: error.errorType }],
|
||||
});
|
||||
}
|
||||
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
32
packages/server/src/api/controllers/Import/_utils.ts
Normal file
32
packages/server/src/api/controllers/Import/_utils.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import Multer from 'multer';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
|
||||
export function allowSheetExtensions(req, file, cb) {
|
||||
if (
|
||||
file.mimetype !== 'text/csv' &&
|
||||
file.mimetype !== 'application/vnd.ms-excel' &&
|
||||
file.mimetype !==
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||
) {
|
||||
cb(new ServiceError('IMPORTED_FILE_EXTENSION_INVALID'));
|
||||
return;
|
||||
}
|
||||
cb(null, true);
|
||||
}
|
||||
|
||||
const storage = Multer.diskStorage({
|
||||
destination: function (req, file, cb) {
|
||||
cb(null, './public/imports');
|
||||
},
|
||||
filename: function (req, file, cb) {
|
||||
// Add the creation timestamp to clean up temp files later.
|
||||
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
|
||||
cb(null, uniqueSuffix);
|
||||
},
|
||||
});
|
||||
|
||||
export const uploadImportFile = Multer({
|
||||
storage,
|
||||
limits: { fileSize: 5 * 1024 * 1024 },
|
||||
fileFilter: allowSheetExtensions,
|
||||
});
|
||||
@@ -6,7 +6,7 @@ import ItemTransactionsController from './ItemsTransactions';
|
||||
|
||||
@Service()
|
||||
export default class ItemsBaseController {
|
||||
router() {
|
||||
public router() {
|
||||
const router = Router();
|
||||
|
||||
router.use('/', Container.get(ItemsController).router());
|
||||
|
||||
@@ -56,6 +56,7 @@ import { ProjectsController } from './controllers/Projects/Projects';
|
||||
import { ProjectTasksController } from './controllers/Projects/Tasks';
|
||||
import { ProjectTimesController } from './controllers/Projects/Times';
|
||||
import { TaxRatesController } from './controllers/TaxRates/TaxRates';
|
||||
import { ImportController } from './controllers/Import/ImportController';
|
||||
import { BankingController } from './controllers/Banking/BankingController';
|
||||
import { Webhooks } from './controllers/Webhooks/Webhooks';
|
||||
|
||||
@@ -135,6 +136,9 @@ export default () => {
|
||||
dashboard.use('/warehouses', Container.get(WarehousesController).router());
|
||||
dashboard.use('/projects', Container.get(ProjectsController).router());
|
||||
dashboard.use('/tax-rates', Container.get(TaxRatesController).router());
|
||||
|
||||
dashboard.use('/import', Container.get(ImportController).router());
|
||||
|
||||
dashboard.use('/', Container.get(ProjectTasksController).router());
|
||||
dashboard.use('/', Container.get(ProjectTimesController).router());
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
exports.up = function (knex) {
|
||||
return knex.schema.table('cashflow_transactions', (table) => {
|
||||
table
|
||||
.integer('uncategorized_transaction_id')
|
||||
.unsigned()
|
||||
.references('id')
|
||||
.inTable('uncategorized_cashflow_transactions');
|
||||
});
|
||||
};
|
||||
|
||||
exports.down = function (knex) {
|
||||
return knex.schema.table('cashflow_transactions', (table) => {
|
||||
table.dropColumn('uncategorized_transaction_id');
|
||||
});
|
||||
};
|
||||
@@ -51,6 +51,7 @@ export interface ICashflowCommandDTO {
|
||||
|
||||
export interface ICashflowNewCommandDTO extends ICashflowCommandDTO {
|
||||
plaidAccountId?: string;
|
||||
uncategorizedTransactionId?: number;
|
||||
}
|
||||
|
||||
export interface ICashflowTransaction {
|
||||
@@ -83,6 +84,8 @@ export interface ICashflowTransaction {
|
||||
|
||||
isCashDebit?: boolean;
|
||||
isCashCredit?: boolean;
|
||||
|
||||
uncategorizedTransactionId?: number;
|
||||
}
|
||||
|
||||
export interface ICashflowTransactionLine {
|
||||
|
||||
@@ -32,22 +32,45 @@ export interface IModelMetaFieldCommon {
|
||||
name: string;
|
||||
column: string;
|
||||
columnable?: boolean;
|
||||
fieldType: IModelColumnType;
|
||||
customQuery?: Function;
|
||||
required?: boolean;
|
||||
importHint?: string;
|
||||
importableRelationLabel?: string;
|
||||
order?: number;
|
||||
unique?: number;
|
||||
dataTransferObjectKey?: string;
|
||||
}
|
||||
|
||||
export interface IModelMetaFieldNumber {
|
||||
fieldType: 'number';
|
||||
export interface IModelMetaFieldText {
|
||||
fieldType: 'text';
|
||||
minLength?: number;
|
||||
maxLength?: number;
|
||||
}
|
||||
|
||||
export interface IModelMetaFieldOther {
|
||||
fieldType: 'text' | 'boolean';
|
||||
export interface IModelMetaFieldBoolean {
|
||||
fieldType: 'boolean';
|
||||
}
|
||||
export interface IModelMetaFieldNumber {
|
||||
fieldType: 'number';
|
||||
min?: number;
|
||||
max?: number;
|
||||
}
|
||||
export interface IModelMetaFieldDate {
|
||||
fieldType: 'date';
|
||||
}
|
||||
export interface IModelMetaFieldUrl {
|
||||
fieldType: 'url';
|
||||
}
|
||||
|
||||
export type IModelMetaField = IModelMetaFieldCommon &
|
||||
(IModelMetaFieldOther | IModelMetaEnumerationField | IModelMetaRelationField);
|
||||
(
|
||||
| IModelMetaFieldText
|
||||
| IModelMetaFieldNumber
|
||||
| IModelMetaFieldBoolean
|
||||
| IModelMetaFieldDate
|
||||
| IModelMetaFieldUrl
|
||||
| IModelMetaEnumerationField
|
||||
| IModelMetaRelationField
|
||||
| IModelMetaCollectionField
|
||||
);
|
||||
|
||||
export interface IModelMetaEnumerationOption {
|
||||
key: string;
|
||||
@@ -70,12 +93,71 @@ export interface IModelMetaRelationEnumerationField {
|
||||
relationEntityKey: string;
|
||||
}
|
||||
|
||||
export type IModelMetaRelationField = IModelMetaRelationFieldCommon & (
|
||||
IModelMetaRelationEnumerationField
|
||||
);
|
||||
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
|
||||
);
|
||||
|
||||
@@ -89,6 +89,7 @@ import { InvoiceChangeStatusOnMailSentSubscriber } from '@/services/Sales/Invoic
|
||||
import { SaleReceiptMarkClosedOnMailSentSubcriber } from '@/services/Sales/Receipts/subscribers/SaleReceiptMarkClosedOnMailSentSubcriber';
|
||||
import { SaleEstimateMarkApprovedOnMailSent } from '@/services/Sales/Estimates/subscribers/SaleEstimateMarkApprovedOnMailSent';
|
||||
import { DeleteCashflowTransactionOnUncategorize } from '@/services/Cashflow/subscribers/DeleteCashflowTransactionOnUncategorize';
|
||||
import { PreventDeleteTransactionOnDelete } from '@/services/Cashflow/subscribers/PreventDeleteTransactionsOnDelete'; }
|
||||
|
||||
export default () => {
|
||||
return new EventPublisher();
|
||||
@@ -217,5 +218,6 @@ export const susbcribers = () => {
|
||||
|
||||
// Cashflow
|
||||
DeleteCashflowTransactionOnUncategorize,
|
||||
PreventDeleteTransactionOnDelete
|
||||
];
|
||||
};
|
||||
|
||||
@@ -4,7 +4,6 @@ import helmet from 'helmet';
|
||||
import boom from 'express-boom';
|
||||
import errorHandler from 'errorhandler';
|
||||
import bodyParser from 'body-parser';
|
||||
import fileUpload from 'express-fileupload';
|
||||
import { Server } from 'socket.io';
|
||||
import Container from 'typedi';
|
||||
import routes from 'api';
|
||||
@@ -47,13 +46,6 @@ export default ({ app }) => {
|
||||
|
||||
app.use('/public', express.static(path.join(global.__storage_dir)));
|
||||
|
||||
// Handle multi-media requests.
|
||||
app.use(
|
||||
fileUpload({
|
||||
createParentPath: true,
|
||||
})
|
||||
);
|
||||
|
||||
// Logger middleware.
|
||||
app.use(LoggerMiddleware);
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import { SendSaleEstimateMailJob } from '@/services/Sales/Estimates/SendSaleEsti
|
||||
import { SaleReceiptMailNotificationJob } from '@/services/Sales/Receipts/SaleReceiptMailNotificationJob';
|
||||
import { PaymentReceiveMailNotificationJob } from '@/services/Sales/PaymentReceives/PaymentReceiveMailNotificationJob';
|
||||
import { PlaidFetchTransactionsJob } from '@/services/Banking/Plaid/PlaidFetchTransactionsJob';
|
||||
import { ImportDeleteExpiredFilesJobs } from '@/services/Import/jobs/ImportDeleteExpiredFilesJob';
|
||||
|
||||
export default ({ agenda }: { agenda: Agenda }) => {
|
||||
new ResetPasswordMailJob(agenda);
|
||||
@@ -25,6 +26,9 @@ export default ({ agenda }: { agenda: Agenda }) => {
|
||||
new SaleReceiptMailNotificationJob(agenda);
|
||||
new PaymentReceiveMailNotificationJob(agenda);
|
||||
new PlaidFetchTransactionsJob(agenda);
|
||||
new ImportDeleteExpiredFilesJobs(agenda);
|
||||
|
||||
agenda.start();
|
||||
agenda.start().then(() => {
|
||||
agenda.every('1 hours', 'delete-expired-imported-files', {});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ export default {
|
||||
sortOrder: 'DESC',
|
||||
sortField: 'name',
|
||||
},
|
||||
importable: true,
|
||||
fields: {
|
||||
name: {
|
||||
name: 'account.field.name',
|
||||
@@ -58,7 +59,7 @@ export default {
|
||||
fieldType: 'enumeration',
|
||||
options: ACCOUNT_TYPES.map((accountType) => ({
|
||||
label: accountType.label,
|
||||
key: accountType.key
|
||||
key: accountType.key,
|
||||
})),
|
||||
},
|
||||
active: {
|
||||
@@ -84,6 +85,49 @@ export default {
|
||||
fieldType: 'date',
|
||||
},
|
||||
},
|
||||
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'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,77 @@ 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,
|
||||
importHint: "Matches the item name or code."
|
||||
},
|
||||
rate: {
|
||||
name: 'Rate',
|
||||
fieldType: 'number',
|
||||
required: true,
|
||||
},
|
||||
quantity: {
|
||||
name: 'Quantity',
|
||||
fieldType: 'number',
|
||||
required: true,
|
||||
},
|
||||
description: {
|
||||
name: 'Line Description',
|
||||
fieldType: 'text',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,67 @@ 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,
|
||||
importHint: "The payment number should be unique."
|
||||
},
|
||||
paymentAccountId: {
|
||||
name: 'bill_payment.field.payment_account',
|
||||
fieldType: 'relation',
|
||||
relationModel: 'Account',
|
||||
relationImportMatch: ['name', 'code'],
|
||||
required: true,
|
||||
importHint: "Matches the account name or code."
|
||||
},
|
||||
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,
|
||||
importHint: "Matches the bill number."
|
||||
},
|
||||
paymentAmount: {
|
||||
name: 'bill_payment.field.entries.payment_amount',
|
||||
fieldType: 'number',
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -13,6 +13,7 @@ export default class CashflowTransaction extends TenantModel {
|
||||
amount: number;
|
||||
exchangeRate: number;
|
||||
uncategorize: boolean;
|
||||
uncategorizedTransaction!: boolean;
|
||||
|
||||
/**
|
||||
* Table name.
|
||||
@@ -86,6 +87,14 @@ export default class CashflowTransaction extends TenantModel {
|
||||
return this.typeMeta?.direction === CASHFLOW_DIRECTION.IN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detarmines whether the transaction imported from uncategorized transaction.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
get isCategroizedTranasction() {
|
||||
return !!this.uncategorizedTransaction;
|
||||
}
|
||||
|
||||
/**
|
||||
* Relationship mapping.
|
||||
*/
|
||||
|
||||
@@ -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,72 @@ 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,
|
||||
importHint: 'Matches the item name or code.',
|
||||
},
|
||||
rate: {
|
||||
name: 'Rate',
|
||||
fieldType: 'number',
|
||||
required: true,
|
||||
},
|
||||
quantity: {
|
||||
name: 'Quantity',
|
||||
fieldType: 'number',
|
||||
required: true,
|
||||
},
|
||||
description: {
|
||||
name: 'Description',
|
||||
fieldType: 'text',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,36 +1,137 @@
|
||||
export default {
|
||||
importable: true,
|
||||
defaultFilterField: 'displayName',
|
||||
defaultSort: {
|
||||
sortOrder: 'DESC',
|
||||
sortField: 'created_at',
|
||||
},
|
||||
fields: {
|
||||
first_name: {
|
||||
name: 'customer.field.first_name',
|
||||
name: 'vendor.field.first_name',
|
||||
column: 'first_name',
|
||||
fieldType: 'text',
|
||||
},
|
||||
last_name: {
|
||||
name: 'customer.field.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',
|
||||
fieldType: 'enumeration',
|
||||
options: [
|
||||
{ key: 'business', label: 'Business' },
|
||||
{ key: 'individual', label: 'Individual' },
|
||||
],
|
||||
required: true,
|
||||
},
|
||||
firstName: {
|
||||
name: 'customer.field.first_name',
|
||||
column: 'first_name',
|
||||
fieldType: 'text',
|
||||
},
|
||||
lastName: {
|
||||
name: 'customer.field.last_name',
|
||||
column: 'last_name',
|
||||
fieldType: 'text',
|
||||
},
|
||||
displayName: {
|
||||
name: 'customer.field.display_name',
|
||||
column: 'display_name',
|
||||
fieldType: 'text',
|
||||
required: true,
|
||||
},
|
||||
email: {
|
||||
name: 'customer.field.email',
|
||||
column: 'email',
|
||||
fieldType: 'text',
|
||||
},
|
||||
work_phone: {
|
||||
workPhone: {
|
||||
name: 'customer.field.work_phone',
|
||||
column: 'work_phone',
|
||||
fieldType: 'text',
|
||||
},
|
||||
personal_phone: {
|
||||
personalPhone: {
|
||||
name: 'customer.field.personal_phone',
|
||||
column: 'personal_phone',
|
||||
fieldType: 'text',
|
||||
},
|
||||
company_name: {
|
||||
companyName: {
|
||||
name: 'customer.field.company_name',
|
||||
column: 'company_name',
|
||||
fieldType: 'text',
|
||||
@@ -38,44 +139,110 @@ export default {
|
||||
website: {
|
||||
name: 'customer.field.website',
|
||||
column: 'website',
|
||||
fieldType: 'text',
|
||||
fieldType: 'url',
|
||||
},
|
||||
created_at: {
|
||||
name: 'customer.field.created_at',
|
||||
column: 'created_at',
|
||||
fieldType: 'date',
|
||||
},
|
||||
balance: {
|
||||
name: 'customer.field.balance',
|
||||
column: 'balance',
|
||||
fieldType: 'number',
|
||||
},
|
||||
opening_balance: {
|
||||
openingBalance: {
|
||||
name: 'customer.field.opening_balance',
|
||||
column: 'opening_balance',
|
||||
fieldType: 'number',
|
||||
},
|
||||
opening_balance_at: {
|
||||
openingBalanceAt: {
|
||||
name: 'customer.field.opening_balance_at',
|
||||
column: 'opening_balance_at',
|
||||
filterable: false,
|
||||
fieldType: 'date',
|
||||
},
|
||||
currency_code: {
|
||||
openingBalanceExchangeRate: {
|
||||
name: 'Opening Balance Ex. Rate',
|
||||
column: 'opening_balance_exchange_rate',
|
||||
fieldType: 'number',
|
||||
},
|
||||
currencyCode: {
|
||||
name: 'customer.field.currency',
|
||||
column: 'currency_code',
|
||||
fieldType: 'text',
|
||||
},
|
||||
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,
|
||||
note: {
|
||||
name: 'Note',
|
||||
column: 'note',
|
||||
fieldType: 'text',
|
||||
},
|
||||
active: {
|
||||
name: 'Active',
|
||||
column: 'active',
|
||||
fieldType: 'boolean',
|
||||
},
|
||||
// Billing Address
|
||||
billingAddress1: {
|
||||
name: 'Billing Address 1',
|
||||
column: 'billing_address1',
|
||||
fieldType: 'text',
|
||||
},
|
||||
billingAddress2: {
|
||||
name: 'Billing Address 2',
|
||||
column: 'billing_address2',
|
||||
fieldType: 'text',
|
||||
},
|
||||
billingAddressCity: {
|
||||
name: 'Billing Address City',
|
||||
column: 'billing_address_city',
|
||||
fieldType: 'text',
|
||||
},
|
||||
billingAddressCountry: {
|
||||
name: 'Billing Address Country',
|
||||
column: 'billing_address_country',
|
||||
fieldType: 'text',
|
||||
},
|
||||
billingAddressPostcode: {
|
||||
name: 'Billing Address Postcode',
|
||||
column: 'billing_address_postcode',
|
||||
fieldType: 'text',
|
||||
},
|
||||
billingAddressState: {
|
||||
name: 'Billing Address State',
|
||||
column: 'billing_address_state',
|
||||
fieldType: 'text',
|
||||
},
|
||||
billingAddressPhone: {
|
||||
name: 'Billing Address Phone',
|
||||
column: 'billing_address_phone',
|
||||
fieldType: 'text',
|
||||
},
|
||||
// Shipping Address
|
||||
shippingAddress1: {
|
||||
name: 'Shipping Address 1',
|
||||
column: 'shipping_address1',
|
||||
fieldType: 'text',
|
||||
},
|
||||
shippingAddress2: {
|
||||
name: 'Shipping Address 2',
|
||||
column: 'shipping_address2',
|
||||
fieldType: 'text',
|
||||
},
|
||||
shippingAddressCity: {
|
||||
name: 'Shipping Address City',
|
||||
column: 'shipping_address_city',
|
||||
fieldType: 'text',
|
||||
},
|
||||
shippingAddressCountry: {
|
||||
name: 'Shipping Address Country',
|
||||
column: 'shipping_address_country',
|
||||
fieldType: 'text',
|
||||
},
|
||||
shippingAddressPostcode: {
|
||||
name: 'Shipping Address Postcode',
|
||||
column: 'shipping_address_postcode',
|
||||
fieldType: 'text',
|
||||
},
|
||||
shippingAddressPhone: {
|
||||
name: 'Shipping Address Phone',
|
||||
column: 'shipping_address_phone',
|
||||
fieldType: 'text',
|
||||
},
|
||||
shippingAddressState: {
|
||||
name: 'Shipping Address State',
|
||||
column: 'shipping_address_state',
|
||||
fieldType: 'text',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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,71 @@ 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,
|
||||
importHint: "Matches the account name or code."
|
||||
},
|
||||
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,
|
||||
importHint: "Matches the account name or code."
|
||||
},
|
||||
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) {
|
||||
|
||||
@@ -1,51 +1,52 @@
|
||||
export default {
|
||||
importable: true,
|
||||
defaultFilterField: 'name',
|
||||
defaultSort: {
|
||||
sortField: 'name',
|
||||
sortOrder: 'DESC',
|
||||
},
|
||||
fields: {
|
||||
'type': {
|
||||
type: {
|
||||
name: 'item.field.type',
|
||||
column: 'type',
|
||||
fieldType: 'enumeration',
|
||||
options: [
|
||||
{ key: 'inventory', label: 'item.field.type.inventory', },
|
||||
{ key: 'inventory', label: 'item.field.type.inventory' },
|
||||
{ key: 'service', label: 'item.field.type.service' },
|
||||
{ key: 'non-inventory', label: 'item.field.type.non-inventory', },
|
||||
{ key: 'non-inventory', label: 'item.field.type.non-inventory' },
|
||||
],
|
||||
},
|
||||
'name': {
|
||||
name: {
|
||||
name: 'item.field.name',
|
||||
column: 'name',
|
||||
fieldType: 'text',
|
||||
},
|
||||
'code': {
|
||||
code: {
|
||||
name: 'item.field.code',
|
||||
column: 'code',
|
||||
fieldType: 'text',
|
||||
},
|
||||
'sellable': {
|
||||
sellable: {
|
||||
name: 'item.field.sellable',
|
||||
column: 'sellable',
|
||||
fieldType: 'boolean',
|
||||
},
|
||||
'purchasable': {
|
||||
purchasable: {
|
||||
name: 'item.field.purchasable',
|
||||
column: 'purchasable',
|
||||
fieldType: 'boolean',
|
||||
},
|
||||
'sell_price': {
|
||||
sell_price: {
|
||||
name: 'item.field.cost_price',
|
||||
column: 'sell_price',
|
||||
fieldType: 'number',
|
||||
},
|
||||
'cost_price': {
|
||||
cost_price: {
|
||||
name: 'item.field.cost_account',
|
||||
column: 'cost_price',
|
||||
fieldType: 'number',
|
||||
},
|
||||
'cost_account': {
|
||||
cost_account: {
|
||||
name: 'item.field.sell_account',
|
||||
column: 'cost_account_id',
|
||||
fieldType: 'relation',
|
||||
@@ -56,7 +57,7 @@ export default {
|
||||
relationEntityLabel: 'name',
|
||||
relationEntityKey: 'slug',
|
||||
},
|
||||
'sell_account': {
|
||||
sell_account: {
|
||||
name: 'item.field.sell_description',
|
||||
column: 'sell_account_id',
|
||||
fieldType: 'relation',
|
||||
@@ -67,7 +68,7 @@ export default {
|
||||
relationEntityLabel: 'name',
|
||||
relationEntityKey: 'slug',
|
||||
},
|
||||
'inventory_account': {
|
||||
inventory_account: {
|
||||
name: 'item.field.inventory_account',
|
||||
column: 'inventory_account_id',
|
||||
|
||||
@@ -77,27 +78,27 @@ export default {
|
||||
relationEntityLabel: 'name',
|
||||
relationEntityKey: 'slug',
|
||||
},
|
||||
'sell_description': {
|
||||
sell_description: {
|
||||
name: 'Sell description',
|
||||
column: 'sell_description',
|
||||
fieldType: 'text',
|
||||
},
|
||||
'purchase_description': {
|
||||
purchase_description: {
|
||||
name: 'Purchase description',
|
||||
column: 'purchase_description',
|
||||
fieldType: 'text',
|
||||
},
|
||||
'quantity_on_hand': {
|
||||
quantity_on_hand: {
|
||||
name: 'item.field.quantity_on_hand',
|
||||
column: 'quantity_on_hand',
|
||||
fieldType: 'number',
|
||||
},
|
||||
'note': {
|
||||
note: {
|
||||
name: 'item.field.note',
|
||||
column: 'note',
|
||||
fieldType: 'text',
|
||||
},
|
||||
'category': {
|
||||
category: {
|
||||
name: 'item.field.category',
|
||||
column: 'category_id',
|
||||
|
||||
@@ -107,17 +108,98 @@ export default {
|
||||
relationEntityLabel: 'name',
|
||||
relationEntityKey: 'id',
|
||||
},
|
||||
'active': {
|
||||
active: {
|
||||
name: 'item.field.active',
|
||||
column: 'active',
|
||||
fieldType: 'boolean',
|
||||
filterable: false,
|
||||
},
|
||||
'created_at': {
|
||||
created_at: {
|
||||
name: 'item.field.created_at',
|
||||
column: 'created_at',
|
||||
columnType: 'date',
|
||||
fieldType: 'date',
|
||||
},
|
||||
},
|
||||
fields2: {
|
||||
type: {
|
||||
name: 'item.field.type',
|
||||
fieldType: 'enumeration',
|
||||
options: [
|
||||
{ key: 'inventory', label: 'item.field.type.inventory' },
|
||||
{ key: 'service', label: 'item.field.type.service' },
|
||||
{ key: 'non-inventory', label: 'item.field.type.non-inventory' },
|
||||
],
|
||||
required: true,
|
||||
},
|
||||
name: {
|
||||
name: 'item.field.name',
|
||||
fieldType: 'text',
|
||||
required: true,
|
||||
},
|
||||
code: {
|
||||
name: 'item.field.code',
|
||||
fieldType: 'text',
|
||||
},
|
||||
sellable: {
|
||||
name: 'item.field.sellable',
|
||||
fieldType: 'boolean',
|
||||
},
|
||||
purchasable: {
|
||||
name: 'item.field.purchasable',
|
||||
fieldType: 'boolean',
|
||||
},
|
||||
sellPrice: {
|
||||
name: 'item.field.sell_price',
|
||||
fieldType: 'number',
|
||||
},
|
||||
cost_price: {
|
||||
name: 'item.field.cost_price',
|
||||
fieldType: 'number',
|
||||
},
|
||||
costAccount: {
|
||||
name: 'item.field.cost_account',
|
||||
fieldType: 'relation',
|
||||
relationModel: 'Account',
|
||||
relationImportMatch: ['name', 'code'],
|
||||
importHint: 'Matches the account name or code.',
|
||||
},
|
||||
sellAccount: {
|
||||
name: 'item.field.sell_account',
|
||||
fieldType: 'relation',
|
||||
relationModel: 'Account',
|
||||
relationImportMatch: ['name', 'code'],
|
||||
importHint: 'Matches the account name or code.',
|
||||
},
|
||||
inventoryAccount: {
|
||||
name: 'item.field.inventory_account',
|
||||
fieldType: 'relation',
|
||||
relationModel: 'Account',
|
||||
relationImportMatch: ['name', 'code'],
|
||||
importHint: 'Matches the account name or code.',
|
||||
},
|
||||
sellDescription: {
|
||||
name: 'Sell Description',
|
||||
fieldType: 'text',
|
||||
},
|
||||
purchaseDescription: {
|
||||
name: 'Purchase Description',
|
||||
fieldType: 'text',
|
||||
},
|
||||
note: {
|
||||
name: 'item.field.note',
|
||||
fieldType: 'text',
|
||||
},
|
||||
category: {
|
||||
name: 'item.field.category',
|
||||
fieldType: 'relation',
|
||||
relationModel: 'ItemCategory',
|
||||
relationImportMatch: ['name'],
|
||||
importHint: "Matches the category name."
|
||||
},
|
||||
active: {
|
||||
name: 'item.field.active',
|
||||
fieldType: 'boolean',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ export default {
|
||||
sortField: 'name',
|
||||
sortOrder: 'DESC',
|
||||
},
|
||||
importable: true,
|
||||
fields: {
|
||||
name: {
|
||||
name: 'item_category.field.name',
|
||||
@@ -27,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);
|
||||
}
|
||||
|
||||
@@ -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,65 @@ 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,
|
||||
importHint: "Matches the account name or code."
|
||||
},
|
||||
paymentReceiveNo: {
|
||||
name: 'payment_receive.field.payment_receive_no',
|
||||
fieldType: 'text',
|
||||
importHint: "The payment number should be unique."
|
||||
},
|
||||
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,
|
||||
importHint: "Matches the invoice number."
|
||||
},
|
||||
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,90 @@ 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,
|
||||
importHint: "Matches the item name or code."
|
||||
},
|
||||
rate: {
|
||||
name: 'invoice.field.rate',
|
||||
fieldType: 'number',
|
||||
required: true,
|
||||
},
|
||||
quantity: {
|
||||
name: 'invoice.field.quantity',
|
||||
fieldType: 'number',
|
||||
required: true,
|
||||
},
|
||||
description: {
|
||||
name: '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,84 @@ 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,
|
||||
importHint: "Matches the item name or code."
|
||||
},
|
||||
rate: {
|
||||
name: 'invoice.field.rate',
|
||||
fieldType: 'number',
|
||||
required: true,
|
||||
},
|
||||
quantity: {
|
||||
name: 'invoice.field.quantity',
|
||||
fieldType: 'number',
|
||||
required: true,
|
||||
},
|
||||
description: {
|
||||
name: 'invoice.field.description',
|
||||
fieldType: 'text',
|
||||
},
|
||||
},
|
||||
},
|
||||
delivered: {
|
||||
name: 'invoice.field.delivered',
|
||||
fieldType: 'boolean',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,82 @@ 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,
|
||||
importHint: "Matches the item name or code."
|
||||
},
|
||||
rate: {
|
||||
name: 'invoice.field.rate',
|
||||
fieldType: 'number',
|
||||
required: true,
|
||||
},
|
||||
quantity: {
|
||||
name: 'invoice.field.quantity',
|
||||
fieldType: 'number',
|
||||
required: true,
|
||||
},
|
||||
description: {
|
||||
name: 'invoice.field.description',
|
||||
fieldType: 'text',
|
||||
},
|
||||
},
|
||||
},
|
||||
statement: {
|
||||
name: 'Statement',
|
||||
fieldType: 'text',
|
||||
},
|
||||
receiptMessage: {
|
||||
name: 'Receipt Message',
|
||||
fieldType: 'text',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
function StatusFieldFilterQuery(query, role) {
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
export default {
|
||||
defaultFilterField: 'createdAt',
|
||||
defaultSort: {
|
||||
sortOrder: 'DESC',
|
||||
sortField: 'created_at',
|
||||
},
|
||||
importable: true,
|
||||
fields: {
|
||||
date: {
|
||||
name: 'Date',
|
||||
column: 'date',
|
||||
fieldType: 'date',
|
||||
},
|
||||
payee: {
|
||||
name: 'Payee',
|
||||
column: 'payee',
|
||||
fieldType: 'text',
|
||||
},
|
||||
description: {
|
||||
name: 'Description',
|
||||
column: 'description',
|
||||
fieldType: 'text',
|
||||
},
|
||||
referenceNo: {
|
||||
name: 'Reference No.',
|
||||
column: 'reference_no',
|
||||
fieldType: 'text',
|
||||
},
|
||||
amount: {
|
||||
name: 'Amount',
|
||||
column: 'Amount',
|
||||
fieldType: 'numeric',
|
||||
required: true,
|
||||
},
|
||||
account: {
|
||||
name: 'Account',
|
||||
column: 'account_id',
|
||||
fieldType: 'relation',
|
||||
to: { model: 'Account', to: 'id' },
|
||||
},
|
||||
createdAt: {
|
||||
name: 'Created At',
|
||||
column: 'createdAt',
|
||||
fieldType: 'date',
|
||||
importable: false,
|
||||
},
|
||||
},
|
||||
fields2: {
|
||||
date: {
|
||||
name: 'Date',
|
||||
fieldType: 'date',
|
||||
required: true,
|
||||
},
|
||||
payee: {
|
||||
name: 'Payee',
|
||||
fieldType: 'text',
|
||||
},
|
||||
description: {
|
||||
name: 'Description',
|
||||
fieldType: 'text',
|
||||
},
|
||||
referenceNo: {
|
||||
name: 'Reference No.',
|
||||
fieldType: 'text',
|
||||
},
|
||||
amount: {
|
||||
name: 'Amount',
|
||||
fieldType: 'number',
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,9 +1,15 @@
|
||||
/* eslint-disable global-require */
|
||||
import * as R from 'ramda';
|
||||
import { Model, ModelOptions, QueryContext, mixin } from 'objection';
|
||||
import TenantModel from 'models/TenantModel';
|
||||
import { Model, ModelOptions, QueryContext } from 'objection';
|
||||
import ModelSettings from './ModelSetting';
|
||||
import Account from './Account';
|
||||
import UncategorizedCashflowTransactionMeta from './UncategorizedCashflowTransaction.meta';
|
||||
|
||||
export default class UncategorizedCashflowTransaction extends TenantModel {
|
||||
export default class UncategorizedCashflowTransaction extends mixin(
|
||||
TenantModel,
|
||||
[ModelSettings]
|
||||
) {
|
||||
id!: number;
|
||||
amount!: number;
|
||||
categorized!: boolean;
|
||||
@@ -35,6 +41,10 @@ export default class UncategorizedCashflowTransaction extends TenantModel {
|
||||
];
|
||||
}
|
||||
|
||||
static get meta() {
|
||||
return UncategorizedCashflowTransactionMeta;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the withdrawal amount.
|
||||
* @returns {number}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
export default {
|
||||
defaultFilterField: 'display_name',
|
||||
defaultFilterField: 'displayName',
|
||||
defaultSort: {
|
||||
sortOrder: 'DESC',
|
||||
sortField: 'created_at',
|
||||
},
|
||||
importable: true,
|
||||
fields: {
|
||||
first_name: {
|
||||
name: 'vendor.field.first_name',
|
||||
@@ -89,4 +90,149 @@ 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',
|
||||
},
|
||||
billingAddress2: {
|
||||
name: 'Billing Address 2',
|
||||
column: 'billing_address2',
|
||||
fieldType: 'text',
|
||||
},
|
||||
billingAddressCity: {
|
||||
name: 'Billing Address City',
|
||||
column: 'billing_address_city',
|
||||
fieldType: 'text',
|
||||
},
|
||||
billingAddressCountry: {
|
||||
name: 'Billing Address Country',
|
||||
column: 'billing_address_country',
|
||||
fieldType: 'text',
|
||||
},
|
||||
billingAddressPostcode: {
|
||||
name: 'Billing Address Postcode',
|
||||
column: 'billing_address_postcode',
|
||||
fieldType: 'text',
|
||||
},
|
||||
billingAddressState: {
|
||||
name: 'Billing Address State',
|
||||
column: 'billing_address_state',
|
||||
fieldType: 'text',
|
||||
},
|
||||
billingAddressPhone: {
|
||||
name: 'Billing Address Phone',
|
||||
column: 'billing_address_phone',
|
||||
fieldType: 'text',
|
||||
},
|
||||
// Shipping Address
|
||||
shippingAddress1: {
|
||||
name: 'Shipping Address 1',
|
||||
column: 'shipping_address1',
|
||||
fieldType: 'text',
|
||||
},
|
||||
shippingAddress2: {
|
||||
name: 'Shipping Address 2',
|
||||
column: 'shipping_address2',
|
||||
fieldType: 'text',
|
||||
},
|
||||
shippingAddressCity: {
|
||||
name: 'Shipping Address City',
|
||||
column: 'shipping_address_city',
|
||||
fieldType: 'text',
|
||||
},
|
||||
shippingAddressCountry: {
|
||||
name: 'Shipping Address Country',
|
||||
column: 'shipping_address_country',
|
||||
fieldType: 'text',
|
||||
},
|
||||
shippingAddressPostcode: {
|
||||
name: 'Shipping Address Postcode',
|
||||
column: 'shipping_address_postcode',
|
||||
fieldType: 'text',
|
||||
},
|
||||
shippingAddressState: {
|
||||
name: 'Shipping Address State',
|
||||
column: 'shipping_address_state',
|
||||
fieldType: 'text',
|
||||
},
|
||||
shippingAddressPhone: {
|
||||
name: 'Shipping Address Phone',
|
||||
column: 'shipping_address_phone',
|
||||
fieldType: 'text',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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,69 @@ 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,
|
||||
importHint: "Matches the item name or code."
|
||||
},
|
||||
rate: {
|
||||
name: 'Rate',
|
||||
fieldType: 'number',
|
||||
required: true,
|
||||
},
|
||||
quantity: {
|
||||
name: 'Quantity',
|
||||
fieldType: 'number',
|
||||
required: true,
|
||||
},
|
||||
description: {
|
||||
name: 'Description',
|
||||
fieldType: 'text',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -16,6 +16,7 @@ import { ActivateAccount } from './ActivateAccount';
|
||||
import { GetAccounts } from './GetAccounts';
|
||||
import { GetAccount } from './GetAccount';
|
||||
import { GetAccountTransactions } from './GetAccountTransactions';
|
||||
import { Knex } from 'knex';
|
||||
|
||||
@Service()
|
||||
export class AccountsApplication {
|
||||
@@ -48,9 +49,10 @@ export class AccountsApplication {
|
||||
*/
|
||||
public createAccount = (
|
||||
tenantId: number,
|
||||
accountDTO: IAccountCreateDTO
|
||||
accountDTO: IAccountCreateDTO,
|
||||
trx?: Knex.Transaction
|
||||
): Promise<IAccount> => {
|
||||
return this.createAccountService.createAccount(tenantId, accountDTO);
|
||||
return this.createAccountService.createAccount(tenantId, accountDTO, trx);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
export const AccountsSampleData = [
|
||||
{
|
||||
'Account Name': 'Utilities Expense',
|
||||
'Account Code': 9000,
|
||||
Type: 'Expense',
|
||||
Description: 'Omnis voluptatum consequatur.',
|
||||
Active: 'T',
|
||||
'Currency Code': '',
|
||||
},
|
||||
{
|
||||
'Account Name': 'Unearned Revenue',
|
||||
'Account Code': 9010,
|
||||
Type: 'Long Term Liability',
|
||||
Description: 'Autem odit voluptas nihil unde.',
|
||||
Active: 'T',
|
||||
'Currency Code': '',
|
||||
},
|
||||
{
|
||||
'Account Name': 'Long-Term Debt',
|
||||
'Account Code': 9020,
|
||||
Type: 'Long Term Liability',
|
||||
Description: 'In voluptas cumque exercitationem.',
|
||||
Active: 'T',
|
||||
'Currency Code': '',
|
||||
},
|
||||
{
|
||||
'Account Name': 'Salaries and Wages Expense',
|
||||
'Account Code': 9030,
|
||||
Type: 'Expense',
|
||||
Description: 'Assumenda aspernatur soluta aliquid perspiciatis quasi.',
|
||||
Active: 'T',
|
||||
'Currency Code': '',
|
||||
},
|
||||
{
|
||||
'Account Name': 'Rental Income',
|
||||
'Account Code': 9040,
|
||||
Type: 'Income',
|
||||
Description: 'Omnis possimus amet occaecati inventore.',
|
||||
Active: 'T',
|
||||
'Currency Code': '',
|
||||
},
|
||||
{
|
||||
'Account Name': 'Paypal',
|
||||
'Account Code': 9050,
|
||||
Type: 'Bank',
|
||||
Description: 'In voluptas cumque exercitationem.',
|
||||
Active: 'T',
|
||||
'Currency Code': '',
|
||||
},
|
||||
];
|
||||
45
packages/server/src/services/Accounts/AccountsImportable.ts
Normal file
45
packages/server/src/services/Accounts/AccountsImportable.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { Knex } from 'knex';
|
||||
import { IAccountCreateDTO } from '@/interfaces';
|
||||
import { CreateAccount } from './CreateAccount';
|
||||
import { Importable } from '../Import/Importable';
|
||||
import { AccountsSampleData } from './AccountsImportable.SampleData';
|
||||
|
||||
@Service()
|
||||
export class AccountsImportable extends Importable {
|
||||
@Inject()
|
||||
private createAccountService: CreateAccount;
|
||||
|
||||
/**
|
||||
* Importing to account service.
|
||||
* @param {number} tenantId
|
||||
* @param {IAccountCreateDTO} createAccountDTO
|
||||
* @returns
|
||||
*/
|
||||
public importable(
|
||||
tenantId: number,
|
||||
createAccountDTO: IAccountCreateDTO,
|
||||
trx?: Knex.Transaction
|
||||
) {
|
||||
return this.createAccountService.createAccount(
|
||||
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 AccountsSampleData;
|
||||
}
|
||||
}
|
||||
@@ -97,9 +97,11 @@ export class CommandAccountValidators {
|
||||
query.whereNot('id', notAccountId);
|
||||
}
|
||||
});
|
||||
|
||||
if (account.length > 0) {
|
||||
throw new ServiceError(ERRORS.ACCOUNT_CODE_NOT_UNIQUE);
|
||||
throw new ServiceError(
|
||||
ERRORS.ACCOUNT_CODE_NOT_UNIQUE,
|
||||
'Account code is not unique.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,7 +126,10 @@ export class CommandAccountValidators {
|
||||
}
|
||||
});
|
||||
if (foundAccount) {
|
||||
throw new ServiceError(ERRORS.ACCOUNT_NAME_NOT_UNIQUE);
|
||||
throw new ServiceError(
|
||||
ERRORS.ACCOUNT_NAME_NOT_UNIQUE,
|
||||
'Account name is not unique.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -97,13 +97,14 @@ export class CreateAccount {
|
||||
|
||||
/**
|
||||
* Creates a new account on the storage.
|
||||
* @param {number} tenantId
|
||||
* @param {IAccountCreateDTO} accountDTO
|
||||
* @param {number} tenantId
|
||||
* @param {IAccountCreateDTO} accountDTO
|
||||
* @returns {Promise<IAccount>}
|
||||
*/
|
||||
public createAccount = async (
|
||||
tenantId: number,
|
||||
accountDTO: IAccountCreateDTO
|
||||
accountDTO: IAccountCreateDTO,
|
||||
trx?: Knex.Transaction
|
||||
): Promise<IAccount> => {
|
||||
const { Account } = this.tenancy.models(tenantId);
|
||||
|
||||
@@ -119,27 +120,31 @@ export class CreateAccount {
|
||||
tenantMeta.baseCurrency
|
||||
);
|
||||
// Creates a new account with associated transactions under unit-of-work envirement.
|
||||
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
|
||||
// Triggers `onAccountCreating` event.
|
||||
await this.eventPublisher.emitAsync(events.accounts.onCreating, {
|
||||
tenantId,
|
||||
accountDTO,
|
||||
trx,
|
||||
} as IAccountEventCreatingPayload);
|
||||
return this.uow.withTransaction(
|
||||
tenantId,
|
||||
async (trx: Knex.Transaction) => {
|
||||
// Triggers `onAccountCreating` event.
|
||||
await this.eventPublisher.emitAsync(events.accounts.onCreating, {
|
||||
tenantId,
|
||||
accountDTO,
|
||||
trx,
|
||||
} as IAccountEventCreatingPayload);
|
||||
|
||||
// Inserts account to the storage.
|
||||
const account = await Account.query(trx).insertAndFetch({
|
||||
...accountInputModel,
|
||||
});
|
||||
// Triggers `onAccountCreated` event.
|
||||
await this.eventPublisher.emitAsync(events.accounts.onCreated, {
|
||||
tenantId,
|
||||
account,
|
||||
accountId: account.id,
|
||||
trx,
|
||||
} as IAccountEventCreatedPayload);
|
||||
// Inserts account to the storage.
|
||||
const account = await Account.query(trx).insertAndFetch({
|
||||
...accountInputModel,
|
||||
});
|
||||
// Triggers `onAccountCreated` event.
|
||||
await this.eventPublisher.emitAsync(events.accounts.onCreated, {
|
||||
tenantId,
|
||||
account,
|
||||
accountId: account.id,
|
||||
trx,
|
||||
} as IAccountEventCreatedPayload);
|
||||
|
||||
return account;
|
||||
});
|
||||
return account;
|
||||
},
|
||||
trx
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { ERRORS } from './constants';
|
||||
@Service()
|
||||
export default class CashflowDeleteAccount {
|
||||
@Inject()
|
||||
tenancy: HasTenancyService;
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
/**
|
||||
* Validate the account has no associated cashflow transactions.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Knex } from 'knex';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import HasTenancyService from '../Tenancy/TenancyService';
|
||||
import UnitOfWork, { IsolationLevel } from '../UnitOfWork';
|
||||
import { Knex } from 'knex';
|
||||
import UnitOfWork from '../UnitOfWork';
|
||||
import { CreateUncategorizedTransactionDTO } from '@/interfaces';
|
||||
|
||||
@Service()
|
||||
@@ -19,10 +19,10 @@ export class CreateUncategorizedTransaction {
|
||||
*/
|
||||
public create(
|
||||
tenantId: number,
|
||||
createDTO: CreateUncategorizedTransactionDTO
|
||||
createDTO: CreateUncategorizedTransactionDTO,
|
||||
trx?: Knex.Transaction
|
||||
) {
|
||||
const { UncategorizedCashflowTransaction, Account } =
|
||||
this.tenancy.models(tenantId);
|
||||
const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);
|
||||
|
||||
return this.uow.withTransaction(
|
||||
tenantId,
|
||||
@@ -32,9 +32,9 @@ export class CreateUncategorizedTransaction {
|
||||
).insertAndFetch({
|
||||
...createDTO,
|
||||
});
|
||||
|
||||
return transaction;
|
||||
},
|
||||
trx
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,6 +86,7 @@ export default class NewCashflowTransactionService {
|
||||
'creditAccountId',
|
||||
'branchId',
|
||||
'plaidTransactionId',
|
||||
'uncategorizedTransactionId',
|
||||
]);
|
||||
// Retreive the next invoice number.
|
||||
const autoNextNumber =
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { Knex } from 'knex';
|
||||
import * as yup from 'yup';
|
||||
import { Importable } from '../Import/Importable';
|
||||
import { CreateUncategorizedTransaction } from './CreateUncategorizedTransaction';
|
||||
import { CreateUncategorizedTransactionDTO } from '@/interfaces';
|
||||
import { ImportableContext } from '../Import/interfaces';
|
||||
import HasTenancyService from '../Tenancy/TenancyService';
|
||||
import { BankTransactionsSampleData } from './constants';
|
||||
|
||||
@Service()
|
||||
export class UncategorizedTransactionsImportable extends Importable {
|
||||
@Inject()
|
||||
private createUncategorizedTransaction: CreateUncategorizedTransaction;
|
||||
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
/**
|
||||
* Passing the sheet DTO to create uncategorized transaction.
|
||||
* @param {number} tenantId
|
||||
* @param {number} tenantId
|
||||
* @param {any} createDTO
|
||||
* @param {Knex.Transaction} trx
|
||||
*/
|
||||
public async importable(
|
||||
tenantId: number,
|
||||
createDTO: CreateUncategorizedTransactionDTO,
|
||||
trx?: Knex.Transaction
|
||||
) {
|
||||
return this.createUncategorizedTransaction.create(tenantId, createDTO, trx);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transformes the DTO before validating and importing.
|
||||
* @param {CreateUncategorizedTransactionDTO} createDTO
|
||||
* @param {ImportableContext} context
|
||||
* @returns {CreateUncategorizedTransactionDTO}
|
||||
*/
|
||||
public transform(
|
||||
createDTO: CreateUncategorizedTransactionDTO,
|
||||
context?: ImportableContext
|
||||
): CreateUncategorizedTransactionDTO {
|
||||
return {
|
||||
...createDTO,
|
||||
accountId: context.import.paramsParsed.accountId,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sample data used to download sample sheet.
|
||||
* @returns {Record<string, any>[]}
|
||||
*/
|
||||
public sampleData(): Record<string, any>[] {
|
||||
return BankTransactionsSampleData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Params validation schema.
|
||||
* @returns {ValidationSchema[]}
|
||||
*/
|
||||
public paramsValidationSchema() {
|
||||
return yup.object().shape({
|
||||
accountId: yup.number().required(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the params existance asyncly.
|
||||
* @param {number} tenantId -
|
||||
* @param {Record<string, any>} params -
|
||||
*/
|
||||
public async validateParams(
|
||||
tenantId: number,
|
||||
params: Record<string, any>
|
||||
): Promise<void> {
|
||||
const { Account } = this.tenancy.models(tenantId);
|
||||
|
||||
if (params.accountId) {
|
||||
await Account.query().findById(params.accountId).throwIfNotFound({});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,10 @@ export const ERRORS = {
|
||||
ACCOUNT_HAS_ASSOCIATED_TRANSACTIONS: 'account_has_associated_transactions',
|
||||
TRANSACTION_ALREADY_CATEGORIZED: 'TRANSACTION_ALREADY_CATEGORIZED',
|
||||
TRANSACTION_ALREADY_UNCATEGORIZED: 'TRANSACTION_ALREADY_UNCATEGORIZED',
|
||||
UNCATEGORIZED_TRANSACTION_TYPE_INVALID: 'UNCATEGORIZED_TRANSACTION_TYPE_INVALID'
|
||||
UNCATEGORIZED_TRANSACTION_TYPE_INVALID:
|
||||
'UNCATEGORIZED_TRANSACTION_TYPE_INVALID',
|
||||
CANNOT_DELETE_TRANSACTION_CONVERTED_FROM_UNCATEGORIZED:
|
||||
'CANNOT_DELETE_TRANSACTION_CONVERTED_FROM_UNCATEGORIZED',
|
||||
};
|
||||
|
||||
export enum CASHFLOW_DIRECTION {
|
||||
@@ -74,3 +77,27 @@ export interface ICashflowTransactionTypeMeta {
|
||||
direction: CASHFLOW_DIRECTION;
|
||||
creditType: string[];
|
||||
}
|
||||
|
||||
export const BankTransactionsSampleData = [
|
||||
{
|
||||
Amount: '6,410.19',
|
||||
Date: '2024-03-26',
|
||||
Payee: 'MacGyver and Sons',
|
||||
'Reference No.': 'REF-1',
|
||||
Description: 'Commodi quo labore.',
|
||||
},
|
||||
{
|
||||
Amount: '8,914.17',
|
||||
Date: '2024-01-05',
|
||||
Payee: 'Eichmann - Bergnaum',
|
||||
'Reference No.': 'REF-1',
|
||||
Description: 'Quia enim et.',
|
||||
},
|
||||
{
|
||||
Amount: '6,200.88',
|
||||
Date: '2024-02-17',
|
||||
Payee: 'Luettgen, Mraz and Legros',
|
||||
'Reference No.': 'REF-1',
|
||||
Description: 'Occaecati consequuntur cum impedit illo.',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Inject, Service } from 'typedi';
|
||||
import events from '@/subscribers/events';
|
||||
import { ICashflowTransactionUncategorizedPayload } from '@/interfaces';
|
||||
import { DeleteCashflowTransaction } from '../DeleteCashflowTransactionService';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
|
||||
@Service()
|
||||
export class DeleteCashflowTransactionOnUncategorize {
|
||||
@@ -33,6 +34,7 @@ export class DeleteCashflowTransactionOnUncategorize {
|
||||
) {
|
||||
await this.deleteCashflowTransactionService.deleteCashflowTransaction(
|
||||
tenantId,
|
||||
|
||||
oldUncategorizedTransaction.categorizeRefId
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import events from '@/subscribers/events';
|
||||
import { ICommandCashflowDeletingPayload } from '@/interfaces';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import { ERRORS } from '../constants';
|
||||
import HasTenancyService from '@/services/Tenancy/TenancyService';
|
||||
|
||||
@Service()
|
||||
export class PreventDeleteTransactionOnDelete {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
/**
|
||||
* Attaches events with handlers.
|
||||
*/
|
||||
public attach = (bus) => {
|
||||
bus.subscribe(
|
||||
events.cashflow.onTransactionDeleting,
|
||||
this.preventDeleteCashflowTransactionHasUncategorizedTransaction.bind(
|
||||
this
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Prevent delete cashflow transaction has converted from uncategorized transaction.
|
||||
* @param {ICommandCashflowDeletingPayload} payload
|
||||
*/
|
||||
public async preventDeleteCashflowTransactionHasUncategorizedTransaction({
|
||||
tenantId,
|
||||
oldCashflowTransaction,
|
||||
trx,
|
||||
}: ICommandCashflowDeletingPayload) {
|
||||
const { UncategorizedCashflowTransaction } = this.tenancy.models(tenantId);
|
||||
if (oldCashflowTransaction.uncategorizedTransactionId) {
|
||||
const foundTransactions = await UncategorizedCashflowTransaction.query(
|
||||
trx
|
||||
).where({
|
||||
categorized: true,
|
||||
categorizeRefId: oldCashflowTransaction.id,
|
||||
categorizeRefType: 'CashflowTransaction',
|
||||
});
|
||||
// Throw the error if the cashflow transaction still linked to uncategorized transaction.
|
||||
if (foundTransactions.length > 0) {
|
||||
throw new ServiceError(
|
||||
ERRORS.CANNOT_DELETE_TRANSACTION_CONVERTED_FROM_UNCATEGORIZED,
|
||||
'Cannot delete cashflow transaction converted from uncategorized transaction.'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -61,6 +61,7 @@ export const transformCategorizeTransToCashflow = (
|
||||
amount: uncategorizeModel.amount,
|
||||
transactionNumber: categorizeDTO.transactionNumber,
|
||||
transactionType: categorizeDTO.transactionType,
|
||||
uncategorizedTransactionId: uncategorizeModel.id,
|
||||
publish: true,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -36,7 +36,7 @@ export class CreateCustomer {
|
||||
public async createCustomer(
|
||||
tenantId: number,
|
||||
customerDTO: ICustomerNewDTO,
|
||||
authorizedUser: ISystemUser
|
||||
trx?: Knex.Transaction
|
||||
): Promise<ICustomer> {
|
||||
const { Contact } = this.tenancy.models(tenantId);
|
||||
|
||||
@@ -46,28 +46,31 @@ export class CreateCustomer {
|
||||
customerDTO
|
||||
);
|
||||
// Creates a new customer under unit-of-work envirement.
|
||||
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
|
||||
// Triggers `onCustomerCreating` event.
|
||||
await this.eventPublisher.emitAsync(events.customers.onCreating, {
|
||||
tenantId,
|
||||
customerDTO,
|
||||
trx,
|
||||
} as ICustomerEventCreatingPayload);
|
||||
return this.uow.withTransaction(
|
||||
tenantId,
|
||||
async (trx: Knex.Transaction) => {
|
||||
// Triggers `onCustomerCreating` event.
|
||||
await this.eventPublisher.emitAsync(events.customers.onCreating, {
|
||||
tenantId,
|
||||
customerDTO,
|
||||
trx,
|
||||
} as ICustomerEventCreatingPayload);
|
||||
|
||||
// Creates a new contact as customer.
|
||||
const customer = await Contact.query(trx).insertAndFetch({
|
||||
...customerObj,
|
||||
});
|
||||
// Triggers `onCustomerCreated` event.
|
||||
await this.eventPublisher.emitAsync(events.customers.onCreated, {
|
||||
customer,
|
||||
tenantId,
|
||||
customerId: customer.id,
|
||||
authorizedUser,
|
||||
trx,
|
||||
} as ICustomerEventCreatedPayload);
|
||||
// Creates a new contact as customer.
|
||||
const customer = await Contact.query(trx).insertAndFetch({
|
||||
...customerObj,
|
||||
});
|
||||
// Triggers `onCustomerCreated` event.
|
||||
await this.eventPublisher.emitAsync(events.customers.onCreated, {
|
||||
customer,
|
||||
tenantId,
|
||||
customerId: customer.id,
|
||||
trx,
|
||||
} as ICustomerEventCreatedPayload);
|
||||
|
||||
return customer;
|
||||
});
|
||||
return customer;
|
||||
},
|
||||
trx
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import moment from 'moment';
|
||||
import { defaultTo, omit, isEmpty } from 'lodash';
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { Service } from 'typedi';
|
||||
import {
|
||||
ContactService,
|
||||
ICustomer,
|
||||
@@ -51,6 +51,10 @@ export class CreateEditCustomerDTO {
|
||||
).toMySqlDateTime(),
|
||||
}
|
||||
: {}),
|
||||
openingBalanceExchangeRate: defaultTo(
|
||||
customerDTO.openingBalanceExchangeRate,
|
||||
1
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
ICustomerEditDTO,
|
||||
ICustomerEventEditedPayload,
|
||||
ICustomerEventEditingPayload,
|
||||
ISystemUser,
|
||||
} from '@/interfaces';
|
||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||
import UnitOfWork from '@/services/UnitOfWork';
|
||||
|
||||
@@ -53,13 +53,8 @@ export class CustomersApplication {
|
||||
public createCustomer = (
|
||||
tenantId: number,
|
||||
customerDTO: ICustomerNewDTO,
|
||||
authorizedUser: ISystemUser
|
||||
) => {
|
||||
return this.createCustomerService.createCustomer(
|
||||
tenantId,
|
||||
customerDTO,
|
||||
authorizedUser
|
||||
);
|
||||
return this.createCustomerService.createCustomer(tenantId, customerDTO);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { Importable } from '@/services/Import/Importable';
|
||||
import { CreateCustomer } from './CRUD/CreateCustomer';
|
||||
import { Knex } from 'knex';
|
||||
import { ICustomer, ICustomerNewDTO } from '@/interfaces';
|
||||
import { CustomersSampleData } from './_SampleData';
|
||||
|
||||
@Service()
|
||||
export class CustomersImportable extends Importable {
|
||||
@Inject()
|
||||
private createCustomerService: CreateCustomer;
|
||||
|
||||
/**
|
||||
* Mapps the imported data to create a new customer service.
|
||||
* @param {number} tenantId
|
||||
* @param {ICustomerNewDTO} createDTO
|
||||
* @param {Knex.Transaction} trx
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public async importable(
|
||||
tenantId: number,
|
||||
createDTO: ICustomerNewDTO,
|
||||
trx?: Knex.Transaction<any, any[]>
|
||||
): Promise<void> {
|
||||
await this.createCustomerService.createCustomer(tenantId, createDTO, trx);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the sample data of customers used to download sample sheet.
|
||||
*/
|
||||
public sampleData(): any[] {
|
||||
return CustomersSampleData;
|
||||
}
|
||||
}
|
||||
158
packages/server/src/services/Contacts/Customers/_SampleData.ts
Normal file
158
packages/server/src/services/Contacts/Customers/_SampleData.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
|
||||
export const CustomersSampleData = [
|
||||
{
|
||||
"Customer Type": "Business",
|
||||
"First Name": "Nicolette",
|
||||
"Last Name": "Schamberger",
|
||||
"Company Name": "Homenick - Hane",
|
||||
"Display Name": "Rowland Rowe",
|
||||
"Email": "cicero86@yahoo.com",
|
||||
"Personal Phone Number": "811-603-2235",
|
||||
"Work Phone Number": "906-993-5190",
|
||||
"Website": "http://google.com",
|
||||
"Opening Balance": 54302.23,
|
||||
"Opening Balance At": "2022-02-02",
|
||||
"Opening Balance Ex. Rate": 2,
|
||||
"Currency": "LYD",
|
||||
"Active": "F",
|
||||
"Note": "Doloribus autem optio temporibus dolores mollitia sit.",
|
||||
"Billing Address 1": "862 Jessika Well",
|
||||
"Billing Address 2": "1091 Dorthy Mount",
|
||||
"Billing Address City": "Deckowfort",
|
||||
"Billing Address Country": "Ghana",
|
||||
"Billing Address Phone": "825-011-5207",
|
||||
"Billing Address Postcode": "38228",
|
||||
"Billing Address State": "Oregon",
|
||||
"Shipping Address 1": "37626 Thiel Villages",
|
||||
"Shipping Address 2": "132 Batz Avenue",
|
||||
"Shipping Address City": "Pagacburgh",
|
||||
"Shipping Address Country": "Albania",
|
||||
"Shipping Address Phone": "171-546-3701",
|
||||
"Shipping Address Postcode": "13709",
|
||||
"Shipping Address State": "Georgia"
|
||||
},
|
||||
{
|
||||
"Customer Type": "Business",
|
||||
"First Name": "Hermann",
|
||||
"Last Name": "Crooks",
|
||||
"Company Name": "Veum - Schaefer",
|
||||
"Display Name": "Harley Veum",
|
||||
"Email": "immanuel56@hotmail.com",
|
||||
"Personal Phone Number": "449-780-9999",
|
||||
"Work Phone Number": "970-473-5785",
|
||||
"Website": "http://google.com",
|
||||
"Opening Balance": 54302.23,
|
||||
"Opening Balance At": "2022-02-02",
|
||||
"Opening Balance Ex. Rate": 2,
|
||||
"Currency": "LYD",
|
||||
"Active": "T",
|
||||
"Note": "Doloribus dolore dolor dicta vitae in fugit nisi quibusdam.",
|
||||
"Billing Address 1": "532 Simonis Spring",
|
||||
"Billing Address 2": "3122 Nicolas Inlet",
|
||||
"Billing Address City": "East Matteofort",
|
||||
"Billing Address Country": "Holy See (Vatican City State)",
|
||||
"Billing Address Phone": "366-084-8629",
|
||||
"Billing Address Postcode": "41607",
|
||||
"Billing Address State": "Montana",
|
||||
"Shipping Address 1": "2889 Tremblay Plaza",
|
||||
"Shipping Address 2": "71355 Kutch Isle",
|
||||
"Shipping Address City": "D'Amorehaven",
|
||||
"Shipping Address Country": "Monaco",
|
||||
"Shipping Address Phone": "614-189-3328",
|
||||
"Shipping Address Postcode": "09634-0435",
|
||||
"Shipping Address State": "Nevada"
|
||||
},
|
||||
{
|
||||
"Customer Type": "Business",
|
||||
"First Name": "Nellie",
|
||||
"Last Name": "Gulgowski",
|
||||
"Company Name": "Boyle, Heller and Jones",
|
||||
"Display Name": "Randall Kohler",
|
||||
"Email": "anibal_frami@yahoo.com",
|
||||
"Personal Phone Number": "498-578-0740",
|
||||
"Work Phone Number": "394-550-6827",
|
||||
"Website": "http://google.com",
|
||||
"Opening Balance": 54302.23,
|
||||
"Opening Balance At": "2022-02-02",
|
||||
"Opening Balance Ex. Rate": 2,
|
||||
"Currency": "LYD",
|
||||
"Active": "T",
|
||||
"Note": "Vero quibusdam rem fugit aperiam est modi.",
|
||||
"Billing Address 1": "214 Sauer Villages",
|
||||
"Billing Address 2": "30687 Kacey Square",
|
||||
"Billing Address City": "Jayceborough",
|
||||
"Billing Address Country": "Benin",
|
||||
"Billing Address Phone": "332-820-1127",
|
||||
"Billing Address Postcode": "16425-3887",
|
||||
"Billing Address State": "Mississippi",
|
||||
"Shipping Address 1": "562 Diamond Loaf",
|
||||
"Shipping Address 2": "9595 Satterfield Trafficway",
|
||||
"Shipping Address City": "Alexandrinefort",
|
||||
"Shipping Address Country": "Puerto Rico",
|
||||
"Shipping Address Phone": "776-500-8456",
|
||||
"Shipping Address Postcode": "30258",
|
||||
"Shipping Address State": "South Dakota"
|
||||
},
|
||||
{
|
||||
"Customer Type": "Business",
|
||||
"First Name": "Stone",
|
||||
"Last Name": "Jerde",
|
||||
"Company Name": "Cassin, Casper and Maggio",
|
||||
"Display Name": "Clint McLaughlin",
|
||||
"Email": "nathanael22@yahoo.com",
|
||||
"Personal Phone Number": "562-790-6059",
|
||||
"Work Phone Number": "686-838-0027",
|
||||
"Website": "http://google.com",
|
||||
"Opening Balance": 54302.23,
|
||||
"Opening Balance At": "2022-02-02",
|
||||
"Opening Balance Ex. Rate": 2,
|
||||
"Currency": "LYD",
|
||||
"Active": "F",
|
||||
"Note": "Quis cumque molestias rerum.",
|
||||
"Billing Address 1": "22590 Cathy Harbor",
|
||||
"Billing Address 2": "24493 Brycen Brooks",
|
||||
"Billing Address City": "Elnorashire",
|
||||
"Billing Address Country": "Andorra",
|
||||
"Billing Address Phone": "701-852-8005",
|
||||
"Billing Address Postcode": "5680",
|
||||
"Billing Address State": "Nevada",
|
||||
"Shipping Address 1": "5355 Erdman Bridge",
|
||||
"Shipping Address 2": "421 Jeanette Camp",
|
||||
"Shipping Address City": "East Philip",
|
||||
"Shipping Address Country": "Venezuela",
|
||||
"Shipping Address Phone": "426-119-0858",
|
||||
"Shipping Address Postcode": "34929-0501",
|
||||
"Shipping Address State": "Tennessee"
|
||||
},
|
||||
{
|
||||
"Customer Type": "Individual",
|
||||
"First Name": "Lempi",
|
||||
"Last Name": "Kling",
|
||||
"Company Name": "Schamberger, O'Connell and Bechtelar",
|
||||
"Display Name": "Alexie Barton",
|
||||
"Email": "eulah.kreiger@hotmail.com",
|
||||
"Personal Phone Number": "745-756-1063",
|
||||
"Work Phone Number": "965-150-1945",
|
||||
"Website": "http://google.com",
|
||||
"Opening Balance": 54302.23,
|
||||
"Opening Balance At": "2022-02-02",
|
||||
"Opening Balance Ex. Rate": 2,
|
||||
"Currency": "LYD",
|
||||
"Active": "F",
|
||||
"Note": "Maxime laboriosam hic voluptate maiores est officia.",
|
||||
"Billing Address 1": "0851 Jones Flat",
|
||||
"Billing Address 2": "845 Bailee Drives",
|
||||
"Billing Address City": "Kamrenport",
|
||||
"Billing Address Country": "Niger",
|
||||
"Billing Address Phone": "220-125-0608",
|
||||
"Billing Address Postcode": "30311",
|
||||
"Billing Address State": "Delaware",
|
||||
"Shipping Address 1": "929 Ferry Row",
|
||||
"Shipping Address 2": "020 Adam Plaza",
|
||||
"Shipping Address City": "West Carmellaside",
|
||||
"Shipping Address Country": "Ghana",
|
||||
"Shipping Address Phone": "053-333-6679",
|
||||
"Shipping Address Postcode": "79221-4681",
|
||||
"Shipping Address State": "Illinois"
|
||||
}
|
||||
]
|
||||
@@ -49,6 +49,10 @@ export class CreateEditVendorDTO {
|
||||
).toMySqlDateTime(),
|
||||
}
|
||||
: {}),
|
||||
openingBalanceExchangeRate: defaultTo(
|
||||
vendorDTO.openingBalanceExchangeRate,
|
||||
1
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ export class CreateVendor {
|
||||
public async createVendor(
|
||||
tenantId: number,
|
||||
vendorDTO: IVendorNewDTO,
|
||||
authorizedUser: ISystemUser
|
||||
trx?: Knex.Transaction
|
||||
) {
|
||||
const { Contact } = this.tenancy.models(tenantId);
|
||||
|
||||
@@ -45,28 +45,31 @@ export class CreateVendor {
|
||||
vendorDTO
|
||||
);
|
||||
// Creates vendor contact under unit-of-work evnirement.
|
||||
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
|
||||
// Triggers `onVendorCreating` event.
|
||||
await this.eventPublisher.emitAsync(events.vendors.onCreating, {
|
||||
tenantId,
|
||||
vendorDTO,
|
||||
trx,
|
||||
} as IVendorEventCreatingPayload);
|
||||
return this.uow.withTransaction(
|
||||
tenantId,
|
||||
async (trx: Knex.Transaction) => {
|
||||
// Triggers `onVendorCreating` event.
|
||||
await this.eventPublisher.emitAsync(events.vendors.onCreating, {
|
||||
tenantId,
|
||||
vendorDTO,
|
||||
trx,
|
||||
} as IVendorEventCreatingPayload);
|
||||
|
||||
// Creates a new contact as vendor.
|
||||
const vendor = await Contact.query(trx).insertAndFetch({
|
||||
...vendorObject,
|
||||
});
|
||||
// Triggers `onVendorCreated` event.
|
||||
await this.eventPublisher.emitAsync(events.vendors.onCreated, {
|
||||
tenantId,
|
||||
vendorId: vendor.id,
|
||||
vendor,
|
||||
authorizedUser,
|
||||
trx,
|
||||
} as IVendorEventCreatedPayload);
|
||||
// Creates a new contact as vendor.
|
||||
const vendor = await Contact.query(trx).insertAndFetch({
|
||||
...vendorObject,
|
||||
});
|
||||
// Triggers `onVendorCreated` event.
|
||||
await this.eventPublisher.emitAsync(events.vendors.onCreated, {
|
||||
tenantId,
|
||||
vendorId: vendor.id,
|
||||
vendor,
|
||||
trx,
|
||||
} as IVendorEventCreatedPayload);
|
||||
|
||||
return vendor;
|
||||
});
|
||||
return vendor;
|
||||
},
|
||||
trx
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { Importable } from '@/services/Import/Importable';
|
||||
import { CreateVendor } from './CRUD/CreateVendor';
|
||||
import { Knex } from 'knex';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { VendorsSampleData } from './_SampleData';
|
||||
|
||||
@Service()
|
||||
export class VendorsImportable extends Importable {
|
||||
@Inject()
|
||||
private createVendorService: CreateVendor;
|
||||
|
||||
/**
|
||||
* Maps the imported data to create a new vendor service.
|
||||
* @param {number} tenantId
|
||||
* @param {} createDTO
|
||||
* @param {Knex.Transaction} trx
|
||||
*/
|
||||
public async importable(
|
||||
tenantId: number,
|
||||
createDTO: any,
|
||||
trx?: Knex.Transaction<any, any[]>
|
||||
): Promise<void> {
|
||||
await this.createVendorService.createVendor(tenantId, createDTO, trx);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the sample data of vendors sample sheet.
|
||||
*/
|
||||
public sampleData(): any[] {
|
||||
return VendorsSampleData;
|
||||
}
|
||||
}
|
||||
122
packages/server/src/services/Contacts/Vendors/_SampleData.ts
Normal file
122
packages/server/src/services/Contacts/Vendors/_SampleData.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
export const VendorsSampleData = [
|
||||
{
|
||||
"First Name": "Nicolette",
|
||||
"Last Name": "Schamberger",
|
||||
"Company Name": "Homenick - Hane",
|
||||
"Display Name": "Rowland Rowe",
|
||||
"Email": "cicero86@yahoo.com",
|
||||
"Personal Phone Number": "811-603-2235",
|
||||
"Work Phone Number": "906-993-5190",
|
||||
"Website": "http://google.com",
|
||||
"Opening Balance": 54302.23,
|
||||
"Opening Balance At": "2022-02-02",
|
||||
"Opening Balance Ex. Rate": 2,
|
||||
"Currency": "LYD",
|
||||
"Active": "T",
|
||||
"Note": "Doloribus autem optio temporibus dolores mollitia sit.",
|
||||
"Billing Address 1": "862 Jessika Well",
|
||||
"Billing Address 2": "1091 Dorthy Mount",
|
||||
"Billing Address City": "Deckowfort",
|
||||
"Billing Address Country": "Ghana",
|
||||
"Billing Address Phone": "825-011-5207",
|
||||
"Billing Address Postcode": "38228",
|
||||
"Billing Address State": "Oregon",
|
||||
"Shipping Address 1": "37626 Thiel Villages",
|
||||
"Shipping Address 2": "132 Batz Avenue",
|
||||
"Shipping Address City": "Pagacburgh",
|
||||
"Shipping Address Country": "Albania",
|
||||
"Shipping Address Phone": "171-546-3701",
|
||||
"Shipping Address Postcode": "13709",
|
||||
"Shipping Address State": "Georgia"
|
||||
},
|
||||
{
|
||||
"First Name": "Hermann",
|
||||
"Last Name": "Crooks",
|
||||
"Company Name": "Veum - Schaefer",
|
||||
"Display Name": "Harley Veum",
|
||||
"Email": "immanuel56@hotmail.com",
|
||||
"Personal Phone Number": "449-780-9999",
|
||||
"Work Phone Number": "970-473-5785",
|
||||
"Website": "http://google.com",
|
||||
"Opening Balance": 54302.23,
|
||||
"Opening Balance At": "2022-02-02",
|
||||
"Opening Balance Ex. Rate": 2,
|
||||
"Currency": "LYD",
|
||||
"Active": "T",
|
||||
"Note": "Doloribus dolore dolor dicta vitae in fugit nisi quibusdam.",
|
||||
"Billing Address 1": "532 Simonis Spring",
|
||||
"Billing Address 2": "3122 Nicolas Inlet",
|
||||
"Billing Address City": "East Matteofort",
|
||||
"Billing Address Country": "Holy See (Vatican City State)",
|
||||
"Billing Address Phone": "366-084-8629",
|
||||
"Billing Address Postcode": "41607",
|
||||
"Billing Address State": "Montana",
|
||||
"Shipping Address 1": "2889 Tremblay Plaza",
|
||||
"Shipping Address 2": "71355 Kutch Isle",
|
||||
"Shipping Address City": "D'Amorehaven",
|
||||
"Shipping Address Country": "Monaco",
|
||||
"Shipping Address Phone": "614-189-3328",
|
||||
"Shipping Address Postcode": "09634-0435",
|
||||
"Shipping Address State": "Nevada"
|
||||
},
|
||||
{
|
||||
"First Name": "Nellie",
|
||||
"Last Name": "Gulgowski",
|
||||
"Company Name": "Boyle, Heller and Jones",
|
||||
"Display Name": "Randall Kohler",
|
||||
"Email": "anibal_frami@yahoo.com",
|
||||
"Personal Phone Number": "498-578-0740",
|
||||
"Work Phone Number": "394-550-6827",
|
||||
"Website": "http://google.com",
|
||||
"Opening Balance": 54302.23,
|
||||
"Opening Balance At": "2022-02-02",
|
||||
"Opening Balance Ex. Rate": 2,
|
||||
"Currency": "LYD",
|
||||
"Active": "T",
|
||||
"Note": "Vero quibusdam rem fugit aperiam est modi.",
|
||||
"Billing Address 1": "214 Sauer Villages",
|
||||
"Billing Address 2": "30687 Kacey Square",
|
||||
"Billing Address City": "Jayceborough",
|
||||
"Billing Address Country": "Benin",
|
||||
"Billing Address Phone": "332-820-1127",
|
||||
"Billing Address Postcode": "16425-3887",
|
||||
"Billing Address State": "Mississippi",
|
||||
"Shipping Address 1": "562 Diamond Loaf",
|
||||
"Shipping Address 2": "9595 Satterfield Trafficway",
|
||||
"Shipping Address City": "Alexandrinefort",
|
||||
"Shipping Address Country": "Puerto Rico",
|
||||
"Shipping Address Phone": "776-500-8456",
|
||||
"Shipping Address Postcode": "30258",
|
||||
"Shipping Address State": "South Dakota"
|
||||
},
|
||||
{
|
||||
"First Name": "Stone",
|
||||
"Last Name": "Jerde",
|
||||
"Company Name": "Cassin, Casper and Maggio",
|
||||
"Display Name": "Clint McLaughlin",
|
||||
"Email": "nathanael22@yahoo.com",
|
||||
"Personal Phone Number": "562-790-6059",
|
||||
"Work Phone Number": "686-838-0027",
|
||||
"Website": "http://google.com",
|
||||
"Opening Balance": 54302.23,
|
||||
"Opening Balance At": "2022-02-02",
|
||||
"Opening Balance Ex. Rate": 2,
|
||||
"Currency": "LYD",
|
||||
"Active": "T",
|
||||
"Note": "Quis cumque molestias rerum.",
|
||||
"Billing Address 1": "22590 Cathy Harbor",
|
||||
"Billing Address 2": "24493 Brycen Brooks",
|
||||
"Billing Address City": "Elnorashire",
|
||||
"Billing Address Country": "Andorra",
|
||||
"Billing Address Phone": "701-852-8005",
|
||||
"Billing Address Postcode": "5680",
|
||||
"Billing Address State": "Nevada",
|
||||
"Shipping Address 1": "5355 Erdman Bridge",
|
||||
"Shipping Address 2": "421 Jeanette Camp",
|
||||
"Shipping Address City": "East Philip",
|
||||
"Shipping Address Country": "Venezuela",
|
||||
"Shipping Address Phone": "426-119-0858",
|
||||
"Shipping Address Postcode": "34929-0501",
|
||||
"Shipping Address State": "Tennessee"
|
||||
}
|
||||
]
|
||||
@@ -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
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
46
packages/server/src/services/Expenses/ExpensesImportable.ts
Normal file
46
packages/server/src/services/Expenses/ExpensesImportable.ts
Normal file
@@ -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',
|
||||
},
|
||||
];
|
||||
|
||||
196
packages/server/src/services/Import/ImportFileCommon.ts
Normal file
196
packages/server/src/services/Import/ImportFileCommon.ts
Normal file
@@ -0,0 +1,196 @@
|
||||
import XLSX from 'xlsx';
|
||||
import bluebird from 'bluebird';
|
||||
import * as R from 'ramda';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { first } from 'lodash';
|
||||
import { ImportFileDataValidator } from './ImportFileDataValidator';
|
||||
import { Knex } from 'knex';
|
||||
import {
|
||||
ImportInsertError,
|
||||
ImportOperError,
|
||||
ImportOperSuccess,
|
||||
ImportableContext,
|
||||
} from './interfaces';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import { getUniqueImportableValue, trimObject } from './_utils';
|
||||
import { ImportableResources } from './ImportableResources';
|
||||
import ResourceService from '../Resource/ResourceService';
|
||||
import HasTenancyService from '../Tenancy/TenancyService';
|
||||
import { Import } from '@/system/models';
|
||||
|
||||
@Service()
|
||||
export class ImportFileCommon {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
private importFileValidator: ImportFileDataValidator;
|
||||
|
||||
@Inject()
|
||||
private importable: ImportableResources;
|
||||
|
||||
@Inject()
|
||||
private resource: ResourceService;
|
||||
|
||||
/**
|
||||
* Maps the columns of the imported data based on the provided mapping attributes.
|
||||
* @param {Record<string, any>[]} body - The array of data objects to map.
|
||||
* @param {ImportMappingAttr[]} map - The mapping attributes.
|
||||
* @returns {Record<string, any>[]} - The mapped data objects.
|
||||
*/
|
||||
public parseXlsxSheet(buffer: Buffer): Record<string, unknown>[] {
|
||||
const workbook = XLSX.read(buffer, { type: 'buffer', raw: true });
|
||||
|
||||
const firstSheetName = workbook.SheetNames[0];
|
||||
const worksheet = workbook.Sheets[firstSheetName];
|
||||
|
||||
return XLSX.utils.sheet_to_json(worksheet, {});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Imports the given parsed data to the resource storage through registered importable service.
|
||||
* @param {number} tenantId -
|
||||
* @param {string} resourceName - Resource name.
|
||||
* @param {Record<string, any>} parsedData - Parsed data.
|
||||
* @param {Knex.Transaction} trx - Knex transaction.
|
||||
* @returns {Promise<[ImportOperSuccess[], ImportOperError[]]>}
|
||||
*/
|
||||
public async import(
|
||||
tenantId: number,
|
||||
importFile: Import,
|
||||
parsedData: Record<string, any>[],
|
||||
trx?: Knex.Transaction
|
||||
): Promise<[ImportOperSuccess[], ImportOperError[]]> {
|
||||
const resourceFields = this.resource.getResourceFields2(
|
||||
tenantId,
|
||||
importFile.resource
|
||||
);
|
||||
const ImportableRegistry = this.importable.registry;
|
||||
const importable = ImportableRegistry.getImportable(importFile.resource);
|
||||
|
||||
const concurrency = importable.concurrency || 10;
|
||||
|
||||
const success: ImportOperSuccess[] = [];
|
||||
const failed: ImportOperError[] = [];
|
||||
|
||||
const importAsync = async (objectDTO, index: number): Promise<void> => {
|
||||
const context: ImportableContext = {
|
||||
rowIndex: index,
|
||||
import: importFile,
|
||||
};
|
||||
const transformedDTO = importable.transform(objectDTO, context);
|
||||
const rowNumber = index + 1;
|
||||
const uniqueValue = getUniqueImportableValue(resourceFields, objectDTO);
|
||||
const errorContext = {
|
||||
rowNumber,
|
||||
uniqueValue,
|
||||
};
|
||||
try {
|
||||
// Validate the DTO object before passing it to the service layer.
|
||||
await this.importFileValidator.validateData(
|
||||
resourceFields,
|
||||
transformedDTO
|
||||
);
|
||||
try {
|
||||
// Run the importable function and listen to the errors.
|
||||
const data = await importable.importable(
|
||||
tenantId,
|
||||
transformedDTO,
|
||||
trx
|
||||
);
|
||||
success.push({ index, data });
|
||||
} catch (err) {
|
||||
if (err instanceof ServiceError) {
|
||||
const error: ImportInsertError[] = [
|
||||
{
|
||||
errorCode: 'ServiceError',
|
||||
errorMessage: err.message || err.errorType,
|
||||
...errorContext,
|
||||
},
|
||||
];
|
||||
failed.push({ index, error });
|
||||
} else {
|
||||
const error: ImportInsertError[] = [
|
||||
{
|
||||
errorCode: 'UnknownError',
|
||||
errorMessage: 'Unknown error occurred',
|
||||
...errorContext,
|
||||
},
|
||||
];
|
||||
failed.push({ index, error });
|
||||
}
|
||||
}
|
||||
} catch (errors) {
|
||||
const error = errors.map((er) => ({ ...er, ...errorContext }));
|
||||
failed.push({ index, error });
|
||||
}
|
||||
};
|
||||
await bluebird.map(parsedData, importAsync, { concurrency });
|
||||
|
||||
return [success, failed];
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} resourceName
|
||||
* @param {Record<string, any>} params
|
||||
*/
|
||||
public async validateParamsSchema(
|
||||
resourceName: string,
|
||||
params: Record<string, any>
|
||||
) {
|
||||
const ImportableRegistry = this.importable.registry;
|
||||
const importable = ImportableRegistry.getImportable(resourceName);
|
||||
|
||||
const yupSchema = importable.paramsValidationSchema();
|
||||
|
||||
try {
|
||||
await yupSchema.validate(params, { abortEarly: false });
|
||||
} catch (validationError) {
|
||||
const errors = validationError.inner.map((error) => ({
|
||||
errorCode: 'ParamsValidationError',
|
||||
errorMessage: error.errors,
|
||||
}));
|
||||
throw errors;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} resourceName
|
||||
* @param {Record<string, any>} params
|
||||
*/
|
||||
public async validateParams(
|
||||
tenantId: number,
|
||||
resourceName: string,
|
||||
params: Record<string, any>
|
||||
) {
|
||||
const ImportableRegistry = this.importable.registry;
|
||||
const importable = ImportableRegistry.getImportable(resourceName);
|
||||
|
||||
await importable.validateParams(tenantId, params);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} resourceName
|
||||
* @param {Record<string, any>} params
|
||||
* @returns
|
||||
*/
|
||||
public transformParams(resourceName: string, params: Record<string, any>) {
|
||||
const ImportableRegistry = this.importable.registry;
|
||||
const importable = ImportableRegistry.getImportable(resourceName);
|
||||
|
||||
return importable.transformParams(params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the sheet columns from the given sheet data.
|
||||
* @param {unknown[]} json
|
||||
* @returns {string[]}
|
||||
*/
|
||||
public parseSheetColumns(json: unknown[]): string[] {
|
||||
return R.compose(Object.keys, trimObject, first)(json);
|
||||
}
|
||||
}
|
||||
151
packages/server/src/services/Import/ImportFileDataTransformer.ts
Normal file
151
packages/server/src/services/Import/ImportFileDataTransformer.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import bluebird from 'bluebird';
|
||||
import { isUndefined, pickBy, set } from 'lodash';
|
||||
import { Knex } from 'knex';
|
||||
import { ImportMappingAttr, ResourceMetaFieldsMap } from './interfaces';
|
||||
import {
|
||||
valueParser,
|
||||
parseKey,
|
||||
getFieldKey,
|
||||
aggregate,
|
||||
sanitizeSheetData,
|
||||
getMapToPath,
|
||||
} from './_utils';
|
||||
import ResourceService from '../Resource/ResourceService';
|
||||
import HasTenancyService from '../Tenancy/TenancyService';
|
||||
import { CurrencyParsingDTOs } from './_constants';
|
||||
|
||||
@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.
|
||||
* @param {number} tenantId -
|
||||
* @param {}
|
||||
*/
|
||||
public async parseSheetData(
|
||||
tenantId: number,
|
||||
importFile: any,
|
||||
importableFields: ResourceMetaFieldsMap,
|
||||
data: Record<string, unknown>[],
|
||||
trx?: Knex.Transaction
|
||||
): Promise<Record<string, any>[]> {
|
||||
// Sanitize the sheet data.
|
||||
const sanitizedData = sanitizeSheetData(data);
|
||||
|
||||
// Map the sheet columns key with the given map.
|
||||
const mappedDTOs = this.mapSheetColumns(
|
||||
sanitizedData,
|
||||
importFile.mappingParsed
|
||||
);
|
||||
// Parse the mapped sheet values.
|
||||
const parsedValues = await this.parseExcelValues(
|
||||
tenantId,
|
||||
importableFields,
|
||||
mappedDTOs,
|
||||
trx
|
||||
);
|
||||
const aggregateValues = this.aggregateParsedValues(
|
||||
tenantId,
|
||||
importFile.resource,
|
||||
parsedValues
|
||||
);
|
||||
return aggregateValues;
|
||||
}
|
||||
|
||||
/**
|
||||
* Aggregates parsed data based on resource metadata configuration.
|
||||
* @param {number} tenantId
|
||||
* @param {string} resourceName
|
||||
* @param {Record<string, any>} parsedData
|
||||
* @returns {Record<string, any>[]}
|
||||
*/
|
||||
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.
|
||||
* @param {Record<string, any>[]} body - The array of data objects to map.
|
||||
* @param {ImportMappingAttr[]} map - The mapping attributes.
|
||||
* @returns {Record<string, any>[]} - The mapped data objects.
|
||||
*/
|
||||
public mapSheetColumns(
|
||||
body: Record<string, any>[],
|
||||
map: ImportMappingAttr[]
|
||||
): Record<string, any>[] {
|
||||
return body.map((item) => {
|
||||
const newItem = {};
|
||||
map
|
||||
.filter((mapping) => !isUndefined(item[mapping.from]))
|
||||
.forEach((mapping) => {
|
||||
const toPath = getMapToPath(mapping.to, mapping.group);
|
||||
newItem[toPath] = item[mapping.from];
|
||||
});
|
||||
return newItem;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses sheet values before passing to the service layer.
|
||||
* @param {ResourceMetaFieldsMap} fields -
|
||||
* @param {Record<string, any>} valueDTOS -
|
||||
* @returns {Record<string, any>}
|
||||
*/
|
||||
public async parseExcelValues(
|
||||
tenantId: number,
|
||||
fields: ResourceMetaFieldsMap,
|
||||
valueDTOs: Record<string, any>[],
|
||||
trx?: Knex.Transaction
|
||||
): Promise<Record<string, any>[]> {
|
||||
const tenantModels = this.tenancy.models(tenantId);
|
||||
const _valueParser = valueParser(fields, tenantModels, trx);
|
||||
const _keyParser = parseKey(fields);
|
||||
|
||||
const parseAsync = async (valueDTO) => {
|
||||
// Clean up the undefined keys that not exist in resource fields.
|
||||
const _valueDTO = pickBy(
|
||||
valueDTO,
|
||||
(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 _valueParser(_valueDTO[key], key);
|
||||
const parsedKey = await _keyParser(key);
|
||||
|
||||
set(acc, parsedKey, parsedValue);
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
);
|
||||
};
|
||||
return bluebird.map(valueDTOs, parseAsync, {
|
||||
concurrency: CurrencyParsingDTOs,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { Service } from 'typedi';
|
||||
import { ImportInsertError, ResourceMetaFieldsMap } from './interfaces';
|
||||
import { ERRORS, convertFieldsToYupValidation } from './_utils';
|
||||
import { IModelMeta } from '@/interfaces';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
|
||||
@Service()
|
||||
export class ImportFileDataValidator {
|
||||
/**
|
||||
* Validates the given resource is importable.
|
||||
* @param {IModelMeta} resourceMeta
|
||||
*/
|
||||
public validateResourceImportable(resourceMeta: IModelMeta) {
|
||||
// Throw service error if the resource does not support importing.
|
||||
if (!resourceMeta.importable) {
|
||||
throw new ServiceError(ERRORS.RESOURCE_NOT_IMPORTABLE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the given mapped DTOs and returns errors with their index.
|
||||
* @param {Record<string, any>} mappedDTOs
|
||||
* @returns {Promise<void | ImportInsertError[]>}
|
||||
*/
|
||||
public async validateData(
|
||||
importableFields: ResourceMetaFieldsMap,
|
||||
data: Record<string, any>
|
||||
): Promise<void | ImportInsertError[]> {
|
||||
const YupSchema = convertFieldsToYupValidation(importableFields);
|
||||
const _data = { ...data };
|
||||
|
||||
try {
|
||||
await YupSchema.validate(_data, { abortEarly: false });
|
||||
} catch (validationError) {
|
||||
const errors = validationError.inner.reduce((errors, error) => {
|
||||
const newErrors = error.errors.map((errMsg) => ({
|
||||
errorCode: 'ValidationError',
|
||||
errorMessage: errMsg,
|
||||
}));
|
||||
return [...errors, ...newErrors];
|
||||
}, []);
|
||||
|
||||
throw errors;
|
||||
}
|
||||
}
|
||||
}
|
||||
154
packages/server/src/services/Import/ImportFileMapping.ts
Normal file
154
packages/server/src/services/Import/ImportFileMapping.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
import { fromPairs, isUndefined } from 'lodash';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import {
|
||||
ImportDateFormats,
|
||||
ImportFileMapPOJO,
|
||||
ImportMappingAttr,
|
||||
} from './interfaces';
|
||||
import ResourceService from '../Resource/ResourceService';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import { ERRORS } from './_utils';
|
||||
import { Import } from '@/system/models';
|
||||
|
||||
@Service()
|
||||
export class ImportFileMapping {
|
||||
@Inject()
|
||||
private resource: ResourceService;
|
||||
|
||||
/**
|
||||
* Mapping the excel sheet columns with resource columns.
|
||||
* @param {number} tenantId
|
||||
* @param {number} importId
|
||||
* @param {ImportMappingAttr} maps
|
||||
*/
|
||||
public async mapping(
|
||||
tenantId: number,
|
||||
importId: number,
|
||||
maps: ImportMappingAttr[]
|
||||
): Promise<ImportFileMapPOJO> {
|
||||
const importFile = await Import.query()
|
||||
.findOne('filename', importId)
|
||||
.throwIfNotFound();
|
||||
|
||||
// Invalidate the from/to map attributes.
|
||||
this.validateMapsAttrs(tenantId, importFile, maps);
|
||||
|
||||
// Validate the diplicated relations of map attrs.
|
||||
this.validateDuplicatedMapAttrs(maps);
|
||||
|
||||
// Validate the date format mapping.
|
||||
this.validateDateFormatMapping(tenantId, importFile.resource, maps);
|
||||
|
||||
const mappingStringified = JSON.stringify(maps);
|
||||
|
||||
await Import.query().findById(importFile.id).patch({
|
||||
mapping: mappingStringified,
|
||||
});
|
||||
return {
|
||||
import: {
|
||||
importId: importFile.importId,
|
||||
resource: importFile.resource,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the mapping attributes.
|
||||
* @param {number} tenantId -
|
||||
* @param {} importFile -
|
||||
* @param {ImportMappingAttr[]} maps
|
||||
* @throws {ServiceError(ERRORS.INVALID_MAP_ATTRS)}
|
||||
*/
|
||||
private validateMapsAttrs(
|
||||
tenantId: number,
|
||||
importFile: any,
|
||||
maps: ImportMappingAttr[]
|
||||
) {
|
||||
const fields = this.resource.getResourceFields2(
|
||||
tenantId,
|
||||
importFile.resource
|
||||
);
|
||||
const columnsMap = fromPairs(
|
||||
importFile.columnsParsed.map((field) => [field, ''])
|
||||
);
|
||||
const invalid = [];
|
||||
|
||||
// is not empty, is not undefined or map.group
|
||||
maps.forEach((map) => {
|
||||
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);
|
||||
}
|
||||
});
|
||||
if (invalid.length > 0) {
|
||||
throw new ServiceError(ERRORS.INVALID_MAP_ATTRS);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the map attrs relation should be one-to-one relation only.
|
||||
* @param {ImportMappingAttr[]} maps
|
||||
*/
|
||||
private validateDuplicatedMapAttrs(maps: ImportMappingAttr[]) {
|
||||
const fromMap = {};
|
||||
const toMap = {};
|
||||
|
||||
maps.forEach((map) => {
|
||||
if (fromMap[map.from]) {
|
||||
throw new ServiceError(ERRORS.DUPLICATED_FROM_MAP_ATTR);
|
||||
} else {
|
||||
fromMap[map.from] = true;
|
||||
}
|
||||
const toPath = !isUndefined(map?.group)
|
||||
? `${map.group}.${map.to}`
|
||||
: map.to;
|
||||
|
||||
if (toMap[toPath]) {
|
||||
throw new ServiceError(ERRORS.DUPLICATED_TO_MAP_ATTR);
|
||||
} else {
|
||||
toMap[toPath] = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the date format mapping.
|
||||
* @param {number} tenantId
|
||||
* @param {string} resource
|
||||
* @param {ImportMappingAttr[]} maps
|
||||
*/
|
||||
private validateDateFormatMapping(
|
||||
tenantId: number,
|
||||
resource: string,
|
||||
maps: ImportMappingAttr[]
|
||||
) {
|
||||
const fields = this.resource.getResourceImportableFields(
|
||||
tenantId,
|
||||
resource
|
||||
);
|
||||
// @todo Validate date type of the nested fields.
|
||||
maps.forEach((map) => {
|
||||
if (
|
||||
typeof fields[map.to] !== 'undefined' &&
|
||||
fields[map.to].fieldType === 'date'
|
||||
) {
|
||||
if (
|
||||
typeof map.dateFormat !== 'undefined' &&
|
||||
ImportDateFormats.indexOf(map.dateFormat) === -1
|
||||
) {
|
||||
throw new ServiceError(ERRORS.INVALID_MAP_DATE_FORMAT);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
33
packages/server/src/services/Import/ImportFileMeta.ts
Normal file
33
packages/server/src/services/Import/ImportFileMeta.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import HasTenancyService from '../Tenancy/TenancyService';
|
||||
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
|
||||
import { ImportFileMetaTransformer } from './ImportFileMetaTransformer';
|
||||
import { Import } from '@/system/models';
|
||||
|
||||
@Service()
|
||||
export class ImportFileMeta {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
private transformer: TransformerInjectable;
|
||||
|
||||
/**
|
||||
* Retrieves the import meta of the given import model id.
|
||||
* @param {number} tenantId
|
||||
* @param {number} importId
|
||||
* @returns {}
|
||||
*/
|
||||
async getImportMeta(tenantId: number, importId: string) {
|
||||
const importFile = await Import.query()
|
||||
.where('tenantId', tenantId)
|
||||
.findOne('importId', importId);
|
||||
|
||||
// Retrieves the transformed accounts collection.
|
||||
return this.transformer.transform(
|
||||
tenantId,
|
||||
importFile,
|
||||
new ImportFileMetaTransformer()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Transformer } from '@/lib/Transformer/Transformer';
|
||||
|
||||
export class ImportFileMetaTransformer extends Transformer {
|
||||
/**
|
||||
* Include these attributes to sale invoice object.
|
||||
* @returns {Array}
|
||||
*/
|
||||
public includeAttributes = (): string[] => {
|
||||
return ['map'];
|
||||
};
|
||||
|
||||
public excludeAttributes = (): string[] => {
|
||||
return ['id', 'filename', 'columns', 'mappingParsed', 'mapping'];
|
||||
}
|
||||
|
||||
map(importFile) {
|
||||
return importFile.mappingParsed;
|
||||
}
|
||||
}
|
||||
34
packages/server/src/services/Import/ImportFilePreview.ts
Normal file
34
packages/server/src/services/Import/ImportFilePreview.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import HasTenancyService from '../Tenancy/TenancyService';
|
||||
import { ImportFilePreviewPOJO } from './interfaces';
|
||||
import { ImportFileProcess } from './ImportFileProcess';
|
||||
|
||||
@Service()
|
||||
export class ImportFilePreview {
|
||||
@Inject()
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
@Inject()
|
||||
private importFile: ImportFileProcess;
|
||||
|
||||
/**
|
||||
* Preview the imported file results before commiting the transactions.
|
||||
* @param {number} tenantId
|
||||
* @param {number} importId
|
||||
* @returns {Promise<ImportFilePreviewPOJO>}
|
||||
*/
|
||||
public async preview(
|
||||
tenantId: number,
|
||||
importId: number
|
||||
): Promise<ImportFilePreviewPOJO> {
|
||||
const knex = this.tenancy.knex(tenantId);
|
||||
const trx = await knex.transaction({ isolationLevel: 'read uncommitted' });
|
||||
|
||||
const meta = await this.importFile.import(tenantId, importId, trx);
|
||||
|
||||
// Rollback the successed transaction.
|
||||
await trx.rollback();
|
||||
|
||||
return meta;
|
||||
}
|
||||
}
|
||||
108
packages/server/src/services/Import/ImportFileProcess.ts
Normal file
108
packages/server/src/services/Import/ImportFileProcess.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { chain } from 'lodash';
|
||||
import { Knex } from 'knex';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import {
|
||||
ERRORS,
|
||||
getSheetColumns,
|
||||
getUnmappedSheetColumns,
|
||||
readImportFile,
|
||||
} from './_utils';
|
||||
import { ImportFileCommon } from './ImportFileCommon';
|
||||
import { ImportFileDataTransformer } from './ImportFileDataTransformer';
|
||||
import ResourceService from '../Resource/ResourceService';
|
||||
import UnitOfWork from '../UnitOfWork';
|
||||
import { ImportFilePreviewPOJO } from './interfaces';
|
||||
import { Import } from '@/system/models';
|
||||
|
||||
@Service()
|
||||
export class ImportFileProcess {
|
||||
@Inject()
|
||||
private resource: ResourceService;
|
||||
|
||||
@Inject()
|
||||
private importCommon: ImportFileCommon;
|
||||
|
||||
@Inject()
|
||||
private importParser: ImportFileDataTransformer;
|
||||
|
||||
@Inject()
|
||||
private uow: UnitOfWork;
|
||||
|
||||
/**
|
||||
* Preview the imported file results before commiting the transactions.
|
||||
* @param {number} tenantId
|
||||
* @param {number} importId
|
||||
* @returns {Promise<ImportFilePreviewPOJO>}
|
||||
*/
|
||||
public async import(
|
||||
tenantId: number,
|
||||
importId: number,
|
||||
trx?: Knex.Transaction
|
||||
): Promise<ImportFilePreviewPOJO> {
|
||||
const importFile = await Import.query()
|
||||
.findOne('importId', importId)
|
||||
.where('tenantId', tenantId)
|
||||
.throwIfNotFound();
|
||||
|
||||
// Throw error if the import file is not mapped yet.
|
||||
if (!importFile.isMapped) {
|
||||
throw new ServiceError(ERRORS.IMPORT_FILE_NOT_MAPPED);
|
||||
}
|
||||
// Read the imported file.
|
||||
const buffer = await readImportFile(importFile.filename);
|
||||
const sheetData = this.importCommon.parseXlsxSheet(buffer);
|
||||
const header = getSheetColumns(sheetData);
|
||||
|
||||
const resource = importFile.resource;
|
||||
const resourceFields = this.resource.getResourceFields2(tenantId, resource);
|
||||
|
||||
// Runs the importing operation with ability to return errors that will happen.
|
||||
const [successedImport, failedImport, allData] =
|
||||
await this.uow.withTransaction(
|
||||
tenantId,
|
||||
async (trx: Knex.Transaction) => {
|
||||
// Prases the sheet json data.
|
||||
const parsedData = await this.importParser.parseSheetData(
|
||||
tenantId,
|
||||
importFile,
|
||||
resourceFields,
|
||||
sheetData,
|
||||
trx
|
||||
);
|
||||
const [successedImport, failedImport] =
|
||||
await this.importCommon.import(
|
||||
tenantId,
|
||||
importFile,
|
||||
parsedData,
|
||||
trx
|
||||
);
|
||||
return [successedImport, failedImport, parsedData];
|
||||
},
|
||||
trx
|
||||
);
|
||||
const mapping = importFile.mappingParsed;
|
||||
const errors = chain(failedImport)
|
||||
.map((oper) => oper.error)
|
||||
.flatten()
|
||||
.value();
|
||||
|
||||
const unmappedColumns = getUnmappedSheetColumns(header, mapping);
|
||||
const totalCount = allData.length;
|
||||
|
||||
const createdCount = successedImport.length;
|
||||
const errorsCount = failedImport.length;
|
||||
const skippedCount = errorsCount;
|
||||
|
||||
return {
|
||||
resource,
|
||||
createdCount,
|
||||
skippedCount,
|
||||
totalCount,
|
||||
errorsCount,
|
||||
errors,
|
||||
unmappedColumns: unmappedColumns,
|
||||
unmappedColumnsCount: unmappedColumns.length,
|
||||
};
|
||||
}
|
||||
}
|
||||
126
packages/server/src/services/Import/ImportFileUpload.ts
Normal file
126
packages/server/src/services/Import/ImportFileUpload.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import {
|
||||
deleteImportFile,
|
||||
getResourceColumns,
|
||||
readImportFile,
|
||||
sanitizeResourceName,
|
||||
validateSheetEmpty,
|
||||
} from './_utils';
|
||||
import ResourceService from '../Resource/ResourceService';
|
||||
import { ImportFileCommon } from './ImportFileCommon';
|
||||
import { ImportFileDataValidator } from './ImportFileDataValidator';
|
||||
import { ImportFileUploadPOJO } from './interfaces';
|
||||
import { Import } from '@/system/models';
|
||||
|
||||
@Service()
|
||||
export class ImportFileUploadService {
|
||||
@Inject()
|
||||
private resourceService: ResourceService;
|
||||
|
||||
@Inject()
|
||||
private importFileCommon: ImportFileCommon;
|
||||
|
||||
@Inject()
|
||||
private importValidator: ImportFileDataValidator;
|
||||
|
||||
/**
|
||||
* Imports the specified file for the given resource.
|
||||
* Deletes the file if an error occurs during the import process.
|
||||
* @param {number} tenantId
|
||||
* @param {string} resourceName
|
||||
* @param {string} filename
|
||||
* @param {Record<string, number | string>} params
|
||||
* @returns {Promise<ImportFileUploadPOJO>}
|
||||
*/
|
||||
public async import(
|
||||
tenantId: number,
|
||||
resourceName: string,
|
||||
filename: string,
|
||||
params: Record<string, number | string>
|
||||
): Promise<ImportFileUploadPOJO> {
|
||||
console.log(filename, 'filename');
|
||||
|
||||
try {
|
||||
return await this.importUnhandled(
|
||||
tenantId,
|
||||
resourceName,
|
||||
filename,
|
||||
params
|
||||
);
|
||||
} catch (err) {
|
||||
deleteImportFile(filename);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the imported file and stores the import file meta under unqiue id.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {string} resource - Resource name.
|
||||
* @param {string} filePath - File path.
|
||||
* @param {string} fileName - File name.
|
||||
* @returns {Promise<ImportFileUploadPOJO>}
|
||||
*/
|
||||
public async importUnhandled(
|
||||
tenantId: number,
|
||||
resourceName: string,
|
||||
filename: string,
|
||||
params: Record<string, number | string>
|
||||
): Promise<ImportFileUploadPOJO> {
|
||||
const resource = sanitizeResourceName(resourceName);
|
||||
const resourceMeta = this.resourceService.getResourceMeta(
|
||||
tenantId,
|
||||
resource
|
||||
);
|
||||
// Throw service error if the resource does not support importing.
|
||||
this.importValidator.validateResourceImportable(resourceMeta);
|
||||
|
||||
// Reads the imported file into buffer.
|
||||
const buffer = await readImportFile(filename);
|
||||
|
||||
// Parse the buffer file to array data.
|
||||
const sheetData = this.importFileCommon.parseXlsxSheet(buffer);
|
||||
|
||||
// Throws service error if the sheet data is empty.
|
||||
validateSheetEmpty(sheetData);
|
||||
|
||||
const sheetColumns = this.importFileCommon.parseSheetColumns(sheetData);
|
||||
const coumnsStringified = JSON.stringify(sheetColumns);
|
||||
|
||||
try {
|
||||
// Validates the params Yup schema.
|
||||
await this.importFileCommon.validateParamsSchema(resource, params);
|
||||
|
||||
// Validates importable params asyncly.
|
||||
await this.importFileCommon.validateParams(tenantId, resource, params);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
const _params = this.importFileCommon.transformParams(resource, params);
|
||||
const paramsStringified = JSON.stringify(_params);
|
||||
|
||||
// Store the import model with related metadata.
|
||||
const importFile = await Import.query().insert({
|
||||
filename,
|
||||
resource,
|
||||
tenantId,
|
||||
importId: filename,
|
||||
columns: coumnsStringified,
|
||||
params: paramsStringified,
|
||||
});
|
||||
const resourceColumnsMap = this.resourceService.getResourceFields2(
|
||||
tenantId,
|
||||
resource
|
||||
);
|
||||
const resourceColumns = getResourceColumns(resourceColumnsMap);
|
||||
|
||||
return {
|
||||
import: {
|
||||
importId: importFile.importId,
|
||||
resource: importFile.resource,
|
||||
},
|
||||
sheetColumns,
|
||||
resourceColumns,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import moment from 'moment';
|
||||
import bluebird from 'bluebird';
|
||||
import { Import } from '@/system/models';
|
||||
import { deleteImportFile } from './_utils';
|
||||
import { Service } from 'typedi';
|
||||
|
||||
@Service()
|
||||
export class ImportDeleteExpiredFiles {
|
||||
/**
|
||||
* Delete expired files.
|
||||
*/
|
||||
async deleteExpiredFiles() {
|
||||
const yesterday = moment().subtract(1, 'hour').format('YYYY-MM-DD HH:mm');
|
||||
|
||||
const expiredImports = await Import.query().where(
|
||||
'createdAt',
|
||||
'<',
|
||||
yesterday
|
||||
);
|
||||
await bluebird.map(
|
||||
expiredImports,
|
||||
async (expiredImport) => {
|
||||
await deleteImportFile(expiredImport.filename);
|
||||
},
|
||||
{ concurrency: 10 }
|
||||
);
|
||||
const expiredImportsIds = expiredImports.map(
|
||||
(expiredImport) => expiredImport.id
|
||||
);
|
||||
if (expiredImportsIds.length > 0) {
|
||||
await Import.query().whereIn('id', expiredImportsIds).delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
102
packages/server/src/services/Import/ImportResourceApplication.ts
Normal file
102
packages/server/src/services/Import/ImportResourceApplication.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { Inject } from 'typedi';
|
||||
import { ImportFileUploadService } from './ImportFileUpload';
|
||||
import { ImportFileMapping } from './ImportFileMapping';
|
||||
import { ImportMappingAttr } from './interfaces';
|
||||
import { ImportFileProcess } from './ImportFileProcess';
|
||||
import { ImportFilePreview } from './ImportFilePreview';
|
||||
import { ImportSampleService } from './ImportSample';
|
||||
import { ImportFileMeta } from './ImportFileMeta';
|
||||
|
||||
@Inject()
|
||||
export class ImportResourceApplication {
|
||||
@Inject()
|
||||
private importFileService: ImportFileUploadService;
|
||||
|
||||
@Inject()
|
||||
private importMappingService: ImportFileMapping;
|
||||
|
||||
@Inject()
|
||||
private importProcessService: ImportFileProcess;
|
||||
|
||||
@Inject()
|
||||
private ImportFilePreviewService: ImportFilePreview;
|
||||
|
||||
@Inject()
|
||||
private importSampleService: ImportSampleService;
|
||||
|
||||
@Inject()
|
||||
private importMetaService: ImportFileMeta;
|
||||
|
||||
/**
|
||||
* Reads the imported file and stores the import file meta under unqiue id.
|
||||
* @param {number} tenantId -
|
||||
* @param {string} resource - Resource name.
|
||||
* @param {string} fileName - File name.
|
||||
* @returns {Promise<ImportFileUploadPOJO>}
|
||||
*/
|
||||
public async import(
|
||||
tenantId: number,
|
||||
resource: string,
|
||||
filename: string,
|
||||
params: Record<string, any>
|
||||
) {
|
||||
return this.importFileService.import(tenantId, resource, filename, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mapping the excel sheet columns with resource columns.
|
||||
* @param {number} tenantId
|
||||
* @param {number} importId - Import id.
|
||||
* @param {ImportMappingAttr} maps
|
||||
*/
|
||||
public async mapping(
|
||||
tenantId: number,
|
||||
importId: number,
|
||||
maps: ImportMappingAttr[]
|
||||
) {
|
||||
return this.importMappingService.mapping(tenantId, importId, maps);
|
||||
}
|
||||
|
||||
/**
|
||||
* Preview the mapped results before process importing.
|
||||
* @param {number} tenantId
|
||||
* @param {number} importId - Import id.
|
||||
* @returns {Promise<ImportFilePreviewPOJO>}
|
||||
*/
|
||||
public async preview(tenantId: number, importId: number) {
|
||||
return this.ImportFilePreviewService.preview(tenantId, importId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the import file sheet through service for creating entities.
|
||||
* @param {number} tenantId
|
||||
* @param {number} importId
|
||||
* @returns {Promise<ImportFilePreviewPOJO>}
|
||||
*/
|
||||
public async process(tenantId: number, importId: number) {
|
||||
return this.importProcessService.import(tenantId, importId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the import meta of the given import id.
|
||||
* @param {number} tenantId -
|
||||
* @param {string} importId - Import id.
|
||||
* @returns {}
|
||||
*/
|
||||
public importMeta(tenantId: number, importId: string) {
|
||||
return this.importMetaService.getImportMeta(tenantId, importId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the csv/xlsx sample sheet of the given
|
||||
* @param {number} tenantId
|
||||
* @param {number} resource - Resource name.
|
||||
*/
|
||||
public sample(
|
||||
tenantId: number,
|
||||
resource: string,
|
||||
format: 'csv' | 'xlsx' = 'csv'
|
||||
) {
|
||||
return this.importSampleService.sample(tenantId, resource, format);
|
||||
}
|
||||
}
|
||||
46
packages/server/src/services/Import/ImportSample.ts
Normal file
46
packages/server/src/services/Import/ImportSample.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import XLSX from 'xlsx';
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { ImportableResources } from './ImportableResources';
|
||||
import { sanitizeResourceName } from './_utils';
|
||||
|
||||
@Service()
|
||||
export class ImportSampleService {
|
||||
@Inject()
|
||||
private importable: ImportableResources;
|
||||
|
||||
/**
|
||||
* Retrieves the sample sheet of the given resource.
|
||||
* @param {number} tenantId
|
||||
* @param {string} resource
|
||||
* @param {string} format
|
||||
* @returns {Buffer | string}
|
||||
*/
|
||||
public sample(
|
||||
tenantId: number,
|
||||
resource: string,
|
||||
format: 'csv' | 'xlsx'
|
||||
): Buffer | string {
|
||||
const _resource = sanitizeResourceName(resource);
|
||||
|
||||
const ImportableRegistry = this.importable.registry;
|
||||
const importable = ImportableRegistry.getImportable(_resource);
|
||||
|
||||
const data = importable.sampleData();
|
||||
|
||||
const workbook = XLSX.utils.book_new();
|
||||
const worksheet = XLSX.utils.json_to_sheet(data);
|
||||
XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1');
|
||||
|
||||
// Determine the output format
|
||||
if (format === 'csv') {
|
||||
const csvOutput = XLSX.utils.sheet_to_csv(worksheet);
|
||||
return csvOutput;
|
||||
} else {
|
||||
const xlsxOutput = XLSX.write(workbook, {
|
||||
bookType: 'xlsx',
|
||||
type: 'buffer',
|
||||
});
|
||||
return xlsxOutput;
|
||||
}
|
||||
}
|
||||
}
|
||||
72
packages/server/src/services/Import/Importable.ts
Normal file
72
packages/server/src/services/Import/Importable.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { Knex } from 'knex';
|
||||
import * as Yup from 'yup';
|
||||
import { ImportableContext } from './interfaces';
|
||||
|
||||
export abstract class Importable {
|
||||
/**
|
||||
*
|
||||
* @param {number} tenantId
|
||||
* @param {any} createDTO
|
||||
* @param {Knex.Transaction} trx
|
||||
*/
|
||||
public importable(tenantId: number, createDTO: any, trx?: Knex.Transaction) {
|
||||
throw new Error(
|
||||
'The `importable` function is not defined in service importable.'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Concurrency controlling of the importing process.
|
||||
* @returns {number}
|
||||
*/
|
||||
public get concurrency() {
|
||||
return 10;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the sample data of importable.
|
||||
* @returns {Array<any>}
|
||||
*/
|
||||
public sampleData(): Array<any> {
|
||||
return [];
|
||||
}
|
||||
|
||||
// ------------------
|
||||
// # Params
|
||||
// ------------------
|
||||
/**
|
||||
* Params Yup validation schema.
|
||||
* @returns {Yup.ObjectSchema<object, object>}
|
||||
*/
|
||||
public paramsValidationSchema(): Yup.ObjectSchema<object, object> {
|
||||
return Yup.object().nullable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the params of the importable service.
|
||||
* @param {Record<string, any>}
|
||||
* @returns {Promise<boolean>} - True means passed and false failed.
|
||||
*/
|
||||
public async validateParams(
|
||||
tenantId: number,
|
||||
params: Record<string, any>
|
||||
): Promise<void> {}
|
||||
|
||||
/**
|
||||
* Transformes the import params before storing them.
|
||||
* @param {Record<string, any>} parmas
|
||||
*/
|
||||
public transformParams(parmas: Record<string, any>) {
|
||||
return parmas;
|
||||
}
|
||||
}
|
||||
46
packages/server/src/services/Import/ImportableRegistry.ts
Normal file
46
packages/server/src/services/Import/ImportableRegistry.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { camelCase, upperFirst } from 'lodash';
|
||||
import { Importable } from './Importable';
|
||||
|
||||
export class ImportableRegistry {
|
||||
private static instance: ImportableRegistry;
|
||||
private importables: Record<string, Importable>;
|
||||
|
||||
constructor() {
|
||||
this.importables = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets singleton instance of registry.
|
||||
* @returns {ImportableRegistry}
|
||||
*/
|
||||
public static getInstance(): ImportableRegistry {
|
||||
if (!ImportableRegistry.instance) {
|
||||
ImportableRegistry.instance = new ImportableRegistry();
|
||||
}
|
||||
return ImportableRegistry.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the given importable service.
|
||||
* @param {string} resource
|
||||
* @param {Importable} importable
|
||||
*/
|
||||
public registerImportable(resource: string, importable: Importable): void {
|
||||
const _resource = this.sanitizeResourceName(resource);
|
||||
this.importables[_resource] = importable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the importable service instance of the given resource name.
|
||||
* @param {string} name
|
||||
* @returns {Importable}
|
||||
*/
|
||||
public getImportable(name: string): Importable {
|
||||
const _name = this.sanitizeResourceName(name);
|
||||
return this.importables[_name];
|
||||
}
|
||||
|
||||
private sanitizeResourceName(resource: string) {
|
||||
return upperFirst(camelCase(resource));
|
||||
}
|
||||
}
|
||||
71
packages/server/src/services/Import/ImportableResources.ts
Normal file
71
packages/server/src/services/Import/ImportableResources.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import Container, { Service } from 'typedi';
|
||||
import { AccountsImportable } from '../Accounts/AccountsImportable';
|
||||
import { ImportableRegistry } from './ImportableRegistry';
|
||||
import { UncategorizedTransactionsImportable } from '../Cashflow/UncategorizedTransactionsImportable';
|
||||
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 {
|
||||
private static registry: ImportableRegistry;
|
||||
|
||||
constructor() {
|
||||
this.boot();
|
||||
}
|
||||
|
||||
/**
|
||||
* Importable instances.
|
||||
*/
|
||||
private importables = [
|
||||
{ resource: 'Account', importable: AccountsImportable },
|
||||
{
|
||||
resource: 'UncategorizedCashflowTransaction',
|
||||
importable: UncategorizedTransactionsImportable,
|
||||
},
|
||||
{ resource: 'Customer', importable: CustomersImportable },
|
||||
{ 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() {
|
||||
return ImportableResources.registry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Boots all the registered importables.
|
||||
*/
|
||||
public boot() {
|
||||
if (!ImportableResources.registry) {
|
||||
const instance = ImportableRegistry.getInstance();
|
||||
|
||||
this.importables.forEach((importable) => {
|
||||
const importableInstance = Container.get(importable.importable);
|
||||
instance.registerImportable(importable.resource, importableInstance);
|
||||
});
|
||||
ImportableResources.registry = instance;
|
||||
}
|
||||
}
|
||||
}
|
||||
3
packages/server/src/services/Import/_constants.ts
Normal file
3
packages/server/src/services/Import/_constants.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
|
||||
|
||||
export const CurrencyParsingDTOs = 10;
|
||||
451
packages/server/src/services/Import/_utils.ts
Normal file
451
packages/server/src/services/Import/_utils.ts
Normal file
@@ -0,0 +1,451 @@
|
||||
import * as Yup from 'yup';
|
||||
import moment from 'moment';
|
||||
import * as R from 'ramda';
|
||||
import { Knex } from 'knex';
|
||||
import fs from 'fs/promises';
|
||||
import {
|
||||
defaultTo,
|
||||
upperFirst,
|
||||
camelCase,
|
||||
first,
|
||||
isUndefined,
|
||||
pickBy,
|
||||
isEmpty,
|
||||
castArray,
|
||||
get,
|
||||
head,
|
||||
split,
|
||||
last,
|
||||
} from 'lodash';
|
||||
import pluralize from 'pluralize';
|
||||
import { ResourceMetaFieldsMap } 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',
|
||||
INVALID_MAP_ATTRS: 'INVALID_MAP_ATTRS',
|
||||
DUPLICATED_FROM_MAP_ATTR: 'DUPLICATED_FROM_MAP_ATTR',
|
||||
DUPLICATED_TO_MAP_ATTR: 'DUPLICATED_TO_MAP_ATTR',
|
||||
IMPORT_FILE_NOT_MAPPED: 'IMPORT_FILE_NOT_MAPPED',
|
||||
INVALID_MAP_DATE_FORMAT: 'INVALID_MAP_DATE_FORMAT',
|
||||
MAP_DATE_FORMAT_NOT_DEFINED: 'MAP_DATE_FORMAT_NOT_DEFINED',
|
||||
IMPORTED_SHEET_EMPTY: 'IMPORTED_SHEET_EMPTY',
|
||||
};
|
||||
|
||||
/**
|
||||
* 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();
|
||||
|
||||
// Trim the value if it's a string, otherwise leave it as is
|
||||
const trimmedValue = typeof value === 'string' ? value.trim() : value;
|
||||
|
||||
// Assign the trimmed key and value to the accumulator object
|
||||
return { ...acc, [trimmedKey]: trimmedValue };
|
||||
}, {});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
fieldSchema = Yup.string().label(field.name);
|
||||
|
||||
if (field.fieldType === 'text') {
|
||||
if (!isUndefined(field.minLength)) {
|
||||
fieldSchema = fieldSchema.min(
|
||||
field.minLength,
|
||||
`Minimum length is ${field.minLength} characters`
|
||||
);
|
||||
}
|
||||
if (!isUndefined(field.maxLength)) {
|
||||
fieldSchema = fieldSchema.max(
|
||||
field.maxLength,
|
||||
`Maximum length is ${field.maxLength} characters`
|
||||
);
|
||||
}
|
||||
} else if (field.fieldType === 'number') {
|
||||
fieldSchema = Yup.number().label(field.name);
|
||||
|
||||
if (!isUndefined(field.max)) {
|
||||
fieldSchema = fieldSchema.max(field.max);
|
||||
}
|
||||
if (!isUndefined(field.min)) {
|
||||
fieldSchema = fieldSchema.min(field.min);
|
||||
}
|
||||
} else if (field.fieldType === 'boolean') {
|
||||
fieldSchema = Yup.boolean().label(field.name);
|
||||
} else if (field.fieldType === 'enumeration') {
|
||||
const options = field.options.reduce((acc, option) => {
|
||||
acc[option.key] = option.label;
|
||||
return acc;
|
||||
}, {});
|
||||
fieldSchema = Yup.string().oneOf(Object.keys(options)).label(field.name);
|
||||
// Validate date field type.
|
||||
} else if (field.fieldType === 'date') {
|
||||
fieldSchema = fieldSchema.test(
|
||||
'date validation',
|
||||
'Invalid date or format. The string should be a valid YYYY-MM-DD format.',
|
||||
(val) => {
|
||||
if (!val) {
|
||||
return true;
|
||||
}
|
||||
return moment(val, 'YYYY-MM-DD', true).isValid();
|
||||
}
|
||||
);
|
||||
} 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();
|
||||
}
|
||||
const _fieldName = parseFieldName(fieldName, field);
|
||||
|
||||
yupSchema[_fieldName] = fieldSchema;
|
||||
});
|
||||
return Yup.object().shape(yupSchema);
|
||||
};
|
||||
|
||||
const parseFieldName = (fieldName: string, field: IModelMetaField) => {
|
||||
let _key = fieldName;
|
||||
|
||||
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)
|
||||
);
|
||||
};
|
||||
|
||||
export const sanitizeResourceName = (resourceName: string) => {
|
||||
return upperFirst(camelCase(pluralize.singular(resourceName)));
|
||||
};
|
||||
|
||||
export const getSheetColumns = (sheetData: unknown[]) => {
|
||||
return Object.keys(first(sheetData));
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the unique value from the given imported object DTO based on the
|
||||
* configured unique resource field.
|
||||
* @param {{ [key: string]: IModelMetaField }} importableFields -
|
||||
* @param {<Record<string, any>}
|
||||
* @returns {string}
|
||||
*/
|
||||
export const getUniqueImportableValue = (
|
||||
importableFields: { [key: string]: IModelMetaField2 },
|
||||
objectDTO: Record<string, any>
|
||||
) => {
|
||||
const uniqueImportableValue = pickBy(
|
||||
importableFields,
|
||||
(field) => field.unique
|
||||
);
|
||||
const uniqueImportableKeys = Object.keys(uniqueImportableValue);
|
||||
const uniqueImportableKey = first(uniqueImportableKeys);
|
||||
|
||||
return defaultTo(objectDTO[uniqueImportableKey], '');
|
||||
};
|
||||
|
||||
/**
|
||||
* Throws service error the given sheet is empty.
|
||||
* @param {Array<any>} sheetData
|
||||
*/
|
||||
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}
|
||||
*/
|
||||
export const parseBoolean = (value: string): boolean | null => {
|
||||
const normalizeValue = (value: string): string =>
|
||||
value.toString().trim().toLowerCase();
|
||||
|
||||
const normalizedValue = normalizeValue(value);
|
||||
const valuesRepresentingTrue =
|
||||
booleanValuesRepresentingTrue.map(normalizeValue);
|
||||
const valueRepresentingFalse =
|
||||
booleanValuesRepresentingFalse.map(normalizeValue);
|
||||
|
||||
if (valuesRepresentingTrue.includes(normalizedValue)) {
|
||||
return true;
|
||||
} else if (valueRepresentingFalse.includes(normalizedValue)) {
|
||||
return false;
|
||||
}
|
||||
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);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the path to map a value to based on the 'to' and 'group' parameters.
|
||||
* @param {string} to - The target key to map the value to.
|
||||
* @param {string} group - The group key to nest the target key under.
|
||||
* @returns {string} - The path to map the value to.
|
||||
*/
|
||||
export const getMapToPath = (to: string, group = '') =>
|
||||
group ? `${group}.${to}` : to;
|
||||
|
||||
/**
|
||||
* Deletes the imported file from the storage and database.
|
||||
* @param {string} filename
|
||||
*/
|
||||
export const deleteImportFile = async (filename: string) => {
|
||||
// Deletes the imported file.
|
||||
await fs.unlink(`public/imports/${filename}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Reads the import file.
|
||||
* @param {string} filename
|
||||
* @returns {Promise<Buffer>}
|
||||
*/
|
||||
export const readImportFile = (filename: string) => {
|
||||
return fs.readFile(`public/imports/${filename}`);
|
||||
};
|
||||
77
packages/server/src/services/Import/interfaces.ts
Normal file
77
packages/server/src/services/Import/interfaces.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { IModelMetaField, IModelMetaField2 } from '@/interfaces';
|
||||
import Import from '@/models/Import';
|
||||
|
||||
export interface ImportMappingAttr {
|
||||
from: string;
|
||||
to: string;
|
||||
group?: string;
|
||||
dateFormat?: string;
|
||||
}
|
||||
|
||||
export interface ImportValidationError {
|
||||
index: number;
|
||||
property: string;
|
||||
constraints: Record<string, string>;
|
||||
}
|
||||
|
||||
export type ResourceMetaFieldsMap = { [key: string]: IModelMetaField2 };
|
||||
|
||||
export interface ImportInsertError {
|
||||
rowNumber: number;
|
||||
errorCode: string;
|
||||
errorMessage: string;
|
||||
}
|
||||
|
||||
export interface ImportFileUploadPOJO {
|
||||
import: {
|
||||
importId: string;
|
||||
resource: string;
|
||||
};
|
||||
sheetColumns: string[];
|
||||
resourceColumns: {
|
||||
key: string;
|
||||
name: string;
|
||||
required?: boolean;
|
||||
hint?: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface ImportFileMapPOJO {
|
||||
import: {
|
||||
importId: string;
|
||||
resource: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ImportFilePreviewPOJO {
|
||||
resource: string;
|
||||
createdCount: number;
|
||||
skippedCount: number;
|
||||
totalCount: number;
|
||||
errorsCount: number;
|
||||
errors: ImportInsertError[];
|
||||
unmappedColumns: string[];
|
||||
unmappedColumnsCount: number;
|
||||
}
|
||||
|
||||
export interface ImportOperSuccess {
|
||||
data: unknown;
|
||||
index: number;
|
||||
}
|
||||
|
||||
export interface ImportOperError {
|
||||
error: ImportInsertError[];
|
||||
index: number;
|
||||
}
|
||||
|
||||
export interface ImportableContext {
|
||||
import: Import;
|
||||
rowIndex: number;
|
||||
}
|
||||
|
||||
export const ImportDateFormats = [
|
||||
'yyyy-MM-dd',
|
||||
'dd.MM.yy',
|
||||
'MM/dd/yy',
|
||||
'dd/MMM/yyyy',
|
||||
];
|
||||
@@ -0,0 +1,28 @@
|
||||
import Container, { Service } from 'typedi';
|
||||
import { ImportDeleteExpiredFiles } from '../ImportRemoveExpiredFiles';
|
||||
|
||||
@Service()
|
||||
export class ImportDeleteExpiredFilesJobs {
|
||||
/**
|
||||
* Constructor method.
|
||||
*/
|
||||
constructor(agenda) {
|
||||
agenda.define('delete-expired-imported-files', this.handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers sending invoice mail.
|
||||
*/
|
||||
private handler = async (job, done: Function) => {
|
||||
const importDeleteExpiredFiles = Container.get(ImportDeleteExpiredFiles);
|
||||
|
||||
try {
|
||||
console.log('Delete expired import files has started.');
|
||||
await importDeleteExpiredFiles.deleteExpiredFiles();
|
||||
done();
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
done(error);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import ItemCategoriesService from './ItemCategoriesService';
|
||||
import { Importable } from '../Import/Importable';
|
||||
import { Knex } from 'knex';
|
||||
import { IItemCategoryOTD } from '@/interfaces';
|
||||
import { ItemCategoriesSampleData } from './constants';
|
||||
|
||||
@Service()
|
||||
export class ItemCategoriesImportable extends Importable {
|
||||
@Inject()
|
||||
private itemCategoriesService: ItemCategoriesService;
|
||||
|
||||
/**
|
||||
* Importing to create new item category service.
|
||||
* @param {number} tenantId
|
||||
* @param {any} createDTO
|
||||
* @param {Knex.Transaction} trx
|
||||
*/
|
||||
public async importable(
|
||||
tenantId: number,
|
||||
createDTO: IItemCategoryOTD,
|
||||
trx?: Knex.Transaction
|
||||
) {
|
||||
await this.itemCategoriesService.newItemCategory(
|
||||
tenantId,
|
||||
createDTO,
|
||||
{},
|
||||
trx
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Item categories sample data used to download sample sheet file.
|
||||
*/
|
||||
public sampleData(): any[] {
|
||||
return ItemCategoriesSampleData;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Inject } from 'typedi';
|
||||
import * as R from 'ramda';
|
||||
import Knex from 'knex';
|
||||
import { Knex } from 'knex';
|
||||
import { ServiceError } from '@/exceptions';
|
||||
import {
|
||||
IItemCategory,
|
||||
@@ -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.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,7 +118,8 @@ export default class ItemCategoriesService implements IItemCategoriesService {
|
||||
public async newItemCategory(
|
||||
tenantId: number,
|
||||
itemCategoryOTD: IItemCategoryOTD,
|
||||
authorizedUser: ISystemUser
|
||||
authorizedUser: ISystemUser,
|
||||
trx?: Knex.Transaction
|
||||
): Promise<IItemCategory> {
|
||||
const { ItemCategory } = this.tenancy.models(tenantId);
|
||||
|
||||
@@ -139,20 +143,24 @@ export default class ItemCategoriesService implements IItemCategoriesService {
|
||||
authorizedUser
|
||||
);
|
||||
// Creates item category under unit-of-work evnirement.
|
||||
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
|
||||
// Inserts the item category.
|
||||
const itemCategory = await ItemCategory.query(trx).insert({
|
||||
...itemCategoryObj,
|
||||
});
|
||||
// Triggers `onItemCategoryCreated` event.
|
||||
await this.eventPublisher.emitAsync(events.itemCategory.onCreated, {
|
||||
itemCategory,
|
||||
tenantId,
|
||||
trx,
|
||||
} as IItemCategoryCreatedPayload);
|
||||
return this.uow.withTransaction(
|
||||
tenantId,
|
||||
async (trx: Knex.Transaction) => {
|
||||
// Inserts the item category.
|
||||
const itemCategory = await ItemCategory.query(trx).insert({
|
||||
...itemCategoryObj,
|
||||
});
|
||||
// Triggers `onItemCategoryCreated` event.
|
||||
await this.eventPublisher.emitAsync(events.itemCategory.onCreated, {
|
||||
itemCategory,
|
||||
tenantId,
|
||||
trx,
|
||||
} as IItemCategoryCreatedPayload);
|
||||
|
||||
return itemCategory;
|
||||
});
|
||||
return itemCategory;
|
||||
},
|
||||
trx
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -308,7 +316,7 @@ export default class ItemCategoriesService implements IItemCategoriesService {
|
||||
} as IItemCategoryDeletedPayload);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Parses items categories filter DTO.
|
||||
* @param {} filterDTO
|
||||
|
||||
@@ -11,3 +11,25 @@ export const ERRORS = {
|
||||
INVENTORY_ACCOUNT_NOT_INVENTORY: 'INVENTORY_ACCOUNT_NOT_INVENTORY',
|
||||
CATEGORY_HAVE_ITEMS: 'CATEGORY_HAVE_ITEMS',
|
||||
};
|
||||
|
||||
export const ItemCategoriesSampleData = [
|
||||
{
|
||||
Name: 'Kassulke Group',
|
||||
Description: 'Optio itaque eaque qui adipisci illo sed.',
|
||||
},
|
||||
{
|
||||
Name: 'Crist, Mraz and Lueilwitz',
|
||||
Description:
|
||||
'Dolores veniam deserunt sed commodi error quia veritatis non.',
|
||||
},
|
||||
{
|
||||
Name: 'Gutmann and Sons',
|
||||
Description:
|
||||
'Ratione aperiam voluptas rem adipisci assumenda eos neque veritatis tempora.',
|
||||
},
|
||||
{
|
||||
Name: 'Reichel - Raynor',
|
||||
Description:
|
||||
'Necessitatibus repellendus placeat possimus dolores excepturi ut.',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -88,7 +88,11 @@ export class CreateItem {
|
||||
* @param {IItemDTO} item
|
||||
* @return {Promise<IItem>}
|
||||
*/
|
||||
public async createItem(tenantId: number, itemDTO: IItemDTO): Promise<IItem> {
|
||||
public async createItem(
|
||||
tenantId: number,
|
||||
itemDTO: IItemDTO,
|
||||
trx?: Knex.Transaction
|
||||
): Promise<IItem> {
|
||||
const { Item } = this.tenancy.models(tenantId);
|
||||
|
||||
// Authorize the item before creating.
|
||||
@@ -111,7 +115,8 @@ export class CreateItem {
|
||||
} as IItemEventCreatedPayload);
|
||||
|
||||
return item;
|
||||
}
|
||||
},
|
||||
trx
|
||||
);
|
||||
return item;
|
||||
}
|
||||
|
||||
@@ -35,7 +35,10 @@ export class ItemsValidators {
|
||||
}
|
||||
});
|
||||
if (foundItems.length > 0) {
|
||||
throw new ServiceError(ERRORS.ITEM_NAME_EXISTS);
|
||||
throw new ServiceError(
|
||||
ERRORS.ITEM_NAME_EXISTS,
|
||||
'The item name is already exist.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
34
packages/server/src/services/Items/ItemsImportable.ts
Normal file
34
packages/server/src/services/Items/ItemsImportable.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { Knex } from 'knex';
|
||||
import { Importable } from '@/services/Import/Importable';
|
||||
import { IItemCreateDTO } from '@/interfaces';
|
||||
import { CreateItem } from './CreateItem';
|
||||
import { ItemsSampleData } from './constants';
|
||||
|
||||
@Service()
|
||||
export class ItemsImportable extends Importable {
|
||||
@Inject()
|
||||
private createItemService: CreateItem;
|
||||
|
||||
/**
|
||||
* Mapps the imported data to create a new item service.
|
||||
* @param {number} tenantId
|
||||
* @param {ICustomerNewDTO} createDTO
|
||||
* @param {Knex.Transaction} trx
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
public async importable(
|
||||
tenantId: number,
|
||||
createDTO: IItemCreateDTO,
|
||||
trx?: Knex.Transaction<any, any[]>
|
||||
): Promise<void> {
|
||||
await this.createItemService.createItem(tenantId, createDTO, trx);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the sample data of customers used to download sample sheet.
|
||||
*/
|
||||
public sampleData(): any[] {
|
||||
return ItemsSampleData;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
export const ERRORS = {
|
||||
NOT_FOUND: 'NOT_FOUND',
|
||||
ITEMS_NOT_FOUND: 'ITEMS_NOT_FOUND',
|
||||
@@ -19,7 +18,8 @@ export const ERRORS = {
|
||||
ITEM_HAS_ASSOCIATED_INVENTORY_ADJUSTMENT:
|
||||
'ITEM_HAS_ASSOCIATED_INVENTORY_ADJUSTMENT',
|
||||
ITEM_CANNOT_CHANGE_INVENTORY_TYPE: 'ITEM_CANNOT_CHANGE_INVENTORY_TYPE',
|
||||
TYPE_CANNOT_CHANGE_WITH_ITEM_HAS_TRANSACTIONS: 'TYPE_CANNOT_CHANGE_WITH_ITEM_HAS_TRANSACTIONS',
|
||||
TYPE_CANNOT_CHANGE_WITH_ITEM_HAS_TRANSACTIONS:
|
||||
'TYPE_CANNOT_CHANGE_WITH_ITEM_HAS_TRANSACTIONS',
|
||||
INVENTORY_ACCOUNT_CANNOT_MODIFIED: 'INVENTORY_ACCOUNT_CANNOT_MODIFIED',
|
||||
|
||||
ITEM_HAS_ASSOCIATED_TRANSACTIONS: 'ITEM_HAS_ASSOCIATED_TRANSACTIONS',
|
||||
@@ -53,8 +53,84 @@ export const DEFAULT_VIEWS = [
|
||||
slug: 'non-inventory',
|
||||
rolesLogicExpression: '1',
|
||||
roles: [
|
||||
{ index: 1, fieldKey: 'type', comparator: 'equals', value: 'non-inventory' },
|
||||
{
|
||||
index: 1,
|
||||
fieldKey: 'type',
|
||||
comparator: 'equals',
|
||||
value: 'non-inventory',
|
||||
},
|
||||
],
|
||||
columns: DEFAULT_VIEW_COLUMNS,
|
||||
},
|
||||
]
|
||||
];
|
||||
|
||||
export const ItemsSampleData = [
|
||||
{
|
||||
'Item Type': 'Inventory',
|
||||
'Item Name': 'Hettinger, Schumm and Bartoletti',
|
||||
'Item Code': '1000',
|
||||
Sellable: 'T',
|
||||
Purchasable: 'T',
|
||||
'Cost Price': '10000',
|
||||
'Sell Price': '1000',
|
||||
'Cost Account': 'Cost of Goods Sold',
|
||||
'Sell Account': 'Other Income',
|
||||
'Inventory Account': 'Inventory Asset',
|
||||
'Sell Description': 'Description ....',
|
||||
'Purchase Description': 'Description ....',
|
||||
Category: 'sdafasdfsadf',
|
||||
Note: 'At dolor est non tempore et quisquam.',
|
||||
Active: 'TRUE',
|
||||
},
|
||||
{
|
||||
'Item Type': 'Inventory',
|
||||
'Item Name': 'Schmitt Group',
|
||||
'Item Code': '1001',
|
||||
Sellable: 'T',
|
||||
Purchasable: 'T',
|
||||
'Cost Price': '10000',
|
||||
'Sell Price': '1000',
|
||||
'Cost Account': 'Cost of Goods Sold',
|
||||
'Sell Account': 'Other Income',
|
||||
'Inventory Account': 'Inventory Asset',
|
||||
'Sell Description': 'Description ....',
|
||||
'Purchase Description': 'Description ....',
|
||||
Category: 'sdafasdfsadf',
|
||||
Note: 'Id perspiciatis at adipisci minus accusamus dolor iure dolore.',
|
||||
Active: 'TRUE',
|
||||
},
|
||||
{
|
||||
'Item Type': 'Inventory',
|
||||
'Item Name': 'Marks - Carroll',
|
||||
'Item Code': '1002',
|
||||
Sellable: 'T',
|
||||
Purchasable: 'T',
|
||||
'Cost Price': '10000',
|
||||
'Sell Price': '1000',
|
||||
'Cost Account': 'Cost of Goods Sold',
|
||||
'Sell Account': 'Other Income',
|
||||
'Inventory Account': 'Inventory Asset',
|
||||
'Sell Description': 'Description ....',
|
||||
'Purchase Description': 'Description ....',
|
||||
Category: 'sdafasdfsadf',
|
||||
Note: 'Odio odio minus similique.',
|
||||
Active: 'TRUE',
|
||||
},
|
||||
{
|
||||
'Item Type': 'Inventory',
|
||||
'Item Name': 'VonRueden, Ruecker and Hettinger',
|
||||
'Item Code': '1003',
|
||||
Sellable: 'T',
|
||||
Purchasable: 'T',
|
||||
'Cost Price': '10000',
|
||||
'Sell Price': '1000',
|
||||
'Cost Account': 'Cost of Goods Sold',
|
||||
'Sell Account': 'Other Income',
|
||||
'Inventory Account': 'Inventory Asset',
|
||||
'Sell Description': 'Description ....',
|
||||
'Purchase Description': 'Description ....',
|
||||
Category: 'sdafasdfsadf',
|
||||
Note: 'Quibusdam dolores illo.',
|
||||
Active: 'TRUE',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -21,25 +21,19 @@ import { ERRORS } from './constants';
|
||||
@Service()
|
||||
export default class OrganizationService {
|
||||
@Inject()
|
||||
eventPublisher: EventPublisher;
|
||||
|
||||
@Inject('logger')
|
||||
logger: any;
|
||||
|
||||
@Inject('repositories')
|
||||
sysRepositories: any;
|
||||
private eventPublisher: EventPublisher;
|
||||
|
||||
@Inject()
|
||||
tenantsManager: TenantsManager;
|
||||
private tenantsManager: TenantsManager;
|
||||
|
||||
@Inject('agenda')
|
||||
agenda: any;
|
||||
private agenda: any;
|
||||
|
||||
@Inject()
|
||||
baseCurrencyMutateLocking: OrganizationBaseCurrencyLocking;
|
||||
private baseCurrencyMutateLocking: OrganizationBaseCurrencyLocking;
|
||||
|
||||
@Inject()
|
||||
tenancy: HasTenancyService;
|
||||
private tenancy: HasTenancyService;
|
||||
|
||||
/**
|
||||
* Builds the database schema and seed data of the given organization id.
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user