mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-16 21:00:31 +00:00
Compare commits
9 Commits
v0.19.9
...
date-forma
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e6f1efe30 | ||
|
|
c43123db76 | ||
|
|
ebbcab3926 | ||
|
|
a235f573c0 | ||
|
|
84a0b8f495 | ||
|
|
b87321c897 | ||
|
|
fc6ebfea5c | ||
|
|
161d60393a | ||
|
|
79413fa85e |
@@ -37,6 +37,7 @@
|
|||||||
"agendash": "^3.1.0",
|
"agendash": "^3.1.0",
|
||||||
"app-root-path": "^3.0.0",
|
"app-root-path": "^3.0.0",
|
||||||
"async": "^3.2.0",
|
"async": "^3.2.0",
|
||||||
|
"async-mutex": "^0.5.0",
|
||||||
"axios": "^1.6.0",
|
"axios": "^1.6.0",
|
||||||
"babel-loader": "^9.1.2",
|
"babel-loader": "^9.1.2",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ export class ImportController extends BaseController {
|
|||||||
body('mapping.*.group').optional(),
|
body('mapping.*.group').optional(),
|
||||||
body('mapping.*.from').exists(),
|
body('mapping.*.from').exists(),
|
||||||
body('mapping.*.to').exists(),
|
body('mapping.*.to').exists(),
|
||||||
|
body('mapping.*.dateFormat').optional({ nullable: true }),
|
||||||
],
|
],
|
||||||
this.validationResult,
|
this.validationResult,
|
||||||
this.asyncMiddleware(this.mapping.bind(this)),
|
this.asyncMiddleware(this.mapping.bind(this)),
|
||||||
|
|||||||
@@ -3,6 +3,6 @@ import { ImportFilePreviewPOJO } from "@/services/Import/interfaces";
|
|||||||
|
|
||||||
export interface IImportFileCommitedEventPayload {
|
export interface IImportFileCommitedEventPayload {
|
||||||
tenantId: number;
|
tenantId: number;
|
||||||
importId: number;
|
importId: string;
|
||||||
meta: ImportFilePreviewPOJO;
|
meta: ImportFilePreviewPOJO;
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,7 @@ import { Inject, Service } from 'typedi';
|
|||||||
import { AccountsApplication } from './AccountsApplication';
|
import { AccountsApplication } from './AccountsApplication';
|
||||||
import { Exportable } from '../Export/Exportable';
|
import { Exportable } from '../Export/Exportable';
|
||||||
import { IAccountsFilter, IAccountsStructureType } from '@/interfaces';
|
import { IAccountsFilter, IAccountsStructureType } from '@/interfaces';
|
||||||
|
import { EXPORT_SIZE_LIMIT } from '../Export/constants';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class AccountsExportable extends Exportable {
|
export class AccountsExportable extends Exportable {
|
||||||
@@ -20,7 +21,7 @@ export class AccountsExportable extends Exportable {
|
|||||||
inactiveMode: false,
|
inactiveMode: false,
|
||||||
...query,
|
...query,
|
||||||
structure: IAccountsStructureType.Flat,
|
structure: IAccountsStructureType.Flat,
|
||||||
pageSize: 12000,
|
pageSize: EXPORT_SIZE_LIMIT,
|
||||||
page: 1,
|
page: 1,
|
||||||
} as IAccountsFilter;
|
} as IAccountsFilter;
|
||||||
|
|
||||||
|
|||||||
@@ -100,7 +100,6 @@ export class TriggerRecognizedTransactions {
|
|||||||
private async triggerRecognizeTransactionsOnImportCommitted({
|
private async triggerRecognizeTransactionsOnImportCommitted({
|
||||||
tenantId,
|
tenantId,
|
||||||
importId,
|
importId,
|
||||||
meta,
|
|
||||||
}: IImportFileCommitedEventPayload) {
|
}: IImportFileCommitedEventPayload) {
|
||||||
const importFile = await Import.query().findOne({ importId });
|
const importFile = await Import.query().findOne({ importId });
|
||||||
const batch = importFile.paramsParsed.batch;
|
const batch = importFile.paramsParsed.batch;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Inject, Service } from 'typedi';
|
|||||||
import { IItemsFilter } from '@/interfaces';
|
import { IItemsFilter } from '@/interfaces';
|
||||||
import { CustomersApplication } from './CustomersApplication';
|
import { CustomersApplication } from './CustomersApplication';
|
||||||
import { Exportable } from '@/services/Export/Exportable';
|
import { Exportable } from '@/services/Export/Exportable';
|
||||||
|
import { EXPORT_SIZE_LIMIT } from '@/services/Export/constants';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class CustomersExportable extends Exportable {
|
export class CustomersExportable extends Exportable {
|
||||||
@@ -17,9 +18,9 @@ export class CustomersExportable extends Exportable {
|
|||||||
const parsedQuery = {
|
const parsedQuery = {
|
||||||
sortOrder: 'DESC',
|
sortOrder: 'DESC',
|
||||||
columnSortBy: 'created_at',
|
columnSortBy: 'created_at',
|
||||||
page: 1,
|
|
||||||
...query,
|
...query,
|
||||||
pageSize: 12,
|
page: 1,
|
||||||
|
pageSize: EXPORT_SIZE_LIMIT,
|
||||||
} as IItemsFilter;
|
} as IItemsFilter;
|
||||||
|
|
||||||
return this.customersApplication
|
return this.customersApplication
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Inject, Service } from 'typedi';
|
|||||||
import { IItemsFilter } from '@/interfaces';
|
import { IItemsFilter } from '@/interfaces';
|
||||||
import { Exportable } from '@/services/Export/Exportable';
|
import { Exportable } from '@/services/Export/Exportable';
|
||||||
import { VendorsApplication } from './VendorsApplication';
|
import { VendorsApplication } from './VendorsApplication';
|
||||||
|
import { EXPORT_SIZE_LIMIT } from '@/services/Export/constants';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class VendorsExportable extends Exportable {
|
export class VendorsExportable extends Exportable {
|
||||||
@@ -17,9 +18,9 @@ export class VendorsExportable extends Exportable {
|
|||||||
const parsedQuery = {
|
const parsedQuery = {
|
||||||
sortOrder: 'DESC',
|
sortOrder: 'DESC',
|
||||||
columnSortBy: 'created_at',
|
columnSortBy: 'created_at',
|
||||||
page: 1,
|
|
||||||
...query,
|
...query,
|
||||||
pageSize: 12,
|
page: 1,
|
||||||
|
pageSize: EXPORT_SIZE_LIMIT,
|
||||||
} as IItemsFilter;
|
} as IItemsFilter;
|
||||||
|
|
||||||
return this.vendorsApplication
|
return this.vendorsApplication
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Inject, Service } from 'typedi';
|
|||||||
import { Exportable } from '../Export/Exportable';
|
import { Exportable } from '../Export/Exportable';
|
||||||
import { IExpensesFilter } from '@/interfaces';
|
import { IExpensesFilter } from '@/interfaces';
|
||||||
import { ExpensesApplication } from './ExpensesApplication';
|
import { ExpensesApplication } from './ExpensesApplication';
|
||||||
|
import { EXPORT_SIZE_LIMIT } from '../Export/constants';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class ExpensesExportable extends Exportable {
|
export class ExpensesExportable extends Exportable {
|
||||||
@@ -19,7 +20,7 @@ export class ExpensesExportable extends Exportable {
|
|||||||
columnSortBy: 'created_at',
|
columnSortBy: 'created_at',
|
||||||
...query,
|
...query,
|
||||||
page: 1,
|
page: 1,
|
||||||
pageSize: 12000,
|
pageSize: EXPORT_SIZE_LIMIT,
|
||||||
} as IExpensesFilter;
|
} as IExpensesFilter;
|
||||||
|
|
||||||
return this.expensesApplication
|
return this.expensesApplication
|
||||||
|
|||||||
1
packages/server/src/services/Export/constants.ts
Normal file
1
packages/server/src/services/Export/constants.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export const EXPORT_SIZE_LIMIT = 9999999;
|
||||||
104
packages/server/src/services/Import/ImportALS.ts
Normal file
104
packages/server/src/services/Import/ImportALS.ts
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import { Service } from 'typedi';
|
||||||
|
import { AsyncLocalStorage } from 'async_hooks';
|
||||||
|
|
||||||
|
@Service()
|
||||||
|
export class ImportAls {
|
||||||
|
private als: AsyncLocalStorage<Map<string, any>>;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.als = new AsyncLocalStorage();
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Runs a callback function within the context of a new AsyncLocalStorage store.
|
||||||
|
* @param callback The function to be executed within the AsyncLocalStorage context.
|
||||||
|
* @returns The result of the callback function.
|
||||||
|
*/
|
||||||
|
public run<T>(callback: () => T): T {
|
||||||
|
return this.als.run<T>(new Map(), callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs a callback function in preview mode within the AsyncLocalStorage context.
|
||||||
|
* @param callback The function to be executed in preview mode.
|
||||||
|
* @returns The result of the callback function.
|
||||||
|
*/
|
||||||
|
public runPreview<T>(callback: () => T): T {
|
||||||
|
return this.run(() => {
|
||||||
|
this.markAsImport();
|
||||||
|
this.markAsImportPreview();
|
||||||
|
return callback();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs a callback function in commit mode within the AsyncLocalStorage context.
|
||||||
|
* @param {() => T} callback - The function to be executed in commit mode.
|
||||||
|
* @returns {T} The result of the callback function.
|
||||||
|
*/
|
||||||
|
public runCommit<T>(callback: () => T): T {
|
||||||
|
return this.run(() => {
|
||||||
|
this.markAsImport();
|
||||||
|
this.markAsImportCommit();
|
||||||
|
return callback();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the current AsyncLocalStorage store.
|
||||||
|
* @returns The current store or undefined if not in a valid context.
|
||||||
|
*/
|
||||||
|
public getStore(): Map<string, any> | undefined {
|
||||||
|
return this.als.getStore();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks the current context as an import operation.
|
||||||
|
* @param flag Boolean flag to set or unset the import status. Defaults to true.
|
||||||
|
*/
|
||||||
|
public markAsImport(flag: boolean = true): void {
|
||||||
|
const store = this.getStore();
|
||||||
|
store?.set('isImport', flag);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks the current context as an import commit operation.
|
||||||
|
* @param flag Boolean flag to set or unset the import commit status. Defaults to true.
|
||||||
|
*/
|
||||||
|
public markAsImportCommit(flag: boolean = true): void {
|
||||||
|
const store = this.getStore();
|
||||||
|
store?.set('isImportCommit', flag);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks the current context as an import preview operation.
|
||||||
|
* @param {boolean} flag - Boolean flag to set or unset the import preview status. Defaults to true.
|
||||||
|
*/
|
||||||
|
public markAsImportPreview(flag: boolean = true): void {
|
||||||
|
const store = this.getStore();
|
||||||
|
store?.set('isImportPreview', flag);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the current context is an import operation.
|
||||||
|
* @returns {boolean} True if the context is an import operation, false otherwise.
|
||||||
|
*/
|
||||||
|
public isImport(): boolean {
|
||||||
|
return !!this.getStore()?.get('isImport');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the current context is an import commit operation.
|
||||||
|
* @returns {boolean} True if the context is an import commit operation, false otherwise.
|
||||||
|
*/
|
||||||
|
public isImportCommit(): boolean {
|
||||||
|
return !!this.getStore()?.get('isImportCommit');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the current context is an import preview operation.
|
||||||
|
* @returns {boolean} True if the context is an import preview operation, false otherwise.
|
||||||
|
*/
|
||||||
|
public isImportPreview(): boolean {
|
||||||
|
return !!this.getStore()?.get('isImportPreview');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,7 +12,11 @@ import {
|
|||||||
ImportableContext,
|
ImportableContext,
|
||||||
} from './interfaces';
|
} from './interfaces';
|
||||||
import { ServiceError } from '@/exceptions';
|
import { ServiceError } from '@/exceptions';
|
||||||
import { getUniqueImportableValue, trimObject } from './_utils';
|
import {
|
||||||
|
convertMappingsToObject,
|
||||||
|
getUniqueImportableValue,
|
||||||
|
trimObject,
|
||||||
|
} from './_utils';
|
||||||
import { ImportableResources } from './ImportableResources';
|
import { ImportableResources } from './ImportableResources';
|
||||||
import ResourceService from '../Resource/ResourceService';
|
import ResourceService from '../Resource/ResourceService';
|
||||||
import { Import } from '@/system/models';
|
import { Import } from '@/system/models';
|
||||||
@@ -43,7 +47,6 @@ export class ImportFileCommon {
|
|||||||
return XLSX.utils.sheet_to_json(worksheet, {});
|
return XLSX.utils.sheet_to_json(worksheet, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Imports the given parsed data to the resource storage through registered importable service.
|
* Imports the given parsed data to the resource storage through registered importable service.
|
||||||
* @param {number} tenantId -
|
* @param {number} tenantId -
|
||||||
@@ -82,11 +85,14 @@ export class ImportFileCommon {
|
|||||||
rowNumber,
|
rowNumber,
|
||||||
uniqueValue,
|
uniqueValue,
|
||||||
};
|
};
|
||||||
|
const mappingSettings = convertMappingsToObject(importFile.columnsParsed);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Validate the DTO object before passing it to the service layer.
|
// Validate the DTO object before passing it to the service layer.
|
||||||
await this.importFileValidator.validateData(
|
await this.importFileValidator.validateData(
|
||||||
resourceFields,
|
resourceFields,
|
||||||
transformedDTO
|
transformedDTO,
|
||||||
|
mappingSettings
|
||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
// Run the importable function and listen to the errors.
|
// Run the importable function and listen to the errors.
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import { Service } from 'typedi';
|
import { Service } from 'typedi';
|
||||||
import { ImportInsertError, ResourceMetaFieldsMap } from './interfaces';
|
import {
|
||||||
|
ImportInsertError,
|
||||||
|
ImportMappingAttr,
|
||||||
|
ResourceMetaFieldsMap,
|
||||||
|
} from './interfaces';
|
||||||
import { ERRORS, convertFieldsToYupValidation } from './_utils';
|
import { ERRORS, convertFieldsToYupValidation } from './_utils';
|
||||||
import { IModelMeta } from '@/interfaces';
|
import { IModelMeta } from '@/interfaces';
|
||||||
import { ServiceError } from '@/exceptions';
|
import { ServiceError } from '@/exceptions';
|
||||||
@@ -24,9 +28,13 @@ export class ImportFileDataValidator {
|
|||||||
*/
|
*/
|
||||||
public async validateData(
|
public async validateData(
|
||||||
importableFields: ResourceMetaFieldsMap,
|
importableFields: ResourceMetaFieldsMap,
|
||||||
data: Record<string, any>
|
data: Record<string, any>,
|
||||||
|
mappingSettings: Record<string, ImportMappingAttr> = {}
|
||||||
): Promise<void | ImportInsertError[]> {
|
): Promise<void | ImportInsertError[]> {
|
||||||
const YupSchema = convertFieldsToYupValidation(importableFields);
|
const YupSchema = convertFieldsToYupValidation(
|
||||||
|
importableFields,
|
||||||
|
mappingSettings
|
||||||
|
);
|
||||||
const _data = { ...data };
|
const _data = { ...data };
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Inject, Service } from 'typedi';
|
|||||||
import HasTenancyService from '../Tenancy/TenancyService';
|
import HasTenancyService from '../Tenancy/TenancyService';
|
||||||
import { ImportFilePreviewPOJO } from './interfaces';
|
import { ImportFilePreviewPOJO } from './interfaces';
|
||||||
import { ImportFileProcess } from './ImportFileProcess';
|
import { ImportFileProcess } from './ImportFileProcess';
|
||||||
|
import { ImportAls } from './ImportALS';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class ImportFilePreview {
|
export class ImportFilePreview {
|
||||||
@@ -11,13 +12,31 @@ export class ImportFilePreview {
|
|||||||
@Inject()
|
@Inject()
|
||||||
private importFile: ImportFileProcess;
|
private importFile: ImportFileProcess;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private importAls: ImportAls;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preview the imported file results before commiting the transactions.
|
||||||
|
* @param {number} tenantId -
|
||||||
|
* @param {string} importId -
|
||||||
|
* @returns {Promise<ImportFilePreviewPOJO>}
|
||||||
|
*/
|
||||||
|
public async preview(
|
||||||
|
tenantId: number,
|
||||||
|
importId: string
|
||||||
|
): Promise<ImportFilePreviewPOJO> {
|
||||||
|
return this.importAls.runPreview<Promise<ImportFilePreviewPOJO>>(() =>
|
||||||
|
this.previewAlsRun(tenantId, importId)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Preview the imported file results before commiting the transactions.
|
* Preview the imported file results before commiting the transactions.
|
||||||
* @param {number} tenantId
|
* @param {number} tenantId
|
||||||
* @param {number} importId
|
* @param {number} importId
|
||||||
* @returns {Promise<ImportFilePreviewPOJO>}
|
* @returns {Promise<ImportFilePreviewPOJO>}
|
||||||
*/
|
*/
|
||||||
public async preview(
|
public async previewAlsRun(
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
importId: string
|
importId: string
|
||||||
): Promise<ImportFilePreviewPOJO> {
|
): Promise<ImportFilePreviewPOJO> {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { ImportFileProcess } from './ImportFileProcess';
|
|||||||
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
|
||||||
import events from '@/subscribers/events';
|
import events from '@/subscribers/events';
|
||||||
import { IImportFileCommitedEventPayload } from '@/interfaces/Import';
|
import { IImportFileCommitedEventPayload } from '@/interfaces/Import';
|
||||||
|
import { ImportAls } from './ImportALS';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class ImportFileProcessCommit {
|
export class ImportFileProcessCommit {
|
||||||
@@ -14,16 +15,34 @@ export class ImportFileProcessCommit {
|
|||||||
@Inject()
|
@Inject()
|
||||||
private importFile: ImportFileProcess;
|
private importFile: ImportFileProcess;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private importAls: ImportAls;
|
||||||
|
|
||||||
@Inject()
|
@Inject()
|
||||||
private eventPublisher: EventPublisher;
|
private eventPublisher: EventPublisher;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Commits the imported file under ALS.
|
||||||
|
* @param {number} tenantId
|
||||||
|
* @param {string} importId
|
||||||
|
* @returns {Promise<ImportFilePreviewPOJO>}
|
||||||
|
*/
|
||||||
|
public commit(
|
||||||
|
tenantId: number,
|
||||||
|
importId: string
|
||||||
|
): Promise<ImportFilePreviewPOJO> {
|
||||||
|
return this.importAls.runCommit<Promise<ImportFilePreviewPOJO>>(() =>
|
||||||
|
this.commitAlsRun(tenantId, importId)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Commits the imported file.
|
* Commits the imported file.
|
||||||
* @param {number} tenantId
|
* @param {number} tenantId
|
||||||
* @param {number} importId
|
* @param {number} importId
|
||||||
* @returns {Promise<ImportFilePreviewPOJO>}
|
* @returns {Promise<ImportFilePreviewPOJO>}
|
||||||
*/
|
*/
|
||||||
public async commit(
|
public async commitAlsRun(
|
||||||
tenantId: number,
|
tenantId: number,
|
||||||
importId: string
|
importId: string
|
||||||
): Promise<ImportFilePreviewPOJO> {
|
): Promise<ImportFilePreviewPOJO> {
|
||||||
|
|||||||
@@ -17,9 +17,10 @@ import {
|
|||||||
head,
|
head,
|
||||||
split,
|
split,
|
||||||
last,
|
last,
|
||||||
|
set,
|
||||||
} from 'lodash';
|
} from 'lodash';
|
||||||
import pluralize from 'pluralize';
|
import pluralize from 'pluralize';
|
||||||
import { ResourceMetaFieldsMap } from './interfaces';
|
import { ImportMappingAttr, ResourceMetaFieldsMap } from './interfaces';
|
||||||
import { IModelMetaField, IModelMetaField2 } from '@/interfaces';
|
import { IModelMetaField, IModelMetaField2 } from '@/interfaces';
|
||||||
import { ServiceError } from '@/exceptions';
|
import { ServiceError } from '@/exceptions';
|
||||||
import { multiNumberParse } from '@/utils/multi-number-parse';
|
import { multiNumberParse } from '@/utils/multi-number-parse';
|
||||||
@@ -58,11 +59,19 @@ export function trimObject(obj: Record<string, string | number>) {
|
|||||||
* @param {ResourceMetaFieldsMap} fields
|
* @param {ResourceMetaFieldsMap} fields
|
||||||
* @returns {Yup}
|
* @returns {Yup}
|
||||||
*/
|
*/
|
||||||
export const convertFieldsToYupValidation = (fields: ResourceMetaFieldsMap) => {
|
export const convertFieldsToYupValidation = (
|
||||||
|
fields: ResourceMetaFieldsMap,
|
||||||
|
parentFieldName: string = '',
|
||||||
|
mappingSettings: Record<string, ImportMappingAttr> = {}
|
||||||
|
) => {
|
||||||
const yupSchema = {};
|
const yupSchema = {};
|
||||||
|
|
||||||
Object.keys(fields).forEach((fieldName: string) => {
|
Object.keys(fields).forEach((fieldName: string) => {
|
||||||
const field = fields[fieldName] as IModelMetaField;
|
const field = fields[fieldName] as IModelMetaField;
|
||||||
|
const fieldPath = parentFieldName
|
||||||
|
? `${parentFieldName}.${fieldName}`
|
||||||
|
: fieldName;
|
||||||
|
|
||||||
let fieldSchema;
|
let fieldSchema;
|
||||||
fieldSchema = Yup.string().label(field.name);
|
fieldSchema = Yup.string().label(field.name);
|
||||||
|
|
||||||
@@ -105,13 +114,23 @@ export const convertFieldsToYupValidation = (fields: ResourceMetaFieldsMap) => {
|
|||||||
if (!val) {
|
if (!val) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return moment(val, 'YYYY-MM-DD', true).isValid();
|
const fieldDateFormat =
|
||||||
|
(get(
|
||||||
|
mappingSettings,
|
||||||
|
`${fieldPath}.dateFormat`
|
||||||
|
) as unknown as string) || 'YYYY-MM-DD';
|
||||||
|
|
||||||
|
return moment(val, fieldDateFormat, true).isValid();
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} else if (field.fieldType === 'url') {
|
} else if (field.fieldType === 'url') {
|
||||||
fieldSchema = fieldSchema.url();
|
fieldSchema = fieldSchema.url();
|
||||||
} else if (field.fieldType === 'collection') {
|
} else if (field.fieldType === 'collection') {
|
||||||
const nestedFieldShema = convertFieldsToYupValidation(field.fields);
|
const nestedFieldShema = convertFieldsToYupValidation(
|
||||||
|
field.fields,
|
||||||
|
field.name,
|
||||||
|
mappingSettings
|
||||||
|
);
|
||||||
fieldSchema = Yup.array().label(field.name);
|
fieldSchema = Yup.array().label(field.name);
|
||||||
|
|
||||||
if (!isUndefined(field.collectionMaxLength)) {
|
if (!isUndefined(field.collectionMaxLength)) {
|
||||||
@@ -258,6 +277,7 @@ export const getResourceColumns = (resourceColumns: {
|
|||||||
]) => {
|
]) => {
|
||||||
const extra: Record<string, any> = {};
|
const extra: Record<string, any> = {};
|
||||||
const key = fieldKey;
|
const key = fieldKey;
|
||||||
|
const type = field.fieldType;
|
||||||
|
|
||||||
if (group) {
|
if (group) {
|
||||||
extra.group = group;
|
extra.group = group;
|
||||||
@@ -270,6 +290,7 @@ export const getResourceColumns = (resourceColumns: {
|
|||||||
name,
|
name,
|
||||||
required,
|
required,
|
||||||
hint: importHint,
|
hint: importHint,
|
||||||
|
type,
|
||||||
order,
|
order,
|
||||||
...extra,
|
...extra,
|
||||||
};
|
};
|
||||||
@@ -322,6 +343,8 @@ export const valueParser =
|
|||||||
});
|
});
|
||||||
const result = await relationQuery.first();
|
const result = await relationQuery.first();
|
||||||
_value = get(result, 'id');
|
_value = get(result, 'id');
|
||||||
|
} else if (field.fieldType === 'date') {
|
||||||
|
|
||||||
} else if (field.fieldType === 'collection') {
|
} else if (field.fieldType === 'collection') {
|
||||||
const ObjectFieldKey = key.includes('.') ? key.split('.')[1] : key;
|
const ObjectFieldKey = key.includes('.') ? key.split('.')[1] : key;
|
||||||
const _valueParser = valueParser(fields, tenantModels);
|
const _valueParser = valueParser(fields, tenantModels);
|
||||||
@@ -433,8 +456,8 @@ export const getMapToPath = (to: string, group = '') =>
|
|||||||
group ? `${group}.${to}` : to;
|
group ? `${group}.${to}` : to;
|
||||||
|
|
||||||
export const getImportsStoragePath = () => {
|
export const getImportsStoragePath = () => {
|
||||||
return path.join(global.__storage_dir, `/imports`);
|
return path.join(global.__storage_dir, `/imports`);
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes the imported file from the storage and database.
|
* Deletes the imported file from the storage and database.
|
||||||
@@ -457,3 +480,19 @@ export const readImportFile = (filename: string) => {
|
|||||||
|
|
||||||
return fs.readFile(`${filePath}/${filename}`);
|
return fs.readFile(`${filePath}/${filename}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts an array of mapping objects to a structured object.
|
||||||
|
* @param {Array<Object>} mappings - Array of mapping objects.
|
||||||
|
* @returns {Object} - Structured object based on the mappings.
|
||||||
|
*/
|
||||||
|
export const convertMappingsToObject = (mappings) => {
|
||||||
|
return mappings.reduce((acc, mapping) => {
|
||||||
|
const { to, group } = mapping;
|
||||||
|
const key = group ? `['${group}.${to}']` : to;
|
||||||
|
|
||||||
|
set(acc, key, mapping);
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
};
|
||||||
|
|||||||
@@ -139,24 +139,25 @@ export default class InventoryService {
|
|||||||
) {
|
) {
|
||||||
const agenda = Container.get('agenda');
|
const agenda = Container.get('agenda');
|
||||||
|
|
||||||
|
const commonJobsQuery = {
|
||||||
|
name: 'compute-item-cost',
|
||||||
|
lastRunAt: { $exists: false },
|
||||||
|
'data.tenantId': tenantId,
|
||||||
|
'data.itemId': itemId,
|
||||||
|
};
|
||||||
// Cancel any `compute-item-cost` in the queue has upper starting date
|
// Cancel any `compute-item-cost` in the queue has upper starting date
|
||||||
// with the same given item.
|
// with the same given item.
|
||||||
await agenda.cancel({
|
await agenda.cancel({
|
||||||
name: 'compute-item-cost',
|
...commonJobsQuery,
|
||||||
nextRunAt: { $ne: null },
|
'data.startingDate': { $lte: startingDate },
|
||||||
'data.tenantId': tenantId,
|
|
||||||
'data.itemId': itemId,
|
|
||||||
'data.startingDate': { $gt: startingDate },
|
|
||||||
});
|
});
|
||||||
// Retrieve any `compute-item-cost` in the queue has lower starting date
|
// Retrieve any `compute-item-cost` in the queue has lower starting date
|
||||||
// with the same given item.
|
// with the same given item.
|
||||||
const dependsJobs = await agenda.jobs({
|
const dependsJobs = await agenda.jobs({
|
||||||
name: 'compute-item-cost',
|
...commonJobsQuery,
|
||||||
nextRunAt: { $ne: null },
|
'data.startingDate': { $gte: startingDate },
|
||||||
'data.tenantId': tenantId,
|
|
||||||
'data.itemId': itemId,
|
|
||||||
'data.startingDate': { $lte: startingDate },
|
|
||||||
});
|
});
|
||||||
|
// If the depends jobs cleared.
|
||||||
if (dependsJobs.length === 0) {
|
if (dependsJobs.length === 0) {
|
||||||
await agenda.schedule(
|
await agenda.schedule(
|
||||||
config.scheduleComputeItemCost,
|
config.scheduleComputeItemCost,
|
||||||
@@ -172,6 +173,13 @@ export default class InventoryService {
|
|||||||
events.inventory.onComputeItemCostJobScheduled,
|
events.inventory.onComputeItemCostJobScheduled,
|
||||||
{ startingDate, itemId, tenantId } as IInventoryItemCostScheduledPayload
|
{ startingDate, itemId, tenantId } as IInventoryItemCostScheduledPayload
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
// Re-schedule the jobs that have higher date from current moment.
|
||||||
|
await Promise.all(
|
||||||
|
dependsJobs.map((job) =>
|
||||||
|
job.schedule(config.scheduleComputeItemCost).save()
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Inject, Service } from 'typedi';
|
|||||||
import { Exportable } from '../Export/Exportable';
|
import { Exportable } from '../Export/Exportable';
|
||||||
import { IItemsFilter } from '@/interfaces';
|
import { IItemsFilter } from '@/interfaces';
|
||||||
import { ItemsApplication } from './ItemsApplication';
|
import { ItemsApplication } from './ItemsApplication';
|
||||||
|
import { EXPORT_SIZE_LIMIT } from '../Export/constants';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class ItemsExportable extends Exportable {
|
export class ItemsExportable extends Exportable {
|
||||||
@@ -19,7 +20,7 @@ export class ItemsExportable extends Exportable {
|
|||||||
columnSortBy: 'created_at',
|
columnSortBy: 'created_at',
|
||||||
page: 1,
|
page: 1,
|
||||||
...query,
|
...query,
|
||||||
pageSize: 12,
|
pageSize: EXPORT_SIZE_LIMIT,
|
||||||
} as IItemsFilter;
|
} as IItemsFilter;
|
||||||
|
|
||||||
return this.itemsApplication
|
return this.itemsApplication
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Inject, Service } from 'typedi';
|
|||||||
import { IManualJournalsFilter } from '@/interfaces';
|
import { IManualJournalsFilter } from '@/interfaces';
|
||||||
import { Exportable } from '../Export/Exportable';
|
import { Exportable } from '../Export/Exportable';
|
||||||
import { ManualJournalsApplication } from './ManualJournalsApplication';
|
import { ManualJournalsApplication } from './ManualJournalsApplication';
|
||||||
|
import { EXPORT_SIZE_LIMIT } from '../Export/constants';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class ManualJournalsExportable extends Exportable {
|
export class ManualJournalsExportable extends Exportable {
|
||||||
@@ -19,7 +20,7 @@ export class ManualJournalsExportable extends Exportable {
|
|||||||
columnSortBy: 'created_at',
|
columnSortBy: 'created_at',
|
||||||
...query,
|
...query,
|
||||||
page: 1,
|
page: 1,
|
||||||
pageSize: 12000,
|
pageSize: EXPORT_SIZE_LIMIT,
|
||||||
} as IManualJournalsFilter;
|
} as IManualJournalsFilter;
|
||||||
|
|
||||||
return this.manualJournalsApplication
|
return this.manualJournalsApplication
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Inject, Service } from 'typedi';
|
import { Inject, Service } from 'typedi';
|
||||||
import { Exportable } from '@/services/Export/Exportable';
|
import { Exportable } from '@/services/Export/Exportable';
|
||||||
import { BillPaymentsApplication } from './BillPaymentsApplication';
|
import { BillPaymentsApplication } from './BillPaymentsApplication';
|
||||||
|
import { EXPORT_SIZE_LIMIT } from '@/services/Export/constants';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class BillPaymentExportable extends Exportable {
|
export class BillPaymentExportable extends Exportable {
|
||||||
@@ -14,11 +15,11 @@ export class BillPaymentExportable extends Exportable {
|
|||||||
*/
|
*/
|
||||||
public exportable(tenantId: number, query: any) {
|
public exportable(tenantId: number, query: any) {
|
||||||
const parsedQuery = {
|
const parsedQuery = {
|
||||||
page: 1,
|
|
||||||
pageSize: 12,
|
|
||||||
...query,
|
|
||||||
sortOrder: 'desc',
|
sortOrder: 'desc',
|
||||||
columnSortBy: 'created_at',
|
columnSortBy: 'created_at',
|
||||||
|
...query,
|
||||||
|
page: 1,
|
||||||
|
pageSize: EXPORT_SIZE_LIMIT,
|
||||||
} as any;
|
} as any;
|
||||||
|
|
||||||
return this.billPaymentsApplication
|
return this.billPaymentsApplication
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Inject, Service } from 'typedi';
|
|||||||
import { IBillsFilter } from '@/interfaces';
|
import { IBillsFilter } from '@/interfaces';
|
||||||
import { Exportable } from '@/services/Export/Exportable';
|
import { Exportable } from '@/services/Export/Exportable';
|
||||||
import { BillsApplication } from './BillsApplication';
|
import { BillsApplication } from './BillsApplication';
|
||||||
|
import { EXPORT_SIZE_LIMIT } from '@/services/Export/constants';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class BillsExportable extends Exportable {
|
export class BillsExportable extends Exportable {
|
||||||
@@ -19,7 +20,7 @@ export class BillsExportable extends Exportable {
|
|||||||
columnSortBy: 'created_at',
|
columnSortBy: 'created_at',
|
||||||
...query,
|
...query,
|
||||||
page: 1,
|
page: 1,
|
||||||
pageSize: 12000,
|
pageSize: EXPORT_SIZE_LIMIT,
|
||||||
} as IBillsFilter;
|
} as IBillsFilter;
|
||||||
|
|
||||||
return this.billsApplication
|
return this.billsApplication
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Inject, Service } from 'typedi';
|
|||||||
import { ISalesInvoicesFilter } from '@/interfaces';
|
import { ISalesInvoicesFilter } from '@/interfaces';
|
||||||
import { Exportable } from '@/services/Export/Exportable';
|
import { Exportable } from '@/services/Export/Exportable';
|
||||||
import { SaleEstimatesApplication } from './SaleEstimatesApplication';
|
import { SaleEstimatesApplication } from './SaleEstimatesApplication';
|
||||||
|
import { EXPORT_SIZE_LIMIT } from '@/services/Export/constants';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class SaleEstimatesExportable extends Exportable {
|
export class SaleEstimatesExportable extends Exportable {
|
||||||
@@ -19,7 +20,7 @@ export class SaleEstimatesExportable extends Exportable {
|
|||||||
columnSortBy: 'created_at',
|
columnSortBy: 'created_at',
|
||||||
...query,
|
...query,
|
||||||
page: 1,
|
page: 1,
|
||||||
pageSize: 12000,
|
pageSize: EXPORT_SIZE_LIMIT,
|
||||||
} as ISalesInvoicesFilter;
|
} as ISalesInvoicesFilter;
|
||||||
|
|
||||||
return this.saleEstimatesApplication
|
return this.saleEstimatesApplication
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Inject, Service } from 'typedi';
|
|||||||
import { ISalesInvoicesFilter } from '@/interfaces';
|
import { ISalesInvoicesFilter } from '@/interfaces';
|
||||||
import { SaleInvoiceApplication } from './SaleInvoicesApplication';
|
import { SaleInvoiceApplication } from './SaleInvoicesApplication';
|
||||||
import { Exportable } from '@/services/Export/Exportable';
|
import { Exportable } from '@/services/Export/Exportable';
|
||||||
|
import { EXPORT_SIZE_LIMIT } from '@/services/Export/constants';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class SaleInvoicesExportable extends Exportable {
|
export class SaleInvoicesExportable extends Exportable {
|
||||||
@@ -19,7 +20,7 @@ export class SaleInvoicesExportable extends Exportable {
|
|||||||
columnSortBy: 'created_at',
|
columnSortBy: 'created_at',
|
||||||
...query,
|
...query,
|
||||||
page: 1,
|
page: 1,
|
||||||
pageSize: 120000,
|
pageSize: EXPORT_SIZE_LIMIT,
|
||||||
} as ISalesInvoicesFilter;
|
} as ISalesInvoicesFilter;
|
||||||
|
|
||||||
return this.saleInvoicesApplication
|
return this.saleInvoicesApplication
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Mutex } from 'async-mutex';
|
||||||
import { Container, Service, Inject } from 'typedi';
|
import { Container, Service, Inject } from 'typedi';
|
||||||
import { chain } from 'lodash';
|
import { chain } from 'lodash';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
@@ -34,17 +35,26 @@ export class SaleInvoicesCost {
|
|||||||
inventoryItemsIds: number[],
|
inventoryItemsIds: number[],
|
||||||
startingDate: Date
|
startingDate: Date
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const asyncOpers: Promise<[]>[] = [];
|
const mutex = new Mutex();
|
||||||
|
|
||||||
inventoryItemsIds.forEach((inventoryItemId: number) => {
|
const asyncOpers = inventoryItemsIds.map(
|
||||||
const oper: Promise<[]> = this.inventoryService.scheduleComputeItemCost(
|
async (inventoryItemId: number) => {
|
||||||
tenantId,
|
// @todo refactor the lock acquire to be distrbuted using Redis
|
||||||
inventoryItemId,
|
// and run the cost schedule job after running invoice transaction.
|
||||||
startingDate
|
const release = await mutex.acquire();
|
||||||
);
|
|
||||||
asyncOpers.push(oper);
|
try {
|
||||||
});
|
await this.inventoryService.scheduleComputeItemCost(
|
||||||
await Promise.all([...asyncOpers]);
|
tenantId,
|
||||||
|
inventoryItemId,
|
||||||
|
startingDate
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
await Promise.all(asyncOpers);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -86,17 +96,22 @@ export class SaleInvoicesCost {
|
|||||||
tenantId: number,
|
tenantId: number,
|
||||||
inventoryTransactions: IInventoryTransaction[]
|
inventoryTransactions: IInventoryTransaction[]
|
||||||
) {
|
) {
|
||||||
const asyncOpers: Promise<[]>[] = [];
|
const mutex = new Mutex();
|
||||||
const reducedTransactions = this.getMaxDateInventoryTransactions(
|
const reducedTransactions = this.getMaxDateInventoryTransactions(
|
||||||
inventoryTransactions
|
inventoryTransactions
|
||||||
);
|
);
|
||||||
reducedTransactions.forEach((transaction) => {
|
const asyncOpers = reducedTransactions.map(async (transaction) => {
|
||||||
const oper: Promise<[]> = this.inventoryService.scheduleComputeItemCost(
|
const release = await mutex.acquire();
|
||||||
tenantId,
|
|
||||||
transaction.itemId,
|
try {
|
||||||
transaction.date
|
await this.inventoryService.scheduleComputeItemCost(
|
||||||
);
|
tenantId,
|
||||||
asyncOpers.push(oper);
|
transaction.itemId,
|
||||||
|
transaction.date
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
release();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
await Promise.all([...asyncOpers]);
|
await Promise.all([...asyncOpers]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Inject, Service } from 'typedi';
|
|||||||
import { IAccountsStructureType, IPaymentsReceivedFilter } from '@/interfaces';
|
import { IAccountsStructureType, IPaymentsReceivedFilter } from '@/interfaces';
|
||||||
import { Exportable } from '@/services/Export/Exportable';
|
import { Exportable } from '@/services/Export/Exportable';
|
||||||
import { PaymentReceivesApplication } from './PaymentReceivedApplication';
|
import { PaymentReceivesApplication } from './PaymentReceivedApplication';
|
||||||
|
import { EXPORT_SIZE_LIMIT } from '@/services/Export/constants';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class PaymentsReceivedExportable extends Exportable {
|
export class PaymentsReceivedExportable extends Exportable {
|
||||||
@@ -21,6 +22,8 @@ export class PaymentsReceivedExportable extends Exportable {
|
|||||||
inactiveMode: false,
|
inactiveMode: false,
|
||||||
...query,
|
...query,
|
||||||
structure: IAccountsStructureType.Flat,
|
structure: IAccountsStructureType.Flat,
|
||||||
|
page: 1,
|
||||||
|
pageSize: EXPORT_SIZE_LIMIT,
|
||||||
} as IPaymentsReceivedFilter;
|
} as IPaymentsReceivedFilter;
|
||||||
|
|
||||||
return this.paymentReceivedApp
|
return this.paymentReceivedApp
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Inject, Service } from 'typedi';
|
|||||||
import { ISalesReceiptsFilter } from '@/interfaces';
|
import { ISalesReceiptsFilter } from '@/interfaces';
|
||||||
import { Exportable } from '@/services/Export/Exportable';
|
import { Exportable } from '@/services/Export/Exportable';
|
||||||
import { SaleReceiptApplication } from './SaleReceiptApplication';
|
import { SaleReceiptApplication } from './SaleReceiptApplication';
|
||||||
|
import { EXPORT_SIZE_LIMIT } from '@/services/Export/constants';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class SaleReceiptsExportable extends Exportable {
|
export class SaleReceiptsExportable extends Exportable {
|
||||||
@@ -19,7 +20,7 @@ export class SaleReceiptsExportable extends Exportable {
|
|||||||
columnSortBy: 'created_at',
|
columnSortBy: 'created_at',
|
||||||
...query,
|
...query,
|
||||||
page: 1,
|
page: 1,
|
||||||
pageSize: 12,
|
pageSize: EXPORT_SIZE_LIMIT,
|
||||||
} as ISalesReceiptsFilter;
|
} as ISalesReceiptsFilter;
|
||||||
|
|
||||||
return this.saleReceiptsApp
|
return this.saleReceiptsApp
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
} from '@/interfaces';
|
} from '@/interfaces';
|
||||||
import { runAfterTransaction } from '@/services/UnitOfWork/TransactionsHooks';
|
import { runAfterTransaction } from '@/services/UnitOfWork/TransactionsHooks';
|
||||||
import { SaleInvoicesCost } from '@/services/Sales/Invoices/SalesInvoicesCost';
|
import { SaleInvoicesCost } from '@/services/Sales/Invoices/SalesInvoicesCost';
|
||||||
|
import { ImportAls } from '@/services/Import/ImportALS';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export default class InventorySubscriber {
|
export default class InventorySubscriber {
|
||||||
@@ -25,6 +26,9 @@ export default class InventorySubscriber {
|
|||||||
@Inject('agenda')
|
@Inject('agenda')
|
||||||
private agenda: any;
|
private agenda: any;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private importAls: ImportAls;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attaches events with handlers.
|
* Attaches events with handlers.
|
||||||
*/
|
*/
|
||||||
@@ -86,20 +90,17 @@ export default class InventorySubscriber {
|
|||||||
private handleScheduleItemsCostOnInventoryTransactionsCreated = async ({
|
private handleScheduleItemsCostOnInventoryTransactionsCreated = async ({
|
||||||
tenantId,
|
tenantId,
|
||||||
inventoryTransactions,
|
inventoryTransactions,
|
||||||
trx
|
trx,
|
||||||
}: IInventoryTransactionsCreatedPayload) => {
|
}: IInventoryTransactionsCreatedPayload) => {
|
||||||
const inventoryItemsIds = map(inventoryTransactions, 'itemId');
|
const inImportPreviewScope = this.importAls.isImportPreview();
|
||||||
|
|
||||||
runAfterTransaction(trx, async () => {
|
// Avoid running the cost items job if the async process is in import preview.
|
||||||
try {
|
if (inImportPreviewScope) return;
|
||||||
await this.saleInvoicesCost.computeItemsCostByInventoryTransactions(
|
|
||||||
tenantId,
|
await this.saleInvoicesCost.computeItemsCostByInventoryTransactions(
|
||||||
inventoryTransactions
|
tenantId,
|
||||||
);
|
inventoryTransactions
|
||||||
} catch (error) {
|
);
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
th.label,
|
th.label,
|
||||||
td.label{
|
td.label{
|
||||||
width: 32% !important;
|
width: 30% !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
thead{
|
thead{
|
||||||
@@ -31,16 +31,14 @@
|
|||||||
tr td {
|
tr td {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
tr td{
|
|
||||||
:global(.bp4-popover-target .bp4-button),
|
|
||||||
:global(.bp4-popover-wrapper){
|
|
||||||
max-width: 250px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.requiredSign{
|
.requiredSign{
|
||||||
color: rgb(250, 82, 82);
|
color: rgb(250, 82, 82);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.columnSelectButton{
|
||||||
|
max-width: 250px;
|
||||||
|
min-width: 250px;
|
||||||
|
}
|
||||||
@@ -8,9 +8,12 @@ import { EntityColumnField, useImportFileContext } from './ImportFileProvider';
|
|||||||
import { CLASSES } from '@/constants';
|
import { CLASSES } from '@/constants';
|
||||||
import { ImportFileContainer } from './ImportFileContainer';
|
import { ImportFileContainer } from './ImportFileContainer';
|
||||||
import { ImportStepperStep } from './_types';
|
import { ImportStepperStep } from './_types';
|
||||||
import { ImportFileMapBootProvider } from './ImportFileMappingBoot';
|
import {
|
||||||
|
ImportFileMapBootProvider,
|
||||||
|
useImportFileMapBootContext,
|
||||||
|
} from './ImportFileMappingBoot';
|
||||||
import styles from './ImportFileMapping.module.scss';
|
import styles from './ImportFileMapping.module.scss';
|
||||||
import { getFieldKey } from './_utils';
|
import { getDateFieldKey, getFieldKey } from './_utils';
|
||||||
|
|
||||||
export function ImportFileMapping() {
|
export function ImportFileMapping() {
|
||||||
const { importId, entityColumns } = useImportFileContext();
|
const { importId, entityColumns } = useImportFileContext();
|
||||||
@@ -82,6 +85,7 @@ interface ImportFileMappingFieldsProps {
|
|||||||
*/
|
*/
|
||||||
function ImportFileMappingFields({ fields }: ImportFileMappingFieldsProps) {
|
function ImportFileMappingFields({ fields }: ImportFileMappingFieldsProps) {
|
||||||
const { sheetColumns } = useImportFileContext();
|
const { sheetColumns } = useImportFileContext();
|
||||||
|
const { dateFormats } = useImportFileMapBootContext();
|
||||||
|
|
||||||
const items = useMemo(
|
const items = useMemo(
|
||||||
() => sheetColumns.map((column) => ({ value: column, text: column })),
|
() => sheetColumns.map((column) => ({ value: column, text: column })),
|
||||||
@@ -95,22 +99,35 @@ function ImportFileMappingFields({ fields }: ImportFileMappingFieldsProps) {
|
|||||||
{column.required && <span className={styles.requiredSign}>*</span>}
|
{column.required && <span className={styles.requiredSign}>*</span>}
|
||||||
</td>
|
</td>
|
||||||
<td className={styles.field}>
|
<td className={styles.field}>
|
||||||
<Group spacing={4}>
|
<Group spacing={12} noWrap>
|
||||||
<FSelect
|
<FSelect
|
||||||
name={getFieldKey(column.key, column.group)}
|
name={`['${getFieldKey(column.key, column.group)}'].from`}
|
||||||
items={items}
|
items={items}
|
||||||
popoverProps={{ minimal: true }}
|
popoverProps={{ minimal: true }}
|
||||||
minimal={true}
|
minimal={true}
|
||||||
fill={true}
|
fill={true}
|
||||||
|
className={styles.columnSelectButton}
|
||||||
/>
|
/>
|
||||||
{column.hint && (
|
{column.hint && (
|
||||||
<Hint content={column.hint} position={Position.BOTTOM} />
|
<Hint content={column.hint} position={Position.BOTTOM} />
|
||||||
)}
|
)}
|
||||||
|
{column.type === 'date' && (
|
||||||
|
<FSelect
|
||||||
|
name={getDateFieldKey(column.key, column.group)}
|
||||||
|
items={dateFormats}
|
||||||
|
placeholder={'Select date format'}
|
||||||
|
minimal={true}
|
||||||
|
fill={true}
|
||||||
|
valueAccessor={'key'}
|
||||||
|
textAccessor={'label'}
|
||||||
|
labelAccessor={''}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Group>
|
</Group>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
),
|
),
|
||||||
[items],
|
[items, dateFormats],
|
||||||
);
|
);
|
||||||
const columns = useMemo(
|
const columns = useMemo(
|
||||||
() => fields.map(columnMapper),
|
() => fields.map(columnMapper),
|
||||||
|
|||||||
@@ -2,8 +2,11 @@ import { Spinner } from '@blueprintjs/core';
|
|||||||
import React, { createContext, useContext } from 'react';
|
import React, { createContext, useContext } from 'react';
|
||||||
import { Box } from '@/components';
|
import { Box } from '@/components';
|
||||||
import { useImportFileMeta } from '@/hooks/query/import';
|
import { useImportFileMeta } from '@/hooks/query/import';
|
||||||
|
import { useDateFormats } from '@/hooks/query';
|
||||||
|
|
||||||
interface ImportFileMapBootContextValue {}
|
interface ImportFileMapBootContextValue {
|
||||||
|
dateFormats: Array<any>;
|
||||||
|
}
|
||||||
|
|
||||||
const ImportFileMapBootContext = createContext<ImportFileMapBootContextValue>(
|
const ImportFileMapBootContext = createContext<ImportFileMapBootContextValue>(
|
||||||
{} as ImportFileMapBootContextValue,
|
{} as ImportFileMapBootContextValue,
|
||||||
@@ -39,14 +42,22 @@ export const ImportFileMapBootProvider = ({
|
|||||||
enabled: Boolean(importId),
|
enabled: Boolean(importId),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Fetch date format options.
|
||||||
|
const { data: dateFormats, isLoading: isDateFormatsLoading } =
|
||||||
|
useDateFormats();
|
||||||
|
|
||||||
const value = {
|
const value = {
|
||||||
importFile,
|
importFile,
|
||||||
isImportFileLoading,
|
isImportFileLoading,
|
||||||
isImportFileFetching,
|
isImportFileFetching,
|
||||||
|
dateFormats,
|
||||||
|
isDateFormatsLoading,
|
||||||
};
|
};
|
||||||
|
const isLoading = isDateFormatsLoading || isImportFileLoading;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ImportFileMapBootContext.Provider value={value}>
|
<ImportFileMapBootContext.Provider value={value}>
|
||||||
{isImportFileLoading ? (
|
{isLoading ? (
|
||||||
<Box style={{ padding: '2rem', textAlign: 'center' }}>
|
<Box style={{ padding: '2rem', textAlign: 'center' }}>
|
||||||
<Spinner size={26} />
|
<Spinner size={26} />
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export type EntityColumnField = {
|
|||||||
required?: boolean;
|
required?: boolean;
|
||||||
hint?: string;
|
hint?: string;
|
||||||
group?: string;
|
group?: string;
|
||||||
|
type: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface EntityColumn {
|
export interface EntityColumn {
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 32px 20px;
|
padding: 32px 20px;
|
||||||
padding-bottom: 80px;
|
padding-bottom: 80px;
|
||||||
min-width: 660px;
|
min-width: 800px;
|
||||||
max-width: 760px;
|
max-width: 800px;
|
||||||
width: 75%;
|
width: 75%;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
|
|||||||
@@ -12,4 +12,13 @@ export interface ImportFileMappingFormProps {
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ImportFileMappingFormValues = Record<string, string | null>;
|
export type ImportFileMappingFormValues = Record<
|
||||||
|
string,
|
||||||
|
{ from: string | null; dateFormat?: string }
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type ImportFileMappingRes = {
|
||||||
|
from: string;
|
||||||
|
to: string;
|
||||||
|
group: string;
|
||||||
|
}[];
|
||||||
|
|||||||
@@ -17,13 +17,15 @@ import {
|
|||||||
} from './ImportFileProvider';
|
} from './ImportFileProvider';
|
||||||
import { useImportFileMapBootContext } from './ImportFileMappingBoot';
|
import { useImportFileMapBootContext } from './ImportFileMappingBoot';
|
||||||
import { deepdash, transformToForm } from '@/utils';
|
import { deepdash, transformToForm } from '@/utils';
|
||||||
import { ImportFileMappingFormValues } from './_types';
|
import { ImportFileMappingFormValues, ImportFileMappingRes } from './_types';
|
||||||
|
|
||||||
export const getFieldKey = (key: string, group = '') => {
|
export const getFieldKey = (key: string, group = '') => {
|
||||||
return group ? `${group}.${key}` : key;
|
return group ? `${group}.${key}` : key;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ImportFileMappingRes = { from: string; to: string; group: string }[];
|
export const getDateFieldKey = (key: string, group: string = '') => {
|
||||||
|
return `${getFieldKey(key, group)}.dateFormat`;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Transformes the mapping form values to request.
|
* Transformes the mapping form values to request.
|
||||||
@@ -34,10 +36,10 @@ export const transformValueToReq = (
|
|||||||
value: ImportFileMappingFormValues,
|
value: ImportFileMappingFormValues,
|
||||||
): { mapping: ImportFileMappingRes[] } => {
|
): { mapping: ImportFileMappingRes[] } => {
|
||||||
const mapping = chain(value)
|
const mapping = chain(value)
|
||||||
.thru(deepdash.index)
|
.pickBy((_value, key) => !isEmpty(_value) && _value?.from)
|
||||||
.pickBy((_value, key) => !isEmpty(get(value, key)))
|
.map((_value, key) => ({
|
||||||
.map((from, key) => ({
|
from: _value.from,
|
||||||
from,
|
dateFormat: _value.dateFormat,
|
||||||
to: key.includes('.') ? last(key.split('.')) : key,
|
to: key.includes('.') ? last(key.split('.')) : key,
|
||||||
group: key.includes('.') ? head(key.split('.')) : '',
|
group: key.includes('.') ? head(key.split('.')) : '',
|
||||||
}))
|
}))
|
||||||
@@ -52,11 +54,15 @@ export const transformValueToReq = (
|
|||||||
* @returns {Record<string, object | string>}
|
* @returns {Record<string, object | string>}
|
||||||
*/
|
*/
|
||||||
export const transformResToFormValues = (
|
export const transformResToFormValues = (
|
||||||
value: { from: string; to: string , group: string }[],
|
value: { from: string; to: string; group: string }[],
|
||||||
): Record<string, object | string> => {
|
): Record<string, object | string> => {
|
||||||
return value?.reduce((acc, map) => {
|
return value?.reduce((acc, map) => {
|
||||||
const path = map?.group ? `${map.group}.${map.to}` : map.to;
|
const path = map?.group ? `['${map.group}.${map.to}']` : map.to;
|
||||||
set(acc, path, map.from);
|
const dateFormatObj = map?.dateFormat
|
||||||
|
? { dateFormat: map?.dateFormat }
|
||||||
|
: {};
|
||||||
|
|
||||||
|
set(acc, path, { from: map?.from, ...dateFormatObj });
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
};
|
};
|
||||||
@@ -76,10 +82,10 @@ const getInitialDefaultValues = (
|
|||||||
const _matched = sheetColumns.find(
|
const _matched = sheetColumns.find(
|
||||||
(column) => lowerCase(column) === _name,
|
(column) => lowerCase(column) === _name,
|
||||||
);
|
);
|
||||||
const _key = groupKey ? `${groupKey}.${key}` : key;
|
const path = groupKey ? `['${groupKey}.${key}']` : key;
|
||||||
const _value = _matched ? _matched : '';
|
const from = _matched ? _matched : '';
|
||||||
|
|
||||||
set(acc, _key, _value);
|
set(acc, path, { from });
|
||||||
});
|
});
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
@@ -102,7 +108,6 @@ export const useImportFileMappingInitialValues = () => {
|
|||||||
() => getInitialDefaultValues(entityColumns, sheetColumns),
|
() => getInitialDefaultValues(entityColumns, sheetColumns),
|
||||||
[entityColumns, sheetColumns],
|
[entityColumns, sheetColumns],
|
||||||
);
|
);
|
||||||
|
|
||||||
return useMemo<Record<string, any>>(
|
return useMemo<Record<string, any>>(
|
||||||
() =>
|
() =>
|
||||||
assign(
|
assign(
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
.container {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 1.15rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.iconText {
|
||||||
|
display: inline-flex;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-right: 16px;
|
||||||
|
color: #00824d;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-right: 0; /* Remove the margin on the last item */
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
margin-right: 2px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import styles from './SubscriptionPlansOfferChecks.module.scss';
|
||||||
|
|
||||||
|
export function SubscriptionPlansOfferChecks() {
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<span className={styles.iconText}>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
height="18px"
|
||||||
|
width="18px"
|
||||||
|
viewBox="0 -960 960 960"
|
||||||
|
fill="rgb(0, 130, 77)"
|
||||||
|
className={styles.icon}
|
||||||
|
>
|
||||||
|
<path d="M378-225 133-470l66-66 179 180 382-382 66 65-448 448Z"></path>
|
||||||
|
</svg>
|
||||||
|
14-day free trial
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span className={styles.iconText}>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
height="18px"
|
||||||
|
width="18px"
|
||||||
|
viewBox="0 -960 960 960"
|
||||||
|
fill="rgb(0, 130, 77)"
|
||||||
|
className={styles.icon}
|
||||||
|
>
|
||||||
|
<path d="M378-225 133-470l66-66 179 180 382-382 66 65-448 448Z"></path>
|
||||||
|
</svg>
|
||||||
|
24/7 online support
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -24,7 +24,7 @@ function SubscriptionPlansPeriodSwitcherRoot({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<Group position={'center'} spacing={10} style={{ marginBottom: '1.2rem' }}>
|
<Group position={'center'} spacing={10} style={{ marginBottom: '1.6rem' }}>
|
||||||
<Text>Pay Monthly</Text>
|
<Text>Pay Monthly</Text>
|
||||||
<Switch
|
<Switch
|
||||||
large
|
large
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Callout } from '@blueprintjs/core';
|
import { Callout } from '@blueprintjs/core';
|
||||||
import { SubscriptionPlans } from './SubscriptionPlans';
|
import { SubscriptionPlans } from './SubscriptionPlans';
|
||||||
import { SubscriptionPlansPeriodSwitcher } from './SubscriptionPlansPeriodSwitcher';
|
import { SubscriptionPlansPeriodSwitcher } from './SubscriptionPlansPeriodSwitcher';
|
||||||
|
import { SubscriptionPlansOfferChecks } from './SubscriptionPlansOfferChecks';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Billing plans.
|
* Billing plans.
|
||||||
@@ -14,6 +15,7 @@ export function SubscriptionPlansSection() {
|
|||||||
include applicable taxes.
|
include applicable taxes.
|
||||||
</Callout>
|
</Callout>
|
||||||
|
|
||||||
|
<SubscriptionPlansOfferChecks />
|
||||||
<SubscriptionPlansPeriodSwitcher />
|
<SubscriptionPlansPeriodSwitcher />
|
||||||
<SubscriptionPlans />
|
<SubscriptionPlans />
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
|
|
||||||
&__content {
|
&__content {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding-bottom: 40px;
|
padding-bottom: 80px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__left-section {
|
&__left-section {
|
||||||
|
|||||||
9
pnpm-lock.yaml
generated
9
pnpm-lock.yaml
generated
@@ -86,6 +86,9 @@ importers:
|
|||||||
async:
|
async:
|
||||||
specifier: ^3.2.0
|
specifier: ^3.2.0
|
||||||
version: 3.2.5
|
version: 3.2.5
|
||||||
|
async-mutex:
|
||||||
|
specifier: ^0.5.0
|
||||||
|
version: 0.5.0
|
||||||
axios:
|
axios:
|
||||||
specifier: ^1.6.0
|
specifier: ^1.6.0
|
||||||
version: 1.7.2
|
version: 1.7.2
|
||||||
@@ -8009,6 +8012,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==}
|
resolution: {integrity: sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/async-mutex@0.5.0:
|
||||||
|
resolution: {integrity: sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==}
|
||||||
|
dependencies:
|
||||||
|
tslib: 2.6.2
|
||||||
|
dev: false
|
||||||
|
|
||||||
/async-settle@1.0.0:
|
/async-settle@1.0.0:
|
||||||
resolution: {integrity: sha512-VPXfB4Vk49z1LHHodrEQ6Xf7W4gg1w0dAPROHngx7qgDjqmIQ+fXmwgGXTW/ITLai0YLSvWepJOP9EVpMnEAcw==}
|
resolution: {integrity: sha512-VPXfB4Vk49z1LHHodrEQ6Xf7W4gg1w0dAPROHngx7qgDjqmIQ+fXmwgGXTW/ITLai0YLSvWepJOP9EVpMnEAcw==}
|
||||||
engines: {node: '>= 0.10'}
|
engines: {node: '>= 0.10'}
|
||||||
|
|||||||
Reference in New Issue
Block a user