mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-20 23:00:34 +00:00
feat: linking relation with id in importing
This commit is contained in:
@@ -245,25 +245,26 @@
|
|||||||
"account.field.currency": "Currency",
|
"account.field.currency": "Currency",
|
||||||
"account.field.balance": "Balance",
|
"account.field.balance": "Balance",
|
||||||
"account.field.created_at": "Created at",
|
"account.field.created_at": "Created at",
|
||||||
"item.field.type": "Item type",
|
"item.field.type": "Item Type",
|
||||||
"item.field.type.inventory": "Inventory",
|
"item.field.type.inventory": "Inventory",
|
||||||
"item.field.type.service": "Service",
|
"item.field.type.service": "Service",
|
||||||
"item.field.type.non-inventory": "Non inventory",
|
"item.field.type.non-inventory": "Non Inventory",
|
||||||
"item.field.name": "Name",
|
"item.field.name": "Item Name",
|
||||||
"item.field.code": "Code",
|
"item.field.code": "Item Code",
|
||||||
"item.field.sellable": "Sellable",
|
"item.field.sellable": "Sellable",
|
||||||
"item.field.purchasable": "Purchasable",
|
"item.field.purchasable": "Purchasable",
|
||||||
"item.field.cost_price": "Cost price",
|
"item.field.cost_price": "Cost Price",
|
||||||
"item.field.cost_account": "Cost account",
|
"item.field.sell_price": "Sell Price",
|
||||||
"item.field.sell_account": "Sell account",
|
"item.field.cost_account": "Cost Account",
|
||||||
"item.field.sell_description": "Sell description",
|
"item.field.sell_account": "Sell Account",
|
||||||
"item.field.inventory_account": "Inventory account",
|
"item.field.sell_description": "Sell Description",
|
||||||
"item.field.purchase_description": "Purchase description",
|
"item.field.inventory_account": "Inventory Account",
|
||||||
"item.field.quantity_on_hand": "Quantity on hand",
|
"item.field.purchase_description": "Purchase Description",
|
||||||
|
"item.field.quantity_on_hand": "Quantity on Hand",
|
||||||
"item.field.note": "Note",
|
"item.field.note": "Note",
|
||||||
"item.field.category": "Category",
|
"item.field.category": "Category",
|
||||||
"item.field.active": "Active",
|
"item.field.active": "Active",
|
||||||
"item.field.created_at": "Created at",
|
"item.field.created_at": "Created At",
|
||||||
"item_category.field.name": "Name",
|
"item_category.field.name": "Name",
|
||||||
"item_category.field.description": "Description",
|
"item_category.field.description": "Description",
|
||||||
"item_category.field.count": "Count",
|
"item_category.field.count": "Count",
|
||||||
|
|||||||
@@ -344,7 +344,7 @@ export default class ItemsController extends BaseController {
|
|||||||
|
|
||||||
const filter = {
|
const filter = {
|
||||||
sortOrder: 'DESC',
|
sortOrder: 'DESC',
|
||||||
columnSortBy: 'created_at',
|
columnSortBy: 'createdAt',
|
||||||
page: 1,
|
page: 1,
|
||||||
pageSize: 12,
|
pageSize: 12,
|
||||||
inactiveMode: false,
|
inactiveMode: false,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import ItemTransactionsController from './ItemsTransactions';
|
|||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class ItemsBaseController {
|
export default class ItemsBaseController {
|
||||||
router() {
|
public router() {
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
router.use('/', Container.get(ItemsController).router());
|
router.use('/', Container.get(ItemsController).router());
|
||||||
|
|||||||
@@ -32,12 +32,13 @@ export interface IModelMetaFieldCommon {
|
|||||||
name: string;
|
name: string;
|
||||||
column: string;
|
column: string;
|
||||||
columnable?: boolean;
|
columnable?: boolean;
|
||||||
fieldType: IModelColumnType;
|
|
||||||
customQuery?: Function;
|
customQuery?: Function;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
importHint?: string;
|
importHint?: string;
|
||||||
|
importableRelationLabel?: string;
|
||||||
order?: number;
|
order?: number;
|
||||||
unique?: number;
|
unique?: number;
|
||||||
|
dataTransferObjectKey?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IModelMetaFieldText {
|
export interface IModelMetaFieldText {
|
||||||
|
|||||||
@@ -16,45 +16,53 @@ export default {
|
|||||||
{ key: 'non-inventory', label: 'item.field.type.non-inventory' },
|
{ key: 'non-inventory', label: 'item.field.type.non-inventory' },
|
||||||
],
|
],
|
||||||
importable: true,
|
importable: true,
|
||||||
|
required: true,
|
||||||
},
|
},
|
||||||
name: {
|
name: {
|
||||||
name: 'item.field.name',
|
name: 'item.field.name',
|
||||||
column: 'name',
|
column: 'name',
|
||||||
fieldType: 'text',
|
fieldType: 'text',
|
||||||
importable: true,
|
importable: true,
|
||||||
|
required: true,
|
||||||
|
unique: true,
|
||||||
},
|
},
|
||||||
code: {
|
code: {
|
||||||
name: 'item.field.code',
|
name: 'item.field.code',
|
||||||
column: 'code',
|
column: 'code',
|
||||||
fieldType: 'text',
|
fieldType: 'text',
|
||||||
importable: true,
|
importable: true,
|
||||||
|
|
||||||
},
|
},
|
||||||
sellable: {
|
sellable: {
|
||||||
name: 'item.field.sellable',
|
name: 'item.field.sellable',
|
||||||
column: 'sellable',
|
column: 'sellable',
|
||||||
fieldType: 'boolean',
|
fieldType: 'boolean',
|
||||||
importable: true,
|
importable: true,
|
||||||
|
required: true,
|
||||||
},
|
},
|
||||||
purchasable: {
|
purchasable: {
|
||||||
name: 'item.field.purchasable',
|
name: 'item.field.purchasable',
|
||||||
column: 'purchasable',
|
column: 'purchasable',
|
||||||
fieldType: 'boolean',
|
fieldType: 'boolean',
|
||||||
importable: true,
|
importable: true,
|
||||||
|
required: true,
|
||||||
},
|
},
|
||||||
sellPrice: {
|
sellPrice: {
|
||||||
name: 'item.field.cost_price',
|
name: 'item.field.sell_price',
|
||||||
column: 'sell_price',
|
column: 'sell_price',
|
||||||
fieldType: 'number',
|
fieldType: 'number',
|
||||||
importable: true,
|
importable: true,
|
||||||
|
required: true,
|
||||||
},
|
},
|
||||||
costPrice: {
|
costPrice: {
|
||||||
name: 'item.field.cost_account',
|
name: 'item.field.cost_price',
|
||||||
column: 'cost_price',
|
column: 'cost_price',
|
||||||
fieldType: 'number',
|
fieldType: 'number',
|
||||||
importable: true,
|
importable: true,
|
||||||
|
required: true,
|
||||||
},
|
},
|
||||||
costAccount: {
|
costAccount: {
|
||||||
name: 'item.field.sell_account',
|
name: 'item.field.cost_account',
|
||||||
column: 'cost_account_id',
|
column: 'cost_account_id',
|
||||||
fieldType: 'relation',
|
fieldType: 'relation',
|
||||||
|
|
||||||
@@ -64,10 +72,13 @@ export default {
|
|||||||
relationEntityLabel: 'name',
|
relationEntityLabel: 'name',
|
||||||
relationEntityKey: 'slug',
|
relationEntityKey: 'slug',
|
||||||
|
|
||||||
|
dataTransferObjectKey: 'costAccountId',
|
||||||
|
importableRelationLabel: ['name', 'code'],
|
||||||
importable: true,
|
importable: true,
|
||||||
|
required: true,
|
||||||
},
|
},
|
||||||
sellAccount: {
|
sellAccount: {
|
||||||
name: 'item.field.sell_description',
|
name: 'item.field.sell_account',
|
||||||
column: 'sell_account_id',
|
column: 'sell_account_id',
|
||||||
fieldType: 'relation',
|
fieldType: 'relation',
|
||||||
|
|
||||||
@@ -77,11 +88,15 @@ export default {
|
|||||||
relationEntityLabel: 'name',
|
relationEntityLabel: 'name',
|
||||||
relationEntityKey: 'slug',
|
relationEntityKey: 'slug',
|
||||||
|
|
||||||
|
importableRelationLabel: ['name', 'code'],
|
||||||
importable: true,
|
importable: true,
|
||||||
|
|
||||||
|
required: true,
|
||||||
},
|
},
|
||||||
inventoryAccount: {
|
inventoryAccount: {
|
||||||
name: 'item.field.inventory_account',
|
name: 'item.field.inventory_account',
|
||||||
column: 'inventory_account_id',
|
column: 'inventory_account_id',
|
||||||
|
fieldType: 'relation',
|
||||||
|
|
||||||
relationType: 'enumeration',
|
relationType: 'enumeration',
|
||||||
relationKey: 'inventoryAccount',
|
relationKey: 'inventoryAccount',
|
||||||
@@ -89,7 +104,10 @@ export default {
|
|||||||
relationEntityLabel: 'name',
|
relationEntityLabel: 'name',
|
||||||
relationEntityKey: 'slug',
|
relationEntityKey: 'slug',
|
||||||
|
|
||||||
|
importableRelationLabel: ['name', 'code'],
|
||||||
importable: true,
|
importable: true,
|
||||||
|
|
||||||
|
required: true,
|
||||||
},
|
},
|
||||||
sellDescription: {
|
sellDescription: {
|
||||||
name: 'Sell description',
|
name: 'Sell description',
|
||||||
@@ -107,7 +125,6 @@ export default {
|
|||||||
name: 'item.field.quantity_on_hand',
|
name: 'item.field.quantity_on_hand',
|
||||||
column: 'quantity_on_hand',
|
column: 'quantity_on_hand',
|
||||||
fieldType: 'number',
|
fieldType: 'number',
|
||||||
importable: true,
|
|
||||||
},
|
},
|
||||||
note: {
|
note: {
|
||||||
name: 'item.field.note',
|
name: 'item.field.note',
|
||||||
@@ -118,12 +135,15 @@ export default {
|
|||||||
category: {
|
category: {
|
||||||
name: 'item.field.category',
|
name: 'item.field.category',
|
||||||
column: 'category_id',
|
column: 'category_id',
|
||||||
|
fieldType: 'relation',
|
||||||
|
|
||||||
relationType: 'enumeration',
|
relationType: 'enumeration',
|
||||||
relationKey: 'category',
|
relationKey: 'category',
|
||||||
|
|
||||||
relationEntityLabel: 'name',
|
relationEntityLabel: 'name',
|
||||||
relationEntityKey: 'id',
|
relationEntityKey: 'id',
|
||||||
|
|
||||||
|
importableRelationLabel: 'name',
|
||||||
importable: true,
|
importable: true,
|
||||||
},
|
},
|
||||||
active: {
|
active: {
|
||||||
|
|||||||
@@ -4,16 +4,19 @@ export default {
|
|||||||
sortField: 'name',
|
sortField: 'name',
|
||||||
sortOrder: 'DESC',
|
sortOrder: 'DESC',
|
||||||
},
|
},
|
||||||
|
importable: true,
|
||||||
fields: {
|
fields: {
|
||||||
name: {
|
name: {
|
||||||
name: 'item_category.field.name',
|
name: 'item_category.field.name',
|
||||||
column: 'name',
|
column: 'name',
|
||||||
fieldType: 'text',
|
fieldType: 'text',
|
||||||
|
importable: true,
|
||||||
},
|
},
|
||||||
description: {
|
description: {
|
||||||
name: 'item_category.field.description',
|
name: 'item_category.field.description',
|
||||||
column: 'description',
|
column: 'description',
|
||||||
fieldType: 'text',
|
fieldType: 'text',
|
||||||
|
importable: true,
|
||||||
},
|
},
|
||||||
count: {
|
count: {
|
||||||
name: 'item_category.field.count',
|
name: 'item_category.field.count',
|
||||||
|
|||||||
@@ -1,21 +1,32 @@
|
|||||||
import { Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
import * as R from 'ramda';
|
import * as R from 'ramda';
|
||||||
import { isUndefined, get, chain } from 'lodash';
|
import bluebird from 'bluebird';
|
||||||
|
import { isUndefined, get, chain, toArray, pickBy, castArray } from 'lodash';
|
||||||
import { ImportMappingAttr, ResourceMetaFieldsMap } from './interfaces';
|
import { ImportMappingAttr, ResourceMetaFieldsMap } from './interfaces';
|
||||||
import { parseBoolean } from '@/utils';
|
import { parseBoolean } from '@/utils';
|
||||||
import { trimObject } from './_utils';
|
import { trimObject } from './_utils';
|
||||||
|
import { Account, Item } from '@/models';
|
||||||
|
import ResourceService from '../Resource/ResourceService';
|
||||||
|
import { Knex } from 'knex';
|
||||||
|
|
||||||
|
const CurrencyParsingDTOs = 10;
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class ImportFileDataTransformer {
|
export class ImportFileDataTransformer {
|
||||||
|
@Inject()
|
||||||
|
private resource: ResourceService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {number} tenantId -
|
* @param {number} tenantId -
|
||||||
* @param {}
|
* @param {}
|
||||||
*/
|
*/
|
||||||
public parseSheetData(
|
public async parseSheetData(
|
||||||
|
tenantId: number,
|
||||||
importFile: any,
|
importFile: any,
|
||||||
importableFields: any,
|
importableFields: any,
|
||||||
data: Record<string, unknown>[]
|
data: Record<string, unknown>[],
|
||||||
|
trx?: Knex.Transaction
|
||||||
) {
|
) {
|
||||||
// Sanitize the sheet data.
|
// Sanitize the sheet data.
|
||||||
const sanitizedData = this.sanitizeSheetData(data);
|
const sanitizedData = this.sanitizeSheetData(data);
|
||||||
@@ -25,10 +36,17 @@ export class ImportFileDataTransformer {
|
|||||||
sanitizedData,
|
sanitizedData,
|
||||||
importFile.mappingParsed
|
importFile.mappingParsed
|
||||||
);
|
);
|
||||||
|
const resourceModel = this.resource.getResourceModel(
|
||||||
|
tenantId,
|
||||||
|
importFile.resource
|
||||||
|
);
|
||||||
// Parse the mapped sheet values.
|
// Parse the mapped sheet values.
|
||||||
const parsedValues = this.parseExcelValues(importableFields, mappedDTOs);
|
return this.parseExcelValues(
|
||||||
|
importableFields,
|
||||||
return parsedValues;
|
mappedDTOs,
|
||||||
|
resourceModel,
|
||||||
|
trx
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -67,35 +85,86 @@ export class ImportFileDataTransformer {
|
|||||||
* @param {Record<string, any>} valueDTOS -
|
* @param {Record<string, any>} valueDTOS -
|
||||||
* @returns {Record<string, any>}
|
* @returns {Record<string, any>}
|
||||||
*/
|
*/
|
||||||
public parseExcelValues(
|
public async parseExcelValues(
|
||||||
fields: ResourceMetaFieldsMap,
|
fields: ResourceMetaFieldsMap,
|
||||||
valueDTOs: Record<string, any>[]
|
valueDTOs: Record<string, any>[],
|
||||||
): Record<string, any> {
|
resourceModel: any,
|
||||||
const parser = (value, key) => {
|
trx?: Knex.Transaction
|
||||||
|
): Promise<Record<string, any>> {
|
||||||
|
// Prases the given object value based on the field key type.
|
||||||
|
const parser = async (value, key) => {
|
||||||
let _value = value;
|
let _value = value;
|
||||||
|
const field = fields[key];
|
||||||
|
|
||||||
// Parses the boolean value.
|
// Parses the boolean value.
|
||||||
if (fields[key].fieldType === 'boolean') {
|
if (fields[key].fieldType === 'boolean') {
|
||||||
_value = parseBoolean(value, false);
|
_value = parseBoolean(value, false);
|
||||||
|
|
||||||
// Parses the enumeration value.
|
// Parses the enumeration value.
|
||||||
} else if (fields[key].fieldType === 'enumeration') {
|
} else if (field.fieldType === 'enumeration') {
|
||||||
const field = fields[key];
|
const field = fields[key];
|
||||||
const option = get(field, 'options', []).find(
|
const option = get(field, 'options', []).find(
|
||||||
(option) => option.label === value
|
(option) => option.label === value
|
||||||
);
|
);
|
||||||
_value = get(option, 'key');
|
_value = get(option, 'key');
|
||||||
// Prases the numeric value.
|
// Parses the numeric value.
|
||||||
} else if (fields[key].fieldType === 'number') {
|
} else if (fields[key].fieldType === 'number') {
|
||||||
_value = parseFloat(value);
|
_value = parseFloat(value);
|
||||||
|
// Parses the relation value.
|
||||||
|
} else if (field.fieldType === 'relation') {
|
||||||
|
const relationModel = resourceModel.relationMappings[field.relationKey];
|
||||||
|
const RelationModel = relationModel?.modelClass;
|
||||||
|
|
||||||
|
if (!relationModel || !RelationModel) {
|
||||||
|
throw new Error(`The relation model of ${key} field is not exist.`);
|
||||||
|
}
|
||||||
|
const relationQuery = RelationModel.query(trx);
|
||||||
|
const relationKeys = field?.importableRelationLabel
|
||||||
|
? castArray(field?.importableRelationLabel)
|
||||||
|
: castArray(field?.relationEntityLabel);
|
||||||
|
|
||||||
|
relationQuery.where(function () {
|
||||||
|
relationKeys.forEach((relationKey: string) => {
|
||||||
|
this.orWhereRaw('LOWER(??) = LOWER(?)', [relationKey, value]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const result = await relationQuery.first();
|
||||||
|
_value = get(result, 'id');
|
||||||
}
|
}
|
||||||
return _value;
|
return _value;
|
||||||
};
|
};
|
||||||
return valueDTOs.map((DTO) => {
|
|
||||||
return chain(DTO)
|
const parseKey = (key: string) => {
|
||||||
.pickBy((value, key) => !isUndefined(fields[key]))
|
const field = fields[key];
|
||||||
.mapValues(parser)
|
let _objectTransferObjectKey = key;
|
||||||
.value();
|
|
||||||
|
if (field.fieldType === 'relation') {
|
||||||
|
_objectTransferObjectKey = `${key}Id`;
|
||||||
|
}
|
||||||
|
return _objectTransferObjectKey;
|
||||||
|
};
|
||||||
|
const parseAsync = async (valueDTO) => {
|
||||||
|
// Remove the undefined fields.
|
||||||
|
const _valueDTO = pickBy(
|
||||||
|
valueDTO,
|
||||||
|
(value, key) => !isUndefined(fields[key])
|
||||||
|
);
|
||||||
|
const keys = Object.keys(_valueDTO);
|
||||||
|
|
||||||
|
// Map the object values.
|
||||||
|
return bluebird.reduce(
|
||||||
|
keys,
|
||||||
|
async (acc, key) => {
|
||||||
|
const parsedValue = await parser(_valueDTO[key], key);
|
||||||
|
const parsedKey = await parseKey(key);
|
||||||
|
acc[parsedKey] = parsedValue;
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
return bluebird.map(valueDTOs, parseAsync, {
|
||||||
|
concurrency: CurrencyParsingDTOs,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,19 +57,31 @@ export class ImportFileProcess {
|
|||||||
tenantId,
|
tenantId,
|
||||||
importFile.resource
|
importFile.resource
|
||||||
);
|
);
|
||||||
// Prases the sheet json data.
|
|
||||||
const parsedData = this.importParser.parseSheetData(
|
|
||||||
importFile,
|
|
||||||
importableFields,
|
|
||||||
sheetData
|
|
||||||
);
|
|
||||||
// Runs the importing operation with ability to return errors that will happen.
|
// Runs the importing operation with ability to return errors that will happen.
|
||||||
const [successedImport, failedImport] = await this.uow.withTransaction(
|
const [successedImport, failedImport, allData] =
|
||||||
tenantId,
|
await this.uow.withTransaction(
|
||||||
(trx: Knex.Transaction) =>
|
tenantId,
|
||||||
this.importCommon.import(tenantId, importFile, parsedData, trx),
|
async (trx: Knex.Transaction) => {
|
||||||
trx
|
// Prases the sheet json data.
|
||||||
);
|
const parsedData = await this.importParser.parseSheetData(
|
||||||
|
tenantId,
|
||||||
|
importFile,
|
||||||
|
importableFields,
|
||||||
|
sheetData,
|
||||||
|
trx
|
||||||
|
);
|
||||||
|
const [successedImport, failedImport] =
|
||||||
|
await this.importCommon.import(
|
||||||
|
tenantId,
|
||||||
|
importFile,
|
||||||
|
parsedData,
|
||||||
|
trx
|
||||||
|
);
|
||||||
|
return [successedImport, failedImport, parsedData];
|
||||||
|
},
|
||||||
|
trx
|
||||||
|
);
|
||||||
const mapping = importFile.mappingParsed;
|
const mapping = importFile.mappingParsed;
|
||||||
const errors = chain(failedImport)
|
const errors = chain(failedImport)
|
||||||
.map((oper) => oper.error)
|
.map((oper) => oper.error)
|
||||||
@@ -77,7 +89,7 @@ export class ImportFileProcess {
|
|||||||
.value();
|
.value();
|
||||||
|
|
||||||
const unmappedColumns = getUnmappedSheetColumns(header, mapping);
|
const unmappedColumns = getUnmappedSheetColumns(header, mapping);
|
||||||
const totalCount = parsedData.length;
|
const totalCount = allData.length;
|
||||||
|
|
||||||
const createdCount = successedImport.length;
|
const createdCount = successedImport.length;
|
||||||
const errorsCount = failedImport.length;
|
const errorsCount = failedImport.length;
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { ImportableRegistry } from './ImportableRegistry';
|
|||||||
import { UncategorizedTransactionsImportable } from '../Cashflow/UncategorizedTransactionsImportable';
|
import { UncategorizedTransactionsImportable } from '../Cashflow/UncategorizedTransactionsImportable';
|
||||||
import { CustomersImportable } from '../Contacts/Customers/CustomersImportable';
|
import { CustomersImportable } from '../Contacts/Customers/CustomersImportable';
|
||||||
import { VendorsImportable } from '../Contacts/Vendors/VendorsImportable';
|
import { VendorsImportable } from '../Contacts/Vendors/VendorsImportable';
|
||||||
|
import { ItemsImportable } from '../Items/ItemsImportable';
|
||||||
|
import { ItemCategoriesImportable } from '../ItemCategories/ItemCategoriesImportable';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class ImportableResources {
|
export class ImportableResources {
|
||||||
@@ -24,6 +26,8 @@ export class ImportableResources {
|
|||||||
},
|
},
|
||||||
{ resource: 'Customer', importable: CustomersImportable },
|
{ resource: 'Customer', importable: CustomersImportable },
|
||||||
{ resource: 'Vendor', importable: VendorsImportable },
|
{ resource: 'Vendor', importable: VendorsImportable },
|
||||||
|
{ resource: 'Item', importable: ItemsImportable },
|
||||||
|
{ resource: 'ItemCategory', importable: ItemCategoriesImportable },
|
||||||
];
|
];
|
||||||
|
|
||||||
public get registry() {
|
public get registry() {
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
import * as Yup from 'yup';
|
import * as Yup from 'yup';
|
||||||
import { defaultTo, upperFirst, camelCase, first, isUndefined, pickBy } from 'lodash';
|
import {
|
||||||
|
defaultTo,
|
||||||
|
upperFirst,
|
||||||
|
camelCase,
|
||||||
|
first,
|
||||||
|
isUndefined,
|
||||||
|
pickBy,
|
||||||
|
} from 'lodash';
|
||||||
import pluralize from 'pluralize';
|
import pluralize from 'pluralize';
|
||||||
import { ResourceMetaFieldsMap } from './interfaces';
|
import { ResourceMetaFieldsMap } from './interfaces';
|
||||||
import { IModelMetaField } from '@/interfaces';
|
import { IModelMetaField } from '@/interfaces';
|
||||||
@@ -83,11 +90,25 @@ export const convertFieldsToYupValidation = (fields: ResourceMetaFieldsMap) => {
|
|||||||
if (field.required) {
|
if (field.required) {
|
||||||
fieldSchema = fieldSchema.required();
|
fieldSchema = fieldSchema.required();
|
||||||
}
|
}
|
||||||
yupSchema[fieldName] = fieldSchema;
|
const _fieldName = parseFieldName(fieldName, field);
|
||||||
|
|
||||||
|
yupSchema[_fieldName] = fieldSchema;
|
||||||
});
|
});
|
||||||
return Yup.object().shape(yupSchema);
|
return Yup.object().shape(yupSchema);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const parseFieldName = (fieldName: string, field: IModelMetaField) => {
|
||||||
|
let _key = fieldName;
|
||||||
|
|
||||||
|
if (field.fieldType === 'relation') {
|
||||||
|
_key = `${fieldName}Id`;
|
||||||
|
}
|
||||||
|
if (field.dataTransferObjectKey) {
|
||||||
|
_key = field.dataTransferObjectKey;
|
||||||
|
}
|
||||||
|
return _key;
|
||||||
|
};
|
||||||
|
|
||||||
export const getUnmappedSheetColumns = (columns, mapping) => {
|
export const getUnmappedSheetColumns = (columns, mapping) => {
|
||||||
return columns.filter(
|
return columns.filter(
|
||||||
(column) => !mapping.some((map) => map.from === column)
|
(column) => !mapping.some((map) => map.from === column)
|
||||||
|
|||||||
@@ -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 { Inject } from 'typedi';
|
||||||
import * as R from 'ramda';
|
import * as R from 'ramda';
|
||||||
import Knex from 'knex';
|
import { Knex } from 'knex';
|
||||||
import { ServiceError } from '@/exceptions';
|
import { ServiceError } from '@/exceptions';
|
||||||
import {
|
import {
|
||||||
IItemCategory,
|
IItemCategory,
|
||||||
@@ -115,7 +115,8 @@ export default class ItemCategoriesService implements IItemCategoriesService {
|
|||||||
public async newItemCategory(
|
public async newItemCategory(
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
itemCategoryOTD: IItemCategoryOTD,
|
itemCategoryOTD: IItemCategoryOTD,
|
||||||
authorizedUser: ISystemUser
|
authorizedUser: ISystemUser,
|
||||||
|
trx?: Knex.Transaction
|
||||||
): Promise<IItemCategory> {
|
): Promise<IItemCategory> {
|
||||||
const { ItemCategory } = this.tenancy.models(tenantId);
|
const { ItemCategory } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
@@ -139,20 +140,24 @@ export default class ItemCategoriesService implements IItemCategoriesService {
|
|||||||
authorizedUser
|
authorizedUser
|
||||||
);
|
);
|
||||||
// Creates item category under unit-of-work evnirement.
|
// Creates item category under unit-of-work evnirement.
|
||||||
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
|
return this.uow.withTransaction(
|
||||||
// Inserts the item category.
|
tenantId,
|
||||||
const itemCategory = await ItemCategory.query(trx).insert({
|
async (trx: Knex.Transaction) => {
|
||||||
...itemCategoryObj,
|
// Inserts the item category.
|
||||||
});
|
const itemCategory = await ItemCategory.query(trx).insert({
|
||||||
// Triggers `onItemCategoryCreated` event.
|
...itemCategoryObj,
|
||||||
await this.eventPublisher.emitAsync(events.itemCategory.onCreated, {
|
});
|
||||||
itemCategory,
|
// Triggers `onItemCategoryCreated` event.
|
||||||
tenantId,
|
await this.eventPublisher.emitAsync(events.itemCategory.onCreated, {
|
||||||
trx,
|
itemCategory,
|
||||||
} as IItemCategoryCreatedPayload);
|
tenantId,
|
||||||
|
trx,
|
||||||
|
} as IItemCategoryCreatedPayload);
|
||||||
|
|
||||||
return itemCategory;
|
return itemCategory;
|
||||||
});
|
},
|
||||||
|
trx
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -308,7 +313,7 @@ export default class ItemCategoriesService implements IItemCategoriesService {
|
|||||||
} as IItemCategoryDeletedPayload);
|
} as IItemCategoryDeletedPayload);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parses items categories filter DTO.
|
* Parses items categories filter DTO.
|
||||||
* @param {} filterDTO
|
* @param {} filterDTO
|
||||||
|
|||||||
@@ -11,3 +11,25 @@ export const ERRORS = {
|
|||||||
INVENTORY_ACCOUNT_NOT_INVENTORY: 'INVENTORY_ACCOUNT_NOT_INVENTORY',
|
INVENTORY_ACCOUNT_NOT_INVENTORY: 'INVENTORY_ACCOUNT_NOT_INVENTORY',
|
||||||
CATEGORY_HAVE_ITEMS: 'CATEGORY_HAVE_ITEMS',
|
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
|
* @param {IItemDTO} item
|
||||||
* @return {Promise<IItem>}
|
* @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);
|
const { Item } = this.tenancy.models(tenantId);
|
||||||
|
|
||||||
// Authorize the item before creating.
|
// Authorize the item before creating.
|
||||||
@@ -111,7 +115,8 @@ export class CreateItem {
|
|||||||
} as IItemEventCreatedPayload);
|
} as IItemEventCreatedPayload);
|
||||||
|
|
||||||
return item;
|
return item;
|
||||||
}
|
},
|
||||||
|
trx
|
||||||
);
|
);
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,10 @@ export class ItemsValidators {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (foundItems.length > 0) {
|
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';
|
||||||
|
|
||||||
|
@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> {
|
||||||
|
console.log(createDTO, tenantId, 'XX');
|
||||||
|
await this.createItemService.createItem(tenantId, createDTO, trx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the sample data of customers used to download sample sheet.
|
||||||
|
*/
|
||||||
|
public sampleData(): any[] {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -94,6 +94,10 @@ function ItemsActionsBar({
|
|||||||
const handleTableRowSizeChange = (size) => {
|
const handleTableRowSizeChange = (size) => {
|
||||||
addSetting('items', 'tableSize', size);
|
addSetting('items', 'tableSize', size);
|
||||||
};
|
};
|
||||||
|
// Handles the import button click.
|
||||||
|
const handleImportBtnClick = () => {
|
||||||
|
history.push('/items/import');
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DashboardActionsBar>
|
<DashboardActionsBar>
|
||||||
@@ -143,6 +147,7 @@ function ItemsActionsBar({
|
|||||||
<Button
|
<Button
|
||||||
className={Classes.MINIMAL}
|
className={Classes.MINIMAL}
|
||||||
icon={<Icon icon="file-import-16" iconSize={16} />}
|
icon={<Icon icon="file-import-16" iconSize={16} />}
|
||||||
|
onClick={handleImportBtnClick}
|
||||||
text={<T id={'import'} />}
|
text={<T id={'import'} />}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
25
packages/webapp/src/containers/Items/ItemsImportPage.tsx
Normal file
25
packages/webapp/src/containers/Items/ItemsImportPage.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import { DashboardInsider } from '@/components';
|
||||||
|
import { ImportView } from '../Import/ImportView';
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
|
export default function ItemsImportpage() {
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
const handleImportSuccess = () => {
|
||||||
|
history.push('/items');
|
||||||
|
};
|
||||||
|
const handleCancelBtnClick = () => {
|
||||||
|
history.push('/items');
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<DashboardInsider name={'import-items'}>
|
||||||
|
<ImportView
|
||||||
|
resource={'items'}
|
||||||
|
onImportSuccess={handleImportSuccess}
|
||||||
|
onCancelClick={handleCancelBtnClick}
|
||||||
|
exampleTitle="Items Example"
|
||||||
|
/>
|
||||||
|
</DashboardInsider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
import { DashboardInsider } from '@/components';
|
||||||
|
import { ImportView } from '../Import/ImportView';
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
|
export default function ItemCategoriesImport() {
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
const handleImportSuccess = () => {
|
||||||
|
history.push('/items/categories');
|
||||||
|
};
|
||||||
|
const handleCancelBtnClick = () => {
|
||||||
|
history.push('/items/categories');
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<DashboardInsider name={'import-item-categories'}>
|
||||||
|
<ImportView
|
||||||
|
resource={'item_category'}
|
||||||
|
onImportSuccess={handleImportSuccess}
|
||||||
|
onCancelClick={handleCancelBtnClick}
|
||||||
|
exampleTitle="Item Categories Example"
|
||||||
|
/>
|
||||||
|
</DashboardInsider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
FormattedMessage as T,
|
FormattedMessage as T,
|
||||||
AdvancedFilterPopover,
|
AdvancedFilterPopover,
|
||||||
DashboardFilterButton,
|
DashboardFilterButton,
|
||||||
DashboardActionsBar
|
DashboardActionsBar,
|
||||||
} from '@/components';
|
} from '@/components';
|
||||||
|
|
||||||
import withItemCategories from './withItemCategories';
|
import withItemCategories from './withItemCategories';
|
||||||
@@ -22,6 +22,7 @@ import withAlertActions from '@/containers/Alert/withAlertActions';
|
|||||||
|
|
||||||
import { compose } from '@/utils';
|
import { compose } from '@/utils';
|
||||||
import { useItemsCategoriesContext } from './ItemsCategoriesProvider';
|
import { useItemsCategoriesContext } from './ItemsCategoriesProvider';
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Items categories actions bar.
|
* Items categories actions bar.
|
||||||
@@ -41,11 +42,16 @@ function ItemsCategoryActionsBar({
|
|||||||
openAlert,
|
openAlert,
|
||||||
}) {
|
}) {
|
||||||
const { fields } = useItemsCategoriesContext();
|
const { fields } = useItemsCategoriesContext();
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
const onClickNewCategory = () => {
|
const onClickNewCategory = () => {
|
||||||
openDialog('item-category-form', {});
|
openDialog('item-category-form', {});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleImportBtnClick = () => {
|
||||||
|
history.push('/item/categories/import');
|
||||||
|
};
|
||||||
|
|
||||||
// Handle the items categories bulk delete.
|
// Handle the items categories bulk delete.
|
||||||
const handelBulkDelete = () => {
|
const handelBulkDelete = () => {
|
||||||
openAlert('item-categories-bulk-delete', {
|
openAlert('item-categories-bulk-delete', {
|
||||||
@@ -93,6 +99,7 @@ function ItemsCategoryActionsBar({
|
|||||||
className={Classes.MINIMAL}
|
className={Classes.MINIMAL}
|
||||||
icon={<Icon icon="file-import-16" iconSize={16} />}
|
icon={<Icon icon="file-import-16" iconSize={16} />}
|
||||||
text={<T id={'import'} />}
|
text={<T id={'import'} />}
|
||||||
|
onClick={handleImportBtnClick}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
className={Classes.MINIMAL}
|
className={Classes.MINIMAL}
|
||||||
|
|||||||
@@ -63,6 +63,16 @@ export const getDashboardRoutes = () => [
|
|||||||
defaultSearchResource: RESOURCES_TYPES.MANUAL_JOURNAL,
|
defaultSearchResource: RESOURCES_TYPES.MANUAL_JOURNAL,
|
||||||
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
|
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: `/item/categories/import`,
|
||||||
|
component: lazy(
|
||||||
|
() => import('@/containers/ItemsCategories/ItemCategoriesImport'),
|
||||||
|
),
|
||||||
|
backLink: true,
|
||||||
|
pageTitle: 'Item Categories Import',
|
||||||
|
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
|
||||||
|
defaultSearchResource: RESOURCES_TYPES.ITEM,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: `/items/categories`,
|
path: `/items/categories`,
|
||||||
component: lazy(
|
component: lazy(
|
||||||
@@ -73,9 +83,16 @@ export const getDashboardRoutes = () => [
|
|||||||
defaultSearchResource: RESOURCES_TYPES.ITEM,
|
defaultSearchResource: RESOURCES_TYPES.ITEM,
|
||||||
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
|
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
|
||||||
},
|
},
|
||||||
|
|
||||||
// Items.
|
// Items.
|
||||||
|
{
|
||||||
|
path: `/items/import`,
|
||||||
|
component: lazy(() => import('@/containers/Items/ItemsImportPage')),
|
||||||
|
backLink: true,
|
||||||
|
pageTitle: 'Items Import',
|
||||||
|
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
|
||||||
|
defaultSearchResource: RESOURCES_TYPES.CUSTOMER,
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
path: `/items/:id/edit`,
|
path: `/items/:id/edit`,
|
||||||
component: lazy(() => import('@/containers/Items/ItemFormPage')),
|
component: lazy(() => import('@/containers/Items/ItemFormPage')),
|
||||||
@@ -514,12 +531,7 @@ export const getDashboardRoutes = () => [
|
|||||||
// Customers
|
// Customers
|
||||||
{
|
{
|
||||||
path: `/customers/import`,
|
path: `/customers/import`,
|
||||||
component: lazy(
|
component: lazy(() => import('@/containers/Customers/CustomersImport')),
|
||||||
() =>
|
|
||||||
import(
|
|
||||||
'@/containers/Customers/CustomersImport'
|
|
||||||
),
|
|
||||||
),
|
|
||||||
backLink: true,
|
backLink: true,
|
||||||
pageTitle: 'Customers Import',
|
pageTitle: 'Customers Import',
|
||||||
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
|
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
|
||||||
@@ -577,9 +589,7 @@ export const getDashboardRoutes = () => [
|
|||||||
// Vendors
|
// Vendors
|
||||||
{
|
{
|
||||||
path: `/vendors/import`,
|
path: `/vendors/import`,
|
||||||
component: lazy(
|
component: lazy(() => import('@/containers/Vendors/VendorsImport')),
|
||||||
() => import('@/containers/Vendors/VendorsImport'),
|
|
||||||
),
|
|
||||||
backLink: true,
|
backLink: true,
|
||||||
pageTitle: 'Vendors Import',
|
pageTitle: 'Vendors Import',
|
||||||
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
|
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
|
||||||
|
|||||||
Reference in New Issue
Block a user