Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: linking relation with id in importing #393

Merged
merged 2 commits into from
Apr 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 13 additions & 12 deletions packages/server/resources/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -245,25 +245,26 @@
"account.field.currency": "Currency",
"account.field.balance": "Balance",
"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",
Expand Down
2 changes: 1 addition & 1 deletion packages/server/src/api/controllers/Items/Items.ts
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,7 @@ export default class ItemsController extends BaseController {

const filter = {
sortOrder: 'DESC',
columnSortBy: 'created_at',
columnSortBy: 'createdAt',
page: 1,
pageSize: 12,
inactiveMode: false,
Expand Down
2 changes: 1 addition & 1 deletion packages/server/src/api/controllers/Items/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
3 changes: 2 additions & 1 deletion packages/server/src/interfaces/Model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,13 @@ 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 IModelMetaFieldText {
Expand Down
30 changes: 25 additions & 5 deletions packages/server/src/models/Item.Settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,45 +16,53 @@ export default {
{ key: 'non-inventory', label: 'item.field.type.non-inventory' },
],
importable: true,
required: true,
},
name: {
name: 'item.field.name',
column: 'name',
fieldType: 'text',
importable: true,
required: true,
unique: true,
},
code: {
name: 'item.field.code',
column: 'code',
fieldType: 'text',
importable: true,

},
sellable: {
name: 'item.field.sellable',
column: 'sellable',
fieldType: 'boolean',
importable: true,
required: true,
},
purchasable: {
name: 'item.field.purchasable',
column: 'purchasable',
fieldType: 'boolean',
importable: true,
required: true,
},
sellPrice: {
name: 'item.field.cost_price',
name: 'item.field.sell_price',
column: 'sell_price',
fieldType: 'number',
importable: true,
required: true,
},
costPrice: {
name: 'item.field.cost_account',
name: 'item.field.cost_price',
column: 'cost_price',
fieldType: 'number',
importable: true,
required: true,
},
costAccount: {
name: 'item.field.sell_account',
name: 'item.field.cost_account',
column: 'cost_account_id',
fieldType: 'relation',

Expand All @@ -64,10 +72,13 @@ export default {
relationEntityLabel: 'name',
relationEntityKey: 'slug',

dataTransferObjectKey: 'costAccountId',
importableRelationLabel: ['name', 'code'],
importable: true,
required: true,
},
sellAccount: {
name: 'item.field.sell_description',
name: 'item.field.sell_account',
column: 'sell_account_id',
fieldType: 'relation',

Expand All @@ -77,19 +88,26 @@ export default {
relationEntityLabel: 'name',
relationEntityKey: 'slug',

importableRelationLabel: ['name', 'code'],
importable: true,

required: true,
},
inventoryAccount: {
name: 'item.field.inventory_account',
column: 'inventory_account_id',
fieldType: 'relation',

relationType: 'enumeration',
relationKey: 'inventoryAccount',

relationEntityLabel: 'name',
relationEntityKey: 'slug',

importableRelationLabel: ['name', 'code'],
importable: true,

required: true,
},
sellDescription: {
name: 'Sell description',
Expand All @@ -107,7 +125,6 @@ export default {
name: 'item.field.quantity_on_hand',
column: 'quantity_on_hand',
fieldType: 'number',
importable: true,
},
note: {
name: 'item.field.note',
Expand All @@ -118,12 +135,15 @@ export default {
category: {
name: 'item.field.category',
column: 'category_id',
fieldType: 'relation',

relationType: 'enumeration',
relationKey: 'category',

relationEntityLabel: 'name',
relationEntityKey: 'id',

importableRelationLabel: 'name',
importable: true,
},
active: {
Expand Down
3 changes: 3 additions & 0 deletions packages/server/src/models/ItemCategory.Settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,19 @@ export default {
sortField: 'name',
sortOrder: 'DESC',
},
importable: true,
fields: {
name: {
name: 'item_category.field.name',
column: 'name',
fieldType: 'text',
importable: true,
},
description: {
name: 'item_category.field.description',
column: 'description',
fieldType: 'text',
importable: true,
},
count: {
name: 'item_category.field.count',
Expand Down
105 changes: 87 additions & 18 deletions packages/server/src/services/Import/ImportFileDataTransformer.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,33 @@
import { Service } from 'typedi';
import { Inject, Service } from 'typedi';
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 { Knex } from 'knex';
import { ImportMappingAttr, ResourceMetaFieldsMap } from './interfaces';
import { trimObject, parseBoolean } from './_utils';
import { Account, Item } from '@/models';
import ResourceService from '../Resource/ResourceService';
import { multiNumberParse } from '@/utils/multi-number-parse';

const CurrencyParsingDTOs = 10;

@Service()
export class ImportFileDataTransformer {
@Inject()
private resource: ResourceService;

/**
* 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 parseSheetData(
public async parseSheetData(
tenantId: number,
importFile: any,
importableFields: any,
data: Record<string, unknown>[]
data: Record<string, unknown>[],
trx?: Knex.Transaction
) {
// Sanitize the sheet data.
const sanitizedData = this.sanitizeSheetData(data);
Expand All @@ -26,10 +37,17 @@ export class ImportFileDataTransformer {
sanitizedData,
importFile.mappingParsed
);
const resourceModel = this.resource.getResourceModel(
tenantId,
importFile.resource
);
// Parse the mapped sheet values.
const parsedValues = this.parseExcelValues(importableFields, mappedDTOs);

return parsedValues;
return this.parseExcelValues(
importableFields,
mappedDTOs,
resourceModel,
trx
);
}

/**
Expand Down Expand Up @@ -68,35 +86,86 @@ export class ImportFileDataTransformer {
* @param {Record<string, any>} valueDTOS -
* @returns {Record<string, any>}
*/
public parseExcelValues(
public async parseExcelValues(
fields: ResourceMetaFieldsMap,
valueDTOs: Record<string, any>[]
): Record<string, any> {
const parser = (value, key) => {
valueDTOs: Record<string, any>[],
resourceModel: any,
trx?: Knex.Transaction
): Promise<Record<string, any>> {
// Prases the given object value based on the field key type.
const parser = async (value, key) => {
let _value = value;
const field = fields[key];

// Parses the boolean value.
if (fields[key].fieldType === 'boolean') {
_value = parseBoolean(value);

// Parses the enumeration value.
} else if (fields[key].fieldType === 'enumeration') {
} else if (field.fieldType === 'enumeration') {
const field = fields[key];
const option = get(field, 'options', []).find(
(option) => option.label === value
);
_value = get(option, 'key');
// Prases the numeric value.
// Parses the numeric value.
} else if (fields[key].fieldType === 'number') {
_value = multiNumberParse(value);
// Parses the relation value.
} else if (field.fieldType === 'relation') {
const relationModel = resourceModel.relationMappings[field.relationKey];
const RelationModel = relationModel?.modelClass;

if (!relationModel || !RelationModel) {
throw new Error(`The relation model of ${key} field is not exist.`);
}
const relationQuery = RelationModel.query(trx);
const relationKeys = field?.importableRelationLabel
? castArray(field?.importableRelationLabel)
: castArray(field?.relationEntityLabel);

relationQuery.where(function () {
relationKeys.forEach((relationKey: string) => {
this.orWhereRaw('LOWER(??) = LOWER(?)', [relationKey, value]);
});
});
const result = await relationQuery.first();
_value = get(result, 'id');
}
return _value;
};
return valueDTOs.map((DTO) => {
return chain(DTO)
.pickBy((value, key) => !isUndefined(fields[key]))
.mapValues(parser)
.value();

const parseKey = (key: string) => {
const field = fields[key];
let _objectTransferObjectKey = key;

if (field.fieldType === 'relation') {
_objectTransferObjectKey = `${key}Id`;
}
return _objectTransferObjectKey;
};
const parseAsync = async (valueDTO) => {
// Remove the undefined fields.
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,
});
}
}
Loading
Loading