mirror of
https://github.com/frappe/books.git
synced 2025-01-03 07:12:21 +00:00
Merge branch 'frappe:master' into batch-wise-item
This commit is contained in:
commit
915c1d2e5c
@ -10,6 +10,7 @@ module.exports = {
|
|||||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
||||||
'arrow-body-style': 'off',
|
'arrow-body-style': 'off',
|
||||||
'prefer-arrow-callback': 'off',
|
'prefer-arrow-callback': 'off',
|
||||||
|
'vue/no-mutating-props': 'off',
|
||||||
'vue/multi-word-component-names': 'off',
|
'vue/multi-word-component-names': 'off',
|
||||||
'vue/no-useless-template-attributes': 'off',
|
'vue/no-useless-template-attributes': 'off',
|
||||||
},
|
},
|
||||||
|
5
.github/workflows/build.yml
vendored
5
.github/workflows/build.yml
vendored
@ -24,11 +24,8 @@ jobs:
|
|||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: yarn
|
run: yarn
|
||||||
|
|
||||||
- name: Install RPM
|
|
||||||
run: HOMEBREW_NO_AUTO_UPDATE=1 brew install rpm
|
|
||||||
|
|
||||||
- name: Run build
|
- name: Run build
|
||||||
env:
|
env:
|
||||||
CSC_IDENTITY_AUTO_DISCOVERY: false
|
CSC_IDENTITY_AUTO_DISCOVERY: false
|
||||||
APPLE_NOTARIZE: 0
|
APPLE_NOTARIZE: 0
|
||||||
run: yarn electron:build -mwl --publish never
|
run: yarn electron:build -mw --publish never
|
||||||
|
3
.github/workflows/publish.yml
vendored
3
.github/workflows/publish.yml
vendored
@ -34,13 +34,12 @@ jobs:
|
|||||||
- name: Run build
|
- name: Run build
|
||||||
env:
|
env:
|
||||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||||
APPLE_APP_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }}
|
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }}
|
||||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||||
CSC_LINK: ${{ secrets.CSC_LINK }}
|
CSC_LINK: ${{ secrets.CSC_LINK }}
|
||||||
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
||||||
CSC_IDENTITY_AUTO_DISCOVERY: true
|
CSC_IDENTITY_AUTO_DISCOVERY: true
|
||||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||||
APPLE_NOTARIZE: 1
|
|
||||||
run: |
|
run: |
|
||||||
yarn set version 1.22.18
|
yarn set version 1.22.18
|
||||||
yarn electron:build --mac --publish always
|
yarn electron:build --mac --publish always
|
||||||
|
@ -121,8 +121,7 @@ If you want to contribute code then you can fork this repo, make changes and rai
|
|||||||
|
|
||||||
## Links
|
## Links
|
||||||
|
|
||||||
- [Telegram Group](https://t.me/frappebooks): Used for discussions regarding features, issues, changes, etc. This group is also be used to make decisions regarding project direction.
|
- [Telegram Group](https://t.me/frappebooks): Used for discussions and decisions regarding everything Frappe Books.
|
||||||
- [Project Board](https://github.com/frappe/books/projects/1): Roadmap that is updated with acceptable latency.
|
|
||||||
- [GitHub Discussions](https://github.com/frappe/books/discussions): Used for discussions around a specific topic.
|
- [GitHub Discussions](https://github.com/frappe/books/discussions): Used for discussions around a specific topic.
|
||||||
- [Frappe Books Blog](https://tech.frappebooks.com/): Sporadically updated dev blog regarding the development of this project.
|
- [Frappe Books Blog](https://tech.frappebooks.com/): Sporadically updated dev blog regarding the development of this project.
|
||||||
|
|
||||||
|
@ -2,9 +2,19 @@ import { getDefaultMetaFieldValueMap } from '../../backend/helpers';
|
|||||||
import { DatabaseManager } from '../database/manager';
|
import { DatabaseManager } from '../database/manager';
|
||||||
|
|
||||||
async function execute(dm: DatabaseManager) {
|
async function execute(dm: DatabaseManager) {
|
||||||
|
const s = (await dm.db?.getAll('SingleValue', {
|
||||||
|
fields: ['value'],
|
||||||
|
filters: { fieldname: 'setupComplete' },
|
||||||
|
})) as { value: string }[];
|
||||||
|
|
||||||
|
if (!Number(s?.[0]?.value ?? '0')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const names: Record<string, string> = {
|
const names: Record<string, string> = {
|
||||||
StockMovement: 'SMOV-',
|
StockMovement: 'SMOV-',
|
||||||
Shipment: 'SHP-',
|
PurchaseReceipt: 'PREC-',
|
||||||
|
Shipment: 'SHPM-',
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const referenceType in names) {
|
for (const referenceType in names) {
|
||||||
|
@ -1,21 +0,0 @@
|
|||||||
const { notarize } = require('electron-notarize');
|
|
||||||
|
|
||||||
exports.default = async (context) => {
|
|
||||||
const { electronPlatformName, appOutDir } = context;
|
|
||||||
if (
|
|
||||||
electronPlatformName !== 'darwin' ||
|
|
||||||
!parseInt(process.env.APPLE_NOTARIZE)
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const appName = context.packager.appInfo.productFilename;
|
|
||||||
|
|
||||||
return await notarize({
|
|
||||||
appBundleId: 'io.frappe.books',
|
|
||||||
appPath: `${appOutDir}/${appName}.app`,
|
|
||||||
appleId: process.env.APPLE_ID,
|
|
||||||
appleIdPassword: process.env.APPLE_APP_PASSWORD,
|
|
||||||
teamId: process.env.APPLE_TEAM_ID,
|
|
||||||
});
|
|
||||||
};
|
|
@ -1,6 +1,5 @@
|
|||||||
productName: Frappe Books
|
productName: Frappe Books
|
||||||
appId: io.frappe.books
|
appId: io.frappe.books
|
||||||
afterSign: build/notarize.js
|
|
||||||
asarUnpack: '**/*.node'
|
asarUnpack: '**/*.node'
|
||||||
extraResources:
|
extraResources:
|
||||||
[
|
[
|
||||||
@ -11,6 +10,8 @@ mac:
|
|||||||
type: distribution
|
type: distribution
|
||||||
category: public.app-category.finance
|
category: public.app-category.finance
|
||||||
icon: build/icon.icns
|
icon: build/icon.icns
|
||||||
|
notarize:
|
||||||
|
appBundleId: io.frappe.books
|
||||||
hardenedRuntime: true
|
hardenedRuntime: true
|
||||||
gatekeeperAssess: false
|
gatekeeperAssess: false
|
||||||
darkModeSupport: false
|
darkModeSupport: false
|
||||||
|
@ -3,7 +3,6 @@ import { Doc } from 'fyo/model/doc';
|
|||||||
import { isPesa } from 'fyo/utils';
|
import { isPesa } from 'fyo/utils';
|
||||||
import { ValueError } from 'fyo/utils/errors';
|
import { ValueError } from 'fyo/utils/errors';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { Money } from 'pesa';
|
|
||||||
import { Field, FieldTypeEnum, RawValue, TargetField } from 'schemas/types';
|
import { Field, FieldTypeEnum, RawValue, TargetField } from 'schemas/types';
|
||||||
import { getIsNullOrUndef, safeParseFloat, safeParseInt } from 'utils';
|
import { getIsNullOrUndef, safeParseFloat, safeParseInt } from 'utils';
|
||||||
import { DatabaseHandler } from './dbHandler';
|
import { DatabaseHandler } from './dbHandler';
|
||||||
|
@ -5,7 +5,6 @@ import { Verb } from 'fyo/telemetry/types';
|
|||||||
import { DEFAULT_USER } from 'fyo/utils/consts';
|
import { DEFAULT_USER } from 'fyo/utils/consts';
|
||||||
import { ConflictError, MandatoryError, NotFoundError } from 'fyo/utils/errors';
|
import { ConflictError, MandatoryError, NotFoundError } from 'fyo/utils/errors';
|
||||||
import Observable from 'fyo/utils/observable';
|
import Observable from 'fyo/utils/observable';
|
||||||
import { Money } from 'pesa';
|
|
||||||
import {
|
import {
|
||||||
DynamicLinkField,
|
DynamicLinkField,
|
||||||
Field,
|
Field,
|
||||||
@ -142,6 +141,10 @@ export class Doc extends Observable<DocValue | Doc[]> {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.schema.isChild) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.schema.isSubmittable) {
|
if (!this.schema.isSubmittable) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -186,6 +189,14 @@ export class Doc extends Observable<DocValue | Doc[]> {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.schema.isSingle) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.schema.isChild) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -188,7 +188,7 @@ function getField(df: string | Field): Field {
|
|||||||
label: '',
|
label: '',
|
||||||
fieldname: '',
|
fieldname: '',
|
||||||
fieldtype: df as FieldType,
|
fieldtype: df as FieldType,
|
||||||
};
|
} as Field;
|
||||||
}
|
}
|
||||||
|
|
||||||
return df;
|
return df;
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import { Fyo } from 'fyo';
|
import { Fyo } from 'fyo';
|
||||||
|
import { DocValue } from 'fyo/core/types';
|
||||||
import { Doc } from 'fyo/model/doc';
|
import { Doc } from 'fyo/model/doc';
|
||||||
import { Action } from 'fyo/model/types';
|
import { Action } from 'fyo/model/types';
|
||||||
import { Money } from 'pesa';
|
import { Money } from 'pesa';
|
||||||
import { Field, OptionField, SelectOption } from 'schemas/types';
|
import { Field, FieldType, OptionField, SelectOption } from 'schemas/types';
|
||||||
import { getIsNullOrUndef, safeParseInt } from 'utils';
|
import { getIsNullOrUndef, safeParseInt } from 'utils';
|
||||||
|
|
||||||
export function slug(str: string) {
|
export function slug(str: string) {
|
||||||
@ -109,3 +110,34 @@ function getRawOptionList(field: Field, doc: Doc | undefined | null) {
|
|||||||
|
|
||||||
return getList(doc!);
|
return getList(doc!);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getEmptyValuesByFieldTypes(
|
||||||
|
fieldtype: FieldType,
|
||||||
|
fyo: Fyo
|
||||||
|
): DocValue {
|
||||||
|
switch (fieldtype) {
|
||||||
|
case 'Date':
|
||||||
|
case 'Datetime':
|
||||||
|
return new Date();
|
||||||
|
case 'Float':
|
||||||
|
case 'Int':
|
||||||
|
return 0;
|
||||||
|
case 'Currency':
|
||||||
|
return fyo.pesa(0);
|
||||||
|
case 'Check':
|
||||||
|
return false;
|
||||||
|
case 'DynamicLink':
|
||||||
|
case 'Link':
|
||||||
|
case 'Select':
|
||||||
|
case 'AutoComplete':
|
||||||
|
case 'Text':
|
||||||
|
case 'Data':
|
||||||
|
case 'Color':
|
||||||
|
return null;
|
||||||
|
case 'Table':
|
||||||
|
case 'Attachment':
|
||||||
|
case 'AttachImage':
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -6,7 +6,12 @@ import {
|
|||||||
FormulaMap,
|
FormulaMap,
|
||||||
ListViewSettings,
|
ListViewSettings,
|
||||||
} from 'fyo/model/types';
|
} from 'fyo/model/types';
|
||||||
import { addItem, getDocStatusListColumn, getLedgerLinkAction } from 'models/helpers';
|
import { ValidationError } from 'fyo/utils/errors';
|
||||||
|
import {
|
||||||
|
addItem,
|
||||||
|
getDocStatusListColumn,
|
||||||
|
getLedgerLinkAction,
|
||||||
|
} from 'models/helpers';
|
||||||
import { LedgerPosting } from 'models/Transactional/LedgerPosting';
|
import { LedgerPosting } from 'models/Transactional/LedgerPosting';
|
||||||
import { ModelNameEnum } from 'models/types';
|
import { ModelNameEnum } from 'models/types';
|
||||||
import { Money } from 'pesa';
|
import { Money } from 'pesa';
|
||||||
@ -42,6 +47,24 @@ export class StockMovement extends Transfer {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
async validate() {
|
||||||
|
await super.validate();
|
||||||
|
if (this.movementType !== MovementType.Manufacture) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasFrom = this.items?.findIndex((f) => f.fromLocation) !== -1;
|
||||||
|
const hasTo = this.items?.findIndex((f) => f.toLocation) !== -1;
|
||||||
|
|
||||||
|
if (!hasFrom) {
|
||||||
|
throw new ValidationError(this.fyo.t`Item with From location not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasTo) {
|
||||||
|
throw new ValidationError(this.fyo.t`Item with To location not found`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static filters: FiltersMap = {
|
static filters: FiltersMap = {
|
||||||
numberSeries: () => ({ referenceType: ModelNameEnum.StockMovement }),
|
numberSeries: () => ({ referenceType: ModelNameEnum.StockMovement }),
|
||||||
};
|
};
|
||||||
@ -68,6 +91,7 @@ export class StockMovement extends Transfer {
|
|||||||
[MovementType.MaterialIssue]: fyo.t`Material Issue`,
|
[MovementType.MaterialIssue]: fyo.t`Material Issue`,
|
||||||
[MovementType.MaterialReceipt]: fyo.t`Material Receipt`,
|
[MovementType.MaterialReceipt]: fyo.t`Material Receipt`,
|
||||||
[MovementType.MaterialTransfer]: fyo.t`Material Transfer`,
|
[MovementType.MaterialTransfer]: fyo.t`Material Transfer`,
|
||||||
|
[MovementType.Manufacture]: fyo.t`Manufacture`,
|
||||||
}[movementType] ?? '';
|
}[movementType] ?? '';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -4,7 +4,9 @@ import {
|
|||||||
FormulaMap,
|
FormulaMap,
|
||||||
ReadOnlyMap,
|
ReadOnlyMap,
|
||||||
RequiredMap,
|
RequiredMap,
|
||||||
|
ValidationMap,
|
||||||
} from 'fyo/model/types';
|
} from 'fyo/model/types';
|
||||||
|
import { ValidationError } from 'fyo/utils/errors';
|
||||||
import { ModelNameEnum } from 'models/types';
|
import { ModelNameEnum } from 'models/types';
|
||||||
import { Money } from 'pesa';
|
import { Money } from 'pesa';
|
||||||
import { StockMovement } from './StockMovement';
|
import { StockMovement } from './StockMovement';
|
||||||
@ -33,6 +35,10 @@ export class StockMovementItem extends Doc {
|
|||||||
return this.parentdoc?.movementType === MovementType.MaterialTransfer;
|
return this.parentdoc?.movementType === MovementType.MaterialTransfer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get isManufacture() {
|
||||||
|
return this.parentdoc?.movementType === MovementType.Manufacture;
|
||||||
|
}
|
||||||
|
|
||||||
static filters: FiltersMap = {
|
static filters: FiltersMap = {
|
||||||
item: () => ({ trackItem: true }),
|
item: () => ({ trackItem: true }),
|
||||||
};
|
};
|
||||||
@ -53,14 +59,14 @@ export class StockMovementItem extends Doc {
|
|||||||
dependsOn: ['item', 'rate', 'quantity'],
|
dependsOn: ['item', 'rate', 'quantity'],
|
||||||
},
|
},
|
||||||
fromLocation: {
|
fromLocation: {
|
||||||
formula: (fn) => {
|
formula: () => {
|
||||||
if (this.isReceipt || this.isTransfer) {
|
if (this.isReceipt || this.isTransfer || this.isManufacture) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultLocation = this.fyo.singles.InventorySettings
|
const defaultLocation = this.fyo.singles.InventorySettings
|
||||||
?.defaultLocation as string | undefined;
|
?.defaultLocation as string | undefined;
|
||||||
if (defaultLocation && !this.location && this.isIssue) {
|
if (defaultLocation && !this.fromLocation && this.isIssue) {
|
||||||
return defaultLocation;
|
return defaultLocation;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,14 +75,14 @@ export class StockMovementItem extends Doc {
|
|||||||
dependsOn: ['movementType'],
|
dependsOn: ['movementType'],
|
||||||
},
|
},
|
||||||
toLocation: {
|
toLocation: {
|
||||||
formula: (fn) => {
|
formula: () => {
|
||||||
if (this.isIssue || this.isTransfer) {
|
if (this.isIssue || this.isTransfer || this.isManufacture) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultLocation = this.fyo.singles.InventorySettings
|
const defaultLocation = this.fyo.singles.InventorySettings
|
||||||
?.defaultLocation as string | undefined;
|
?.defaultLocation as string | undefined;
|
||||||
if (defaultLocation && !this.location && this.isReceipt) {
|
if (defaultLocation && !this.toLocation && this.isReceipt) {
|
||||||
return defaultLocation;
|
return defaultLocation;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,6 +92,31 @@ export class StockMovementItem extends Doc {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
validations: ValidationMap = {
|
||||||
|
fromLocation: (value) => {
|
||||||
|
if (!this.isManufacture) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value && this.toLocation) {
|
||||||
|
throw new ValidationError(
|
||||||
|
this.fyo.t`Only From or To can be set for Manucature`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
toLocation: (value) => {
|
||||||
|
if (!this.isManufacture) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value && this.fromLocation) {
|
||||||
|
throw new ValidationError(
|
||||||
|
this.fyo.t`Only From or To can be set for Manufacture`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
required: RequiredMap = {
|
required: RequiredMap = {
|
||||||
fromLocation: () => this.isIssue || this.isTransfer,
|
fromLocation: () => this.isIssue || this.isTransfer,
|
||||||
toLocation: () => this.isReceipt || this.isTransfer,
|
toLocation: () => this.isReceipt || this.isTransfer,
|
||||||
|
136
models/inventory/tests/testStockMovement.spec.ts
Normal file
136
models/inventory/tests/testStockMovement.spec.ts
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
import { ModelNameEnum } from 'models/types';
|
||||||
|
import test from 'tape';
|
||||||
|
import { getItem } from './helpers';
|
||||||
|
import { closeTestFyo, getTestFyo, setupTestFyo } from 'tests/helpers';
|
||||||
|
import { MovementType } from '../types';
|
||||||
|
import {
|
||||||
|
assertDoesNotThrow,
|
||||||
|
assertThrows,
|
||||||
|
} from 'backend/database/tests/helpers';
|
||||||
|
import { StockMovement } from '../StockMovement';
|
||||||
|
|
||||||
|
const fyo = getTestFyo();
|
||||||
|
setupTestFyo(fyo, __filename);
|
||||||
|
|
||||||
|
test('check store and create test items', async (t) => {
|
||||||
|
const e = await fyo.db.exists(ModelNameEnum.Location, 'Stores');
|
||||||
|
t.equals(e, true, 'location Stores exist');
|
||||||
|
|
||||||
|
const items = [
|
||||||
|
getItem('RawOne', 100),
|
||||||
|
getItem('RawTwo', 100),
|
||||||
|
getItem('Final', 200),
|
||||||
|
];
|
||||||
|
|
||||||
|
const exists: boolean[] = [];
|
||||||
|
for (const item of items) {
|
||||||
|
await fyo.doc.getNewDoc('Item', item).sync();
|
||||||
|
exists.push(await fyo.db.exists('Item', item.name));
|
||||||
|
}
|
||||||
|
|
||||||
|
t.ok(exists.every(Boolean), 'items created');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Stock Movement, Material Receipt', async (t) => {
|
||||||
|
const sm = fyo.doc.getNewDoc(ModelNameEnum.StockMovement);
|
||||||
|
|
||||||
|
await sm.set({
|
||||||
|
date: new Date('2022-01-01'),
|
||||||
|
movementType: MovementType.MaterialReceipt,
|
||||||
|
});
|
||||||
|
|
||||||
|
await sm.append('items', {
|
||||||
|
item: 'RawOne',
|
||||||
|
quantity: 1,
|
||||||
|
rate: 100,
|
||||||
|
toLocation: 'Stores',
|
||||||
|
});
|
||||||
|
|
||||||
|
await sm.append('items', {
|
||||||
|
item: 'RawTwo',
|
||||||
|
quantity: 1,
|
||||||
|
rate: 100,
|
||||||
|
toLocation: 'Stores',
|
||||||
|
});
|
||||||
|
|
||||||
|
await assertDoesNotThrow(async () => await sm.sync());
|
||||||
|
await assertDoesNotThrow(async () => await sm.submit());
|
||||||
|
|
||||||
|
t.equal(
|
||||||
|
await fyo.db.getStockQuantity('RawOne', 'Stores'),
|
||||||
|
1,
|
||||||
|
'item RawOne added'
|
||||||
|
);
|
||||||
|
t.equal(
|
||||||
|
await fyo.db.getStockQuantity('RawTwo', 'Stores'),
|
||||||
|
1,
|
||||||
|
'item RawTwo added'
|
||||||
|
);
|
||||||
|
t.equal(
|
||||||
|
await fyo.db.getStockQuantity('Final', 'Stores'),
|
||||||
|
null,
|
||||||
|
'item Final not yet added'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Stock Movement, Manufacture', async (t) => {
|
||||||
|
const sm = fyo.doc.getNewDoc(ModelNameEnum.StockMovement) as StockMovement;
|
||||||
|
|
||||||
|
await sm.set({
|
||||||
|
date: new Date('2022-01-02'),
|
||||||
|
movementType: MovementType.Manufacture,
|
||||||
|
});
|
||||||
|
|
||||||
|
await sm.append('items', {
|
||||||
|
item: 'RawOne',
|
||||||
|
quantity: 1,
|
||||||
|
rate: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
await assertDoesNotThrow(
|
||||||
|
async () => await sm.items?.[0].set('fromLocation', 'Stores')
|
||||||
|
);
|
||||||
|
await assertThrows(
|
||||||
|
async () => await sm.items?.[0].set('toLocation', 'Stores')
|
||||||
|
);
|
||||||
|
t.notOk(sm.items?.[0].to, 'to location not set');
|
||||||
|
|
||||||
|
await sm.append('items', {
|
||||||
|
item: 'RawTwo',
|
||||||
|
quantity: 1,
|
||||||
|
rate: 100,
|
||||||
|
fromLocation: 'Stores',
|
||||||
|
});
|
||||||
|
|
||||||
|
await assertThrows(async () => await sm.sync());
|
||||||
|
|
||||||
|
await sm.append('items', {
|
||||||
|
item: 'Final',
|
||||||
|
quantity: 1,
|
||||||
|
rate: 100,
|
||||||
|
toLocation: 'Stores',
|
||||||
|
});
|
||||||
|
|
||||||
|
await assertDoesNotThrow(async () => await sm.sync());
|
||||||
|
await assertDoesNotThrow(async () => await sm.submit());
|
||||||
|
|
||||||
|
t.equal(
|
||||||
|
await fyo.db.getStockQuantity('RawOne', 'Stores'),
|
||||||
|
0,
|
||||||
|
'item RawOne removed'
|
||||||
|
);
|
||||||
|
|
||||||
|
t.equal(
|
||||||
|
await fyo.db.getStockQuantity('RawTwo', 'Stores'),
|
||||||
|
0,
|
||||||
|
'item RawTwo removed'
|
||||||
|
);
|
||||||
|
|
||||||
|
t.equal(
|
||||||
|
await fyo.db.getStockQuantity('Final', 'Stores'),
|
||||||
|
1,
|
||||||
|
'item Final added'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
closeTestFyo(fyo, __filename);
|
@ -9,6 +9,7 @@ export enum MovementType {
|
|||||||
'MaterialIssue' = 'MaterialIssue',
|
'MaterialIssue' = 'MaterialIssue',
|
||||||
'MaterialReceipt' = 'MaterialReceipt',
|
'MaterialReceipt' = 'MaterialReceipt',
|
||||||
'MaterialTransfer' = 'MaterialTransfer',
|
'MaterialTransfer' = 'MaterialTransfer',
|
||||||
|
'Manufacture' = 'Manufacture',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SMDetails {
|
export interface SMDetails {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "frappe-books",
|
"name": "frappe-books",
|
||||||
"version": "0.8.0",
|
"version": "0.9.0",
|
||||||
"description": "Simple book-keeping app for everyone",
|
"description": "Simple book-keeping app for everyone",
|
||||||
"main": "background.js",
|
"main": "background.js",
|
||||||
"author": {
|
"author": {
|
||||||
@ -55,10 +55,9 @@
|
|||||||
"autoprefixer": "^9",
|
"autoprefixer": "^9",
|
||||||
"babel-loader": "^8.2.3",
|
"babel-loader": "^8.2.3",
|
||||||
"dotenv": "^16.0.0",
|
"dotenv": "^16.0.0",
|
||||||
"electron": "^18.3.7",
|
"electron": "18.3.7",
|
||||||
"electron-builder": "^23.0.3",
|
"electron-builder": "24.0.0-alpha.12",
|
||||||
"electron-devtools-installer": "^3.2.0",
|
"electron-devtools-installer": "^3.2.0",
|
||||||
"electron-notarize": "^1.1.1",
|
|
||||||
"electron-rebuild": "^3.2.9",
|
"electron-rebuild": "^3.2.9",
|
||||||
"electron-updater": "^5.2.1",
|
"electron-updater": "^5.2.1",
|
||||||
"eslint": "^7.32.0",
|
"eslint": "^7.32.0",
|
||||||
@ -81,7 +80,7 @@
|
|||||||
"webpack": "^5.66.0"
|
"webpack": "^5.66.0"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"electron-builder": "^23.3.3"
|
"electron-builder": "24.0.0-alpha.12"
|
||||||
},
|
},
|
||||||
"prettier": {
|
"prettier": {
|
||||||
"semi": true,
|
"semi": true,
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { AccountRootType } from 'models/baseModels/Account/types';
|
import { AccountRootType } from 'models/baseModels/Account/types';
|
||||||
import { BaseField, RawValue } from 'schemas/types';
|
import { BaseField, FieldType, RawValue } from 'schemas/types';
|
||||||
|
|
||||||
export type ExportExtention = 'csv' | 'json';
|
export type ExportExtention = 'csv' | 'json';
|
||||||
|
|
||||||
@ -24,7 +24,8 @@ export interface ReportRow {
|
|||||||
foldedBelow?: boolean;
|
foldedBelow?: boolean;
|
||||||
}
|
}
|
||||||
export type ReportData = ReportRow[];
|
export type ReportData = ReportRow[];
|
||||||
export interface ColumnField extends BaseField {
|
export interface ColumnField extends Omit<BaseField, 'fieldtype'> {
|
||||||
|
fieldtype: FieldType;
|
||||||
align?: 'left' | 'right' | 'center';
|
align?: 'left' | 'right' | 'center';
|
||||||
width?: number;
|
width?: number;
|
||||||
}
|
}
|
||||||
|
@ -73,7 +73,6 @@
|
|||||||
"label": "Address",
|
"label": "Address",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"target": "Address",
|
"target": "Address",
|
||||||
"placeholder": "Click to create",
|
|
||||||
"inline": true
|
"inline": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -40,7 +40,6 @@
|
|||||||
"label": "Address",
|
"label": "Address",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"target": "Address",
|
"target": "Address",
|
||||||
"placeholder": "Click to create",
|
|
||||||
"inline": true
|
"inline": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -16,7 +16,6 @@
|
|||||||
"label": "Address",
|
"label": "Address",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"target": "Address",
|
"target": "Address",
|
||||||
"placeholder": "Click to create",
|
|
||||||
"inline": true
|
"inline": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@ -35,6 +35,10 @@
|
|||||||
{
|
{
|
||||||
"value": "MaterialTransfer",
|
"value": "MaterialTransfer",
|
||||||
"label": "Material Transfer"
|
"label": "Material Transfer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "Manufacture",
|
||||||
|
"label": "Manufacture"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"required": true
|
"required": true
|
||||||
|
@ -73,7 +73,6 @@
|
|||||||
"label": "Address",
|
"label": "Address",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"target": "Address",
|
"target": "Address",
|
||||||
"placeholder": "Click to create",
|
|
||||||
"inline": true
|
"inline": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@ -1,28 +1,56 @@
|
|||||||
export enum FieldTypeEnum {
|
import { PropertyEnum } from "utils/types";
|
||||||
Data = 'Data',
|
|
||||||
Select = 'Select',
|
export type FieldType =
|
||||||
Link = 'Link',
|
| 'Data'
|
||||||
Date = 'Date',
|
| 'Select'
|
||||||
Datetime = 'Datetime',
|
| 'Link'
|
||||||
Table = 'Table',
|
| 'Date'
|
||||||
AutoComplete = 'AutoComplete',
|
| 'Datetime'
|
||||||
Check = 'Check',
|
| 'Table'
|
||||||
AttachImage = 'AttachImage',
|
| 'AutoComplete'
|
||||||
DynamicLink = 'DynamicLink',
|
| 'Check'
|
||||||
Int = 'Int',
|
| 'AttachImage'
|
||||||
Float = 'Float',
|
| 'DynamicLink'
|
||||||
Currency = 'Currency',
|
| 'Int'
|
||||||
Text = 'Text',
|
| 'Float'
|
||||||
Color = 'Color',
|
| 'Currency'
|
||||||
Attachment = 'Attachment',
|
| 'Text'
|
||||||
}
|
| 'Color'
|
||||||
|
| 'Attachment';
|
||||||
|
|
||||||
|
export const FieldTypeEnum: PropertyEnum<Record<FieldType, FieldType>> = {
|
||||||
|
Data: 'Data',
|
||||||
|
Select: 'Select',
|
||||||
|
Link: 'Link',
|
||||||
|
Date: 'Date',
|
||||||
|
Datetime: 'Datetime',
|
||||||
|
Table: 'Table',
|
||||||
|
AutoComplete: 'AutoComplete',
|
||||||
|
Check: 'Check',
|
||||||
|
AttachImage: 'AttachImage',
|
||||||
|
DynamicLink: 'DynamicLink',
|
||||||
|
Int: 'Int',
|
||||||
|
Float: 'Float',
|
||||||
|
Currency: 'Currency',
|
||||||
|
Text: 'Text',
|
||||||
|
Color: 'Color',
|
||||||
|
Attachment: 'Attachment',
|
||||||
|
};
|
||||||
|
|
||||||
|
type OptionFieldType = 'Select' | 'AutoComplete' | 'Color';
|
||||||
|
type TargetFieldType = 'Table' | 'Link';
|
||||||
|
type NumberFieldType = 'Int' | 'Float';
|
||||||
|
type DynamicLinkFieldType = 'DynamicLink';
|
||||||
|
type BaseFieldType = Exclude<
|
||||||
|
FieldType,
|
||||||
|
TargetFieldType | DynamicLinkFieldType | OptionFieldType | NumberFieldType
|
||||||
|
>;
|
||||||
|
|
||||||
export type FieldType = keyof typeof FieldTypeEnum;
|
|
||||||
export type RawValue = string | number | boolean | null;
|
export type RawValue = string | number | boolean | null;
|
||||||
|
|
||||||
export interface BaseField {
|
export interface BaseField {
|
||||||
fieldname: string; // Column name in the db
|
fieldname: string; // Column name in the db
|
||||||
fieldtype: FieldType; // UI Descriptive field types that map to column types
|
fieldtype: BaseFieldType; // UI Descriptive field types that map to column types
|
||||||
label: string; // Translateable UI facing name
|
label: string; // Translateable UI facing name
|
||||||
schemaName?: string; // Convenient access to schemaName incase just the field is passed
|
schemaName?: string; // Convenient access to schemaName incase just the field is passed
|
||||||
required?: boolean; // Implies Not Null
|
required?: boolean; // Implies Not Null
|
||||||
@ -39,31 +67,28 @@ export interface BaseField {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type SelectOption = { value: string; label: string };
|
export type SelectOption = { value: string; label: string };
|
||||||
export interface OptionField extends BaseField {
|
export interface OptionField extends Omit<BaseField, 'fieldtype'> {
|
||||||
fieldtype:
|
fieldtype: OptionFieldType;
|
||||||
| FieldTypeEnum.Select
|
|
||||||
| FieldTypeEnum.AutoComplete
|
|
||||||
| FieldTypeEnum.Color;
|
|
||||||
options: SelectOption[];
|
options: SelectOption[];
|
||||||
emptyMessage?: string;
|
emptyMessage?: string;
|
||||||
allowCustom?: boolean;
|
allowCustom?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TargetField extends BaseField {
|
export interface TargetField extends Omit<BaseField, 'fieldtype'> {
|
||||||
fieldtype: FieldTypeEnum.Table | FieldTypeEnum.Link;
|
fieldtype: TargetFieldType;
|
||||||
target: string; // Name of the table or group of tables to fetch values
|
target: string; // Name of the table or group of tables to fetch values
|
||||||
create?: boolean; // Whether to show Create in the dropdown
|
create?: boolean; // Whether to show Create in the dropdown
|
||||||
edit?: boolean; // Whether the Table has quick editable columns
|
edit?: boolean; // Whether the Table has quick editable columns
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DynamicLinkField extends BaseField {
|
export interface DynamicLinkField extends Omit<BaseField, 'fieldtype'> {
|
||||||
fieldtype: FieldTypeEnum.DynamicLink;
|
fieldtype: DynamicLinkFieldType;
|
||||||
emptyMessage?: string;
|
emptyMessage?: string;
|
||||||
references: string; // Reference to an option field that links to schema
|
references: string; // Reference to an option field that links to schema
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NumberField extends BaseField {
|
export interface NumberField extends Omit<BaseField, 'fieldtype'> {
|
||||||
fieldtype: FieldTypeEnum.Float | FieldTypeEnum.Int;
|
fieldtype: NumberFieldType;
|
||||||
minvalue?: number; // UI Facing used to restrict lower bound
|
minvalue?: number; // UI Facing used to restrict lower bound
|
||||||
maxvalue?: number; // UI Facing used to restrict upper bound
|
maxvalue?: number; // UI Facing used to restrict upper bound
|
||||||
}
|
}
|
||||||
@ -80,7 +105,7 @@ export type Naming = 'autoincrement' | 'random' | 'numberSeries' | 'manual';
|
|||||||
export interface Schema {
|
export interface Schema {
|
||||||
name: string; // Table name
|
name: string; // Table name
|
||||||
label: string; // Translateable UI facing name
|
label: string; // Translateable UI facing name
|
||||||
fields: Field[]; // Maps to database columns
|
fields: Field[]; // Maps to database columns
|
||||||
isTree?: boolean; // Used for nested set, eg for Chart of Accounts
|
isTree?: boolean; // Used for nested set, eg for Chart of Accounts
|
||||||
extends?: string; // Value points to an Abstract schema. Indicates Subclass schema
|
extends?: string; // Value points to an Abstract schema. Indicates Subclass schema
|
||||||
isChild?: boolean; // Indicates a child table, i.e table with "parent" FK column
|
isChild?: boolean; // Indicates a child table, i.e table with "parent" FK column
|
||||||
|
21
src/App.vue
21
src/App.vue
@ -41,6 +41,7 @@
|
|||||||
import { ConfigKeys } from 'fyo/core/types';
|
import { ConfigKeys } from 'fyo/core/types';
|
||||||
import { RTL_LANGUAGES } from 'fyo/utils/consts';
|
import { RTL_LANGUAGES } from 'fyo/utils/consts';
|
||||||
import { ModelNameEnum } from 'models/types';
|
import { ModelNameEnum } from 'models/types';
|
||||||
|
import { systemLanguageRef } from 'src/utils/refs';
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import WindowsTitleBar from './components/WindowsTitleBar.vue';
|
import WindowsTitleBar from './components/WindowsTitleBar.vue';
|
||||||
import { handleErrorWithDialog } from './errorHandling';
|
import { handleErrorWithDialog } from './errorHandling';
|
||||||
@ -54,6 +55,7 @@ import { initializeInstance } from './utils/initialization';
|
|||||||
import { checkForUpdates } from './utils/ipcCalls';
|
import { checkForUpdates } from './utils/ipcCalls';
|
||||||
import { updateConfigFiles } from './utils/misc';
|
import { updateConfigFiles } from './utils/misc';
|
||||||
import { Search } from './utils/search';
|
import { Search } from './utils/search';
|
||||||
|
import { setGlobalShortcuts } from './utils/shortcuts';
|
||||||
import { routeTo } from './utils/ui';
|
import { routeTo } from './utils/ui';
|
||||||
import { Shortcuts, useKeys } from './utils/vueUtils';
|
import { Shortcuts, useKeys } from './utils/vueUtils';
|
||||||
|
|
||||||
@ -69,8 +71,6 @@ export default {
|
|||||||
companyName: '',
|
companyName: '',
|
||||||
searcher: null,
|
searcher: null,
|
||||||
shortcuts: null,
|
shortcuts: null,
|
||||||
languageDirection: 'ltr',
|
|
||||||
language: '',
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
provide() {
|
provide() {
|
||||||
@ -88,11 +88,8 @@ export default {
|
|||||||
WindowsTitleBar,
|
WindowsTitleBar,
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
this.language = fyo.config.get('language');
|
const shortcuts = new Shortcuts(this.keys);
|
||||||
this.languageDirection = RTL_LANGUAGES.includes(this.language)
|
this.shortcuts = shortcuts;
|
||||||
? 'rtl'
|
|
||||||
: 'ltr';
|
|
||||||
this.shortcuts = new Shortcuts(this.keys);
|
|
||||||
const lastSelectedFilePath = fyo.config.get(
|
const lastSelectedFilePath = fyo.config.get(
|
||||||
ConfigKeys.LastSelectedFilePath,
|
ConfigKeys.LastSelectedFilePath,
|
||||||
null
|
null
|
||||||
@ -108,6 +105,16 @@ export default {
|
|||||||
await handleErrorWithDialog(err, undefined, true, true);
|
await handleErrorWithDialog(err, undefined, true, true);
|
||||||
await this.showDbSelector();
|
await this.showDbSelector();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setGlobalShortcuts(shortcuts);
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
language() {
|
||||||
|
return systemLanguageRef.value;
|
||||||
|
},
|
||||||
|
languageDirection() {
|
||||||
|
return RTL_LANGUAGES.includes(this.language) ? 'rtl' : 'ltr';
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async setDesk(filePath) {
|
async setDesk(filePath) {
|
||||||
|
@ -108,6 +108,10 @@ export default {
|
|||||||
option = this.options.find((o) => o.label === value);
|
option = this.options.find((o) => o.label === value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!value && option === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return option?.label ?? oldValue;
|
return option?.label ?? oldValue;
|
||||||
},
|
},
|
||||||
async updateSuggestions(keyword) {
|
async updateSuggestions(keyword) {
|
||||||
|
@ -23,7 +23,7 @@
|
|||||||
@change="(e) => triggerChange(e.target.value)"
|
@change="(e) => triggerChange(e.target.value)"
|
||||||
@focus="(e) => $emit('focus', e)"
|
@focus="(e) => $emit('focus', e)"
|
||||||
>
|
>
|
||||||
<option value="" disabled selected>
|
<option value="" disabled selected v-if="inputPlaceholder">
|
||||||
{{ inputPlaceholder }}
|
{{ inputPlaceholder }}
|
||||||
</option>
|
</option>
|
||||||
<option
|
<option
|
||||||
|
@ -62,6 +62,7 @@
|
|||||||
import { Doc } from 'fyo/model/doc';
|
import { Doc } from 'fyo/model/doc';
|
||||||
import Row from 'src/components/Row.vue';
|
import Row from 'src/components/Row.vue';
|
||||||
import { getErrorMessage } from 'src/utils';
|
import { getErrorMessage } from 'src/utils';
|
||||||
|
import { nextTick } from 'vue';
|
||||||
import Button from '../Button.vue';
|
import Button from '../Button.vue';
|
||||||
import FormControl from './FormControl.vue';
|
import FormControl from './FormControl.vue';
|
||||||
|
|
||||||
@ -102,15 +103,18 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onChange(df, value) {
|
async onChange(df, value) {
|
||||||
if (value == null) {
|
const fieldname = df.fieldname;
|
||||||
return;
|
this.errors[fieldname] = null;
|
||||||
}
|
const oldValue = this.row[fieldname];
|
||||||
|
|
||||||
this.errors[df.fieldname] = null;
|
try {
|
||||||
this.row.set(df.fieldname, value).catch((e) => {
|
await this.row.set(fieldname, value);
|
||||||
this.errors[df.fieldname] = getErrorMessage(e, this.row);
|
} catch (e) {
|
||||||
});
|
this.errors[fieldname] = getErrorMessage(e, this.row);
|
||||||
|
this.row[fieldname] = '';
|
||||||
|
nextTick(() => (this.row[fieldname] = oldValue));
|
||||||
|
}
|
||||||
},
|
},
|
||||||
getErrorString() {
|
getErrorString() {
|
||||||
return Object.values(this.errors).filter(Boolean).join(' ');
|
return Object.values(this.errors).filter(Boolean).join(' ');
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
<div :class="showMandatory ? 'show-mandatory' : ''">
|
<div :class="showMandatory ? 'show-mandatory' : ''">
|
||||||
<textarea
|
<textarea
|
||||||
ref="input"
|
ref="input"
|
||||||
rows="3"
|
:rows="rows"
|
||||||
:class="['resize-none', inputClasses, containerClasses]"
|
:class="['resize-none', inputClasses, containerClasses]"
|
||||||
:value="value"
|
:value="value"
|
||||||
:placeholder="inputPlaceholder"
|
:placeholder="inputPlaceholder"
|
||||||
@ -27,5 +27,6 @@ export default {
|
|||||||
name: 'Text',
|
name: 'Text',
|
||||||
extends: Base,
|
extends: Base,
|
||||||
emits: ['focus', 'input'],
|
emits: ['focus', 'input'],
|
||||||
|
props: { rows: { type: Number, default: 3 } },
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex bg-gray-25">
|
<div class="flex bg-gray-25 overflow-x-auto">
|
||||||
<div class="flex flex-1 flex-col">
|
<div class="flex flex-1 flex-col">
|
||||||
<!-- Page Header (Title, Buttons, etc) -->
|
<!-- Page Header (Title, Buttons, etc) -->
|
||||||
<PageHeader :title="title" :border="false" :searchborder="searchborder">
|
<PageHeader :title="title" :border="false" :searchborder="searchborder">
|
||||||
|
@ -1,50 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div id="importWizard" class="modal-body" style="overflow: hidden;">
|
|
||||||
<div class="mx-auto col-12 text-center pb-3">
|
|
||||||
<input ref="fileInput" @change="importCSV" id="file-input" type="file" />
|
|
||||||
<div class="form-check-label bold">{{ "Drag & Drop a CSV file" }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="row footer-divider mb-3">
|
|
||||||
<div class="col-12" style="border-bottom:1px solid #e9ecef"></div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-12 text-end">
|
|
||||||
<f-button primary @click="importCSV">{{ 'Import' }}</f-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
props: ['importHandler', 'report'],
|
|
||||||
methods: {
|
|
||||||
close() {
|
|
||||||
this.$modal.hide();
|
|
||||||
},
|
|
||||||
async importCSV(evt) {
|
|
||||||
if (evt.target.files) {
|
|
||||||
const file = evt.target.files[0];
|
|
||||||
this.importHandler(file, this.report);
|
|
||||||
this.$modal.hide();
|
|
||||||
}
|
|
||||||
this.$refs.fileInput.click();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
<style scoped>
|
|
||||||
.fixed-btn-width {
|
|
||||||
width: 5vw !important;
|
|
||||||
}
|
|
||||||
.bold {
|
|
||||||
font-size: 1.1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
#file-input {
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
opacity: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -4,7 +4,7 @@
|
|||||||
class="
|
class="
|
||||||
fixed
|
fixed
|
||||||
top-0
|
top-0
|
||||||
left-0
|
start-0
|
||||||
w-screen
|
w-screen
|
||||||
h-screen
|
h-screen
|
||||||
z-20
|
z-20
|
||||||
@ -12,19 +12,16 @@
|
|||||||
justify-center
|
justify-center
|
||||||
items-center
|
items-center
|
||||||
"
|
"
|
||||||
style="background: rgba(0, 0, 0, 0.2); backdrop-filter: blur(4px)"
|
:style="
|
||||||
|
useBackdrop
|
||||||
|
? 'background: rgba(0, 0, 0, 0.1); backdrop-filter: blur(2px)'
|
||||||
|
: ''
|
||||||
|
"
|
||||||
@click="$emit('closemodal')"
|
@click="$emit('closemodal')"
|
||||||
v-if="openModal"
|
v-if="openModal"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="
|
class="bg-white rounded-lg shadow-2xl border overflow-hidden inner"
|
||||||
bg-white
|
|
||||||
rounded-lg
|
|
||||||
shadow-2xl
|
|
||||||
border
|
|
||||||
overflow-hidden
|
|
||||||
inner
|
|
||||||
"
|
|
||||||
v-bind="$attrs"
|
v-bind="$attrs"
|
||||||
@click.stop
|
@click.stop
|
||||||
>
|
>
|
||||||
@ -43,6 +40,10 @@ export default defineComponent({
|
|||||||
default: false,
|
default: false,
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
},
|
},
|
||||||
|
useBackdrop: {
|
||||||
|
default: true,
|
||||||
|
type: Boolean,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
emits: ['closemodal'],
|
emits: ['closemodal'],
|
||||||
watch: {
|
watch: {
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
>
|
>
|
||||||
<Transition name="spacer">
|
<Transition name="spacer">
|
||||||
<div
|
<div
|
||||||
v-if="!sidebar && platform === 'Mac'"
|
v-if="!sidebar && platform === 'Mac' && languageDirection !== 'rtl'"
|
||||||
class="h-full"
|
class="h-full"
|
||||||
:class="sidebar ? '' : 'w-tl me-4 border-e'"
|
:class="sidebar ? '' : 'w-tl me-4 border-e'"
|
||||||
/>
|
/>
|
||||||
@ -16,7 +16,10 @@
|
|||||||
<h1 class="text-xl font-semibold select-none" v-if="title">
|
<h1 class="text-xl font-semibold select-none" v-if="title">
|
||||||
{{ title }}
|
{{ title }}
|
||||||
</h1>
|
</h1>
|
||||||
<div class="flex items-stretch window-no-drag gap-2 ms-auto">
|
<div
|
||||||
|
class="flex items-stretch window-no-drag gap-2 ms-auto"
|
||||||
|
:class="platform === 'Mac' && languageDirection === 'rtl' ? 'me-18' : ''"
|
||||||
|
>
|
||||||
<slot />
|
<slot />
|
||||||
<div class="border-e" v-if="showBorder" />
|
<div class="border-e" v-if="showBorder" />
|
||||||
<BackLink v-if="backLink" class="window-no-drag rtl-rotate-180" />
|
<BackLink v-if="backLink" class="window-no-drag rtl-rotate-180" />
|
||||||
@ -30,7 +33,7 @@ import BackLink from './BackLink.vue';
|
|||||||
import SearchBar from './SearchBar.vue';
|
import SearchBar from './SearchBar.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
inject: ['sidebar'],
|
inject: ['sidebar', 'languageDirection'],
|
||||||
props: {
|
props: {
|
||||||
title: { type: String, default: '' },
|
title: { type: String, default: '' },
|
||||||
backLink: { type: Boolean, default: true },
|
backLink: { type: Boolean, default: true },
|
||||||
|
@ -4,8 +4,8 @@
|
|||||||
<Button @click="open" class="px-2" :padding="false">
|
<Button @click="open" class="px-2" :padding="false">
|
||||||
<feather-icon name="search" class="w-4 h-4 me-1 text-gray-800" />
|
<feather-icon name="search" class="w-4 h-4 me-1 text-gray-800" />
|
||||||
<p>{{ t`Search` }}</p>
|
<p>{{ t`Search` }}</p>
|
||||||
<div class="text-gray-500 px-1 ms-4 text-sm">
|
<div class="text-gray-500 px-1 ms-4 text-sm whitespace-nowrap">
|
||||||
{{ modKey('k') }}
|
{{ modKeyText('k') }}
|
||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@ -16,174 +16,183 @@
|
|||||||
@closemodal="close"
|
@closemodal="close"
|
||||||
:set-close-listener="false"
|
:set-close-listener="false"
|
||||||
>
|
>
|
||||||
<!-- Search Input -->
|
<div class="w-form">
|
||||||
<div class="p-1 w-form">
|
<!-- Search Input -->
|
||||||
<input
|
<div class="p-1">
|
||||||
ref="input"
|
<input
|
||||||
type="search"
|
ref="input"
|
||||||
autocomplete="off"
|
type="search"
|
||||||
spellcheck="false"
|
autocomplete="off"
|
||||||
:placeholder="t`Type to search...`"
|
spellcheck="false"
|
||||||
v-model="inputValue"
|
:placeholder="t`Type to search...`"
|
||||||
@focus="search"
|
v-model="inputValue"
|
||||||
@input="search"
|
@focus="search"
|
||||||
@keydown.up="up"
|
@input="search"
|
||||||
@keydown.down="down"
|
@keydown.up="up"
|
||||||
@keydown.enter="() => select()"
|
@keydown.down="down"
|
||||||
@keydown.esc="close"
|
@keydown.enter="() => select()"
|
||||||
class="
|
@keydown.esc="close"
|
||||||
bg-gray-100
|
class="
|
||||||
text-2xl
|
bg-gray-100
|
||||||
focus:outline-none
|
text-2xl
|
||||||
w-full
|
focus:outline-none
|
||||||
placeholder-gray-500
|
w-full
|
||||||
text-gray-900
|
placeholder-gray-500
|
||||||
rounded-md
|
text-gray-900
|
||||||
p-3
|
rounded-md
|
||||||
"
|
p-3
|
||||||
/>
|
"
|
||||||
</div>
|
/>
|
||||||
<hr v-if="suggestions.length" />
|
</div>
|
||||||
|
<hr v-if="suggestions.length" />
|
||||||
|
|
||||||
<!-- Search List -->
|
<!-- Search List -->
|
||||||
<div :style="`max-height: ${49 * 6 - 1}px`" class="overflow-auto">
|
<div :style="`max-height: ${49 * 6 - 1}px`" class="overflow-auto">
|
||||||
<div
|
|
||||||
v-for="(si, i) in suggestions"
|
|
||||||
:key="`${i}-${si.key}`"
|
|
||||||
ref="suggestions"
|
|
||||||
class="hover:bg-gray-50 cursor-pointer"
|
|
||||||
:class="idx === i ? 'border-blue-500 bg-gray-50 border-s-4' : ''"
|
|
||||||
@click="select(i)"
|
|
||||||
>
|
|
||||||
<!-- Search List Item -->
|
|
||||||
<div
|
<div
|
||||||
class="flex w-full justify-between px-3 items-center"
|
v-for="(si, i) in suggestions"
|
||||||
style="height: var(--h-row-mid)"
|
:key="`${i}-${si.key}`"
|
||||||
|
ref="suggestions"
|
||||||
|
class="hover:bg-gray-50 cursor-pointer"
|
||||||
|
:class="idx === i ? 'border-blue-500 bg-gray-50 border-s-4' : ''"
|
||||||
|
@click="select(i)"
|
||||||
>
|
>
|
||||||
<div class="flex items-center">
|
<!-- Search List Item -->
|
||||||
|
<div
|
||||||
|
class="flex w-full justify-between px-3 items-center"
|
||||||
|
style="height: var(--h-row-mid)"
|
||||||
|
>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<p
|
||||||
|
:class="idx === i ? 'text-blue-600' : 'text-gray-900'"
|
||||||
|
:style="idx === i ? 'margin-left: -4px' : ''"
|
||||||
|
>
|
||||||
|
{{ si.label }}
|
||||||
|
</p>
|
||||||
|
<p class="text-gray-600 text-sm ms-3" v-if="si.group === 'Docs'">
|
||||||
|
{{ si.more.filter(Boolean).join(', ') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<p
|
<p
|
||||||
:class="idx === i ? 'text-blue-600' : 'text-gray-900'"
|
class="text-sm text-end justify-self-end"
|
||||||
:style="idx === i ? 'margin-left: -4px' : ''"
|
:class="`text-${groupColorMap[si.group]}-500`"
|
||||||
>
|
>
|
||||||
{{ si.label }}
|
{{
|
||||||
</p>
|
si.group === 'Docs' ? si.schemaLabel : groupLabelMap[si.group]
|
||||||
<p class="text-gray-600 text-sm ms-3" v-if="si.group === 'Docs'">
|
}}
|
||||||
{{ si.more.filter(Boolean).join(', ') }}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<p
|
|
||||||
class="text-sm text-end justify-self-end"
|
|
||||||
:class="`text-${groupColorMap[si.group]}-500`"
|
|
||||||
>
|
|
||||||
{{ si.group === 'Docs' ? si.schemaLabel : groupLabelMap[si.group] }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr v-if="i !== suggestions.length - 1" />
|
<hr v-if="i !== suggestions.length - 1" />
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Footer -->
|
|
||||||
<hr />
|
|
||||||
<div class="m-1 flex justify-between flex-col gap-2 text-sm select-none">
|
|
||||||
<!-- Group Filters -->
|
|
||||||
<div class="flex justify-between">
|
|
||||||
<div class="flex gap-1">
|
|
||||||
<button
|
|
||||||
v-for="g in searchGroups"
|
|
||||||
:key="g"
|
|
||||||
class="border px-1 py-0.5 rounded-lg"
|
|
||||||
:class="getGroupFilterButtonClass(g)"
|
|
||||||
@click="searcher.set(g, !searcher.filters.groupFilters[g])"
|
|
||||||
>
|
|
||||||
{{ groupLabelMap[g] }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
class="hover:text-gray-900 py-0.5 rounded text-gray-700"
|
|
||||||
@click="showMore = !showMore"
|
|
||||||
>
|
|
||||||
{{ showMore ? t`Less Filters` : t`More Filters` }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Additional Filters -->
|
|
||||||
<div v-if="showMore" class="-mt-1">
|
|
||||||
<!-- Group Skip Filters -->
|
|
||||||
<div class="flex gap-1 text-gray-800">
|
|
||||||
<button
|
|
||||||
v-for="s in ['skipTables', 'skipTransactions']"
|
|
||||||
:key="s"
|
|
||||||
class="border px-1 py-0.5 rounded-lg"
|
|
||||||
:class="{ 'bg-gray-200': searcher.filters[s] }"
|
|
||||||
@click="searcher.set(s, !searcher.filters[s])"
|
|
||||||
>
|
|
||||||
{{
|
|
||||||
s === 'skipTables' ? t`Skip Child Tables` : t`Skip Transactions`
|
|
||||||
}}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Schema Name Filters -->
|
|
||||||
<div class="flex mt-1 gap-1 text-blue-500 flex-wrap">
|
|
||||||
<button
|
|
||||||
v-for="sf in schemaFilters"
|
|
||||||
:key="sf.value"
|
|
||||||
class="
|
|
||||||
border
|
|
||||||
px-1
|
|
||||||
py-0.5
|
|
||||||
rounded-lg
|
|
||||||
border-blue-100
|
|
||||||
whitespace-nowrap
|
|
||||||
"
|
|
||||||
:class="{ 'bg-blue-100': searcher.filters.schemaFilters[sf.value] }"
|
|
||||||
@click="
|
|
||||||
searcher.set(sf.value, !searcher.filters.schemaFilters[sf.value])
|
|
||||||
"
|
|
||||||
>
|
|
||||||
{{ sf.label }}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Keybindings Help -->
|
<!-- Footer -->
|
||||||
<div class="flex text-sm text-gray-500 justify-between items-baseline">
|
<hr />
|
||||||
<div class="flex gap-4">
|
<div class="m-1 flex justify-between flex-col gap-2 text-sm select-none">
|
||||||
<p>↑↓ {{ t`Navigate` }}</p>
|
<!-- Group Filters -->
|
||||||
<p>↩ {{ t`Select` }}</p>
|
<div class="flex justify-between">
|
||||||
<p><span class="tracking-tighter">esc</span> {{ t`Close` }}</p>
|
<div class="flex gap-1">
|
||||||
<button
|
|
||||||
class="flex items-center hover:text-gray-800"
|
|
||||||
@click="openDocs"
|
|
||||||
>
|
|
||||||
<feather-icon name="help-circle" class="w-4 h-4 me-1" />
|
|
||||||
{{ t`Help` }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p v-if="searcher?.numSearches" class="ms-auto">
|
|
||||||
{{ t`${suggestions.length} out of ${searcher.numSearches}` }}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="border border-gray-100 rounded flex justify-self-end ms-2"
|
|
||||||
v-if="(searcher?.numSearches ?? 0) > 50"
|
|
||||||
>
|
|
||||||
<template
|
|
||||||
v-for="c in allowedLimits.filter(
|
|
||||||
(c) => c < searcher.numSearches || c === -1
|
|
||||||
)"
|
|
||||||
:key="c + '-count'"
|
|
||||||
>
|
|
||||||
<button
|
<button
|
||||||
@click="limit = parseInt(c)"
|
v-for="g in searchGroups"
|
||||||
class="w-9"
|
:key="g"
|
||||||
:class="limit === c ? 'bg-gray-100' : ''"
|
class="border px-1 py-0.5 rounded-lg"
|
||||||
|
:class="getGroupFilterButtonClass(g)"
|
||||||
|
@click="searcher.set(g, !searcher.filters.groupFilters[g])"
|
||||||
>
|
>
|
||||||
{{ c === -1 ? t`All` : c }}
|
{{ groupLabelMap[g] }}
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</div>
|
||||||
|
<button
|
||||||
|
class="hover:text-gray-900 py-0.5 rounded text-gray-700"
|
||||||
|
@click="showMore = !showMore"
|
||||||
|
>
|
||||||
|
{{ showMore ? t`Less Filters` : t`More Filters` }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Additional Filters -->
|
||||||
|
<div v-if="showMore" class="-mt-1">
|
||||||
|
<!-- Group Skip Filters -->
|
||||||
|
<div class="flex gap-1 text-gray-800">
|
||||||
|
<button
|
||||||
|
v-for="s in ['skipTables', 'skipTransactions']"
|
||||||
|
:key="s"
|
||||||
|
class="border px-1 py-0.5 rounded-lg"
|
||||||
|
:class="{ 'bg-gray-200': searcher.filters[s] }"
|
||||||
|
@click="searcher.set(s, !searcher.filters[s])"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
s === 'skipTables' ? t`Skip Child Tables` : t`Skip Transactions`
|
||||||
|
}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Schema Name Filters -->
|
||||||
|
<div class="flex mt-1 gap-1 text-blue-500 flex-wrap">
|
||||||
|
<button
|
||||||
|
v-for="sf in schemaFilters"
|
||||||
|
:key="sf.value"
|
||||||
|
class="
|
||||||
|
border
|
||||||
|
px-1
|
||||||
|
py-0.5
|
||||||
|
rounded-lg
|
||||||
|
border-blue-100
|
||||||
|
whitespace-nowrap
|
||||||
|
"
|
||||||
|
:class="{
|
||||||
|
'bg-blue-100': searcher.filters.schemaFilters[sf.value],
|
||||||
|
}"
|
||||||
|
@click="
|
||||||
|
searcher.set(
|
||||||
|
sf.value,
|
||||||
|
!searcher.filters.schemaFilters[sf.value]
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ sf.label }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Keybindings Help -->
|
||||||
|
<div class="flex text-sm text-gray-500 justify-between items-baseline">
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<p>↑↓ {{ t`Navigate` }}</p>
|
||||||
|
<p>↩ {{ t`Select` }}</p>
|
||||||
|
<p><span class="tracking-tighter">esc</span> {{ t`Close` }}</p>
|
||||||
|
<button
|
||||||
|
class="flex items-center hover:text-gray-800"
|
||||||
|
@click="openDocs"
|
||||||
|
>
|
||||||
|
<feather-icon name="help-circle" class="w-4 h-4 me-1" />
|
||||||
|
{{ t`Help` }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p v-if="searcher?.numSearches" class="ms-auto">
|
||||||
|
{{ t`${suggestions.length} out of ${searcher.numSearches}` }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="border border-gray-100 rounded flex justify-self-end ms-2"
|
||||||
|
v-if="(searcher?.numSearches ?? 0) > 50"
|
||||||
|
>
|
||||||
|
<template
|
||||||
|
v-for="c in allowedLimits.filter(
|
||||||
|
(c) => c < searcher.numSearches || c === -1
|
||||||
|
)"
|
||||||
|
:key="c + '-count'"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
@click="limit = parseInt(c)"
|
||||||
|
class="w-9"
|
||||||
|
:class="limit === c ? 'bg-gray-100' : ''"
|
||||||
|
>
|
||||||
|
{{ c === -1 ? t`All` : c }}
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -195,7 +204,6 @@ import { getBgTextColorClass } from 'src/utils/colors';
|
|||||||
import { openLink } from 'src/utils/ipcCalls';
|
import { openLink } from 'src/utils/ipcCalls';
|
||||||
import { docsPathMap } from 'src/utils/misc';
|
import { docsPathMap } from 'src/utils/misc';
|
||||||
import { getGroupLabelMap, searchGroups } from 'src/utils/search';
|
import { getGroupLabelMap, searchGroups } from 'src/utils/search';
|
||||||
import { getModKeyCode } from 'src/utils/vueUtils';
|
|
||||||
import { nextTick } from 'vue';
|
import { nextTick } from 'vue';
|
||||||
import Button from './Button.vue';
|
import Button from './Button.vue';
|
||||||
import Modal from './Modal.vue';
|
import Modal from './Modal.vue';
|
||||||
@ -233,18 +241,19 @@ export default {
|
|||||||
openLink('https://docs.frappebooks.com/' + docsPathMap.Search);
|
openLink('https://docs.frappebooks.com/' + docsPathMap.Search);
|
||||||
},
|
},
|
||||||
getShortcuts() {
|
getShortcuts() {
|
||||||
const modKey = getModKeyCode(this.platform);
|
|
||||||
const ifOpen = (cb) => () => this.openModal && cb();
|
const ifOpen = (cb) => () => this.openModal && cb();
|
||||||
const ifClose = (cb) => () => !this.openModal && cb();
|
const ifClose = (cb) => () => !this.openModal && cb();
|
||||||
|
|
||||||
const shortcuts = [
|
const shortcuts = [
|
||||||
{ shortcut: ['KeyK', modKey], callback: ifClose(() => this.open()) },
|
{
|
||||||
{ shortcut: ['Escape'], callback: ifOpen(() => this.close()) },
|
shortcut: 'KeyK',
|
||||||
|
callback: ifClose(() => this.open()),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const i in searchGroups) {
|
for (const i in searchGroups) {
|
||||||
shortcuts.push({
|
shortcuts.push({
|
||||||
shortcut: [modKey, `Digit${Number(i) + 1}`],
|
shortcut: `Digit${Number(i) + 1}`,
|
||||||
callback: ifOpen(() => {
|
callback: ifOpen(() => {
|
||||||
const group = searchGroups[i];
|
const group = searchGroups[i];
|
||||||
const value = this.searcher.filters.groupFilters[group];
|
const value = this.searcher.filters.groupFilters[group];
|
||||||
@ -261,15 +270,15 @@ export default {
|
|||||||
},
|
},
|
||||||
setShortcuts() {
|
setShortcuts() {
|
||||||
for (const { shortcut, callback } of this.getShortcuts()) {
|
for (const { shortcut, callback } of this.getShortcuts()) {
|
||||||
this.shortcuts.set(shortcut, callback);
|
this.shortcuts.pmod.set([shortcut], callback);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
deleteShortcuts() {
|
deleteShortcuts() {
|
||||||
for (const { shortcut } of this.getShortcuts()) {
|
for (const { shortcut } of this.getShortcuts()) {
|
||||||
this.shortcuts.delete(shortcut);
|
this.shortcuts.pmod.delete([shortcut]);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
modKey(key) {
|
modKeyText(key) {
|
||||||
key = key.toUpperCase();
|
key = key.toUpperCase();
|
||||||
if (this.platform === 'Mac') {
|
if (this.platform === 'Mac') {
|
||||||
return `⌘ ${key}`;
|
return `⌘ ${key}`;
|
||||||
|
199
src/components/ShortcutsHelper.vue
Normal file
199
src/components/ShortcutsHelper.vue
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<FormHeader :form-title="t`Shortcuts`" />
|
||||||
|
<hr />
|
||||||
|
<div class="h-96 overflow-y-auto text-gray-900">
|
||||||
|
<template v-for="g in groups" :key="g.label">
|
||||||
|
<div class="p-4 w-full">
|
||||||
|
<!-- Shortcut Group Header -->
|
||||||
|
<div @click="g.collapsed = !g.collapsed" class="cursor-pointer mb-4">
|
||||||
|
<div class="font-semibold">
|
||||||
|
{{ g.label }}
|
||||||
|
</div>
|
||||||
|
<div class="text-base">
|
||||||
|
{{ g.description }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Shortcuts -->
|
||||||
|
<div v-if="!g.collapsed" class="flex flex-col gap-4">
|
||||||
|
<div
|
||||||
|
v-for="(s, i) in g.shortcuts"
|
||||||
|
:key="g.label + ' ' + i"
|
||||||
|
class="grid gap-4 items-start"
|
||||||
|
style="grid-template-columns: 6rem auto"
|
||||||
|
>
|
||||||
|
<!-- <div class="w-2 text-base">{{ i + 1 }}.</div> -->
|
||||||
|
<div
|
||||||
|
class="
|
||||||
|
text-base
|
||||||
|
font-medium
|
||||||
|
flex-shrink-0 flex
|
||||||
|
items-center
|
||||||
|
gap-1
|
||||||
|
bg-gray-200
|
||||||
|
text-gray-700
|
||||||
|
px-1.5
|
||||||
|
py-0.5
|
||||||
|
rounded
|
||||||
|
"
|
||||||
|
style="width: fit-content"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-for="k in s.shortcut"
|
||||||
|
:key="k"
|
||||||
|
class="tracking-tighter"
|
||||||
|
>{{ k }}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="whitespace-normal text-base">{{ s.description }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Shortcut count if collapsed -->
|
||||||
|
<div v-else class="text-base text-gray-600">
|
||||||
|
{{ t`${g.shortcuts.length} shortcuts` }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr />
|
||||||
|
</template>
|
||||||
|
<div class="p-4 text-base text-gray-600">
|
||||||
|
{{ t`More shortcuts will be added soon.` }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { t } from 'fyo';
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import FormHeader from './FormHeader.vue';
|
||||||
|
|
||||||
|
type Group = {
|
||||||
|
label: string;
|
||||||
|
description: string;
|
||||||
|
collapsed: boolean;
|
||||||
|
shortcuts: { shortcut: string[]; description: string }[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
data() {
|
||||||
|
return { groups: [] } as { groups: Group[] };
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.groups = [
|
||||||
|
{
|
||||||
|
label: t`Global`,
|
||||||
|
description: t`Applicable anywhere in Frappe Books`,
|
||||||
|
collapsed: false,
|
||||||
|
shortcuts: [
|
||||||
|
{
|
||||||
|
shortcut: [this.pmod, 'K'],
|
||||||
|
description: t`Open Quick Search`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
shortcut: [this.del],
|
||||||
|
description: t`Go back to the previous page`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
shortcut: [this.shift, 'H'],
|
||||||
|
description: t`Toggle sidebar`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
shortcut: ['F1'],
|
||||||
|
description: t`Open Documentation`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t`Doc`,
|
||||||
|
description: t`Applicable when a Doc is open in the Form view or Quick Edit view`,
|
||||||
|
collapsed: false,
|
||||||
|
shortcuts: [
|
||||||
|
{
|
||||||
|
shortcut: [this.pmod, 'S'],
|
||||||
|
description: [
|
||||||
|
t`Save or Submit a doc.`,
|
||||||
|
t`A doc is submitted only if it is submittable and is in the saved state.`,
|
||||||
|
].join(' '),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
shortcut: [this.pmod, this.del],
|
||||||
|
description: [
|
||||||
|
t`Cancel or Delete a doc.`,
|
||||||
|
t`A doc is cancelled only if it is in the submitted state.`,
|
||||||
|
t`A submittable doc is deleted only if it is in the cancelled state.`,
|
||||||
|
].join(' '),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t`Quick Search`,
|
||||||
|
description: t`Applicable when Quick Search is open`,
|
||||||
|
collapsed: false,
|
||||||
|
shortcuts: [
|
||||||
|
{ shortcut: [this.esc], description: t`Close Quick Search` },
|
||||||
|
{
|
||||||
|
shortcut: [this.pmod, '1'],
|
||||||
|
description: t`Toggle the Docs filter`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
shortcut: [this.pmod, '2'],
|
||||||
|
description: t`Toggle the List filter`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
shortcut: [this.pmod, '3'],
|
||||||
|
description: t`Toggle the Create filter`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
shortcut: [this.pmod, '4'],
|
||||||
|
description: t`Toggle the Report filter`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
shortcut: [this.pmod, '5'],
|
||||||
|
description: t`Toggle the Page filter`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
pmod() {
|
||||||
|
if (this.isMac) {
|
||||||
|
return '⌘';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Ctrl';
|
||||||
|
},
|
||||||
|
shift() {
|
||||||
|
if (this.isMac) {
|
||||||
|
return 'shift';
|
||||||
|
}
|
||||||
|
|
||||||
|
return '⇧';
|
||||||
|
},
|
||||||
|
alt() {
|
||||||
|
if (this.isMac) {
|
||||||
|
return '⌥';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Alt';
|
||||||
|
},
|
||||||
|
del() {
|
||||||
|
if (this.isMac) {
|
||||||
|
return 'delete';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Backspace';
|
||||||
|
},
|
||||||
|
esc() {
|
||||||
|
if (this.isMac) {
|
||||||
|
return 'esc';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Esc';
|
||||||
|
},
|
||||||
|
isMac() {
|
||||||
|
return this.platform === 'Mac';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: { FormHeader },
|
||||||
|
});
|
||||||
|
</script>
|
@ -9,7 +9,9 @@
|
|||||||
<!-- Company name and DB Switcher -->
|
<!-- Company name and DB Switcher -->
|
||||||
<div
|
<div
|
||||||
class="px-4 flex flex-row items-center justify-between mb-4"
|
class="px-4 flex flex-row items-center justify-between mb-4"
|
||||||
:class="platform === 'Mac' ? 'mt-10' : 'mt-2'"
|
:class="
|
||||||
|
platform === 'Mac' && languageDirection === 'ltr' ? 'mt-10' : 'mt-2'
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<h6
|
<h6
|
||||||
class="
|
class="
|
||||||
@ -98,6 +100,20 @@
|
|||||||
</p>
|
</p>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="
|
||||||
|
flex
|
||||||
|
text-sm text-gray-600
|
||||||
|
hover:text-gray-800
|
||||||
|
gap-1
|
||||||
|
items-center
|
||||||
|
"
|
||||||
|
@click="viewShortcuts = true"
|
||||||
|
>
|
||||||
|
<feather-icon name="command" class="h-4 w-4 flex-shrink-0" />
|
||||||
|
<p>{{ t`Shortcuts` }}</p>
|
||||||
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="
|
class="
|
||||||
flex
|
flex
|
||||||
@ -153,6 +169,10 @@
|
|||||||
>
|
>
|
||||||
<feather-icon name="chevrons-left" class="w-4 h-4" />
|
<feather-icon name="chevrons-left" class="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<Modal :open-modal="viewShortcuts" @closemodal="viewShortcuts = false">
|
||||||
|
<ShortcutsHelper class="w-form" />
|
||||||
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
@ -160,18 +180,23 @@ import Button from 'src/components/Button.vue';
|
|||||||
import { reportIssue } from 'src/errorHandling';
|
import { reportIssue } from 'src/errorHandling';
|
||||||
import { fyo } from 'src/initFyo';
|
import { fyo } from 'src/initFyo';
|
||||||
import { openLink } from 'src/utils/ipcCalls';
|
import { openLink } from 'src/utils/ipcCalls';
|
||||||
|
import { docsPathRef } from 'src/utils/refs';
|
||||||
import { getSidebarConfig } from 'src/utils/sidebarConfig';
|
import { getSidebarConfig } from 'src/utils/sidebarConfig';
|
||||||
import { docsPath, routeTo } from 'src/utils/ui';
|
import { routeTo } from 'src/utils/ui';
|
||||||
import router from '../router';
|
import router from '../router';
|
||||||
import Icon from './Icon.vue';
|
import Icon from './Icon.vue';
|
||||||
|
import Modal from './Modal.vue';
|
||||||
|
import ShortcutsHelper from './ShortcutsHelper.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: [Button],
|
components: [Button],
|
||||||
|
inject: ['languageDirection', 'shortcuts'],
|
||||||
emits: ['change-db-file', 'toggle-sidebar'],
|
emits: ['change-db-file', 'toggle-sidebar'],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
companyName: '',
|
companyName: '',
|
||||||
groups: [],
|
groups: [],
|
||||||
|
viewShortcuts: false,
|
||||||
activeGroup: null,
|
activeGroup: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@ -182,6 +207,8 @@ export default {
|
|||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
Icon,
|
Icon,
|
||||||
|
Modal,
|
||||||
|
ShortcutsHelper,
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
const { companyName } = await fyo.doc.getDoc('AccountingSettings');
|
const { companyName } = await fyo.doc.getDoc('AccountingSettings');
|
||||||
@ -192,12 +219,23 @@ export default {
|
|||||||
router.afterEach(() => {
|
router.afterEach(() => {
|
||||||
this.setActiveGroup();
|
this.setActiveGroup();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.shortcuts.shift.set(['KeyH'], () => {
|
||||||
|
if (document.body === document.activeElement) {
|
||||||
|
this.$emit('toggle-sidebar');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.shortcuts.set(['F1'], () => this.openDocumentation());
|
||||||
|
},
|
||||||
|
unmounted() {
|
||||||
|
this.shortcuts.alt.delete(['KeyH']);
|
||||||
|
this.shortcuts.delete(['F1']);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
routeTo,
|
routeTo,
|
||||||
reportIssue,
|
reportIssue,
|
||||||
openDocumentation() {
|
openDocumentation() {
|
||||||
openLink('https://docs.frappebooks.com/' + docsPath.value);
|
openLink('https://docs.frappebooks.com/' + docsPathRef.value);
|
||||||
},
|
},
|
||||||
setActiveGroup() {
|
setActiveGroup() {
|
||||||
const { fullPath } = this.$router.currentRoute.value;
|
const { fullPath } = this.$router.currentRoute.value;
|
||||||
|
@ -1,421 +0,0 @@
|
|||||||
import { Fyo, t } from 'fyo';
|
|
||||||
import { Converter } from 'fyo/core/converter';
|
|
||||||
import { DocValueMap } from 'fyo/core/types';
|
|
||||||
import { Doc } from 'fyo/model/doc';
|
|
||||||
import { isNameAutoSet } from 'fyo/model/naming';
|
|
||||||
import { Noun, Verb } from 'fyo/telemetry/types';
|
|
||||||
import { ModelNameEnum } from 'models/types';
|
|
||||||
import {
|
|
||||||
Field,
|
|
||||||
FieldType,
|
|
||||||
FieldTypeEnum,
|
|
||||||
OptionField,
|
|
||||||
SelectOption,
|
|
||||||
TargetField,
|
|
||||||
} from 'schemas/types';
|
|
||||||
import {
|
|
||||||
getDefaultMapFromList,
|
|
||||||
getMapFromList,
|
|
||||||
getValueMapFromList,
|
|
||||||
} from 'utils';
|
|
||||||
import { generateCSV, parseCSV } from '../utils/csvParser';
|
|
||||||
|
|
||||||
export const importable = [
|
|
||||||
ModelNameEnum.SalesInvoice,
|
|
||||||
ModelNameEnum.PurchaseInvoice,
|
|
||||||
ModelNameEnum.Payment,
|
|
||||||
ModelNameEnum.Party,
|
|
||||||
ModelNameEnum.Item,
|
|
||||||
ModelNameEnum.JournalEntry,
|
|
||||||
];
|
|
||||||
|
|
||||||
type Status = {
|
|
||||||
success: boolean;
|
|
||||||
message: string;
|
|
||||||
names: string[];
|
|
||||||
};
|
|
||||||
|
|
||||||
type Exclusion = Record<string, string[]>;
|
|
||||||
|
|
||||||
type LoadingStatusCallback = (
|
|
||||||
isMakingEntries: boolean,
|
|
||||||
entriesMade: number,
|
|
||||||
totalEntries: number
|
|
||||||
) => void;
|
|
||||||
|
|
||||||
interface TemplateField {
|
|
||||||
label: string;
|
|
||||||
fieldname: string;
|
|
||||||
required: boolean;
|
|
||||||
schemaName: string;
|
|
||||||
options?: SelectOption[];
|
|
||||||
fieldtype: FieldType;
|
|
||||||
parentField: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const exclusion: Exclusion = {
|
|
||||||
Item: ['image'],
|
|
||||||
Party: ['address', 'outstandingAmount', 'image'],
|
|
||||||
};
|
|
||||||
|
|
||||||
function getFilteredDocFields(
|
|
||||||
df: string | string[],
|
|
||||||
fyo: Fyo
|
|
||||||
): [TemplateField[], string[][]] {
|
|
||||||
let schemaName = df[0];
|
|
||||||
let parentField = df[1] ?? '';
|
|
||||||
|
|
||||||
if (typeof df === 'string') {
|
|
||||||
schemaName = df;
|
|
||||||
parentField = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
const primaryFields: Field[] = fyo.schemaMap[schemaName]!.fields;
|
|
||||||
const fields: TemplateField[] = [];
|
|
||||||
const tableTypes: string[][] = [];
|
|
||||||
const exclusionFields: string[] = exclusion[schemaName] ?? [];
|
|
||||||
|
|
||||||
for (const field of primaryFields) {
|
|
||||||
const { label, fieldtype, fieldname, required } = field;
|
|
||||||
|
|
||||||
if (shouldSkip(field, exclusionFields, parentField)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fieldtype === FieldTypeEnum.Table) {
|
|
||||||
const { target } = field as TargetField;
|
|
||||||
tableTypes.push([target, fieldname]);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const options: SelectOption[] = (field as OptionField).options ?? [];
|
|
||||||
|
|
||||||
fields.push({
|
|
||||||
label,
|
|
||||||
fieldname,
|
|
||||||
schemaName,
|
|
||||||
options,
|
|
||||||
fieldtype,
|
|
||||||
parentField,
|
|
||||||
required: required ?? false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return [fields, tableTypes];
|
|
||||||
}
|
|
||||||
|
|
||||||
function shouldSkip(
|
|
||||||
field: Field,
|
|
||||||
exclusionFields: string[],
|
|
||||||
parentField: string
|
|
||||||
): boolean {
|
|
||||||
if (field.meta) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (field.fieldname === 'name' && parentField) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (field.required) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (exclusionFields.includes(field.fieldname)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (field.hidden || field.readOnly) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTemplateFields(schemaName: string, fyo: Fyo): TemplateField[] {
|
|
||||||
const fields: TemplateField[] = [];
|
|
||||||
if (!schemaName) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const schemaNames: string[][] = [[schemaName]];
|
|
||||||
while (schemaNames.length > 0) {
|
|
||||||
const sn = schemaNames.pop();
|
|
||||||
if (!sn) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [templateFields, tableTypes] = getFilteredDocFields(sn, fyo);
|
|
||||||
fields.push(...templateFields);
|
|
||||||
schemaNames.push(...tableTypes);
|
|
||||||
}
|
|
||||||
return fields;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Importer {
|
|
||||||
schemaName: string;
|
|
||||||
templateFields: TemplateField[];
|
|
||||||
labelTemplateFieldMap: Record<string, TemplateField> = {};
|
|
||||||
template: string;
|
|
||||||
indices: number[] = [];
|
|
||||||
parsedLabels: string[] = [];
|
|
||||||
parsedValues: string[][] = [];
|
|
||||||
assignedMap: Record<string, string> = {}; // target: import
|
|
||||||
requiredMap: Record<string, boolean> = {};
|
|
||||||
shouldSubmit: boolean = false;
|
|
||||||
labelIndex: number = -1;
|
|
||||||
csv: string[][] = [];
|
|
||||||
fyo: Fyo;
|
|
||||||
|
|
||||||
constructor(schemaName: string, fyo: Fyo) {
|
|
||||||
this.schemaName = schemaName;
|
|
||||||
this.fyo = fyo;
|
|
||||||
this.templateFields = getTemplateFields(schemaName, this.fyo);
|
|
||||||
this.template = generateCSV([this.templateFields.map((t) => t.label)]);
|
|
||||||
this.labelTemplateFieldMap = getMapFromList(this.templateFields, 'label');
|
|
||||||
this.assignedMap = getDefaultMapFromList(this.templateFields, '', 'label');
|
|
||||||
this.requiredMap = getValueMapFromList(
|
|
||||||
this.templateFields,
|
|
||||||
'label',
|
|
||||||
'required'
|
|
||||||
) as Record<string, boolean>;
|
|
||||||
}
|
|
||||||
|
|
||||||
get assignableLabels() {
|
|
||||||
const req: string[] = [];
|
|
||||||
const nreq: string[] = [];
|
|
||||||
|
|
||||||
for (const label in this.labelTemplateFieldMap) {
|
|
||||||
if (this.requiredMap[label]) {
|
|
||||||
req.push(label);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
nreq.push(label);
|
|
||||||
}
|
|
||||||
|
|
||||||
return [req, nreq].flat();
|
|
||||||
}
|
|
||||||
|
|
||||||
get unassignedLabels() {
|
|
||||||
const assigned = Object.keys(this.assignedMap).map(
|
|
||||||
(k) => this.assignedMap[k]
|
|
||||||
);
|
|
||||||
return this.parsedLabels.filter((l) => !assigned.includes(l));
|
|
||||||
}
|
|
||||||
|
|
||||||
get columnLabels() {
|
|
||||||
const req: string[] = [];
|
|
||||||
const nreq: string[] = [];
|
|
||||||
|
|
||||||
this.assignableLabels.forEach((k) => {
|
|
||||||
if (!this.assignedMap[k]) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.requiredMap[k]) {
|
|
||||||
req.push(k);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
nreq.push(k);
|
|
||||||
});
|
|
||||||
|
|
||||||
return [...req, ...nreq];
|
|
||||||
}
|
|
||||||
|
|
||||||
get assignedMatrix() {
|
|
||||||
this.indices = this.columnLabels
|
|
||||||
.map((k) => this.assignedMap[k])
|
|
||||||
.filter(Boolean)
|
|
||||||
.map((k) => this.parsedLabels.indexOf(k as string));
|
|
||||||
|
|
||||||
const rows = this.parsedValues.length;
|
|
||||||
const cols = this.columnLabels.length;
|
|
||||||
|
|
||||||
const matrix = [];
|
|
||||||
for (let i = 0; i < rows; i++) {
|
|
||||||
const row = [];
|
|
||||||
for (let j = 0; j < cols; j++) {
|
|
||||||
const ix = this.indices[j];
|
|
||||||
const value = this.parsedValues[i][ix] ?? '';
|
|
||||||
row.push(value);
|
|
||||||
}
|
|
||||||
matrix.push(row);
|
|
||||||
}
|
|
||||||
|
|
||||||
return matrix;
|
|
||||||
}
|
|
||||||
|
|
||||||
dropRow(i: number) {
|
|
||||||
this.parsedValues = this.parsedValues.filter((_, ix) => i !== ix);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateValue(value: string, i: number, j: number) {
|
|
||||||
this.parsedValues[i][this.indices[j]] = value ?? '';
|
|
||||||
}
|
|
||||||
|
|
||||||
selectFile(text: string): boolean {
|
|
||||||
this.csv = parseCSV(text);
|
|
||||||
try {
|
|
||||||
this.initialize(0, true);
|
|
||||||
} catch (err) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
initialize(labelIndex: number, force: boolean) {
|
|
||||||
if (
|
|
||||||
(typeof labelIndex !== 'number' && !labelIndex) ||
|
|
||||||
(labelIndex === this.labelIndex && !force)
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const source = this.csv.map((row) => [...row]);
|
|
||||||
this.labelIndex = labelIndex;
|
|
||||||
this.parsedLabels = source[labelIndex];
|
|
||||||
this.parsedValues = source.slice(labelIndex + 1);
|
|
||||||
this.setAssigned();
|
|
||||||
}
|
|
||||||
|
|
||||||
setAssigned() {
|
|
||||||
const labels = [...this.parsedLabels];
|
|
||||||
|
|
||||||
for (const k of Object.keys(this.assignedMap)) {
|
|
||||||
const l = this.assignedMap[k] as string;
|
|
||||||
if (!labels.includes(l)) {
|
|
||||||
this.assignedMap[k] = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
labels.forEach((l) => {
|
|
||||||
if (this.assignedMap[l] !== '') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.assignedMap[l] = l;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getDocs(): DocValueMap[] {
|
|
||||||
const fields = this.columnLabels.map((k) => this.labelTemplateFieldMap[k]);
|
|
||||||
const nameIndex = fields.findIndex(({ fieldname }) => fieldname === 'name');
|
|
||||||
|
|
||||||
const docMap: Record<string, DocValueMap> = {};
|
|
||||||
|
|
||||||
const assignedMatrix = this.assignedMatrix;
|
|
||||||
for (let r = 0; r < assignedMatrix.length; r++) {
|
|
||||||
const row = assignedMatrix[r];
|
|
||||||
const cts: Record<string, DocValueMap> = {};
|
|
||||||
const name = row[nameIndex];
|
|
||||||
|
|
||||||
docMap[name] ??= {};
|
|
||||||
|
|
||||||
for (let f = 0; f < fields.length; f++) {
|
|
||||||
const field = fields[f];
|
|
||||||
const value = Converter.toDocValue(row[f], field, this.fyo);
|
|
||||||
|
|
||||||
if (field.parentField) {
|
|
||||||
cts[field.parentField] ??= {};
|
|
||||||
cts[field.parentField][field.fieldname] = value;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
docMap[name][field.fieldname] = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const k of Object.keys(cts)) {
|
|
||||||
docMap[name][k] ??= [];
|
|
||||||
(docMap[name][k] as DocValueMap[]).push(cts[k]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Object.keys(docMap).map((k) => docMap[k]);
|
|
||||||
}
|
|
||||||
|
|
||||||
async importData(setLoadingStatus: LoadingStatusCallback): Promise<Status> {
|
|
||||||
const status: Status = { success: false, names: [], message: '' };
|
|
||||||
const shouldDeleteName = isNameAutoSet(this.schemaName, this.fyo);
|
|
||||||
const docObjs = this.getDocs();
|
|
||||||
|
|
||||||
let entriesMade = 0;
|
|
||||||
setLoadingStatus(true, 0, docObjs.length);
|
|
||||||
|
|
||||||
for (const docObj of docObjs) {
|
|
||||||
if (shouldDeleteName) {
|
|
||||||
delete docObj.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const key in docObj) {
|
|
||||||
if (docObj[key] !== '') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
delete docObj[key];
|
|
||||||
}
|
|
||||||
|
|
||||||
const doc: Doc = this.fyo.doc.getNewDoc(this.schemaName, {}, false);
|
|
||||||
try {
|
|
||||||
await this.makeEntry(doc, docObj);
|
|
||||||
entriesMade += 1;
|
|
||||||
setLoadingStatus(true, entriesMade, docObjs.length);
|
|
||||||
} catch (err) {
|
|
||||||
setLoadingStatus(false, entriesMade, docObjs.length);
|
|
||||||
|
|
||||||
this.fyo.telemetry.log(Verb.Imported, this.schemaName as Noun, {
|
|
||||||
success: false,
|
|
||||||
count: entriesMade,
|
|
||||||
});
|
|
||||||
|
|
||||||
return this.handleError(doc, err as Error, status);
|
|
||||||
}
|
|
||||||
|
|
||||||
status.names.push(doc.name!);
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoadingStatus(false, entriesMade, docObjs.length);
|
|
||||||
status.success = true;
|
|
||||||
|
|
||||||
this.fyo.telemetry.log(Verb.Imported, this.schemaName as Noun, {
|
|
||||||
success: true,
|
|
||||||
count: entriesMade,
|
|
||||||
});
|
|
||||||
return status;
|
|
||||||
}
|
|
||||||
|
|
||||||
addRow() {
|
|
||||||
const emptyRow = Array(this.columnLabels.length).fill('');
|
|
||||||
this.parsedValues.push(emptyRow);
|
|
||||||
}
|
|
||||||
|
|
||||||
async makeEntry(doc: Doc, docObj: DocValueMap) {
|
|
||||||
await doc.setAndSync(docObj);
|
|
||||||
if (this.shouldSubmit) {
|
|
||||||
await doc.submit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleError(doc: Doc, err: Error, status: Status): Status {
|
|
||||||
const messages = [t`Could not import ${this.schemaName} ${doc.name!}.`];
|
|
||||||
|
|
||||||
const message = err.message;
|
|
||||||
if (message?.includes('UNIQUE constraint failed')) {
|
|
||||||
messages.push(t`${doc.name!} already exists.`);
|
|
||||||
} else if (message) {
|
|
||||||
messages.push(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status.names.length) {
|
|
||||||
messages.push(
|
|
||||||
t`The following ${
|
|
||||||
status.names.length
|
|
||||||
} entries were created: ${status.names.join(', ')}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
status.message = messages.join(' ');
|
|
||||||
return status;
|
|
||||||
}
|
|
||||||
}
|
|
634
src/importer.ts
Normal file
634
src/importer.ts
Normal file
@ -0,0 +1,634 @@
|
|||||||
|
import { Fyo } from 'fyo';
|
||||||
|
import { Converter } from 'fyo/core/converter';
|
||||||
|
import { DocValue, DocValueMap } from 'fyo/core/types';
|
||||||
|
import { Doc } from 'fyo/model/doc';
|
||||||
|
import { getEmptyValuesByFieldTypes } from 'fyo/utils';
|
||||||
|
import { ValidationError } from 'fyo/utils/errors';
|
||||||
|
import {
|
||||||
|
Field,
|
||||||
|
FieldType,
|
||||||
|
FieldTypeEnum,
|
||||||
|
OptionField,
|
||||||
|
RawValue,
|
||||||
|
Schema,
|
||||||
|
TargetField,
|
||||||
|
} from 'schemas/types';
|
||||||
|
import { generateCSV, parseCSV } from 'utils/csvParser';
|
||||||
|
import { getValueMapFromList } from 'utils/index';
|
||||||
|
|
||||||
|
export type TemplateField = Field & TemplateFieldProps;
|
||||||
|
|
||||||
|
type TemplateFieldProps = {
|
||||||
|
schemaName: string;
|
||||||
|
schemaLabel: string;
|
||||||
|
fieldKey: string;
|
||||||
|
parentSchemaChildField?: TargetField;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ValueMatrixItem =
|
||||||
|
| {
|
||||||
|
value: DocValue;
|
||||||
|
rawValue?: RawValue;
|
||||||
|
error?: boolean;
|
||||||
|
}
|
||||||
|
| { value?: DocValue; rawValue: RawValue; error?: boolean };
|
||||||
|
|
||||||
|
type ValueMatrix = ValueMatrixItem[][];
|
||||||
|
|
||||||
|
const skippedFieldsTypes: FieldType[] = [
|
||||||
|
FieldTypeEnum.AttachImage,
|
||||||
|
FieldTypeEnum.Attachment,
|
||||||
|
FieldTypeEnum.Table,
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tool that
|
||||||
|
* - Can make bulk entries for any kind of Doc
|
||||||
|
* - Takes in unstructured CSV data, converts it into Docs
|
||||||
|
* - Saves and or Submits the converted Docs
|
||||||
|
*/
|
||||||
|
export class Importer {
|
||||||
|
schemaName: string;
|
||||||
|
fyo: Fyo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of template fields that have been assigned a column, in
|
||||||
|
* the order they have been assigned.
|
||||||
|
*/
|
||||||
|
assignedTemplateFields: (string | null)[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map of all the template fields that can be imported.
|
||||||
|
*/
|
||||||
|
templateFieldsMap: Map<string, TemplateField>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map of Fields that have been picked, i.e.
|
||||||
|
* - Fields which will be included in the template
|
||||||
|
* - Fields for which values will be provided
|
||||||
|
*/
|
||||||
|
templateFieldsPicked: Map<string, boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the schema type being imported has table fields
|
||||||
|
*/
|
||||||
|
hasChildTables: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matrix containing the raw values which will be converted to
|
||||||
|
* doc values before importing.
|
||||||
|
*/
|
||||||
|
valueMatrix: ValueMatrix;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data from the valueMatrix rows will be converted into Docs
|
||||||
|
* which will be stored in this array.
|
||||||
|
*/
|
||||||
|
docs: Doc[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used if an options field is imported where the import data
|
||||||
|
* provided maybe the label and not the value
|
||||||
|
*/
|
||||||
|
optionsMap: {
|
||||||
|
values: Record<string, Set<string>>;
|
||||||
|
labelValueMap: Record<string, Record<string, string>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(schemaName: string, fyo: Fyo) {
|
||||||
|
if (!fyo.schemaMap[schemaName]) {
|
||||||
|
throw new ValidationError(
|
||||||
|
`Invalid schemaName ${schemaName} found in importer`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.hasChildTables = false;
|
||||||
|
this.schemaName = schemaName;
|
||||||
|
this.fyo = fyo;
|
||||||
|
this.docs = [];
|
||||||
|
this.valueMatrix = [];
|
||||||
|
this.optionsMap = {
|
||||||
|
values: {},
|
||||||
|
labelValueMap: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const templateFields = getTemplateFields(schemaName, fyo, this);
|
||||||
|
this.assignedTemplateFields = templateFields.map((f) => f.fieldKey);
|
||||||
|
this.templateFieldsMap = new Map();
|
||||||
|
this.templateFieldsPicked = new Map();
|
||||||
|
|
||||||
|
templateFields.forEach((f, i) => {
|
||||||
|
this.templateFieldsMap.set(f.fieldKey, f);
|
||||||
|
this.templateFieldsPicked.set(f.fieldKey, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
selectFile(data: string): boolean {
|
||||||
|
try {
|
||||||
|
const parsed = parseCSV(data);
|
||||||
|
this.selectParsed(parsed);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkLinks() {
|
||||||
|
const tfKeys = this.assignedTemplateFields
|
||||||
|
.map((key, index) => ({
|
||||||
|
key,
|
||||||
|
index,
|
||||||
|
tf: this.templateFieldsMap.get(key ?? ''),
|
||||||
|
}))
|
||||||
|
.filter(({ key, tf }) => {
|
||||||
|
if (!key || !tf) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return tf.fieldtype === FieldTypeEnum.Link;
|
||||||
|
}) as { key: string; index: number; tf: TemplateField }[];
|
||||||
|
|
||||||
|
const linksNames: Map<string, Set<string>> = new Map();
|
||||||
|
for (const row of this.valueMatrix) {
|
||||||
|
for (const { tf, index } of tfKeys) {
|
||||||
|
const target = (tf as TargetField).target;
|
||||||
|
const value = row[index]?.value;
|
||||||
|
if (typeof value !== 'string' || !value) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!linksNames.has(target)) {
|
||||||
|
linksNames.set(target, new Set());
|
||||||
|
}
|
||||||
|
|
||||||
|
linksNames.get(target)?.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const doesNotExist = [];
|
||||||
|
for (const [target, values] of linksNames.entries()) {
|
||||||
|
for (const value of values) {
|
||||||
|
const exists = await this.fyo.db.exists(target, value);
|
||||||
|
if (exists) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
doesNotExist.push({
|
||||||
|
schemaName: target,
|
||||||
|
schemaLabel: this.fyo.schemaMap[this.schemaName]?.label,
|
||||||
|
name: value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return doesNotExist;
|
||||||
|
}
|
||||||
|
|
||||||
|
checkCellErrors() {
|
||||||
|
const assigned = this.assignedTemplateFields
|
||||||
|
.map((key, index) => ({
|
||||||
|
key,
|
||||||
|
index,
|
||||||
|
tf: this.templateFieldsMap.get(key ?? ''),
|
||||||
|
}))
|
||||||
|
.filter(({ key, tf }) => !!key && !!tf) as {
|
||||||
|
key: string;
|
||||||
|
index: number;
|
||||||
|
tf: TemplateField;
|
||||||
|
}[];
|
||||||
|
|
||||||
|
const cellErrors = [];
|
||||||
|
for (const i in this.valueMatrix) {
|
||||||
|
const row = this.valueMatrix[i];
|
||||||
|
for (const { tf, index } of assigned) {
|
||||||
|
if (!row[index]?.error) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rowLabel = this.fyo.t`Row ${i + 1}`;
|
||||||
|
const columnLabel = getColumnLabel(tf);
|
||||||
|
cellErrors.push(`(${rowLabel}, ${columnLabel})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cellErrors;
|
||||||
|
}
|
||||||
|
|
||||||
|
populateDocs() {
|
||||||
|
const { dataMap, childTableMap } =
|
||||||
|
this.getDataAndChildTableMapFromValueMatrix();
|
||||||
|
|
||||||
|
const schema = this.fyo.schemaMap[this.schemaName];
|
||||||
|
const targetFieldnameMap = schema?.fields
|
||||||
|
.filter((f) => f.fieldtype === FieldTypeEnum.Table)
|
||||||
|
.reduce((acc, f) => {
|
||||||
|
const { target, fieldname } = f as TargetField;
|
||||||
|
acc[target] = fieldname;
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, string>);
|
||||||
|
|
||||||
|
for (const [name, data] of dataMap.entries()) {
|
||||||
|
const doc = this.fyo.doc.getNewDoc(this.schemaName, data, false);
|
||||||
|
for (const schemaName in targetFieldnameMap) {
|
||||||
|
const fieldname = targetFieldnameMap[schemaName];
|
||||||
|
const childTable = childTableMap[name][schemaName];
|
||||||
|
if (!childTable) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const childData of childTable.values()) {
|
||||||
|
doc.push(fieldname, childData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.docs.push(doc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getDataAndChildTableMapFromValueMatrix() {
|
||||||
|
/**
|
||||||
|
* Record key is the doc.name value
|
||||||
|
*/
|
||||||
|
const dataMap: Map<string, DocValueMap> = new Map();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Record key is doc.name, childSchemaName, childDoc.name
|
||||||
|
*/
|
||||||
|
const childTableMap: Record<
|
||||||
|
string,
|
||||||
|
Record<string, Map<string, DocValueMap>>
|
||||||
|
> = {};
|
||||||
|
|
||||||
|
const nameIndices = this.assignedTemplateFields
|
||||||
|
.map((key, index) => ({ key, index }))
|
||||||
|
.filter((f) => f.key?.endsWith('.name'))
|
||||||
|
.reduce((acc, f) => {
|
||||||
|
if (f.key == null) {
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
|
||||||
|
const schemaName = f.key.split('.')[0];
|
||||||
|
acc[schemaName] = f.index;
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, number>);
|
||||||
|
|
||||||
|
const nameIndex = nameIndices?.[this.schemaName];
|
||||||
|
if (nameIndex < 0) {
|
||||||
|
return { dataMap, childTableMap };
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const i in this.valueMatrix) {
|
||||||
|
const row = this.valueMatrix[i];
|
||||||
|
const name = row[nameIndex]?.value;
|
||||||
|
if (typeof name !== 'string') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const j in row) {
|
||||||
|
const key = this.assignedTemplateFields[j];
|
||||||
|
const tf = this.templateFieldsMap.get(key ?? '');
|
||||||
|
if (!tf || !key) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isChild = this.fyo.schemaMap[tf.schemaName]?.isChild;
|
||||||
|
const vmi = row[j];
|
||||||
|
if (vmi.value == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isChild && !dataMap.has(name)) {
|
||||||
|
dataMap.set(name, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isChild) {
|
||||||
|
dataMap.get(name)![tf.fieldname] = vmi.value;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const childNameIndex = nameIndices[tf.schemaName];
|
||||||
|
let childName = row[childNameIndex]?.value;
|
||||||
|
if (typeof childName !== 'string') {
|
||||||
|
childName = `${tf.schemaName}-${i}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
childTableMap[name] ??= {};
|
||||||
|
childTableMap[name][tf.schemaName] ??= new Map();
|
||||||
|
|
||||||
|
const childMap = childTableMap[name][tf.schemaName];
|
||||||
|
if (!childMap.has(childName)) {
|
||||||
|
childMap.set(childName, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
const childDocValueMap = childMap.get(childName);
|
||||||
|
if (!childDocValueMap) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
childDocValueMap[tf.fieldname] = vmi.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { dataMap, childTableMap };
|
||||||
|
}
|
||||||
|
|
||||||
|
selectParsed(parsed: string[][]): void {
|
||||||
|
if (!parsed?.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let startIndex = -1;
|
||||||
|
let templateFieldsAssigned;
|
||||||
|
|
||||||
|
for (let i = 3; i >= 0; i--) {
|
||||||
|
const row = parsed[i];
|
||||||
|
if (!row?.length) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
templateFieldsAssigned = this.assignTemplateFieldsFromParsedRow(row);
|
||||||
|
if (templateFieldsAssigned) {
|
||||||
|
startIndex = i + 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!templateFieldsAssigned) {
|
||||||
|
this.clearAndResizeAssignedTemplateFields(parsed[0].length);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startIndex === -1) {
|
||||||
|
startIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.assignValueMatrixFromParsed(parsed.slice(startIndex));
|
||||||
|
}
|
||||||
|
|
||||||
|
clearAndResizeAssignedTemplateFields(size: number) {
|
||||||
|
for (let i = 0; i < size; i++) {
|
||||||
|
if (i >= this.assignedTemplateFields.length) {
|
||||||
|
this.assignedTemplateFields.push(null);
|
||||||
|
} else {
|
||||||
|
this.assignedTemplateFields[i] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assignValueMatrixFromParsed(parsed: string[][]) {
|
||||||
|
if (!parsed?.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const row of parsed) {
|
||||||
|
this.pushToValueMatrixFromParsedRow(row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pushToValueMatrixFromParsedRow(row: string[]) {
|
||||||
|
const vmRow: ValueMatrix[number] = [];
|
||||||
|
for (const i in row) {
|
||||||
|
const rawValue = row[i];
|
||||||
|
const index = Number(i);
|
||||||
|
|
||||||
|
if (index >= this.assignedTemplateFields.length) {
|
||||||
|
this.assignedTemplateFields.push(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
vmRow.push(this.getValueMatrixItem(index, rawValue));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.valueMatrix.push(vmRow);
|
||||||
|
}
|
||||||
|
|
||||||
|
setTemplateField(index: number, key: string | null) {
|
||||||
|
if (index >= this.assignedTemplateFields.length) {
|
||||||
|
this.assignedTemplateFields.push(key);
|
||||||
|
} else {
|
||||||
|
this.assignedTemplateFields[index] = key;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateValueMatrixColumn(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateValueMatrixColumn(index: number) {
|
||||||
|
for (const row of this.valueMatrix) {
|
||||||
|
const vmi = this.getValueMatrixItem(index, row[index].rawValue ?? null);
|
||||||
|
|
||||||
|
if (index >= row.length) {
|
||||||
|
row.push(vmi);
|
||||||
|
} else {
|
||||||
|
row[index] = vmi;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getValueMatrixItem(index: number, rawValue: RawValue) {
|
||||||
|
const vmi: ValueMatrixItem = { rawValue };
|
||||||
|
const key = this.assignedTemplateFields[index];
|
||||||
|
if (!key) {
|
||||||
|
return vmi;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tf = this.templateFieldsMap.get(key);
|
||||||
|
if (!tf) {
|
||||||
|
return vmi;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vmi.rawValue === '') {
|
||||||
|
vmi.value = null;
|
||||||
|
return vmi;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('options' in tf && typeof vmi.rawValue === 'string') {
|
||||||
|
return this.getOptionFieldVmi(vmi, tf);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
vmi.value = Converter.toDocValue(rawValue, tf, this.fyo);
|
||||||
|
} catch {
|
||||||
|
vmi.error = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return vmi;
|
||||||
|
}
|
||||||
|
|
||||||
|
getOptionFieldVmi(
|
||||||
|
{ rawValue }: ValueMatrixItem,
|
||||||
|
tf: OptionField & TemplateFieldProps
|
||||||
|
): ValueMatrixItem {
|
||||||
|
if (typeof rawValue !== 'string') {
|
||||||
|
return { error: true, value: null, rawValue };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tf?.options.length) {
|
||||||
|
return { value: null, rawValue };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.optionsMap.labelValueMap[tf.fieldKey]) {
|
||||||
|
const values = new Set(tf.options.map(({ value }) => value));
|
||||||
|
const labelValueMap = getValueMapFromList(tf.options, 'label', 'value');
|
||||||
|
|
||||||
|
this.optionsMap.labelValueMap[tf.fieldKey] = labelValueMap;
|
||||||
|
this.optionsMap.values[tf.fieldKey] = values;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasValue = this.optionsMap.values[tf.fieldKey].has(rawValue);
|
||||||
|
if (hasValue) {
|
||||||
|
return { value: rawValue, rawValue };
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = this.optionsMap.labelValueMap[tf.fieldKey][rawValue];
|
||||||
|
if (value) {
|
||||||
|
return { value, rawValue };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { error: true, value: null, rawValue };
|
||||||
|
}
|
||||||
|
|
||||||
|
assignTemplateFieldsFromParsedRow(row: string[]): boolean {
|
||||||
|
const isKeyRow = row.some((key) => this.templateFieldsMap.has(key));
|
||||||
|
if (!isKeyRow) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const i in row) {
|
||||||
|
const value = row[i];
|
||||||
|
const tf = this.templateFieldsMap.get(value);
|
||||||
|
let key: string | null = value;
|
||||||
|
|
||||||
|
if (!tf) {
|
||||||
|
key = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key !== null && !this.templateFieldsPicked.get(value)) {
|
||||||
|
key = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Number(i) >= this.assignedTemplateFields.length) {
|
||||||
|
this.assignedTemplateFields.push(key);
|
||||||
|
} else {
|
||||||
|
this.assignedTemplateFields[i] = key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
addRow() {
|
||||||
|
const valueRow: ValueMatrix[number] = this.assignedTemplateFields.map(
|
||||||
|
(key) => {
|
||||||
|
key ??= '';
|
||||||
|
const { fieldtype } = this.templateFieldsMap.get(key) ?? {};
|
||||||
|
let value = null;
|
||||||
|
if (fieldtype) {
|
||||||
|
value = getEmptyValuesByFieldTypes(fieldtype, this.fyo);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { value };
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
this.valueMatrix.push(valueRow);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeRow(index: number) {
|
||||||
|
this.valueMatrix = this.valueMatrix.filter((_, i) => i !== index);
|
||||||
|
}
|
||||||
|
|
||||||
|
getCSVTemplate(): string {
|
||||||
|
const schemaLabels: string[] = [];
|
||||||
|
const fieldLabels: string[] = [];
|
||||||
|
const fieldKey: string[] = [];
|
||||||
|
|
||||||
|
for (const [name, picked] of this.templateFieldsPicked.entries()) {
|
||||||
|
if (!picked) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const field = this.templateFieldsMap.get(name);
|
||||||
|
if (!field) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
schemaLabels.push(field.schemaLabel);
|
||||||
|
fieldLabels.push(field.label);
|
||||||
|
fieldKey.push(field.fieldKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
return generateCSV([schemaLabels, fieldLabels, fieldKey]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTemplateFields(
|
||||||
|
schemaName: string,
|
||||||
|
fyo: Fyo,
|
||||||
|
importer: Importer
|
||||||
|
): TemplateField[] {
|
||||||
|
const schemas: { schema: Schema; parentSchemaChildField?: TargetField }[] = [
|
||||||
|
{ schema: fyo.schemaMap[schemaName]! },
|
||||||
|
];
|
||||||
|
const fields: TemplateField[] = [];
|
||||||
|
|
||||||
|
while (schemas.length) {
|
||||||
|
const { schema, parentSchemaChildField } = schemas.pop() ?? {};
|
||||||
|
if (!schema) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const field of schema.fields) {
|
||||||
|
if (
|
||||||
|
field.computed ||
|
||||||
|
field.meta ||
|
||||||
|
field.hidden ||
|
||||||
|
(field.readOnly && !field.required)
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.fieldtype === FieldTypeEnum.Table) {
|
||||||
|
importer.hasChildTables = true;
|
||||||
|
schemas.push({
|
||||||
|
schema: fyo.schemaMap[field.target]!,
|
||||||
|
parentSchemaChildField: field,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (skippedFieldsTypes.includes(field.fieldtype)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tf = { ...field };
|
||||||
|
|
||||||
|
if (tf.readOnly) {
|
||||||
|
tf.readOnly = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (schema.isChild && tf.fieldname === 'name') {
|
||||||
|
tf.required = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const schemaName = schema.name;
|
||||||
|
const schemaLabel = schema.label;
|
||||||
|
const fieldKey = `${schema.name}.${field.fieldname}`;
|
||||||
|
|
||||||
|
fields.push({
|
||||||
|
...tf,
|
||||||
|
schemaName,
|
||||||
|
schemaLabel,
|
||||||
|
fieldKey,
|
||||||
|
parentSchemaChildField,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fields;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getColumnLabel(field: TemplateField): string {
|
||||||
|
if (field.parentSchemaChildField) {
|
||||||
|
return `${field.label} (${field.parentSchemaChildField.label})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return field.label;
|
||||||
|
}
|
@ -147,7 +147,8 @@ import { ModelNameEnum } from 'models/types';
|
|||||||
import PageHeader from 'src/components/PageHeader.vue';
|
import PageHeader from 'src/components/PageHeader.vue';
|
||||||
import { fyo } from 'src/initFyo';
|
import { fyo } from 'src/initFyo';
|
||||||
import { docsPathMap } from 'src/utils/misc';
|
import { docsPathMap } from 'src/utils/misc';
|
||||||
import { docsPath, openQuickEdit } from 'src/utils/ui';
|
import { docsPathRef } from 'src/utils/refs';
|
||||||
|
import { openQuickEdit } from 'src/utils/ui';
|
||||||
import { getMapFromList, removeAtIndex } from 'utils/index';
|
import { getMapFromList, removeAtIndex } from 'utils/index';
|
||||||
import { nextTick } from 'vue';
|
import { nextTick } from 'vue';
|
||||||
import Button from '../components/Button.vue';
|
import Button from '../components/Button.vue';
|
||||||
@ -184,7 +185,7 @@ export default {
|
|||||||
window.coa = this;
|
window.coa = this;
|
||||||
}
|
}
|
||||||
|
|
||||||
docsPath.value = docsPathMap.ChartOfAccounts;
|
docsPathRef.value = docsPathMap.ChartOfAccounts;
|
||||||
|
|
||||||
if (this.refetchTotals) {
|
if (this.refetchTotals) {
|
||||||
await this.setTotalDebitAndCredit();
|
await this.setTotalDebitAndCredit();
|
||||||
@ -192,7 +193,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
deactivated() {
|
deactivated() {
|
||||||
docsPath.value = '';
|
docsPathRef.value = '';
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async expand() {
|
async expand() {
|
||||||
|
@ -65,12 +65,12 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import PageHeader from 'src/components/PageHeader.vue';
|
import PageHeader from 'src/components/PageHeader.vue';
|
||||||
import { docsPath } from 'src/utils/ui';
|
|
||||||
import UnpaidInvoices from './UnpaidInvoices.vue';
|
import UnpaidInvoices from './UnpaidInvoices.vue';
|
||||||
import Cashflow from './Cashflow.vue';
|
import Cashflow from './Cashflow.vue';
|
||||||
import Expenses from './Expenses.vue';
|
import Expenses from './Expenses.vue';
|
||||||
import PeriodSelector from './PeriodSelector.vue';
|
import PeriodSelector from './PeriodSelector.vue';
|
||||||
import ProfitAndLoss from './ProfitAndLoss.vue';
|
import ProfitAndLoss from './ProfitAndLoss.vue';
|
||||||
|
import { docsPathRef } from 'src/utils/refs';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Dashboard',
|
name: 'Dashboard',
|
||||||
@ -86,10 +86,10 @@ export default {
|
|||||||
return { period: 'This Year' };
|
return { period: 'This Year' };
|
||||||
},
|
},
|
||||||
activated() {
|
activated() {
|
||||||
docsPath.value = 'analytics/dashboard';
|
docsPathRef.value = 'analytics/dashboard';
|
||||||
},
|
},
|
||||||
deactivated() {
|
deactivated() {
|
||||||
docsPath.value = '';
|
docsPathRef.value = '';
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
handlePeriodChange(period) {
|
handlePeriodChange(period) {
|
||||||
|
@ -1,653 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="flex flex-col overflow-hidden w-full">
|
|
||||||
<!-- Header -->
|
|
||||||
<PageHeader :title="t`Data Import`">
|
|
||||||
<DropdownWithActions
|
|
||||||
:actions="actions"
|
|
||||||
v-if="(canCancel || importType) && !complete"
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
v-if="importType && !complete"
|
|
||||||
type="primary"
|
|
||||||
class="text-sm"
|
|
||||||
@click="handlePrimaryClick"
|
|
||||||
>{{ primaryLabel }}</Button
|
|
||||||
>
|
|
||||||
</PageHeader>
|
|
||||||
|
|
||||||
<div class="flex text-base w-full flex-col" v-if="!complete">
|
|
||||||
<!-- Type selector -->
|
|
||||||
<div
|
|
||||||
class="
|
|
||||||
flex flex-row
|
|
||||||
justify-start
|
|
||||||
items-center
|
|
||||||
w-full
|
|
||||||
gap-2
|
|
||||||
border-b
|
|
||||||
p-4
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<FormControl
|
|
||||||
:df="importableDf"
|
|
||||||
input-class="bg-transparent text-gray-900 text-base"
|
|
||||||
class="w-40 bg-gray-100 rounded"
|
|
||||||
:value="importType"
|
|
||||||
size="small"
|
|
||||||
@change="setImportType"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<p
|
|
||||||
class="text-base ms-2"
|
|
||||||
:class="fileName ? 'text-gray-900 font-semibold' : 'text-gray-700'"
|
|
||||||
>
|
|
||||||
<span v-if="fileName" class="font-normal"
|
|
||||||
>{{ t`Selected file` }}
|
|
||||||
</span>
|
|
||||||
{{ helperText }}{{ fileName ? ',' : '' }}
|
|
||||||
<span v-if="fileName" class="font-normal">
|
|
||||||
{{ t`verify the imported data and click on` }} </span
|
|
||||||
>{{ ' ' }}<span v-if="fileName">{{ t`Import Data` }}</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Settings -->
|
|
||||||
<div v-if="fileName" class="border-b p-4">
|
|
||||||
<h2 class="text-lg font-semibold">{{ t`Importer Settings` }}</h2>
|
|
||||||
<div class="mt-2 flex gap-2">
|
|
||||||
<div
|
|
||||||
v-if="file && isSubmittable"
|
|
||||||
class="
|
|
||||||
gap-2
|
|
||||||
flex
|
|
||||||
justify-between
|
|
||||||
items-center
|
|
||||||
bg-gray-100
|
|
||||||
px-2
|
|
||||||
rounded
|
|
||||||
text-gray-900
|
|
||||||
w-40
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<p>{{ t`Submit on Import` }}</p>
|
|
||||||
<FormControl
|
|
||||||
size="small"
|
|
||||||
input-class="bg-gray-100"
|
|
||||||
:df="{
|
|
||||||
fieldname: 'shouldSubmit',
|
|
||||||
fieldtype: 'Check',
|
|
||||||
}"
|
|
||||||
:value="Number(importer.shouldSubmit)"
|
|
||||||
@change="(value) => (importer.shouldSubmit = !!value)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="
|
|
||||||
flex flex-row
|
|
||||||
justify-center
|
|
||||||
items-center
|
|
||||||
gap-2
|
|
||||||
bg-gray-100
|
|
||||||
ps-2
|
|
||||||
rounded
|
|
||||||
text-gray-900
|
|
||||||
w-40
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<p class="text-gray-900">{{ t`Label Index` }}</p>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
class="
|
|
||||||
bg-gray-100
|
|
||||||
outline-none
|
|
||||||
focus:bg-gray-200
|
|
||||||
px-2
|
|
||||||
py-1
|
|
||||||
rounded-md
|
|
||||||
w-10
|
|
||||||
text-end
|
|
||||||
"
|
|
||||||
min="1"
|
|
||||||
:max="importer.csv.length - 1"
|
|
||||||
:value="labelIndex + 1"
|
|
||||||
@change="setLabelIndex"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
class="w-28 bg-gray-100 focus:bg-gray-200 rounded-md"
|
|
||||||
v-if="canReset"
|
|
||||||
@click="
|
|
||||||
() => {
|
|
||||||
importer.initialize(0, true);
|
|
||||||
canReset = false;
|
|
||||||
}
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<span class="text-gray-900">
|
|
||||||
{{ t`Reset` }}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Label Assigner -->
|
|
||||||
<div v-if="fileName" class="p-4 border-b">
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<h2 class="text-lg font-semibold">{{ t`Assign Imported Labels` }}</h2>
|
|
||||||
<p class="text-red-400 text-sm" v-if="isRequiredUnassigned">
|
|
||||||
{{ t`* required fields` }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="gap-2 mt-4 grid grid-flow-col overflow-x-auto no-scrollbar">
|
|
||||||
<div
|
|
||||||
v-for="(f, k) in importer.assignableLabels"
|
|
||||||
:key="'assigner-' + f + '-' + k"
|
|
||||||
>
|
|
||||||
<p class="text-gray-600 text-sm mb-1">
|
|
||||||
{{ f }}
|
|
||||||
<span
|
|
||||||
v-if="importer.requiredMap[f] && !importer.assignedMap[f]"
|
|
||||||
class="text-red-400"
|
|
||||||
>*</span
|
|
||||||
>
|
|
||||||
</p>
|
|
||||||
<FormControl
|
|
||||||
size="small"
|
|
||||||
class="w-28"
|
|
||||||
input-class="bg-gray-100"
|
|
||||||
:df="getAssignerField(f)"
|
|
||||||
:value="importer.assignedMap[f] ?? ''"
|
|
||||||
@change="(v) => onAssignedChange(f, v)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Data Verifier -->
|
|
||||||
<div v-if="fileName">
|
|
||||||
<div class="overflow-auto border-b">
|
|
||||||
<!-- Column Name Rows -->
|
|
||||||
<div
|
|
||||||
class="
|
|
||||||
grid grid-flow-col
|
|
||||||
border-b
|
|
||||||
gap-2
|
|
||||||
sticky
|
|
||||||
top-0
|
|
||||||
bg-white
|
|
||||||
px-4
|
|
||||||
h-row-mid
|
|
||||||
items-center
|
|
||||||
"
|
|
||||||
style="width: fit-content"
|
|
||||||
v-if="importer.columnLabels.length > 0"
|
|
||||||
>
|
|
||||||
<div class="w-4 h-4" />
|
|
||||||
<p
|
|
||||||
v-for="(c, i) in importer.columnLabels"
|
|
||||||
class="px-2 w-28 font-semibold text-gray-600"
|
|
||||||
:key="'column-' + i"
|
|
||||||
>
|
|
||||||
{{ c }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div v-else>
|
|
||||||
<p class="text-gray-600">
|
|
||||||
{{ t`No labels have been assigned.` }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Data Rows -->
|
|
||||||
<div
|
|
||||||
v-if="importer.columnLabels.length > 0"
|
|
||||||
style="max-height: 500px"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="
|
|
||||||
grid grid-flow-col
|
|
||||||
border-b
|
|
||||||
gap-2
|
|
||||||
items-center
|
|
||||||
px-4
|
|
||||||
h-row-mid
|
|
||||||
"
|
|
||||||
style="width: fit-content"
|
|
||||||
v-for="(r, i) in assignedMatrix"
|
|
||||||
:key="'matrix-row-' + i"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
class="
|
|
||||||
w-4
|
|
||||||
h-4
|
|
||||||
text-gray-600
|
|
||||||
hover:text-gray-900
|
|
||||||
cursor-pointer
|
|
||||||
outline-none
|
|
||||||
"
|
|
||||||
@click="
|
|
||||||
() => {
|
|
||||||
importer.dropRow(i);
|
|
||||||
canReset = true;
|
|
||||||
}
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<FeatherIcon name="x" />
|
|
||||||
</button>
|
|
||||||
<input
|
|
||||||
v-for="(c, j) in r"
|
|
||||||
type="text"
|
|
||||||
class="
|
|
||||||
w-28
|
|
||||||
text-gray-900
|
|
||||||
px-2
|
|
||||||
py-1
|
|
||||||
outline-none
|
|
||||||
rounded
|
|
||||||
focus:bg-gray-200
|
|
||||||
"
|
|
||||||
@change="
|
|
||||||
(e) => {
|
|
||||||
onValueChange(e, i, j);
|
|
||||||
canReset = true;
|
|
||||||
}
|
|
||||||
"
|
|
||||||
:key="'matrix-cell-' + i + '-' + j"
|
|
||||||
:value="c"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Add Row button -->
|
|
||||||
<button
|
|
||||||
class="
|
|
||||||
text-gray-600
|
|
||||||
hover:bg-gray-50
|
|
||||||
flex flex-row
|
|
||||||
w-full
|
|
||||||
px-4
|
|
||||||
h-row-mid
|
|
||||||
border-b
|
|
||||||
items-center
|
|
||||||
outline-none
|
|
||||||
"
|
|
||||||
@click="
|
|
||||||
() => {
|
|
||||||
importer.addRow();
|
|
||||||
canReset = true;
|
|
||||||
}
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<FeatherIcon name="plus" class="w-4 h-4" />
|
|
||||||
<p class="ps-4">
|
|
||||||
{{ t`Add Row` }}
|
|
||||||
</p>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="complete" class="flex justify-center h-full items-center">
|
|
||||||
<div
|
|
||||||
class="
|
|
||||||
flex flex-col
|
|
||||||
justify-center
|
|
||||||
items-center
|
|
||||||
gap-8
|
|
||||||
rounded-lg
|
|
||||||
shadow-md
|
|
||||||
p-6
|
|
||||||
"
|
|
||||||
style="width: 450px"
|
|
||||||
>
|
|
||||||
<h2 class="text-xl font-semibold mt-4">{{ t`Import Success` }} 🎉</h2>
|
|
||||||
<p class="text-lg text-center">
|
|
||||||
{{ t`Successfully created the following ${names.length} entries:` }}
|
|
||||||
</p>
|
|
||||||
<div class="max-h-96 overflow-y-auto">
|
|
||||||
<div
|
|
||||||
v-for="(n, i) in names"
|
|
||||||
:key="'name-' + i"
|
|
||||||
class="grid grid-cols-2 gap-2 border-b pb-2 mb-2 pe-4 text-lg w-60"
|
|
||||||
style="grid-template-columns: 2rem auto"
|
|
||||||
>
|
|
||||||
<p class="text-end">{{ i + 1 }}.</p>
|
|
||||||
<p>
|
|
||||||
{{ n }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex w-full justify-between">
|
|
||||||
<Button type="secondary" class="text-sm w-32" @click="clear">{{
|
|
||||||
t`Import More`
|
|
||||||
}}</Button>
|
|
||||||
<Button type="primary" class="text-sm w-32" @click="showMe">{{
|
|
||||||
t`Show Me`
|
|
||||||
}}</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="!importType"
|
|
||||||
class="flex justify-center h-full w-full items-center mb-16"
|
|
||||||
>
|
|
||||||
<HowTo
|
|
||||||
link="https://youtu.be/ukHAgcnVxTQ"
|
|
||||||
class="text-gray-900 rounded-lg text-base border px-3 py-2"
|
|
||||||
>
|
|
||||||
{{ t`How to Use Data Import` }}
|
|
||||||
</HowTo>
|
|
||||||
</div>
|
|
||||||
<Loading
|
|
||||||
v-if="isMakingEntries"
|
|
||||||
:open="isMakingEntries"
|
|
||||||
:percent="percentLoading"
|
|
||||||
:message="messageLoading"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<script>
|
|
||||||
import Button from 'src/components/Button.vue';
|
|
||||||
import FormControl from 'src/components/Controls/FormControl.vue';
|
|
||||||
import DropdownWithActions from 'src/components/DropdownWithActions.vue';
|
|
||||||
import FeatherIcon from 'src/components/FeatherIcon.vue';
|
|
||||||
import HowTo from 'src/components/HowTo.vue';
|
|
||||||
import PageHeader from 'src/components/PageHeader.vue';
|
|
||||||
import { importable, Importer } from 'src/dataImport';
|
|
||||||
import { fyo } from 'src/initFyo';
|
|
||||||
import { getSavePath, saveData, selectFile } from 'src/utils/ipcCalls';
|
|
||||||
import { docsPathMap } from 'src/utils/misc';
|
|
||||||
import { docsPath, showMessageDialog } from 'src/utils/ui';
|
|
||||||
import Loading from '../components/Loading.vue';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
PageHeader,
|
|
||||||
FormControl,
|
|
||||||
Button,
|
|
||||||
DropdownWithActions,
|
|
||||||
FeatherIcon,
|
|
||||||
HowTo,
|
|
||||||
Loading,
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
canReset: false,
|
|
||||||
complete: false,
|
|
||||||
names: ['Bat', 'Baseball', 'Other Shit'],
|
|
||||||
file: null,
|
|
||||||
importer: null,
|
|
||||||
importType: '',
|
|
||||||
isMakingEntries: false,
|
|
||||||
percentLoading: 0,
|
|
||||||
messageLoading: '',
|
|
||||||
};
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
if (fyo.store.isDevelopment) {
|
|
||||||
window.di = this;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
labelIndex() {
|
|
||||||
return this.importer.labelIndex;
|
|
||||||
},
|
|
||||||
requiredUnassigned() {
|
|
||||||
return this.importer.assignableLabels.filter(
|
|
||||||
(k) => this.importer.requiredMap[k] && !this.importer.assignedMap[k]
|
|
||||||
);
|
|
||||||
},
|
|
||||||
isRequiredUnassigned() {
|
|
||||||
return this.requiredUnassigned.length > 0;
|
|
||||||
},
|
|
||||||
assignedMatrix() {
|
|
||||||
return this.importer.assignedMatrix;
|
|
||||||
},
|
|
||||||
actions() {
|
|
||||||
const actions = [];
|
|
||||||
|
|
||||||
const secondaryAction = {
|
|
||||||
component: {
|
|
||||||
template: '<span>{{ t`Save Template` }}</span>',
|
|
||||||
},
|
|
||||||
condition: () => true,
|
|
||||||
action: this.handleSecondaryClick,
|
|
||||||
};
|
|
||||||
actions.push(secondaryAction);
|
|
||||||
|
|
||||||
if (this.file) {
|
|
||||||
actions.push({
|
|
||||||
component: {
|
|
||||||
template: '<span>{{ t`Change File` }}</span>',
|
|
||||||
},
|
|
||||||
condition: () => true,
|
|
||||||
action: this.selectFile,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const cancelAction = {
|
|
||||||
component: {
|
|
||||||
template: '<span class="text-red-700" >{{ t`Cancel` }}</span>',
|
|
||||||
},
|
|
||||||
condition: () => true,
|
|
||||||
action: this.clear,
|
|
||||||
};
|
|
||||||
actions.push(cancelAction);
|
|
||||||
|
|
||||||
return actions;
|
|
||||||
},
|
|
||||||
|
|
||||||
fileName() {
|
|
||||||
if (!this.file) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
return this.file.name;
|
|
||||||
},
|
|
||||||
helperText() {
|
|
||||||
if (!this.importType) {
|
|
||||||
return this.t`Set an Import Type`;
|
|
||||||
} else if (!this.fileName) {
|
|
||||||
return this.t`Select a file for import`;
|
|
||||||
}
|
|
||||||
return this.fileName;
|
|
||||||
},
|
|
||||||
primaryLabel() {
|
|
||||||
return this.file ? this.t`Import Data` : this.t`Select File`;
|
|
||||||
},
|
|
||||||
isSubmittable() {
|
|
||||||
const schemaName = this.importer?.schemaName;
|
|
||||||
if (schemaName) {
|
|
||||||
return fyo.schemaMap[schemaName].isSubmittable ?? false;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
importableDf() {
|
|
||||||
return {
|
|
||||||
fieldname: 'importType',
|
|
||||||
label: this.t`Import Type`,
|
|
||||||
fieldtype: 'AutoComplete',
|
|
||||||
placeholder: this.t`Import Type`,
|
|
||||||
options: Object.keys(this.labelSchemaNameMap),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
labelSchemaNameMap() {
|
|
||||||
return importable
|
|
||||||
.map((i) => ({
|
|
||||||
name: i,
|
|
||||||
label: fyo.schemaMap[i].label,
|
|
||||||
}))
|
|
||||||
.reduce((acc, { name, label }) => {
|
|
||||||
acc[label] = name;
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
},
|
|
||||||
canCancel() {
|
|
||||||
return !!(this.file || this.importType);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
activated() {
|
|
||||||
docsPath.value = docsPathMap.DataImport;
|
|
||||||
},
|
|
||||||
deactivated() {
|
|
||||||
docsPath.value = '';
|
|
||||||
if (!this.complete) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.clear();
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
showMe() {
|
|
||||||
const schemaName = this.importer.schemaName;
|
|
||||||
this.clear();
|
|
||||||
this.$router.push(`/list/${schemaName}`);
|
|
||||||
},
|
|
||||||
clear() {
|
|
||||||
this.file = null;
|
|
||||||
this.names = [];
|
|
||||||
this.importer = null;
|
|
||||||
this.importType = '';
|
|
||||||
this.complete = false;
|
|
||||||
this.canReset = false;
|
|
||||||
this.isMakingEntries = false;
|
|
||||||
this.percentLoading = 0;
|
|
||||||
this.messageLoading = '';
|
|
||||||
},
|
|
||||||
handlePrimaryClick() {
|
|
||||||
if (!this.file) {
|
|
||||||
this.selectFile();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.importData();
|
|
||||||
},
|
|
||||||
handleSecondaryClick() {
|
|
||||||
if (!this.importer) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.saveTemplate();
|
|
||||||
},
|
|
||||||
setLabelIndex(e) {
|
|
||||||
const labelIndex = (e.target.value ?? 1) - 1;
|
|
||||||
this.importer.initialize(labelIndex);
|
|
||||||
},
|
|
||||||
async saveTemplate() {
|
|
||||||
const template = this.importer.template;
|
|
||||||
const templateName = this.importType + ' ' + this.t`Template`;
|
|
||||||
const { cancelled, filePath } = await getSavePath(templateName, 'csv');
|
|
||||||
|
|
||||||
if (cancelled || filePath === '') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await saveData(template, filePath);
|
|
||||||
},
|
|
||||||
getAssignerField(targetLabel) {
|
|
||||||
const assigned = this.importer.assignedMap[targetLabel];
|
|
||||||
return {
|
|
||||||
fieldname: 'assignerField',
|
|
||||||
label: targetLabel,
|
|
||||||
placeholder: `Select Label`,
|
|
||||||
fieldtype: 'Select',
|
|
||||||
options: [
|
|
||||||
'',
|
|
||||||
...(assigned ? [assigned] : []),
|
|
||||||
...this.importer.unassignedLabels,
|
|
||||||
],
|
|
||||||
default: assigned ?? '',
|
|
||||||
};
|
|
||||||
},
|
|
||||||
onAssignedChange(target, value) {
|
|
||||||
this.importer.assignedMap[target] = value;
|
|
||||||
},
|
|
||||||
onValueChange(event, i, j) {
|
|
||||||
this.importer.updateValue(event.target.value, i, j);
|
|
||||||
},
|
|
||||||
async importData() {
|
|
||||||
if (this.isMakingEntries || this.complete) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.isRequiredUnassigned) {
|
|
||||||
return await showMessageDialog({
|
|
||||||
message: this.t`Required Fields not Assigned`,
|
|
||||||
detail: this
|
|
||||||
.t`Please assign the following fields ${this.requiredUnassigned.join(
|
|
||||||
', '
|
|
||||||
)}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.importer.assignedMatrix.length === 0) {
|
|
||||||
return await showMessageDialog({
|
|
||||||
message: this.t`No Data to Import`,
|
|
||||||
detail: this.t`Please select a file with data to import.`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const { success, names, message } = await this.importer.importData(
|
|
||||||
this.setLoadingStatus
|
|
||||||
);
|
|
||||||
if (!success) {
|
|
||||||
return await showMessageDialog({
|
|
||||||
message: this.t`Import Failed`,
|
|
||||||
detail: message,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.names = names;
|
|
||||||
this.complete = true;
|
|
||||||
},
|
|
||||||
setImportType(importType) {
|
|
||||||
if (this.importType) {
|
|
||||||
this.clear();
|
|
||||||
}
|
|
||||||
this.importType = importType;
|
|
||||||
this.importer = new Importer(
|
|
||||||
this.labelSchemaNameMap[this.importType],
|
|
||||||
fyo
|
|
||||||
);
|
|
||||||
},
|
|
||||||
setLoadingStatus(isMakingEntries, entriesMade, totalEntries) {
|
|
||||||
this.isMakingEntries = isMakingEntries;
|
|
||||||
this.percentLoading = entriesMade / totalEntries;
|
|
||||||
this.messageLoading = isMakingEntries
|
|
||||||
? `${entriesMade} entries made out of ${totalEntries}...`
|
|
||||||
: '';
|
|
||||||
},
|
|
||||||
async selectFile() {
|
|
||||||
const options = {
|
|
||||||
title: this.t`Select File`,
|
|
||||||
filters: [{ name: 'CSV', extensions: ['csv'] }],
|
|
||||||
};
|
|
||||||
|
|
||||||
const { success, canceled, filePath, data, name } = await selectFile(
|
|
||||||
options
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!success && !canceled) {
|
|
||||||
return await showMessageDialog({
|
|
||||||
message: this.t`File selection failed.`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!success || canceled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const text = new TextDecoder().decode(data);
|
|
||||||
const isValid = this.importer.selectFile(text);
|
|
||||||
if (!isValid) {
|
|
||||||
return await showMessageDialog({
|
|
||||||
message: this.t`Bad import data.`,
|
|
||||||
detail: this.t`Could not select file.`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.file = {
|
|
||||||
name,
|
|
||||||
filePath,
|
|
||||||
text,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
@ -36,15 +36,16 @@
|
|||||||
class="
|
class="
|
||||||
absolute
|
absolute
|
||||||
bottom-0
|
bottom-0
|
||||||
left-0
|
start-0
|
||||||
text-gray-600
|
text-gray-600
|
||||||
bg-gray-100
|
bg-gray-100
|
||||||
rounded
|
rounded
|
||||||
|
rtl-rotate-180
|
||||||
p-1
|
p-1
|
||||||
m-4
|
m-4
|
||||||
opacity-0
|
|
||||||
hover:opacity-100 hover:shadow-md
|
hover:opacity-100 hover:shadow-md
|
||||||
"
|
"
|
||||||
|
|
||||||
@click="sidebar = !sidebar"
|
@click="sidebar = !sidebar"
|
||||||
>
|
>
|
||||||
<feather-icon name="chevrons-right" class="w-4 h-4" />
|
<feather-icon name="chevrons-right" class="w-4 h-4" />
|
||||||
@ -76,6 +77,11 @@ export default {
|
|||||||
transform: translateX(calc(-1 * var(--w-sidebar)));
|
transform: translateX(calc(-1 * var(--w-sidebar)));
|
||||||
width: 0px;
|
width: 0px;
|
||||||
}
|
}
|
||||||
|
[dir='rtl'] .sidebar-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(calc(1 * var(--w-sidebar)));
|
||||||
|
width: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
.sidebar-enter-to,
|
.sidebar-enter-to,
|
||||||
.sidebar-leave-from {
|
.sidebar-leave-from {
|
||||||
|
@ -130,7 +130,6 @@
|
|||||||
|
|
||||||
<template #quickedit v-if="quickEditDoc">
|
<template #quickedit v-if="quickEditDoc">
|
||||||
<QuickEditForm
|
<QuickEditForm
|
||||||
class="w-quick-edit"
|
|
||||||
:name="quickEditDoc.name"
|
:name="quickEditDoc.name"
|
||||||
:show-name="false"
|
:show-name="false"
|
||||||
:show-save="false"
|
:show-save="false"
|
||||||
@ -160,8 +159,8 @@ import FormHeader from 'src/components/FormHeader.vue';
|
|||||||
import StatusBadge from 'src/components/StatusBadge.vue';
|
import StatusBadge from 'src/components/StatusBadge.vue';
|
||||||
import { fyo } from 'src/initFyo';
|
import { fyo } from 'src/initFyo';
|
||||||
import { docsPathMap } from 'src/utils/misc';
|
import { docsPathMap } from 'src/utils/misc';
|
||||||
|
import { docsPathRef, focusedDocsRef } from 'src/utils/refs';
|
||||||
import {
|
import {
|
||||||
docsPath,
|
|
||||||
getGroupedActionsForDoc,
|
getGroupedActionsForDoc,
|
||||||
routeTo,
|
routeTo,
|
||||||
showMessageDialog,
|
showMessageDialog,
|
||||||
@ -232,14 +231,17 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
activated() {
|
activated() {
|
||||||
docsPath.value = docsPathMap[this.schemaName];
|
docsPathRef.value = docsPathMap[this.schemaName];
|
||||||
|
focusedDocsRef.add(this.doc);
|
||||||
},
|
},
|
||||||
deactivated() {
|
deactivated() {
|
||||||
docsPath.value = '';
|
docsPathRef.value = '';
|
||||||
|
focusedDocsRef.delete(this.doc);
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
try {
|
try {
|
||||||
this.doc = await fyo.doc.getDoc(this.schemaName, this.name);
|
this.doc = await fyo.doc.getDoc(this.schemaName, this.name);
|
||||||
|
focusedDocsRef.add(this.doc);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof fyo.errors.NotFoundError) {
|
if (error instanceof fyo.errors.NotFoundError) {
|
||||||
routeTo(`/list/${this.schemaName}`);
|
routeTo(`/list/${this.schemaName}`);
|
||||||
|
952
src/pages/ImportWizard.vue
Normal file
952
src/pages/ImportWizard.vue
Normal file
@ -0,0 +1,952 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col overflow-hidden w-full">
|
||||||
|
<!-- Header -->
|
||||||
|
<PageHeader :title="t`Import Wizard`">
|
||||||
|
<DropdownWithActions
|
||||||
|
:actions="actions"
|
||||||
|
v-if="hasImporter"
|
||||||
|
:disabled="isMakingEntries"
|
||||||
|
:title="t`More`"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
v-if="hasImporter"
|
||||||
|
:title="t`Add Row`"
|
||||||
|
@click="() => importer.addRow()"
|
||||||
|
:disabled="isMakingEntries"
|
||||||
|
:icon="true"
|
||||||
|
>
|
||||||
|
<feather-icon name="plus" class="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
v-if="hasImporter"
|
||||||
|
:title="t`Save Template`"
|
||||||
|
@click="saveTemplate"
|
||||||
|
:icon="true"
|
||||||
|
>
|
||||||
|
<feather-icon name="download" class="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
v-if="canImportData"
|
||||||
|
:title="t`Import Data`"
|
||||||
|
type="primary"
|
||||||
|
@click="importData"
|
||||||
|
:disabled="errorMessage.length > 0 || isMakingEntries"
|
||||||
|
>
|
||||||
|
{{ t`Import Data` }}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
v-if="importType && !canImportData"
|
||||||
|
:title="t`Select File`"
|
||||||
|
type="primary"
|
||||||
|
@click="selectFile"
|
||||||
|
>
|
||||||
|
{{ t`Select File` }}
|
||||||
|
</Button>
|
||||||
|
</PageHeader>
|
||||||
|
|
||||||
|
<!-- Main Body of the Wizard -->
|
||||||
|
<div class="flex text-base w-full flex-col">
|
||||||
|
<!-- Select Import Type -->
|
||||||
|
<div
|
||||||
|
class="
|
||||||
|
h-row-largest
|
||||||
|
flex flex-row
|
||||||
|
justify-start
|
||||||
|
items-center
|
||||||
|
w-full
|
||||||
|
gap-2
|
||||||
|
border-b
|
||||||
|
p-4
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<AutoComplete
|
||||||
|
:df="{
|
||||||
|
fieldname: 'importType',
|
||||||
|
label: t`Import Type`,
|
||||||
|
fieldtype: 'AutoComplete',
|
||||||
|
options: importableSchemaNames.map((value) => ({
|
||||||
|
value,
|
||||||
|
label: fyo.schemaMap[value]?.label ?? value,
|
||||||
|
})),
|
||||||
|
}"
|
||||||
|
input-class="bg-transparent text-gray-900 text-base"
|
||||||
|
class="w-40"
|
||||||
|
:border="true"
|
||||||
|
:value="importType"
|
||||||
|
size="small"
|
||||||
|
@change="setImportType"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<p v-if="errorMessage.length > 0" class="text-base ms-2 text-red-500">
|
||||||
|
{{ errorMessage }}
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
v-else
|
||||||
|
class="text-base ms-2"
|
||||||
|
:class="fileName ? 'text-gray-900 font-semibold' : 'text-gray-700'"
|
||||||
|
>
|
||||||
|
<span v-if="fileName" class="font-normal">{{ t`Selected` }} </span>
|
||||||
|
{{ helperMessage }}{{ fileName ? ',' : '' }}
|
||||||
|
<span v-if="fileName" class="font-normal">
|
||||||
|
{{ t`check values and click on` }} </span
|
||||||
|
>{{ ' ' }}<span v-if="fileName">{{ t`Import Data.` }}</span>
|
||||||
|
<span
|
||||||
|
v-if="hasImporter && importer.valueMatrix.length > 0"
|
||||||
|
class="font-normal"
|
||||||
|
>{{
|
||||||
|
' ' +
|
||||||
|
(importer.valueMatrix.length === 2
|
||||||
|
? t`${importer.valueMatrix.length} row added.`
|
||||||
|
: t`${importer.valueMatrix.length} rows added.`)
|
||||||
|
}}</span
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Assignment Row and Value Grid container -->
|
||||||
|
<div
|
||||||
|
v-if="hasImporter"
|
||||||
|
class="overflow-auto custom-scroll"
|
||||||
|
style="max-height: calc(100vh - (2 * var(--h-row-largest)) - 2px)"
|
||||||
|
>
|
||||||
|
<!-- Column Assignment Row -->
|
||||||
|
<div
|
||||||
|
class="grid sticky top-0 py-4 pe-4 bg-white border-b gap-4"
|
||||||
|
style="z-index: 1; width: fit-content"
|
||||||
|
:style="gridTemplateColumn"
|
||||||
|
>
|
||||||
|
<div class="index-cell">#</div>
|
||||||
|
<Select
|
||||||
|
v-for="index in columnIterator"
|
||||||
|
class="flex-shrink-0"
|
||||||
|
size="small"
|
||||||
|
:border="true"
|
||||||
|
:key="index"
|
||||||
|
:df="gridColumnTitleDf"
|
||||||
|
:value="importer.assignedTemplateFields[index]"
|
||||||
|
@change="(value: string | null) => importer.setTemplateField(index, value)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Values Grid -->
|
||||||
|
<div
|
||||||
|
v-if="importer.valueMatrix.length"
|
||||||
|
class="grid py-4 pe-4 bg-white gap-4"
|
||||||
|
style="width: fit-content"
|
||||||
|
:style="gridTemplateColumn"
|
||||||
|
>
|
||||||
|
<!-- Grid Value Row Cells, Allow Editing Values -->
|
||||||
|
<template v-for="(row, ridx) of importer.valueMatrix" :key="ridx">
|
||||||
|
<div
|
||||||
|
class="index-cell group cursor-pointer"
|
||||||
|
@click="importer.removeRow(ridx)"
|
||||||
|
>
|
||||||
|
<feather-icon
|
||||||
|
name="x"
|
||||||
|
class="w-4 h-4 hidden group-hover:inline-block -me-1"
|
||||||
|
:button="true"
|
||||||
|
/>
|
||||||
|
<span class="group-hover:hidden">
|
||||||
|
{{ ridx + 1 }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template
|
||||||
|
v-for="(val, cidx) of row.slice(0, columnCount)"
|
||||||
|
:key="`cell-${ridx}-${cidx}`"
|
||||||
|
>
|
||||||
|
<!-- Raw Data Field if Column is Not Assigned -->
|
||||||
|
<Data
|
||||||
|
v-if="!importer.assignedTemplateFields[cidx]"
|
||||||
|
:title="getFieldTitle(val)"
|
||||||
|
:df="{
|
||||||
|
fieldname: 'tempField',
|
||||||
|
label: t`Temporary`,
|
||||||
|
placeholder: t`Select column`,
|
||||||
|
}"
|
||||||
|
size="small"
|
||||||
|
:border="true"
|
||||||
|
:value="
|
||||||
|
val.value != null
|
||||||
|
? String(val.value)
|
||||||
|
: val.rawValue != null
|
||||||
|
? String(val.rawValue)
|
||||||
|
: ''
|
||||||
|
"
|
||||||
|
:read-only="true"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- FormControl Field if Column is Assigned -->
|
||||||
|
<FormControl
|
||||||
|
v-else
|
||||||
|
:class="val.error ? 'border border-red-300 rounded-md' : ''"
|
||||||
|
:title="getFieldTitle(val)"
|
||||||
|
:df="
|
||||||
|
importer.templateFieldsMap.get(
|
||||||
|
importer.assignedTemplateFields[cidx]!
|
||||||
|
)
|
||||||
|
"
|
||||||
|
size="small"
|
||||||
|
:rows="1"
|
||||||
|
:border="true"
|
||||||
|
:value="val.error ? null : val.value"
|
||||||
|
@change="(value: DocValue)=> {
|
||||||
|
importer.valueMatrix[ridx][cidx]!.error = false
|
||||||
|
importer.valueMatrix[ridx][cidx]!.value = value
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="ps-4 text-gray-700 sticky left-0 flex items-center"
|
||||||
|
style="height: 62.5px"
|
||||||
|
>
|
||||||
|
{{ t`No rows added. Select a file or add rows.` }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Loading Bar when Saving Docs -->
|
||||||
|
<Loading
|
||||||
|
v-if="isMakingEntries"
|
||||||
|
:open="isMakingEntries"
|
||||||
|
:percent="percentLoading"
|
||||||
|
:message="messageLoading"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Pick Column Modal -->
|
||||||
|
<Modal
|
||||||
|
:open-modal="showColumnPicker"
|
||||||
|
@closemodal="showColumnPicker = false"
|
||||||
|
>
|
||||||
|
<div class="w-form">
|
||||||
|
<!-- Pick Column Header -->
|
||||||
|
<FormHeader :form-title="t`Pick Import Columns`" />
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<!-- Pick Column Checkboxes -->
|
||||||
|
<div
|
||||||
|
class="p-4 max-h-80 overflow-auto custom-scroll"
|
||||||
|
v-for="[key, value] of columnPickerFieldsMap.entries()"
|
||||||
|
:key="key"
|
||||||
|
>
|
||||||
|
<h2 class="text-sm font-semibold text-gray-800">
|
||||||
|
{{ key }}
|
||||||
|
</h2>
|
||||||
|
<div class="grid grid-cols-3 border rounded mt-1">
|
||||||
|
<div
|
||||||
|
v-for="tf of value"
|
||||||
|
:key="tf.fieldKey"
|
||||||
|
class="flex items-center"
|
||||||
|
>
|
||||||
|
<Check
|
||||||
|
:df="{
|
||||||
|
fieldname: tf.fieldname,
|
||||||
|
label: tf.label,
|
||||||
|
}"
|
||||||
|
:show-label="true"
|
||||||
|
:read-only="tf.required"
|
||||||
|
:value="importer.templateFieldsPicked.get(tf.fieldKey)"
|
||||||
|
@change="(value:boolean) => pickColumn(tf.fieldKey, value)"
|
||||||
|
/>
|
||||||
|
<p v-if="tf.required" class="w-0 text-red-600 -ml-4">*</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pick Column Footer -->
|
||||||
|
<hr />
|
||||||
|
<div class="p-4 flex justify-between items-center">
|
||||||
|
<p class="text-sm text-gray-600">
|
||||||
|
{{ t`${numColumnsPicked} fields selected` }}
|
||||||
|
</p>
|
||||||
|
<Button type="primary" @click="showColumnPicker = false">{{
|
||||||
|
t`Done`
|
||||||
|
}}</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<!-- Import Completed Modal -->
|
||||||
|
<Modal :open-modal="complete" @closemodal="clear">
|
||||||
|
<div class="w-form">
|
||||||
|
<!-- Import Completed Header -->
|
||||||
|
<FormHeader :form-title="t`Import Complete`" />
|
||||||
|
<hr />
|
||||||
|
<!-- Success -->
|
||||||
|
<div v-if="success.length > 0">
|
||||||
|
<!-- Success Section Header -->
|
||||||
|
<div class="flex justify-between px-4 pt-4 pb-1">
|
||||||
|
<p class="text-base font-semibold">{{ t`Success` }}</p>
|
||||||
|
<p class="text-sm text-gray-600">
|
||||||
|
{{
|
||||||
|
success.length === 1
|
||||||
|
? t`${success.length} entry imported`
|
||||||
|
: t`${success.length} entries imported`
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<!-- Success Body -->
|
||||||
|
<div class="max-h-40 overflow-auto text-gray-900">
|
||||||
|
<div
|
||||||
|
v-for="(name, i) of success"
|
||||||
|
:key="name"
|
||||||
|
class="px-4 py-1 grid grid-cols-2 text-base gap-4"
|
||||||
|
style="grid-template-columns: 1rem auto"
|
||||||
|
>
|
||||||
|
<div class="text-end">{{ i + 1 }}.</div>
|
||||||
|
<p class="whitespace-nowrap overflow-auto no-scrollbar">
|
||||||
|
{{ name }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Failed -->
|
||||||
|
<div v-if="failed.length > 0">
|
||||||
|
<!-- Failed Section Header -->
|
||||||
|
<div class="flex justify-between px-4 pt-4 pb-1">
|
||||||
|
<p class="text-base font-semibold">{{ t`Failed` }}</p>
|
||||||
|
<p class="text-sm text-gray-600">
|
||||||
|
{{
|
||||||
|
failed.length === 1
|
||||||
|
? t`${failed.length} entry failed`
|
||||||
|
: t`${failed.length} entries failed`
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<!-- Failed Body -->
|
||||||
|
<div class="max-h-40 overflow-auto text-gray-900">
|
||||||
|
<div
|
||||||
|
v-for="(f, i) of failed"
|
||||||
|
:key="f.name"
|
||||||
|
class="px-4 py-1 grid grid-cols-2 text-base gap-4"
|
||||||
|
style="grid-template-columns: 1rem 8rem auto"
|
||||||
|
>
|
||||||
|
<div class="text-end">{{ i + 1 }}.</div>
|
||||||
|
<p class="whitespace-nowrap overflow-auto no-scrollbar">
|
||||||
|
{{ f.name }}
|
||||||
|
</p>
|
||||||
|
<p class="whitespace-nowrap overflow-auto no-scrollbar">
|
||||||
|
{{ f.error.message }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Fallback Div -->
|
||||||
|
<div
|
||||||
|
v-if="failed.length === 0 && success.length === 0"
|
||||||
|
class="p-4 text-base"
|
||||||
|
>
|
||||||
|
{{ t`No entries were imported.` }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer Button -->
|
||||||
|
<div class="flex justify-between p-4">
|
||||||
|
<Button
|
||||||
|
v-if="failed.length > 0"
|
||||||
|
@click="clearSuccessfullyImportedEntries"
|
||||||
|
>{{ t`Fix Failed` }}</Button
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
v-if="failed.length === 0 && success.length > 0"
|
||||||
|
@click="showMe"
|
||||||
|
>{{ t`Show Me` }}</Button
|
||||||
|
>
|
||||||
|
<Button @click="clear">{{ t`Done` }}</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { DocValue } from 'fyo/core/types';
|
||||||
|
import { Action as BaseAction } from 'fyo/model/types';
|
||||||
|
import { ValidationError } from 'fyo/utils/errors';
|
||||||
|
import { ModelNameEnum } from 'models/types';
|
||||||
|
import { OptionField, RawValue, SelectOption } from 'schemas/types';
|
||||||
|
import Button from 'src/components/Button.vue';
|
||||||
|
import AutoComplete from 'src/components/Controls/AutoComplete.vue';
|
||||||
|
import Check from 'src/components/Controls/Check.vue';
|
||||||
|
import Data from 'src/components/Controls/Data.vue';
|
||||||
|
import FormControl from 'src/components/Controls/FormControl.vue';
|
||||||
|
import Select from 'src/components/Controls/Select.vue';
|
||||||
|
import DropdownWithActions from 'src/components/DropdownWithActions.vue';
|
||||||
|
import FormHeader from 'src/components/FormHeader.vue';
|
||||||
|
import Modal from 'src/components/Modal.vue';
|
||||||
|
import PageHeader from 'src/components/PageHeader.vue';
|
||||||
|
import { getColumnLabel, Importer, TemplateField } from 'src/importer';
|
||||||
|
import { fyo } from 'src/initFyo';
|
||||||
|
import { getSavePath, saveData, selectFile } from 'src/utils/ipcCalls';
|
||||||
|
import { docsPathMap } from 'src/utils/misc';
|
||||||
|
import { docsPathRef } from 'src/utils/refs';
|
||||||
|
import { showMessageDialog } from 'src/utils/ui';
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
import Loading from '../components/Loading.vue';
|
||||||
|
|
||||||
|
type Action = Pick<BaseAction, 'condition' | 'component'> & {
|
||||||
|
action: Function;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ImportWizardData = {
|
||||||
|
showColumnPicker: boolean;
|
||||||
|
complete: boolean;
|
||||||
|
success: string[];
|
||||||
|
successOldName: string[];
|
||||||
|
failed: { name: string; error: Error }[];
|
||||||
|
file: null | { name: string; filePath: string; text: string };
|
||||||
|
nullOrImporter: null | Importer;
|
||||||
|
importType: string;
|
||||||
|
isMakingEntries: boolean;
|
||||||
|
percentLoading: number;
|
||||||
|
messageLoading: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: {
|
||||||
|
PageHeader,
|
||||||
|
FormControl,
|
||||||
|
Button,
|
||||||
|
DropdownWithActions,
|
||||||
|
Loading,
|
||||||
|
AutoComplete,
|
||||||
|
Data,
|
||||||
|
Modal,
|
||||||
|
FormHeader,
|
||||||
|
Check,
|
||||||
|
Select,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
showColumnPicker: false,
|
||||||
|
complete: false,
|
||||||
|
success: [],
|
||||||
|
successOldName: [],
|
||||||
|
failed: [],
|
||||||
|
file: null,
|
||||||
|
nullOrImporter: null,
|
||||||
|
importType: '',
|
||||||
|
isMakingEntries: false,
|
||||||
|
percentLoading: 0,
|
||||||
|
messageLoading: '',
|
||||||
|
} as ImportWizardData;
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
if (fyo.store.isDevelopment) {
|
||||||
|
// @ts-ignore
|
||||||
|
window.iw = this;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
columnCount(val) {
|
||||||
|
if (!this.hasImporter) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const possiblyAssigned = this.importer.assignedTemplateFields.length;
|
||||||
|
if (val >= this.importer.assignedTemplateFields.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = val; i < possiblyAssigned; i++) {
|
||||||
|
this.importer.assignedTemplateFields[i] = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
gridTemplateColumn(): string {
|
||||||
|
return `grid-template-columns: 4rem repeat(${this.columnCount}, 10rem)`;
|
||||||
|
},
|
||||||
|
duplicates(): string[] {
|
||||||
|
if (!this.hasImporter) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const dupes = new Set<string>();
|
||||||
|
const assignedSet = new Set<string>();
|
||||||
|
|
||||||
|
for (const key of this.importer.assignedTemplateFields) {
|
||||||
|
if (!key) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tf = this.importer.templateFieldsMap.get(key);
|
||||||
|
if (assignedSet.has(key) && tf) {
|
||||||
|
dupes.add(getColumnLabel(tf));
|
||||||
|
}
|
||||||
|
|
||||||
|
assignedSet.add(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(dupes);
|
||||||
|
},
|
||||||
|
requiredNotSelected(): string[] {
|
||||||
|
if (!this.hasImporter) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const assigned = new Set(this.importer.assignedTemplateFields);
|
||||||
|
return [...this.importer.templateFieldsMap.values()]
|
||||||
|
.filter((f) => f.required && !assigned.has(f.fieldKey))
|
||||||
|
.map((f) => getColumnLabel(f));
|
||||||
|
},
|
||||||
|
errorMessage(): string {
|
||||||
|
if (this.duplicates.length) {
|
||||||
|
return this.t`Duplicate columns found: ${this.duplicates.join(', ')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.requiredNotSelected.length) {
|
||||||
|
return this
|
||||||
|
.t`Required fields not selected: ${this.requiredNotSelected.join(
|
||||||
|
', '
|
||||||
|
)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
canImportData(): boolean {
|
||||||
|
if (!this.hasImporter) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.importer.valueMatrix.length > 0;
|
||||||
|
},
|
||||||
|
canSelectFile(): boolean {
|
||||||
|
return !this.file;
|
||||||
|
},
|
||||||
|
columnCount(): number {
|
||||||
|
if (!this.hasImporter) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.file) {
|
||||||
|
return this.numColumnsPicked;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.importer.valueMatrix.length) {
|
||||||
|
return this.importer.assignedTemplateFields.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.min(
|
||||||
|
this.importer.assignedTemplateFields.length,
|
||||||
|
this.importer.valueMatrix[0].length
|
||||||
|
);
|
||||||
|
},
|
||||||
|
columnIterator(): number[] {
|
||||||
|
return Array(this.columnCount)
|
||||||
|
.fill(null)
|
||||||
|
.map((_, i) => i);
|
||||||
|
},
|
||||||
|
hasImporter(): boolean {
|
||||||
|
return !!this.nullOrImporter;
|
||||||
|
},
|
||||||
|
numColumnsPicked(): number {
|
||||||
|
return [...this.importer.templateFieldsPicked.values()].filter(Boolean)
|
||||||
|
.length;
|
||||||
|
},
|
||||||
|
columnPickerFieldsMap(): Map<string, TemplateField[]> {
|
||||||
|
const map: Map<string, TemplateField[]> = new Map();
|
||||||
|
|
||||||
|
for (const value of this.importer.templateFieldsMap.values()) {
|
||||||
|
let label = value.schemaLabel;
|
||||||
|
if (value.parentSchemaChildField) {
|
||||||
|
label = `${value.parentSchemaChildField.label} (${value.schemaLabel})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!map.has(label)) {
|
||||||
|
map.set(label, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
map.get(label)!.push(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return map;
|
||||||
|
},
|
||||||
|
importer(): Importer {
|
||||||
|
if (!this.nullOrImporter) {
|
||||||
|
throw new ValidationError(this.t`Importer not set, reload tool`, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.nullOrImporter as Importer;
|
||||||
|
},
|
||||||
|
importableSchemaNames(): ModelNameEnum[] {
|
||||||
|
const importables = [
|
||||||
|
ModelNameEnum.SalesInvoice,
|
||||||
|
ModelNameEnum.PurchaseInvoice,
|
||||||
|
ModelNameEnum.Payment,
|
||||||
|
ModelNameEnum.Party,
|
||||||
|
ModelNameEnum.Item,
|
||||||
|
ModelNameEnum.JournalEntry,
|
||||||
|
ModelNameEnum.Tax,
|
||||||
|
ModelNameEnum.Account,
|
||||||
|
ModelNameEnum.Address,
|
||||||
|
ModelNameEnum.NumberSeries,
|
||||||
|
];
|
||||||
|
|
||||||
|
const hasInventory = fyo.doc.singles.AccountingSettings?.enableInventory;
|
||||||
|
if (hasInventory) {
|
||||||
|
importables.push(
|
||||||
|
ModelNameEnum.StockMovement,
|
||||||
|
ModelNameEnum.Shipment,
|
||||||
|
ModelNameEnum.PurchaseReceipt,
|
||||||
|
ModelNameEnum.Location
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return importables;
|
||||||
|
},
|
||||||
|
actions(): Action[] {
|
||||||
|
const actions: Action[] = [];
|
||||||
|
|
||||||
|
let selectFileLabel = this.t`Select File`;
|
||||||
|
if (this.file) {
|
||||||
|
selectFileLabel = this.t`Change File`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.canImportData) {
|
||||||
|
actions.push({
|
||||||
|
component: {
|
||||||
|
template: `<span>{{ "${selectFileLabel}" }}</span>`,
|
||||||
|
},
|
||||||
|
action: this.selectFile,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const pickColumnsAction = {
|
||||||
|
component: {
|
||||||
|
template: '<span>{{ t`Pick Import Columns` }}</span>',
|
||||||
|
},
|
||||||
|
action: () => (this.showColumnPicker = true),
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancelAction = {
|
||||||
|
component: {
|
||||||
|
template: '<span class="text-red-700" >{{ t`Cancel` }}</span>',
|
||||||
|
},
|
||||||
|
action: this.clear,
|
||||||
|
};
|
||||||
|
actions.push(pickColumnsAction, cancelAction);
|
||||||
|
|
||||||
|
return actions;
|
||||||
|
},
|
||||||
|
fileName(): string {
|
||||||
|
if (!this.file) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.file.name;
|
||||||
|
},
|
||||||
|
helperMessage(): string {
|
||||||
|
if (!this.importType) {
|
||||||
|
return this.t`Set an Import Type`;
|
||||||
|
} else if (!this.fileName) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.fileName;
|
||||||
|
},
|
||||||
|
isSubmittable(): boolean {
|
||||||
|
const schemaName = this.importer.schemaName;
|
||||||
|
return fyo.schemaMap[schemaName]?.isSubmittable ?? false;
|
||||||
|
},
|
||||||
|
gridColumnTitleDf(): OptionField {
|
||||||
|
const options: SelectOption[] = [];
|
||||||
|
for (const field of this.importer.templateFieldsMap.values()) {
|
||||||
|
const value = field.fieldKey;
|
||||||
|
if (!this.importer.templateFieldsPicked.get(value)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const label = getColumnLabel(field);
|
||||||
|
|
||||||
|
options.push({ value, label });
|
||||||
|
}
|
||||||
|
|
||||||
|
options.push({ value: '', label: this.t`None` });
|
||||||
|
return {
|
||||||
|
fieldname: 'col',
|
||||||
|
fieldtype: 'Select',
|
||||||
|
options,
|
||||||
|
} as OptionField;
|
||||||
|
},
|
||||||
|
pickedArray(): string[] {
|
||||||
|
return [...this.importer.templateFieldsPicked.entries()]
|
||||||
|
.filter(([_, picked]) => picked)
|
||||||
|
.map(([key, _]) => key);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
activated(): void {
|
||||||
|
docsPathRef.value = docsPathMap.ImportWizard ?? '';
|
||||||
|
},
|
||||||
|
deactivated(): void {
|
||||||
|
docsPathRef.value = '';
|
||||||
|
if (!this.complete) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.clear();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getFieldTitle(vmi: {
|
||||||
|
value?: DocValue;
|
||||||
|
rawValue?: RawValue;
|
||||||
|
error?: boolean;
|
||||||
|
}): string {
|
||||||
|
const title: string[] = [];
|
||||||
|
if (vmi.value != null) {
|
||||||
|
title.push(this.t`Value: ${String(vmi.value)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vmi.rawValue != null) {
|
||||||
|
title.push(this.t`Raw Value: ${String(vmi.rawValue)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vmi.error) {
|
||||||
|
title.push(this.t`Conversion Error`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!title.length) {
|
||||||
|
return this.t`No Value`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return title.join(', ');
|
||||||
|
},
|
||||||
|
pickColumn(fieldKey: string, value: boolean): void {
|
||||||
|
this.importer.templateFieldsPicked.set(fieldKey, value);
|
||||||
|
if (value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const idx = this.importer.assignedTemplateFields.findIndex(
|
||||||
|
(f) => f === fieldKey
|
||||||
|
);
|
||||||
|
|
||||||
|
if (idx >= 0) {
|
||||||
|
this.importer.assignedTemplateFields[idx] = null;
|
||||||
|
this.reassignTemplateFields();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
reassignTemplateFields(): void {
|
||||||
|
if (this.importer.valueMatrix.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const idx in this.importer.assignedTemplateFields) {
|
||||||
|
this.importer.assignedTemplateFields[idx] = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let idx = 0;
|
||||||
|
for (const [fieldKey, value] of this.importer.templateFieldsPicked) {
|
||||||
|
if (!value) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.importer.assignedTemplateFields[idx] = fieldKey;
|
||||||
|
idx += 1;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
showMe(): void {
|
||||||
|
const schemaName = this.importer.schemaName;
|
||||||
|
this.clear();
|
||||||
|
this.$router.push(`/list/${schemaName}`);
|
||||||
|
},
|
||||||
|
clear(): void {
|
||||||
|
this.file = null;
|
||||||
|
this.success = [];
|
||||||
|
this.successOldName = [];
|
||||||
|
this.failed = [];
|
||||||
|
this.nullOrImporter = null;
|
||||||
|
this.importType = '';
|
||||||
|
this.complete = false;
|
||||||
|
this.isMakingEntries = false;
|
||||||
|
this.percentLoading = 0;
|
||||||
|
this.messageLoading = '';
|
||||||
|
},
|
||||||
|
async saveTemplate(): Promise<void> {
|
||||||
|
const template = this.importer.getCSVTemplate();
|
||||||
|
const templateName = this.importType + ' ' + this.t`Template`;
|
||||||
|
const { canceled, filePath } = await getSavePath(templateName, 'csv');
|
||||||
|
|
||||||
|
if (canceled || !filePath) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await saveData(template, filePath);
|
||||||
|
},
|
||||||
|
async preImportValidations(): Promise<boolean> {
|
||||||
|
const message = this.t`Cannot Import`;
|
||||||
|
if (this.errorMessage.length) {
|
||||||
|
await showMessageDialog({
|
||||||
|
message,
|
||||||
|
detail: this.errorMessage,
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cellErrors = this.importer.checkCellErrors();
|
||||||
|
if (cellErrors.length) {
|
||||||
|
await showMessageDialog({
|
||||||
|
message,
|
||||||
|
detail: this.t`Following cells have errors: ${cellErrors.join(', ')}`,
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const absentLinks = await this.importer.checkLinks();
|
||||||
|
if (absentLinks.length) {
|
||||||
|
await showMessageDialog({
|
||||||
|
message,
|
||||||
|
detail: this.t`Following links do not exist: ${absentLinks
|
||||||
|
.map((l) => `(${l.schemaLabel}, ${l.name})`)
|
||||||
|
.join(', ')}`,
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
async importData(): Promise<void> {
|
||||||
|
const isValid = await this.preImportValidations();
|
||||||
|
if (!isValid || this.isMakingEntries || this.complete) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isMakingEntries = true;
|
||||||
|
this.importer.populateDocs();
|
||||||
|
|
||||||
|
const shouldSubmit = await this.askShouldSubmit();
|
||||||
|
|
||||||
|
let doneCount = 0;
|
||||||
|
for (const doc of this.importer.docs) {
|
||||||
|
this.setLoadingStatus(doneCount, this.importer.docs.length);
|
||||||
|
const oldName = doc.name ?? '';
|
||||||
|
try {
|
||||||
|
await doc.sync();
|
||||||
|
if (shouldSubmit) {
|
||||||
|
await doc.submit();
|
||||||
|
}
|
||||||
|
doneCount += 1;
|
||||||
|
|
||||||
|
this.success.push(doc.name!);
|
||||||
|
this.successOldName.push(oldName);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
this.failed.push({ name: doc.name!, error });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isMakingEntries = false;
|
||||||
|
this.complete = true;
|
||||||
|
},
|
||||||
|
async askShouldSubmit(): Promise<boolean> {
|
||||||
|
if (!this.fyo.schemaMap[this.importType]?.isSubmittable) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let shouldSubmit = false;
|
||||||
|
await showMessageDialog({
|
||||||
|
message: this.t`Should entries be submitted after syncing?`,
|
||||||
|
buttons: [
|
||||||
|
{
|
||||||
|
label: this.t`Yes`,
|
||||||
|
action() {
|
||||||
|
shouldSubmit = true;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: this.t`No`,
|
||||||
|
action() {},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
return shouldSubmit;
|
||||||
|
},
|
||||||
|
clearSuccessfullyImportedEntries() {
|
||||||
|
const schemaName = this.importer.schemaName;
|
||||||
|
const nameFieldKey = `${schemaName}.name`;
|
||||||
|
const nameIndex = this.importer.assignedTemplateFields.findIndex(
|
||||||
|
(n) => n === nameFieldKey
|
||||||
|
);
|
||||||
|
|
||||||
|
const failedEntriesValueMatrix = this.importer.valueMatrix.filter(
|
||||||
|
(row) => {
|
||||||
|
const value = row[nameIndex].value;
|
||||||
|
if (typeof value !== 'string') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !this.successOldName.includes(value);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
this.setImportType(this.importType);
|
||||||
|
this.importer.valueMatrix = failedEntriesValueMatrix;
|
||||||
|
},
|
||||||
|
setImportType(importType: string): void {
|
||||||
|
this.clear();
|
||||||
|
if (!importType) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.importType = importType;
|
||||||
|
this.nullOrImporter = new Importer(importType, fyo);
|
||||||
|
},
|
||||||
|
setLoadingStatus(entriesMade: number, totalEntries: number): void {
|
||||||
|
this.percentLoading = entriesMade / totalEntries;
|
||||||
|
this.messageLoading = this.isMakingEntries
|
||||||
|
? `${entriesMade} entries made out of ${totalEntries}...`
|
||||||
|
: '';
|
||||||
|
},
|
||||||
|
async selectFile(): Promise<void> {
|
||||||
|
const options = {
|
||||||
|
title: this.t`Select File`,
|
||||||
|
filters: [{ name: 'CSV', extensions: ['csv'] }],
|
||||||
|
};
|
||||||
|
|
||||||
|
const { success, canceled, filePath, data, name } = await selectFile(
|
||||||
|
options
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!success && !canceled) {
|
||||||
|
await showMessageDialog({
|
||||||
|
message: this.t`File selection failed.`,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!success || canceled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = new TextDecoder().decode(data);
|
||||||
|
const isValid = this.importer.selectFile(text);
|
||||||
|
if (!isValid) {
|
||||||
|
await showMessageDialog({
|
||||||
|
message: this.t`Bad import data`,
|
||||||
|
detail: this.t`Could not read file`,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.file = {
|
||||||
|
name,
|
||||||
|
filePath,
|
||||||
|
text,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.index-cell {
|
||||||
|
@apply flex pe-4 justify-end items-center border-e bg-white sticky left-0 -my-4 text-gray-600;
|
||||||
|
}
|
||||||
|
</style>
|
@ -273,7 +273,6 @@
|
|||||||
<Transition name="quickedit">
|
<Transition name="quickedit">
|
||||||
<QuickEditForm
|
<QuickEditForm
|
||||||
v-if="quickEditDoc && !linked"
|
v-if="quickEditDoc && !linked"
|
||||||
class="w-quick-edit"
|
|
||||||
:name="quickEditDoc.name"
|
:name="quickEditDoc.name"
|
||||||
:show-name="false"
|
:show-name="false"
|
||||||
:show-save="false"
|
:show-save="false"
|
||||||
@ -313,8 +312,8 @@ import StatusBadge from 'src/components/StatusBadge.vue';
|
|||||||
import LinkedEntryWidget from 'src/components/Widgets/LinkedEntryWidget.vue';
|
import LinkedEntryWidget from 'src/components/Widgets/LinkedEntryWidget.vue';
|
||||||
import { fyo } from 'src/initFyo';
|
import { fyo } from 'src/initFyo';
|
||||||
import { docsPathMap } from 'src/utils/misc';
|
import { docsPathMap } from 'src/utils/misc';
|
||||||
|
import { docsPathRef, focusedDocsRef } from 'src/utils/refs';
|
||||||
import {
|
import {
|
||||||
docsPath,
|
|
||||||
getGroupedActionsForDoc,
|
getGroupedActionsForDoc,
|
||||||
routeTo,
|
routeTo,
|
||||||
showMessageDialog,
|
showMessageDialog,
|
||||||
@ -339,6 +338,7 @@ export default {
|
|||||||
LinkedEntryWidget,
|
LinkedEntryWidget,
|
||||||
Barcode,
|
Barcode,
|
||||||
},
|
},
|
||||||
|
inject: ['shortcuts'],
|
||||||
provide() {
|
provide() {
|
||||||
return {
|
return {
|
||||||
schemaName: this.schemaName,
|
schemaName: this.schemaName,
|
||||||
@ -455,14 +455,17 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
activated() {
|
activated() {
|
||||||
docsPath.value = docsPathMap[this.schemaName];
|
docsPathRef.value = docsPathMap[this.schemaName];
|
||||||
|
focusedDocsRef.add(this.doc);
|
||||||
},
|
},
|
||||||
deactivated() {
|
deactivated() {
|
||||||
docsPath.value = '';
|
docsPathRef.value = '';
|
||||||
|
focusedDocsRef.delete(this.doc);
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
try {
|
try {
|
||||||
this.doc = await fyo.doc.getDoc(this.schemaName, this.name);
|
this.doc = await fyo.doc.getDoc(this.schemaName, this.name);
|
||||||
|
focusedDocsRef.add(this.doc);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof fyo.errors.NotFoundError) {
|
if (error instanceof fyo.errors.NotFoundError) {
|
||||||
routeTo(`/list/${this.schemaName}`);
|
routeTo(`/list/${this.schemaName}`);
|
||||||
|
@ -148,8 +148,8 @@ import FormHeader from 'src/components/FormHeader.vue';
|
|||||||
import StatusBadge from 'src/components/StatusBadge.vue';
|
import StatusBadge from 'src/components/StatusBadge.vue';
|
||||||
import { fyo } from 'src/initFyo';
|
import { fyo } from 'src/initFyo';
|
||||||
import { docsPathMap } from 'src/utils/misc';
|
import { docsPathMap } from 'src/utils/misc';
|
||||||
|
import { docsPathRef, focusedDocsRef } from 'src/utils/refs';
|
||||||
import {
|
import {
|
||||||
docsPath,
|
|
||||||
getGroupedActionsForDoc,
|
getGroupedActionsForDoc,
|
||||||
routeTo,
|
routeTo,
|
||||||
showMessageDialog,
|
showMessageDialog,
|
||||||
@ -182,14 +182,17 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
activated() {
|
activated() {
|
||||||
docsPath.value = docsPathMap.JournalEntry;
|
docsPathRef.value = docsPathMap.JournalEntry;
|
||||||
|
focusedDocsRef.add(this.doc);
|
||||||
},
|
},
|
||||||
deactivated() {
|
deactivated() {
|
||||||
docsPath.value = '';
|
docsPathRef.value = '';
|
||||||
|
focusedDocsRef.delete(this.doc);
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
try {
|
try {
|
||||||
this.doc = await fyo.doc.getDoc(this.schemaName, this.name);
|
this.doc = await fyo.doc.getDoc(this.schemaName, this.name);
|
||||||
|
focusedDocsRef.add(this.doc);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof fyo.errors.NotFoundError) {
|
if (error instanceof fyo.errors.NotFoundError) {
|
||||||
routeTo(`/list/${this.schemaName}`);
|
routeTo(`/list/${this.schemaName}`);
|
||||||
|
@ -51,7 +51,8 @@ import {
|
|||||||
docsPathMap,
|
docsPathMap,
|
||||||
getCreateFiltersFromListViewFilters,
|
getCreateFiltersFromListViewFilters,
|
||||||
} from 'src/utils/misc';
|
} from 'src/utils/misc';
|
||||||
import { docsPath, openQuickEdit, routeTo } from 'src/utils/ui';
|
import { docsPathRef } from 'src/utils/refs';
|
||||||
|
import { openQuickEdit, routeTo } from 'src/utils/ui';
|
||||||
import List from './List.vue';
|
import List from './List.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -82,14 +83,14 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.listConfig = getListConfig(this.schemaName);
|
this.listConfig = getListConfig(this.schemaName);
|
||||||
docsPath.value = docsPathMap[this.schemaName] ?? docsPathMap.Entries;
|
docsPathRef.value = docsPathMap[this.schemaName] ?? docsPathMap.Entries;
|
||||||
|
|
||||||
if (this.fyo.store.isDevelopment) {
|
if (this.fyo.store.isDevelopment) {
|
||||||
window.lv = this;
|
window.lv = this;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
deactivated() {
|
deactivated() {
|
||||||
docsPath.value = '';
|
docsPathRef.value = '';
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
updatedData(listFilters) {
|
updatedData(listFilters) {
|
||||||
|
@ -105,6 +105,7 @@ import StatusBadge from 'src/components/StatusBadge.vue';
|
|||||||
import TwoColumnForm from 'src/components/TwoColumnForm.vue';
|
import TwoColumnForm from 'src/components/TwoColumnForm.vue';
|
||||||
import { fyo } from 'src/initFyo';
|
import { fyo } from 'src/initFyo';
|
||||||
import { getQuickEditWidget } from 'src/utils/quickEditWidgets';
|
import { getQuickEditWidget } from 'src/utils/quickEditWidgets';
|
||||||
|
import { focusedDocsRef } from 'src/utils/refs';
|
||||||
import { getActionsForDoc, openQuickEdit } from 'src/utils/ui';
|
import { getActionsForDoc, openQuickEdit } from 'src/utils/ui';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -131,6 +132,7 @@ export default {
|
|||||||
DropdownWithActions,
|
DropdownWithActions,
|
||||||
},
|
},
|
||||||
emits: ['close'],
|
emits: ['close'],
|
||||||
|
inject: ['shortcuts'],
|
||||||
provide() {
|
provide() {
|
||||||
return {
|
return {
|
||||||
schemaName: this.schemaName,
|
schemaName: this.schemaName,
|
||||||
@ -147,17 +149,26 @@ export default {
|
|||||||
statusText: null,
|
statusText: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
mounted() {
|
async mounted() {
|
||||||
if (this.defaults) {
|
if (this.defaults) {
|
||||||
this.values = JSON.parse(this.defaults);
|
this.values = JSON.parse(this.defaults);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.fetchFieldsAndDoc();
|
||||||
|
focusedDocsRef.add(this.doc);
|
||||||
|
|
||||||
if (fyo.store.isDevelopment) {
|
if (fyo.store.isDevelopment) {
|
||||||
window.qef = this;
|
window.qef = this;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async created() {
|
activated() {
|
||||||
await this.fetchFieldsAndDoc();
|
focusedDocsRef.add(this.doc);
|
||||||
|
},
|
||||||
|
deactivated() {
|
||||||
|
focusedDocsRef.delete(this.doc);
|
||||||
|
},
|
||||||
|
unmounted() {
|
||||||
|
focusedDocsRef.delete(this.doc);
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
isChild() {
|
isChild() {
|
||||||
|
@ -46,7 +46,7 @@ import PageHeader from 'src/components/PageHeader.vue';
|
|||||||
import ListReport from 'src/components/Report/ListReport.vue';
|
import ListReport from 'src/components/Report/ListReport.vue';
|
||||||
import { fyo } from 'src/initFyo';
|
import { fyo } from 'src/initFyo';
|
||||||
import { docsPathMap } from 'src/utils/misc';
|
import { docsPathMap } from 'src/utils/misc';
|
||||||
import { docsPath } from 'src/utils/ui';
|
import { docsPathRef } from 'src/utils/refs';
|
||||||
import { defineComponent } from 'vue';
|
import { defineComponent } from 'vue';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
@ -70,7 +70,7 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
components: { PageHeader, FormControl, ListReport, DropdownWithActions },
|
components: { PageHeader, FormControl, ListReport, DropdownWithActions },
|
||||||
async activated() {
|
async activated() {
|
||||||
docsPath.value = docsPathMap[this.reportClassName] ?? docsPathMap.Reports;
|
docsPathRef.value = docsPathMap[this.reportClassName] ?? docsPathMap.Reports;
|
||||||
await this.setReportData();
|
await this.setReportData();
|
||||||
|
|
||||||
const filters = JSON.parse(this.defaultFilters);
|
const filters = JSON.parse(this.defaultFilters);
|
||||||
@ -88,7 +88,7 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
deactivated() {
|
deactivated() {
|
||||||
docsPath.value = '';
|
docsPathRef.value = '';
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
title() {
|
title() {
|
||||||
|
@ -49,7 +49,8 @@ import Row from 'src/components/Row.vue';
|
|||||||
import StatusBadge from 'src/components/StatusBadge.vue';
|
import StatusBadge from 'src/components/StatusBadge.vue';
|
||||||
import { fyo } from 'src/initFyo';
|
import { fyo } from 'src/initFyo';
|
||||||
import { docsPathMap } from 'src/utils/misc';
|
import { docsPathMap } from 'src/utils/misc';
|
||||||
import { docsPath, showToast } from 'src/utils/ui';
|
import { docsPathRef } from 'src/utils/refs';
|
||||||
|
import { showToast } from 'src/utils/ui';
|
||||||
import { IPC_MESSAGES } from 'utils/messages';
|
import { IPC_MESSAGES } from 'utils/messages';
|
||||||
import { h, markRaw } from 'vue';
|
import { h, markRaw } from 'vue';
|
||||||
import TabBase from './TabBase.vue';
|
import TabBase from './TabBase.vue';
|
||||||
@ -112,10 +113,10 @@ export default {
|
|||||||
},
|
},
|
||||||
activated() {
|
activated() {
|
||||||
this.setActiveTab();
|
this.setActiveTab();
|
||||||
docsPath.value = docsPathMap.Settings;
|
docsPathRef.value = docsPathMap.Settings;
|
||||||
},
|
},
|
||||||
deactivated() {
|
deactivated() {
|
||||||
docsPath.value = '';
|
docsPathRef.value = '';
|
||||||
if (this.fieldsChanged.length === 0) {
|
if (this.fieldsChanged.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { ModelNameEnum } from 'models/types';
|
import { ModelNameEnum } from 'models/types';
|
||||||
import ChartOfAccounts from 'src/pages/ChartOfAccounts.vue';
|
import ChartOfAccounts from 'src/pages/ChartOfAccounts.vue';
|
||||||
import Dashboard from 'src/pages/Dashboard/Dashboard.vue';
|
import Dashboard from 'src/pages/Dashboard/Dashboard.vue';
|
||||||
import DataImport from 'src/pages/DataImport.vue';
|
import ImportWizard from 'src/pages/ImportWizard.vue';
|
||||||
import GeneralForm from 'src/pages/GeneralForm.vue';
|
import GeneralForm from 'src/pages/GeneralForm.vue';
|
||||||
import GetStarted from 'src/pages/GetStarted.vue';
|
import GetStarted from 'src/pages/GetStarted.vue';
|
||||||
import InvoiceForm from 'src/pages/InvoiceForm.vue';
|
import InvoiceForm from 'src/pages/InvoiceForm.vue';
|
||||||
@ -138,9 +138,9 @@ const routes: RouteRecordRaw[] = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/data-import',
|
path: '/import-wizard',
|
||||||
name: 'Data Import',
|
name: 'Import Wizard',
|
||||||
component: DataImport,
|
component: ImportWizard,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/settings',
|
path: '/settings',
|
||||||
|
@ -82,6 +82,7 @@ input[type='number']::-webkit-inner-spin-button {
|
|||||||
|
|
||||||
.w-quick-edit {
|
.w-quick-edit {
|
||||||
width: var(--w-quick-edit);
|
width: var(--w-quick-edit);
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.h-form {
|
.h-form {
|
||||||
|
@ -1,12 +1,18 @@
|
|||||||
import { Fyo } from 'fyo';
|
import { Fyo } from 'fyo';
|
||||||
import { RawValueMap } from 'fyo/core/types';
|
import { RawValueMap } from 'fyo/core/types';
|
||||||
import { Field, FieldTypeEnum, RawValue, TargetField } from 'schemas/types';
|
import {
|
||||||
|
Field,
|
||||||
|
FieldType,
|
||||||
|
FieldTypeEnum,
|
||||||
|
RawValue,
|
||||||
|
TargetField,
|
||||||
|
} from 'schemas/types';
|
||||||
import { generateCSV } from 'utils/csvParser';
|
import { generateCSV } from 'utils/csvParser';
|
||||||
import { GetAllOptions, QueryFilter } from 'utils/db/types';
|
import { GetAllOptions, QueryFilter } from 'utils/db/types';
|
||||||
import { getMapFromList, safeParseFloat } from 'utils/index';
|
import { getMapFromList, safeParseFloat } from 'utils/index';
|
||||||
import { ExportField, ExportTableField } from './types';
|
import { ExportField, ExportTableField } from './types';
|
||||||
|
|
||||||
const excludedFieldTypes = [
|
const excludedFieldTypes: FieldType[] = [
|
||||||
FieldTypeEnum.AttachImage,
|
FieldTypeEnum.AttachImage,
|
||||||
FieldTypeEnum.Attachment,
|
FieldTypeEnum.Attachment,
|
||||||
];
|
];
|
||||||
@ -26,7 +32,7 @@ export function getExportFields(
|
|||||||
.filter((f) => !f.computed && f.label && !exclude.includes(f.fieldname))
|
.filter((f) => !f.computed && f.label && !exclude.includes(f.fieldname))
|
||||||
.map((field) => {
|
.map((field) => {
|
||||||
const { fieldname, label } = field;
|
const { fieldname, label } = field;
|
||||||
const fieldtype = field.fieldtype as FieldTypeEnum;
|
const fieldtype = field.fieldtype as FieldType;
|
||||||
return {
|
return {
|
||||||
fieldname,
|
fieldname,
|
||||||
fieldtype,
|
fieldtype,
|
||||||
@ -323,7 +329,7 @@ async function getChildTableData(
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
function convertRawPesaToFloat(data: RawValueMap[], fields: Field[]) {
|
function convertRawPesaToFloat(data: RawValueMap[], fields: ExportField[]) {
|
||||||
const currencyFields = fields.filter(
|
const currencyFields = fields.filter(
|
||||||
(f) => f.fieldtype === FieldTypeEnum.Currency
|
(f) => f.fieldtype === FieldTypeEnum.Currency
|
||||||
);
|
);
|
||||||
|
@ -3,6 +3,7 @@ import { DEFAULT_LANGUAGE } from 'fyo/utils/consts';
|
|||||||
import { setLanguageMapOnTranslationString } from 'fyo/utils/translation';
|
import { setLanguageMapOnTranslationString } from 'fyo/utils/translation';
|
||||||
import { fyo } from 'src/initFyo';
|
import { fyo } from 'src/initFyo';
|
||||||
import { IPC_ACTIONS, IPC_MESSAGES } from 'utils/messages';
|
import { IPC_ACTIONS, IPC_MESSAGES } from 'utils/messages';
|
||||||
|
import { systemLanguageRef } from './refs';
|
||||||
import { showToast } from './ui';
|
import { showToast } from './ui';
|
||||||
|
|
||||||
// Language: Language Code in books/translations
|
// Language: Language Code in books/translations
|
||||||
@ -42,6 +43,7 @@ export async function setLanguageMap(
|
|||||||
|
|
||||||
if (success && !usingDefault) {
|
if (success && !usingDefault) {
|
||||||
fyo.config.set('language', language);
|
fyo.config.set('language', language);
|
||||||
|
systemLanguageRef.value = language;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!dontReload && success && initLanguage !== oldLanguage) {
|
if (!dontReload && success && initLanguage !== oldLanguage) {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { Fyo } from 'fyo';
|
import { Fyo } from 'fyo';
|
||||||
import { ConfigFile, ConfigKeys } from 'fyo/core/types';
|
import { ConfigFile, ConfigKeys } from 'fyo/core/types';
|
||||||
|
import { Doc } from 'fyo/model/doc';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { SetupWizard } from 'models/baseModels/SetupWizard/SetupWizard';
|
import { SetupWizard } from 'models/baseModels/SetupWizard/SetupWizard';
|
||||||
import { ModelNameEnum } from 'models/types';
|
import { ModelNameEnum } from 'models/types';
|
||||||
@ -117,7 +118,7 @@ export const docsPathMap: Record<string, string | undefined> = {
|
|||||||
// Miscellaneous
|
// Miscellaneous
|
||||||
Search: 'miscellaneous/search',
|
Search: 'miscellaneous/search',
|
||||||
NumberSeries: 'miscellaneous/number-series',
|
NumberSeries: 'miscellaneous/number-series',
|
||||||
DataImport: 'miscellaneous/data-import',
|
ImportWizard: 'miscellaneous/import-wizard',
|
||||||
Settings: 'miscellaneous/settings',
|
Settings: 'miscellaneous/settings',
|
||||||
ChartOfAccounts: 'miscellaneous/chart-of-accounts',
|
ChartOfAccounts: 'miscellaneous/chart-of-accounts',
|
||||||
};
|
};
|
||||||
@ -160,3 +161,45 @@ export function getCreateFiltersFromListViewFilters(filters: QueryFilter) {
|
|||||||
|
|
||||||
return createFilters;
|
return createFilters;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class FocusedDocContextSet {
|
||||||
|
set: Doc[];
|
||||||
|
constructor() {
|
||||||
|
this.set = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
add(doc: unknown) {
|
||||||
|
if (!(doc instanceof Doc)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const index = this.findIndex(doc);
|
||||||
|
if (index !== -1) {
|
||||||
|
this.delete(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.set.push(doc);
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(index: Doc | number) {
|
||||||
|
if (typeof index !== 'number') {
|
||||||
|
index = this.findIndex(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (index === -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.set = this.set.filter((_, i) => i !== index);
|
||||||
|
}
|
||||||
|
|
||||||
|
last() {
|
||||||
|
return this.set.at(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
findIndex(doc: Doc) {
|
||||||
|
return this.set.findIndex(
|
||||||
|
(d) => d.name === doc.name && d.schemaName === doc.schemaName
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
8
src/utils/refs.ts
Normal file
8
src/utils/refs.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { reactive, ref } from 'vue';
|
||||||
|
import { FocusedDocContextSet } from './misc';
|
||||||
|
|
||||||
|
export const docsPathRef = ref<string>('');
|
||||||
|
export const systemLanguageRef = ref<string>('');
|
||||||
|
export const focusedDocsRef = reactive<FocusedDocContextSet>(
|
||||||
|
new FocusedDocContextSet()
|
||||||
|
);
|
@ -323,8 +323,8 @@ function getSetupList(): SearchItem[] {
|
|||||||
group: 'Page',
|
group: 'Page',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t`Data Import`,
|
label: t`Import Wizard`,
|
||||||
route: '/data-import',
|
route: '/import-wizard',
|
||||||
group: 'Page',
|
group: 'Page',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -594,10 +594,7 @@ export class Search {
|
|||||||
keys.sort((a, b) => safeParseFloat(b) - safeParseFloat(a));
|
keys.sort((a, b) => safeParseFloat(b) - safeParseFloat(a));
|
||||||
const array: SearchItems = [];
|
const array: SearchItems = [];
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
const keywords = groupedKeywords[key];
|
const keywords = groupedKeywords[key] ?? [];
|
||||||
if (!keywords?.length) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._pushDocSearchItems(keywords, array, input);
|
this._pushDocSearchItems(keywords, array, input);
|
||||||
if (key === '0') {
|
if (key === '0') {
|
||||||
|
78
src/utils/shortcuts.ts
Normal file
78
src/utils/shortcuts.ts
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import { t } from 'fyo';
|
||||||
|
import type { Doc } from 'fyo/model/doc';
|
||||||
|
import { fyo } from 'src/initFyo';
|
||||||
|
import router from 'src/router';
|
||||||
|
import { focusedDocsRef } from './refs';
|
||||||
|
import { showMessageDialog } from './ui';
|
||||||
|
import { Shortcuts } from './vueUtils';
|
||||||
|
|
||||||
|
export function setGlobalShortcuts(shortcuts: Shortcuts) {
|
||||||
|
/**
|
||||||
|
* PMod : if macOS then Meta (⌘) else Ctrl, both Left and Right
|
||||||
|
*
|
||||||
|
* Backspace : Go to the previous page
|
||||||
|
* PMod + S : Save or Submit focused doc if possible
|
||||||
|
* PMod + Backspace : Cancel or Delete focused doc if possible
|
||||||
|
*/
|
||||||
|
shortcuts.set(['Backspace'], async () => {
|
||||||
|
if (document.body !== document.activeElement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
router.back();
|
||||||
|
});
|
||||||
|
|
||||||
|
shortcuts.pmod.set(['KeyS'], async () => {
|
||||||
|
const doc = focusedDocsRef.last();
|
||||||
|
if (!doc) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (doc.canSave) {
|
||||||
|
await showDocStateChangeMessageDialog(doc, 'sync');
|
||||||
|
} else if (doc.canSubmit) {
|
||||||
|
await showDocStateChangeMessageDialog(doc, 'submit');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
shortcuts.pmod.set(['Backspace'], async () => {
|
||||||
|
const doc = focusedDocsRef.last();
|
||||||
|
if (!doc) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (doc.canCancel) {
|
||||||
|
await showDocStateChangeMessageDialog(doc, 'cancel');
|
||||||
|
} else if (doc.canDelete) {
|
||||||
|
await showDocStateChangeMessageDialog(doc, 'delete');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function showDocStateChangeMessageDialog(
|
||||||
|
doc: Doc,
|
||||||
|
state: 'sync' | 'submit' | 'cancel' | 'delete'
|
||||||
|
) {
|
||||||
|
const label = fyo.schemaMap[doc.schemaName]?.label ?? t`Doc`;
|
||||||
|
const name = doc.name ?? '';
|
||||||
|
const message =
|
||||||
|
{ sync: t`Save`, submit: t`Submit`, cancel: t`Cancel`, delete: t`Delete` }[
|
||||||
|
state
|
||||||
|
] + ` ${label} ${name}`;
|
||||||
|
|
||||||
|
await showMessageDialog({
|
||||||
|
message,
|
||||||
|
buttons: [
|
||||||
|
{
|
||||||
|
label: t`Yes`,
|
||||||
|
async action() {
|
||||||
|
await doc[state]();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t`No`,
|
||||||
|
action() {},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
@ -268,9 +268,9 @@ async function getCompleteSidebar(): Promise<SidebarConfig> {
|
|||||||
schemaName: 'Tax',
|
schemaName: 'Tax',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t`Data Import`,
|
label: t`Import Wizard`,
|
||||||
name: 'data-import',
|
name: 'import-wizard',
|
||||||
route: '/data-import',
|
route: '/import-wizard',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t`Settings`,
|
label: t`Settings`,
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Doc } from "fyo/model/doc";
|
import { Doc } from "fyo/model/doc";
|
||||||
import { FieldTypeEnum } from "schemas/types";
|
import { FieldType } from "schemas/types";
|
||||||
import { QueryFilter } from "utils/db/types";
|
import { QueryFilter } from "utils/db/types";
|
||||||
|
|
||||||
export interface MessageDialogButton {
|
export interface MessageDialogButton {
|
||||||
@ -58,7 +58,7 @@ export interface SidebarItem {
|
|||||||
|
|
||||||
export interface ExportField {
|
export interface ExportField {
|
||||||
fieldname: string;
|
fieldname: string;
|
||||||
fieldtype: FieldTypeEnum;
|
fieldtype: FieldType;
|
||||||
label: string;
|
label: string;
|
||||||
export: boolean;
|
export: boolean;
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
import { ipcRenderer } from 'electron';
|
import { ipcRenderer } from 'electron';
|
||||||
import { t } from 'fyo';
|
import { t } from 'fyo';
|
||||||
import { Doc } from 'fyo/model/doc';
|
import type { Doc } from 'fyo/model/doc';
|
||||||
import { Action } from 'fyo/model/types';
|
import { Action } from 'fyo/model/types';
|
||||||
import { getActions } from 'fyo/utils';
|
import { getActions } from 'fyo/utils';
|
||||||
import { getDbError, LinkValidationError, ValueError } from 'fyo/utils/errors';
|
import { getDbError, LinkValidationError, ValueError } from 'fyo/utils/errors';
|
||||||
@ -23,8 +23,6 @@ import {
|
|||||||
ToastOptions,
|
ToastOptions,
|
||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
export const docsPath = ref('');
|
|
||||||
|
|
||||||
export async function openQuickEdit({
|
export async function openQuickEdit({
|
||||||
doc,
|
doc,
|
||||||
schemaName,
|
schemaName,
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { onMounted, onUnmounted, Ref, ref, watch } from 'vue';
|
import { onMounted, onUnmounted, reactive, ref, watch } from 'vue';
|
||||||
|
|
||||||
interface Keys {
|
interface ModMap {
|
||||||
pressed: Set<string>;
|
|
||||||
alt: boolean;
|
alt: boolean;
|
||||||
ctrl: boolean;
|
ctrl: boolean;
|
||||||
meta: boolean;
|
meta: boolean;
|
||||||
@ -9,13 +8,27 @@ interface Keys {
|
|||||||
repeat: boolean;
|
repeat: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Shortcuts {
|
type Mod = keyof ModMap;
|
||||||
keys: Ref<Keys>;
|
|
||||||
shortcuts: Map<string, Function>;
|
|
||||||
|
|
||||||
constructor(keys?: Ref<Keys>) {
|
interface Keys extends ModMap {
|
||||||
|
pressed: Set<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ShortcutFunction = () => void;
|
||||||
|
|
||||||
|
const mods: Readonly<Mod[]> = ['alt', 'ctrl', 'meta', 'repeat', 'shift'];
|
||||||
|
|
||||||
|
export class Shortcuts {
|
||||||
|
keys: Keys;
|
||||||
|
isMac: boolean;
|
||||||
|
shortcuts: Map<string, ShortcutFunction>;
|
||||||
|
modMap: Partial<Record<Mod, boolean>>;
|
||||||
|
|
||||||
|
constructor(keys?: Keys) {
|
||||||
|
this.modMap = {};
|
||||||
this.keys = keys ?? useKeys();
|
this.keys = keys ?? useKeys();
|
||||||
this.shortcuts = new Map();
|
this.shortcuts = new Map();
|
||||||
|
this.isMac = getIsMac();
|
||||||
|
|
||||||
watch(this.keys, (keys) => {
|
watch(this.keys, (keys) => {
|
||||||
this.#trigger(keys);
|
this.#trigger(keys);
|
||||||
@ -23,17 +36,22 @@ export class Shortcuts {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#trigger(keys: Keys) {
|
#trigger(keys: Keys) {
|
||||||
const key = Array.from(keys.pressed).sort().join('+');
|
const key = this.getKey(Array.from(keys.pressed), keys);
|
||||||
this.shortcuts.get(key)?.();
|
this.shortcuts.get(key)?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
has(shortcut: string[]) {
|
has(shortcut: string[]) {
|
||||||
const key = shortcut.sort().join('+');
|
const key = this.getKey(shortcut);
|
||||||
return this.shortcuts.has(key);
|
return this.shortcuts.has(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
set(shortcut: string[], callback: Function, removeIfSet: boolean = true) {
|
set(
|
||||||
const key = shortcut.sort().join('+');
|
shortcut: string[],
|
||||||
|
callback: ShortcutFunction,
|
||||||
|
removeIfSet: boolean = true
|
||||||
|
) {
|
||||||
|
const key = this.getKey(shortcut);
|
||||||
|
|
||||||
if (removeIfSet) {
|
if (removeIfSet) {
|
||||||
this.shortcuts.delete(key);
|
this.shortcuts.delete(key);
|
||||||
}
|
}
|
||||||
@ -46,13 +64,68 @@ export class Shortcuts {
|
|||||||
}
|
}
|
||||||
|
|
||||||
delete(shortcut: string[]) {
|
delete(shortcut: string[]) {
|
||||||
const key = shortcut.sort().join('+');
|
const key = this.getKey(shortcut);
|
||||||
this.shortcuts.delete(key);
|
this.shortcuts.delete(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getKey(shortcut: string[], modMap?: Partial<ModMap>): string {
|
||||||
|
const _modMap = modMap || this.modMap;
|
||||||
|
this.modMap = {};
|
||||||
|
|
||||||
|
const shortcutString = shortcut.sort().join('+');
|
||||||
|
const modString = mods.filter((k) => _modMap[k]).join('+');
|
||||||
|
if (shortcutString && modString) {
|
||||||
|
return modString + '+' + shortcutString;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!modString) {
|
||||||
|
return shortcutString;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!shortcutString) {
|
||||||
|
return modString;
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
get alt() {
|
||||||
|
this.modMap['alt'] = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
get ctrl() {
|
||||||
|
this.modMap['ctrl'] = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
get meta() {
|
||||||
|
this.modMap['meta'] = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
get shift() {
|
||||||
|
this.modMap['shift'] = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
get repeat() {
|
||||||
|
this.modMap['repeat'] = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
get pmod() {
|
||||||
|
if (this.isMac) {
|
||||||
|
return this.meta;
|
||||||
|
} else {
|
||||||
|
return this.ctrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useKeys() {
|
export function useKeys() {
|
||||||
const keys: Ref<Keys> = ref({
|
const isMac = getIsMac();
|
||||||
|
const keys: Keys = reactive({
|
||||||
pressed: new Set<string>(),
|
pressed: new Set<string>(),
|
||||||
alt: false,
|
alt: false,
|
||||||
ctrl: false,
|
ctrl: false,
|
||||||
@ -62,21 +135,32 @@ export function useKeys() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const keydownListener = (e: KeyboardEvent) => {
|
const keydownListener = (e: KeyboardEvent) => {
|
||||||
keys.value.pressed.add(e.code);
|
keys.alt = e.altKey;
|
||||||
keys.value.alt = e.altKey;
|
keys.ctrl = e.ctrlKey;
|
||||||
keys.value.ctrl = e.ctrlKey;
|
keys.meta = e.metaKey;
|
||||||
keys.value.meta = e.metaKey;
|
keys.shift = e.shiftKey;
|
||||||
keys.value.shift = e.shiftKey;
|
keys.repeat = e.repeat;
|
||||||
keys.value.repeat = e.repeat;
|
|
||||||
|
const { code } = e;
|
||||||
|
if (
|
||||||
|
code.startsWith('Alt') ||
|
||||||
|
code.startsWith('Control') ||
|
||||||
|
code.startsWith('Meta') ||
|
||||||
|
code.startsWith('Shift')
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
keys.pressed.add(code);
|
||||||
};
|
};
|
||||||
|
|
||||||
const keyupListener = (e: KeyboardEvent) => {
|
const keyupListener = (e: KeyboardEvent) => {
|
||||||
keys.value.pressed.delete(e.code);
|
const { code } = e;
|
||||||
|
if (code.startsWith('Meta') && isMac) {
|
||||||
// Key up won't trigger on macOS for other keys.
|
return keys.pressed.clear();
|
||||||
if (e.code === 'MetaLeft') {
|
|
||||||
keys.value.pressed.clear();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
keys.pressed.delete(code);
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@ -110,10 +194,6 @@ export function useMouseLocation() {
|
|||||||
return loc;
|
return loc;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getModKeyCode(platform: 'Windows' | 'Linux' | 'Mac') {
|
function getIsMac() {
|
||||||
if (platform === 'Mac') {
|
return navigator.userAgent.indexOf('Mac') !== -1;
|
||||||
return 'MetaLeft';
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'CtrlLeft';
|
|
||||||
}
|
}
|
||||||
|
13
tests/items.csv
Normal file
13
tests/items.csv
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
"Item Name",Description,"Unit Type",Type,For,"Sales Acc.","Purchase Acc.",Tax,Rate,HSN/SAC,Barcode,"Track Item","Created By","Modified By",Created,Modified
|
||||||
|
Item.name,Item.description,Item.unit,Item.itemType,Item.for,Item.incomeAccount,Item.expenseAccount,Item.tax,Item.rate,Item.hsnCode,Item.barcode,Item.trackItem,Item.createdBy,Item.modifiedBy,Item.created,Item.modified
|
||||||
|
"Final Item","A final item made from raw items",Unit,Product,Both,Sales,"Stock Received But Not Billed",,500,0,,1,lin@to.co,lin@to.co,2023-01-31T06:46:00.200Z,2023-01-31T06:46:00.200Z
|
||||||
|
"Raw Two","Another Raw item used to make a final item",Unit,Product,Both,Sales,"Stock Received But Not Billed",,200,0,,1,lin@to.co,lin@to.co,2023-01-31T06:45:32.449Z,2023-01-31T06:45:32.449Z
|
||||||
|
"Raw One","A raw item used to make a final item.",Unit,Product,Both,Sales,"Stock Received But Not Billed",,100,0,,1,lin@to.co,lin@to.co,2023-01-31T06:44:58.047Z,2023-01-31T06:44:58.047Z
|
||||||
|
"Test One",,Unit,Product,Both,Sales,"Stock Received But Not Billed",GST-18,200,0,,1,lin@to.co,lin@to.co,2023-01-09T10:46:02.217Z,2023-01-09T10:46:02.217Z
|
||||||
|
Stuff,"Some stuff.",Unit,Product,Both,Sales,"Stock Received But Not Billed",GST-18,200,101192,,1,lin@to.co,lin@to.co,2023-01-09T07:14:12.208Z,2023-01-09T07:14:12.208Z
|
||||||
|
"Something Sellable",,Unit,Product,Sales,Sales,"Cost of Goods Sold",,300,0,,0,lin@to.co,lin@to.co,2022-10-11T09:15:15.724Z,2023-01-16T08:49:49.267Z
|
||||||
|
Ball,"Just a ball..",Unit,Product,Both,Sales,"Cost of Goods Sold",,30,0,,0,Administrator,Administrator,2022-02-24T04:38:09.181Z,2022-02-24T04:38:09.181Z
|
||||||
|
Bat,"Regular old bat...",Unit,Product,Both,Sales,"Cost of Goods Sold",,129,0,,0,Administrator,Administrator,2022-02-24T04:38:09.174Z,2022-02-24T04:38:09.174Z
|
||||||
|
"Holy Icon","The holiest of icons.",Unit,Product,Both,Sales,"Cost of Goods Sold",GST-3,330,0,,0,Administrator,Administrator,2022-02-11T11:32:33.342Z,2022-02-11T11:32:33.342Z
|
||||||
|
"Flower Pot","Just a flower pot.",Unit,Product,Both,Sales,"Cost of Goods Sold",GST-12,200,,,0,Administrator,Administrator,2021-12-16T07:04:08.233Z,2021-12-16T07:04:08.233Z
|
||||||
|
Flow,"Used to test the flow of operations.",Unit,Product,Both,Sales,"Cost of Goods Sold",GST-12,100,,,0,Administrator,Administrator,2021-12-16T05:42:02.081Z,2021-12-16T05:48:48.203Z
|
|
8
tests/parties.csv
Normal file
8
tests/parties.csv
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
Name,Role,"Default Account","Outstanding Amount",Currency,Email,Phone,Address,"GSTIN No.","GST Registration","Created By","Modified By",Created,Modified
|
||||||
|
Party.name,Party.role,Party.defaultAccount,Party.outstandingAmount,Party.currency,Party.email,Party.phone,Party.address,Party.gstin,Party.gstType,Party.createdBy,Party.modifiedBy,Party.created,Party.modified
|
||||||
|
Randoe,Both,,259.6,INR,,,,,Unregistered,lin@to.co,lin@to.co,2023-01-09T04:58:16.050Z,2023-01-09T10:46:46.128Z
|
||||||
|
Saipan,Customer,Debtors,299.99999999921,USD,sai@pan.co,,,,Unregistered,lin@to.co,lin@to.co,2022-07-18T17:07:35.103Z,2023-01-30T14:36:50.058Z
|
||||||
|
Lordham,Customer,Debtors,851.8,INR,lo@gamil.com,8989004444,,,Unregistered,Administrator,lin@to.co,2022-02-04T06:35:19.404Z,2022-07-18T17:05:42.976Z
|
||||||
|
Lyn,Customer,Debtors,100,INR,lyn@to.co,,,,Consumer,Administrator,Administrator,2022-02-04T06:21:19.069Z,2022-02-28T05:18:32.743Z
|
||||||
|
Bølèn,Customer,Debtors,0,INR,bo@len.co,,,22ABCIK123401Z5,"Registered Regular",Administrator,Administrator,2022-01-12T08:44:58.879Z,2022-01-12T08:45:26.714Z
|
||||||
|
Bé,Customer,Debtors,46,INR,bey@more.tips,6969969600,,,Consumer,Administrator,Administrator,2021-12-16T11:32:11.595Z,2021-12-16T12:00:28.558Z
|
|
30
tests/sales_invoices.csv
Normal file
30
tests/sales_invoices.csv
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
"Invoice No",Date,Party,Account,"Customer Currency","Exchange Rate","Net Total","Grand Total","Base Grand Total","Outstanding Amount","Set Discount Amount","Discount Amount","Discount Percent","Discount After Tax","Entry Currency",Notes,"Stock Not Transferred","Number Series","Created By","Modified By",Created,Modified,Submitted,Cancelled,"Tax Account",Rate,Amount,Item,Description,Quantity,Rate,Account,Tax,Amount,"Set Discount Amount","Discount Amount","Discount Percent",HSN/SAC,"Stock Not Transferred"
|
||||||
|
SalesInvoice.name,SalesInvoice.date,SalesInvoice.party,SalesInvoice.account,SalesInvoice.currency,SalesInvoice.exchangeRate,SalesInvoice.netTotal,SalesInvoice.grandTotal,SalesInvoice.baseGrandTotal,SalesInvoice.outstandingAmount,SalesInvoice.setDiscountAmount,SalesInvoice.discountAmount,SalesInvoice.discountPercent,SalesInvoice.discountAfterTax,SalesInvoice.entryCurrency,SalesInvoice.terms,SalesInvoice.stockNotTransferred,SalesInvoice.numberSeries,SalesInvoice.createdBy,SalesInvoice.modifiedBy,SalesInvoice.created,SalesInvoice.modified,SalesInvoice.submitted,SalesInvoice.cancelled,TaxSummary.account,TaxSummary.rate,TaxSummary.amount,SalesInvoiceItem.item,SalesInvoiceItem.description,SalesInvoiceItem.quantity,SalesInvoiceItem.rate,SalesInvoiceItem.account,SalesInvoiceItem.tax,SalesInvoiceItem.amount,SalesInvoiceItem.setItemDiscountAmount,SalesInvoiceItem.itemDiscountAmount,SalesInvoiceItem.itemDiscountPercent,SalesInvoiceItem.hsnCode,SalesInvoiceItem.stockNotTransferred
|
||||||
|
1020,2023-01-31,Lordham,Debtors,INR,1,500,500,500,500,0,0,0,0,Party,,1,SINV-,lin@to.co,lin@to.co,2023-01-31T09:00:45.858Z,2023-01-31T09:00:45.858Z,0,0,,,,"Final Item","A final item made from raw items",1,500,Sales,,500,0,0,0,0,1
|
||||||
|
1019,2023-01-09,Randoe,Debtors,INR,1,220,259.6,259.6,259.6,0,0,0,0,Party,,1,SINV-,lin@to.co,lin@to.co,2023-01-09T10:46:40.923Z,2023-01-09T10:46:46.085Z,1,0,CGST,9,19.8,,,,,,,,,,,,
|
||||||
|
1019,2023-01-09,Randoe,Debtors,INR,1,220,259.6,259.6,259.6,0,0,0,0,Party,,1,SINV-,lin@to.co,lin@to.co,2023-01-09T10:46:40.923Z,2023-01-09T10:46:46.085Z,1,0,SGST,9,19.8,,,,,,,,,,,,
|
||||||
|
1019,2023-01-09,Randoe,Debtors,INR,1,220,259.6,259.6,259.6,0,0,0,0,Party,,1,SINV-,lin@to.co,lin@to.co,2023-01-09T10:46:40.923Z,2023-01-09T10:46:46.085Z,1,0,,,,"Test One",,1,220,Sales,GST-18,220,0,0,0,0,1
|
||||||
|
1018,2022-10-11,Saipan,Debtors,USD,82.47,3.63768643142,3.63768643142,299.99999999921,299.99999999921,0,0,0,0,Party,,0,SINV-,lin@to.co,lin@to.co,2022-10-11T09:16:50.373Z,2023-01-30T14:36:50.021Z,1,0,,,,"Something Sellable",,1,3.63768643142,Sales,,3.63768643142,0,0,0,0,0
|
||||||
|
1014,2022-07-11,Lordham,Debtors,INR,1,660,679.8,679.8,679.8,0,0,,0,Party,,,SINV-,lin@to.co,lin@to.co,2022-07-11T11:06:18.788Z,2022-07-18T17:05:42.922Z,1,0,CGST,1.5,9.9,,,,,,,,,,,,
|
||||||
|
1014,2022-07-11,Lordham,Debtors,INR,1,660,679.8,679.8,679.8,0,0,,0,Party,,,SINV-,lin@to.co,lin@to.co,2022-07-11T11:06:18.788Z,2022-07-18T17:05:42.922Z,1,0,SGST,1.5,9.9,,,,,,,,,,,,
|
||||||
|
1014,2022-07-11,Lordham,Debtors,INR,1,660,679.8,679.8,679.8,0,0,,0,Party,,,SINV-,lin@to.co,lin@to.co,2022-07-11T11:06:18.788Z,2022-07-18T17:05:42.922Z,1,0,,,,"Holy Icon","The holiest of icons.",2,330,Sales,GST-3,660,0,0,,0,
|
||||||
|
1011,2022-02-22,Lyn,Debtors,INR,1,1410,1471.2,1471.2,100,0,0,,0,Party,,,SINV-,Administrator,Administrator,2022-02-24T05:07:23.656Z,2022-02-28T05:18:32.731Z,1,0,SGST,1.5,30.6,,,,,,,,,,,,
|
||||||
|
1011,2022-02-22,Lyn,Debtors,INR,1,1410,1471.2,1471.2,100,0,0,,0,Party,,,SINV-,Administrator,Administrator,2022-02-24T05:07:23.656Z,2022-02-28T05:18:32.731Z,1,0,CGST,1.5,30.6,,,,,,,,,,,,
|
||||||
|
1011,2022-02-22,Lyn,Debtors,INR,1,1410,1471.2,1471.2,100,0,0,,0,Party,,,SINV-,Administrator,Administrator,2022-02-24T05:07:23.656Z,2022-02-28T05:18:32.731Z,1,0,,,,"Flower Pot","Just a flower pot.",1,210,Sales,GST-12,210,0,0,,,
|
||||||
|
1011,2022-02-22,Lyn,Debtors,INR,1,1410,1471.2,1471.2,100,0,0,,0,Party,,,SINV-,Administrator,Administrator,2022-02-24T05:07:23.656Z,2022-02-28T05:18:32.731Z,1,0,,,,"Holy Icon","The holiest of icons.",3,400,Sales,GST-3,1200,0,0,,0,
|
||||||
|
1012,2022-02-20,Lordham,Debtors,INR,1,2230,2353.6,2353.6,0,0,0,,0,Party,,,SINV-,Administrator,Administrator,2022-02-24T05:07:23.685Z,2022-02-28T05:18:11.657Z,1,0,SGST,1.5,61.8,,,,,,,,,,,,
|
||||||
|
1012,2022-02-20,Lordham,Debtors,INR,1,2230,2353.6,2353.6,0,0,0,,0,Party,,,SINV-,Administrator,Administrator,2022-02-24T05:07:23.685Z,2022-02-28T05:18:11.657Z,1,0,CGST,1.5,61.8,,,,,,,,,,,,
|
||||||
|
1012,2022-02-20,Lordham,Debtors,INR,1,2230,2353.6,2353.6,0,0,0,,0,Party,,,SINV-,Administrator,Administrator,2022-02-24T05:07:23.685Z,2022-02-28T05:18:11.657Z,1,0,,,,"Flower Pot","Just a flower pot.",3,210,Sales,GST-12,630,0,0,,,
|
||||||
|
1012,2022-02-20,Lordham,Debtors,INR,1,2230,2353.6,2353.6,0,0,0,,0,Party,,,SINV-,Administrator,Administrator,2022-02-24T05:07:23.685Z,2022-02-28T05:18:11.657Z,1,0,,,,"Holy Icon","The holiest of icons.",4,400,Sales,GST-3,1600,0,0,,0,
|
||||||
|
1008,2022-02-11,Lordham,Debtors,INR,1,600,672,672,172,0,0,,0,Party,,,SINV-,Administrator,Administrator,2022-02-11T08:40:28.193Z,2022-02-11T09:48:18.274Z,1,0,SGST,6,36,,,,,,,,,,,,
|
||||||
|
1008,2022-02-11,Lordham,Debtors,INR,1,600,672,672,172,0,0,,0,Party,,,SINV-,Administrator,Administrator,2022-02-11T08:40:28.193Z,2022-02-11T09:48:18.274Z,1,0,CGST,6,36,,,,,,,,,,,,
|
||||||
|
1008,2022-02-11,Lordham,Debtors,INR,1,600,672,672,172,0,0,,0,Party,,,SINV-,Administrator,Administrator,2022-02-11T08:40:28.193Z,2022-02-11T09:48:18.274Z,1,0,,,,"Flower Pot","Just a flower pot.",3,200,Sales,GST-12,600,0,0,,,
|
||||||
|
1007,2022-02-04,Lyn,Debtors,INR,1,200,224,224,0,0,0,,0,Party,,,SINV-,Administrator,Administrator,2022-02-04T06:21:24.058Z,2022-02-04T06:21:46.308Z,1,0,SGST,6,12,,,,,,,,,,,,
|
||||||
|
1007,2022-02-04,Lyn,Debtors,INR,1,200,224,224,0,0,0,,0,Party,,,SINV-,Administrator,Administrator,2022-02-04T06:21:24.058Z,2022-02-04T06:21:46.308Z,1,0,CGST,6,12,,,,,,,,,,,,
|
||||||
|
1007,2022-02-04,Lyn,Debtors,INR,1,200,224,224,0,0,0,,0,Party,,,SINV-,Administrator,Administrator,2022-02-04T06:21:24.058Z,2022-02-04T06:21:46.308Z,1,0,,,,"Flower Pot","Just a flower pot.",1,200,Sales,GST-12,200,0,0,,,
|
||||||
|
1004,2022-01-12,Bølèn,Debtors,INR,1,1800,2016,2016,0,0,0,,0,Party,,,SINV-,Administrator,Administrator,2022-01-12T08:45:19.774Z,2022-01-12T08:45:26.702Z,1,0,SGST,6,108,,,,,,,,,,,,
|
||||||
|
1004,2022-01-12,Bølèn,Debtors,INR,1,1800,2016,2016,0,0,0,,0,Party,,,SINV-,Administrator,Administrator,2022-01-12T08:45:19.774Z,2022-01-12T08:45:26.702Z,1,0,CGST,6,108,,,,,,,,,,,,
|
||||||
|
1004,2022-01-12,Bølèn,Debtors,INR,1,1800,2016,2016,0,0,0,,0,Party,,,SINV-,Administrator,Administrator,2022-01-12T08:45:19.774Z,2022-01-12T08:45:26.702Z,1,0,,,,"Flower Pot","Just a flower pot.",9,200,Sales,GST-12,1800,0,0,,,
|
||||||
|
1001,2021-12-16,Bé,Debtors,INR,1,100,112,112,46,0,0,,0,Party,,,SINV-,Administrator,Administrator,2021-12-16T11:34:06.174Z,2021-12-16T12:00:28.526Z,1,0,SGST,6,6,,,,,,,,,,,,
|
||||||
|
1001,2021-12-16,Bé,Debtors,INR,1,100,112,112,46,0,0,,0,Party,,,SINV-,Administrator,Administrator,2021-12-16T11:34:06.174Z,2021-12-16T12:00:28.526Z,1,0,CGST,6,6,,,,,,,,,,,,
|
||||||
|
1001,2021-12-16,Bé,Debtors,INR,1,100,112,112,46,0,0,,0,Party,,,SINV-,Administrator,Administrator,2021-12-16T11:34:06.174Z,2021-12-16T12:00:28.526Z,1,0,,,,Flow,"Used to test the flow of operations.",1,100,Sales,GST-12,100,0,0,,,
|
|
67
tests/testImporter.spec.ts
Normal file
67
tests/testImporter.spec.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import { assertDoesNotThrow } from 'backend/database/tests/helpers';
|
||||||
|
import { readFileSync } from 'fs';
|
||||||
|
import { ModelNameEnum } from 'models/types';
|
||||||
|
import { join } from 'path';
|
||||||
|
import { Importer } from 'src/importer';
|
||||||
|
import test from 'tape';
|
||||||
|
import { closeTestFyo, getTestFyo, setupTestFyo } from './helpers';
|
||||||
|
|
||||||
|
const fyo = getTestFyo();
|
||||||
|
setupTestFyo(fyo, __filename);
|
||||||
|
|
||||||
|
test('importer init', (t) => {
|
||||||
|
const importer = new Importer(ModelNameEnum.SalesInvoice, fyo);
|
||||||
|
t.equal(
|
||||||
|
typeof importer.getCSVTemplate(),
|
||||||
|
'string',
|
||||||
|
'csv template is a string'
|
||||||
|
);
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('import Items', async (t) => {
|
||||||
|
const importer = new Importer(ModelNameEnum.Item, fyo);
|
||||||
|
const csvPath = join(__dirname, 'items.csv');
|
||||||
|
const data = readFileSync(csvPath, { encoding: 'utf-8' });
|
||||||
|
t.equal(importer.selectFile(data), true, 'file selection');
|
||||||
|
t.equal((await importer.checkLinks()).length, 0, 'all links exist');
|
||||||
|
t.doesNotThrow(() => importer.populateDocs(), 'populating docs');
|
||||||
|
for (const doc of importer.docs) {
|
||||||
|
await assertDoesNotThrow(async () => await doc.sync());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('import Party', async (t) => {
|
||||||
|
const importer = new Importer(ModelNameEnum.Party, fyo);
|
||||||
|
const csvPath = join(__dirname, 'parties.csv');
|
||||||
|
const data = readFileSync(csvPath, { encoding: 'utf-8' });
|
||||||
|
t.equal(importer.selectFile(data), true, 'file selection');
|
||||||
|
t.equal((await importer.checkLinks()).length, 0, 'all links exist');
|
||||||
|
t.doesNotThrow(() => importer.populateDocs(), 'populating docs');
|
||||||
|
for (const doc of importer.docs) {
|
||||||
|
await assertDoesNotThrow(async () => await doc.sync());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('import SalesInvoices', async (t) => {
|
||||||
|
const importer = new Importer(ModelNameEnum.SalesInvoice, fyo);
|
||||||
|
const csvPath = join(__dirname, 'sales_invoices.csv');
|
||||||
|
const data = readFileSync(csvPath, { encoding: 'utf-8' });
|
||||||
|
|
||||||
|
t.equal(importer.selectFile(data), true, 'file selection');
|
||||||
|
t.equal((await importer.checkLinks()).length, 0, 'all links exist');
|
||||||
|
t.doesNotThrow(() => importer.populateDocs(), 'populating docs');
|
||||||
|
|
||||||
|
const names = [];
|
||||||
|
for (const doc of importer.docs.slice(0, 2)) {
|
||||||
|
await assertDoesNotThrow(async () => await doc.sync());
|
||||||
|
names.push(doc.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
t.ok(
|
||||||
|
names.every((n) => n?.startsWith('SINV-')),
|
||||||
|
'numberSeries assigned'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
closeTestFyo(fyo, __filename);
|
@ -38,7 +38,7 @@ Accounts,Comptes,
|
|||||||
"Add a remark","Ajouter une remarque",
|
"Add a remark","Ajouter une remarque",
|
||||||
"Add invoice terms","Ajouter votre politique de vente",
|
"Add invoice terms","Ajouter votre politique de vente",
|
||||||
"Add products or services that you buy from your suppliers","Ajoutez des produits ou services que vous achetez à vos fournisseurs",
|
"Add products or services that you buy from your suppliers","Ajoutez des produits ou services que vous achetez à vos fournisseurs",
|
||||||
"Add products or services that you sell to your customers","Ajouter les produits ou services que vous vendez à vos clients.",
|
"Add products or services that you sell to your customers","Ajouter les produits ou services que vous vendez à vos clients",
|
||||||
Address,Adresse,
|
Address,Adresse,
|
||||||
"Address Display","Affichage de l'adresse",
|
"Address Display","Affichage de l'adresse",
|
||||||
"Address Line 1","Ligne d'adresse 1",
|
"Address Line 1","Ligne d'adresse 1",
|
||||||
@ -76,7 +76,7 @@ Buildings,Bâtiments,
|
|||||||
Business,Entreprise,
|
Business,Entreprise,
|
||||||
Cancel,Annuler,
|
Cancel,Annuler,
|
||||||
"Cancel ${0} ${1}?","Annuler ${0} ${1} ?",
|
"Cancel ${0} ${1}?","Annuler ${0} ${1} ?",
|
||||||
Cancelled,Anulé,
|
Cancelled,Annulé,
|
||||||
"Cannot Delete","Impossible à supprimer",
|
"Cannot Delete","Impossible à supprimer",
|
||||||
"Cannot delete ${0} ${1} because of linked entries.","Impossible de supprimer ${0} ${1} à cause des entrées liées.",
|
"Cannot delete ${0} ${1} because of linked entries.","Impossible de supprimer ${0} ${1} à cause des entrées liées.",
|
||||||
"Capital Equipments","Biens d'équipement",
|
"Capital Equipments","Biens d'équipement",
|
||||||
@ -102,6 +102,7 @@ Close,Fermer,
|
|||||||
Closing,Fermeture,
|
Closing,Fermeture,
|
||||||
"Closing (Cr)","Fermeture (Cr)",
|
"Closing (Cr)","Fermeture (Cr)",
|
||||||
"Closing (Dr)","Fermeture (Dr)",
|
"Closing (Dr)","Fermeture (Dr)",
|
||||||
|
Collapse,Réduire,
|
||||||
Color,Couleur,
|
Color,Couleur,
|
||||||
"Commission on Sales","Commission sur les ventes",
|
"Commission on Sales","Commission sur les ventes",
|
||||||
Common,Autres,
|
Common,Autres,
|
||||||
@ -143,8 +144,8 @@ Credit,Crédit,
|
|||||||
"Credit Card Entry","Entrée Carte de Crédit",
|
"Credit Card Entry","Entrée Carte de Crédit",
|
||||||
"Credit Note",Avoir,
|
"Credit Note",Avoir,
|
||||||
Creditors,Créanciers,
|
Creditors,Créanciers,
|
||||||
Currency,Monnaie,
|
Currency,Devise,
|
||||||
"Currency Name","Nom de la monnaie",
|
"Currency Name","Nom de la devise",
|
||||||
Current,Actuel,
|
Current,Actuel,
|
||||||
"Current Assets","Actifs courants",
|
"Current Assets","Actifs courants",
|
||||||
"Current Liabilities","Passifs courants",
|
"Current Liabilities","Passifs courants",
|
||||||
@ -153,11 +154,11 @@ Customer,Client,
|
|||||||
"Customer Created","Client créé",
|
"Customer Created","Client créé",
|
||||||
"Customer Currency","Monnaie du client",
|
"Customer Currency","Monnaie du client",
|
||||||
Customers,Clients,
|
Customers,Clients,
|
||||||
Customise,Personnalisez,
|
Customise,Personnaliser,
|
||||||
"Customize your invoices by adding a logo and address details","Customisez vos factures en y ajoutant votre logo et adresse",
|
"Customize your invoices by adding a logo and address details",Personnalisez vos factures en y ajoutant votre logo et adresse,
|
||||||
Dashboard,"Tableau de bord",
|
Dashboard,"Tableau de bord",
|
||||||
"Data Import","Importation de données",
|
"Data Import","Importation de données",
|
||||||
"Database file: ${0}","Fichier de base de donnée : ${0}",
|
"Database file: ${0}","Fichier de base de données : ${0}",
|
||||||
Date,,
|
Date,,
|
||||||
"Date Format","Format de la date",
|
"Date Format","Format de la date",
|
||||||
Debit,Débit,
|
Debit,Débit,
|
||||||
@ -176,7 +177,7 @@ Details,Détails,
|
|||||||
"Direct Income","Revenu direct",
|
"Direct Income","Revenu direct",
|
||||||
Discount,Réduction,
|
Discount,Réduction,
|
||||||
"Discount Account","Compte des réductions",
|
"Discount Account","Compte des réductions",
|
||||||
"Discount Account is not set.","Le compte des réductions n'est pas défini",
|
"Discount Account is not set.","Le compte des réductions n'est pas défini.",
|
||||||
"Discount After Tax","Réduction après taxes",
|
"Discount After Tax","Réduction après taxes",
|
||||||
"Discount Amount","Montant de la réduction",
|
"Discount Amount","Montant de la réduction",
|
||||||
"Discount Amount (${0}) cannot be greated than Amount (${1}).","Le montant de la réduction (${0}) ne peut pas être plus grand que le montant (${1}).",
|
"Discount Amount (${0}) cannot be greated than Amount (${1}).","Le montant de la réduction (${0}) ne peut pas être plus grand que le montant (${1}).",
|
||||||
@ -189,7 +190,7 @@ Discounts,"Réductions",
|
|||||||
"Display Logo in Invoice","Afficher le logo sur la facture",
|
"Display Logo in Invoice","Afficher le logo sur la facture",
|
||||||
"Display Precision","Précision de l'affichage",
|
"Display Precision","Précision de l'affichage",
|
||||||
"Display Precision should have a value between 0 and 9.","La précision de l'affichage doit avoir une valeur comprise entre 0 et 9.",
|
"Display Precision should have a value between 0 and 9.","La précision de l'affichage doit avoir une valeur comprise entre 0 et 9.",
|
||||||
"Dividends Paid","Dividendes versés",
|
"Dividends Paid","Dividendes versées",
|
||||||
Docs,Documents,
|
Docs,Documents,
|
||||||
Documentation,Documentation,
|
Documentation,Documentation,
|
||||||
"Does Not Contain","Ne contient pas",
|
"Does Not Contain","Ne contient pas",
|
||||||
@ -205,6 +206,7 @@ Email,,
|
|||||||
"Email Address","Adresse électronique",
|
"Email Address","Adresse électronique",
|
||||||
Empty,Vide,
|
Empty,Vide,
|
||||||
"Enable Discount Accounting","Activer la gestion des réductions",
|
"Enable Discount Accounting","Activer la gestion des réductions",
|
||||||
|
"Enable Inventory","Activer l'inventaire",
|
||||||
"Enter Country to load States","Entrez le Pays pour charger le département",
|
"Enter Country to load States","Entrez le Pays pour charger le département",
|
||||||
"Enter State","Entrez le département",
|
"Enter State","Entrez le département",
|
||||||
"Entertainment Expenses","Dépenses liées au divertissement",
|
"Entertainment Expenses","Dépenses liées au divertissement",
|
||||||
@ -216,13 +218,14 @@ Error,Erreur,
|
|||||||
"Exchange Rate","Taux de change",
|
"Exchange Rate","Taux de change",
|
||||||
"Excise Entry","Entrée d'acquis",
|
"Excise Entry","Entrée d'acquis",
|
||||||
"Existing File","Fichier existant",
|
"Existing File","Fichier existant",
|
||||||
|
Expand,Développer,
|
||||||
Expense,Dépenses,
|
Expense,Dépenses,
|
||||||
"Expense Account","Compte de dépenses",
|
"Expense Account","Compte de dépenses",
|
||||||
Expenses,Dépenses,
|
Expenses,Dépenses,
|
||||||
"Expenses Included In Valuation","Dépenses incluses dans la valorisation ",
|
"Expenses Included In Valuation","Dépenses incluses dans la valorisation ",
|
||||||
Export,Exporter,
|
Export,Exporter,
|
||||||
"Export Failed","Export Échoué",
|
"Export Failed","Export Échoué",
|
||||||
"Export Successful","Export réussie",
|
"Export Successful","Export réussi",
|
||||||
Fax,Fax,
|
Fax,Fax,
|
||||||
Field,Champ,
|
Field,Champ,
|
||||||
Fieldname,"Nom du champ",
|
Fieldname,"Nom du champ",
|
||||||
@ -377,13 +380,13 @@ Orange,,
|
|||||||
Organisation,Organisation,
|
Organisation,Organisation,
|
||||||
Outflow,Dépenses,
|
Outflow,Dépenses,
|
||||||
"Outstanding Amount","Montant impayé",
|
"Outstanding Amount","Montant impayé",
|
||||||
"Pad Zeros","Remplir de zéros",
|
"Pad Zeros","Remplir de zéros",
|
||||||
Page,,
|
Page,,
|
||||||
Paid,Payé,
|
Paid,Payé,
|
||||||
Parent,,
|
Parent,,
|
||||||
"Parent Account","Compte parent",
|
"Parent Account","Compte parent",
|
||||||
Party,Partie,
|
Party,Partie,
|
||||||
"Patch Run","Éxecuter les correctifs",
|
"Patch Run","Exécuter les correctifs",
|
||||||
Pay,Payer,
|
Pay,Payer,
|
||||||
Payable,Payable,
|
Payable,Payable,
|
||||||
Payment,Paiement,
|
Payment,Paiement,
|
||||||
@ -434,7 +437,7 @@ Purchases,Achats,
|
|||||||
Purple,Violet,
|
Purple,Violet,
|
||||||
Quantity,Quantité,
|
Quantity,Quantité,
|
||||||
Quarterly,Trimestriel,
|
Quarterly,Trimestriel,
|
||||||
Quarters,Trimestre,
|
Quarters,Trimestres,
|
||||||
Rate,Tarif,
|
Rate,Tarif,
|
||||||
"Rate (${0}) cannot be less zero.","Le Tarif (${0}) ne peut pas être inférieur à zéro.",
|
"Rate (${0}) cannot be less zero.","Le Tarif (${0}) ne peut pas être inférieur à zéro.",
|
||||||
"Rate (Company Currency)","Tarif (devise utilisée par la société)",
|
"Rate (Company Currency)","Tarif (devise utilisée par la société)",
|
||||||
@ -511,6 +514,7 @@ Service,,
|
|||||||
"Sets how many digits are shown after the decimal point.","Définit le nombre de chiffres affichés après le point décimal.",
|
"Sets how many digits are shown after the decimal point.","Définit le nombre de chiffres affichés après le point décimal.",
|
||||||
"Sets the app-wide date display format.","Définit le format d'affichage de la date pour l'application.",
|
"Sets the app-wide date display format.","Définit le format d'affichage de la date pour l'application.",
|
||||||
"Sets the internal precision used for monetary calculations. Above 6 should be sufficient for most currencies.","Définit la précision interne utilisée pour les calculs monétaires. Une précision supérieure à 6 devrait être suffisante pour la plupart des monnaies.",
|
"Sets the internal precision used for monetary calculations. Above 6 should be sufficient for most currencies.","Définit la précision interne utilisée pour les calculs monétaires. Une précision supérieure à 6 devrait être suffisante pour la plupart des monnaies.",
|
||||||
|
"Set Up","Configurer",
|
||||||
"Setting Up Instance","Paramétrage de l'Instance",
|
"Setting Up Instance","Paramétrage de l'Instance",
|
||||||
"Setting Up...",Paramétrage...,
|
"Setting Up...",Paramétrage...,
|
||||||
Settings,Paramètres,
|
Settings,Paramètres,
|
||||||
@ -584,7 +588,7 @@ Terms,Conditions,
|
|||||||
"This action is permanent","Cette action est permanente",
|
"This action is permanent","Cette action est permanente",
|
||||||
"This action is permanent and will cancel the following payment: ${0}","Cette action est permanente et annulera le paiement suivant : ${0}",
|
"This action is permanent and will cancel the following payment: ${0}","Cette action est permanente et annulera le paiement suivant : ${0}",
|
||||||
"This action is permanent and will cancel the following payments: ${0}","Cette action est permanente et annulera les paiements suivants : ${0}",
|
"This action is permanent and will cancel the following payments: ${0}","Cette action est permanente et annulera les paiements suivants : ${0}",
|
||||||
"This action is permanent and will delete associated ledger entries.","Cette action est permanente et supprimera les écritures dans le registre associées.",
|
"This action is permanent and will delete associated ledger entries.","Cette action est permanente et supprimera les écritures dans le registre associés.",
|
||||||
"This action is permanent.","Cette action est permanente",
|
"This action is permanent.","Cette action est permanente",
|
||||||
"Times New Roman",,
|
"Times New Roman",,
|
||||||
"To Account","Au compte",
|
"To Account","Au compte",
|
||||||
|
|
@ -1,35 +1,36 @@
|
|||||||
${0} ${1} already exists.,${0} ${1} પહેલાથી અસ્તિત્વમાં છે.,
|
${0} ${1} already exists.,${0} ${1} પહેલાથી અસ્તિત્વમાં છે.,
|
||||||
${0} ${1} does not exist,${0} ${1} અસ્તિત્વમાં નથી,
|
${0} ${1} does not exist,${0} ${1} અસ્તિત્વમાં નથી,
|
||||||
${0} ${1} has been modified after loading,${0} ${1} લોડિંગ પછી ફેરબદલ કરવામાં આવેલ છે,
|
${0} ${1} has been modified after loading,${0} ${1} લોડિંગ પછી ફેરબદલ થયેલ છે,
|
||||||
${0} ${1} is linked with existing records.,${0} ${1} હાલમાં રેકોર્ડ્સ સાથે જોડાયેલ છે.,
|
${0} ${1} is linked with existing records.,${0} ${1} હાલમાં રેકોર્ડ્સ સાથે જોડાયેલ છે.,
|
||||||
${0} account not set in Inventory Settings.,${0} ખાતું માલસૂચી સંયોજન માં સક્ષમ નથી,
|
${0} account not set in Inventory Settings.,${0} ખાતું માલસૂચી સંયોજન માં સક્ષમ નથી,
|
||||||
${0} already exists.,${0} પહેલેથી જ અસ્તિત્વમાં છે.,
|
${0} already exists.,${0} પહેલેથી જ અસ્તિત્વમાં છે.,
|
||||||
${0} fields selected,${0} ખાનાઓ લાગુ,
|
${0} fields selected,${0} ખાનાઓ લાગુ,
|
||||||
${0} filters applied,${0} તપાસો લાગુ,
|
${0} filters applied,${0} તપાસો લાગુ,
|
||||||
${0} of type ${1} does not exist,${0}નો પ્રકાર ${1} અસ્તિત્વ માં નથી,
|
${0} of type ${1} does not exist,${0}નો પ્રકાર ${1} અસ્તિત્વ માં નથી,
|
||||||
${0} out of ${1},${0} માંથી ${1},
|
${0} out of ${1},${1} માંથી ${0},
|
||||||
${0} party ${1} is different from ${2},${0} {1} પેઢી ${2} થી અલગ છે,
|
${0} party ${1} is different from ${2},${0} {1} પેઢી ${2} થી અલગ છે,
|
||||||
|
${0} quantity 1 added.,${0} માત્રા 1 ઉમેરાણી,
|
||||||
${0} rows,${0} પંક્તિઓ,
|
${0} rows,${0} પંક્તિઓ,
|
||||||
${0} value ${1} does not exist.,{0} મૂલ્ય ${1} અસ્તિત્વમાં નથી,
|
${0} value ${1} does not exist.,{0} મૂલ્ય ${1} અસ્તિત્વમાં નથી,
|
||||||
* required fields,* જરૂરી કોઠા,
|
* required fields,* જરૂરી કોઠા,
|
||||||
0%,0%,
|
0%,0%,
|
||||||
03-23-2022,03-23-2022,
|
03-23-2022,03-23-2022,
|
||||||
03/23/2022,03/23/2022,
|
03/23/2022,03/23/2022,
|
||||||
1 filter applied,1 તપાસ લાગુ,
|
1 filter applied,1 તપાસ લાગુ,
|
||||||
|
2022-03-23,,
|
||||||
|
"23 Mar, 2022",,
|
||||||
23-03-2022,23-03-2022,
|
23-03-2022,23-03-2022,
|
||||||
23-Mar-22,"23 માર્ચ, 2022",
|
23.03.2022,,
|
||||||
23-03-2022,23-03-2022,
|
23/03/2022,,
|
||||||
0.983819444,0.983819444,
|
|
||||||
23-03-2022,23-03-2022,
|
|
||||||
9888900000,9888900000,
|
9888900000,9888900000,
|
||||||
Account,ખાતું,
|
Account,ખાતું,
|
||||||
Account ${0} does not exist.,${0} ખાતું અસ્તિત્વ માં નથી,
|
Account ${0} does not exist.,${0} ખાતું અસ્તિત્વ માં નથી,
|
||||||
Account Entries,ખાતા લેવડદેવડ,
|
Account Entries,ખાતા લેવડદેવડ,
|
||||||
Account Name,ખાતા નું નામ,
|
Account Name,ખાતા નું નામ,
|
||||||
Account Type,ખાતાના પ્રકાર,
|
Account Type,ખાતાના પ્રકાર,
|
||||||
Accounting Entries,હિસાબી લેવડદેવડ,
|
Accounting Entries,હિસાબી લેવડદેવડ,
|
||||||
Accounting Ledger Entry,ખાતાવહી ખતવણી,
|
Accounting Ledger Entry,ખાતાવહી ખતવણી,
|
||||||
Accounting Settings,હિસાબી સંયોજન,
|
Accounting Settings,હિસાબી સંયોજન,
|
||||||
Accounts,હિસાબ,
|
Accounts,હિસાબ,
|
||||||
Accounts Payable,ચુકવવાપાત્ર ખાતાઓ,
|
Accounts Payable,ચુકવવાપાત્ર ખાતાઓ,
|
||||||
Accounts Receivable,મળવાપાત્ર હિસાબ,
|
Accounts Receivable,મળવાપાત્ર હિસાબ,
|
||||||
@ -40,17 +41,17 @@ Add Group,સમૂહ ઉમેરો,
|
|||||||
Add Items,ઉત્પાદન ઉમેરો,
|
Add Items,ઉત્પાદન ઉમેરો,
|
||||||
Add Row,વિગત ઉમેરો,
|
Add Row,વિગત ઉમેરો,
|
||||||
Add Suppliers,વિક્રેતાઓ ઉમેરો,
|
Add Suppliers,વિક્રેતાઓ ઉમેરો,
|
||||||
Add Taxes,કરવેરા ઉમેરો,
|
Add Taxes,કરવેરા ઉમેરો,
|
||||||
Add a few customers to create your first sales invoice,પ્રથમ વેચાણ ભરતિયું નિર્માણ કરવા પહેલા થોડાક ગ્રાહક ઉમેરો,
|
Add a few customers to create your first sales invoice,પ્રથમ વેચાણ ભરતિયું નિર્માણ કરવા પહેલા થોડાક ગ્રાહક ઉમેરો,
|
||||||
Add a few suppliers to create your first purchase invoice,પ્રથમ ખરીદી ભરતિયું નિર્માણ કરવા પહેલા થોડાક વિક્રેતા ઉમેરો,
|
Add a few suppliers to create your first purchase invoice,પ્રથમ ખરીદી ભરતિયું નિર્માણ કરવા પહેલા થોડાક વિક્રેતા ઉમેરો,
|
||||||
Add a filter,તપાસ ઉમેરો,
|
Add a filter,તપાસ ઉમેરો,
|
||||||
Add a remark,એક ટિપ્પણી ઉમેરો,
|
Add a remark,એક ટિપ્પણી ઉમેરો,
|
||||||
Add attachment,ટાંચ ઉમેરો,
|
Add attachment,ટાંચ ઉમેરો,
|
||||||
Add invoice terms,બિલ શરતો ઉમેરો,
|
Add invoice terms,બિલ શરતો ઉમેરો,
|
||||||
Add products or services that you buy from your suppliers,તમે તમારા વિક્રેતા પાસેથી ખરીદેલા ઉત્પાદનો અથવા સેવાઓ ઉમેરો,
|
Add products or services that you buy from your suppliers,તમે તમારા વિક્રેતા પાસેથી ખરીદેલા ઉત્પાદનો અથવા સેવાઓ ઉમેરો,
|
||||||
Add products or services that you sell to your customers,તમે તમારા ગ્રાહકોને વેચતા ઉત્પાદનો અથવા સેવાઓ ઉમેરો,
|
Add products or services that you sell to your customers,તમે તમારા ગ્રાહકોને વેચતા ઉત્પાદનો અથવા સેવાઓ ઉમેરો,
|
||||||
Add transfer terms,સ્થળાંતર શરતો ઉમેરો,
|
Add transfer terms,સ્થળાંતર શરતો ઉમેરો,
|
||||||
Additional quantity (${0}) required to make outward transfer of item ${1} from ${2} on ${3},વધારાની (${0}) માત્રા જરૂરી છે બાહ્ય સ્થળાંતર માટે વસ્તુ ${2} માંથી ${1} પર ${3},
|
Additional quantity (${0}) required to make outward transfer of item ${1} from ${2} on ${3},વધારાની (${0}) માત્રા જરૂરી છે બાહ્ય સ્થળાંતર માટે વસ્તુ ${2} માંથી ${1} પર ${3},
|
||||||
Address,સરનામું,
|
Address,સરનામું,
|
||||||
Address Display,પ્રદર્શિત સરનામુ,
|
Address Display,પ્રદર્શિત સરનામુ,
|
||||||
Address Line 1,સરનામું કડી 1,
|
Address Line 1,સરનામું કડી 1,
|
||||||
@ -70,7 +71,7 @@ Asset,રોકાણ,
|
|||||||
Assign Imported Labels,આયાત કરેલા લેબલ્સ સોંપો,
|
Assign Imported Labels,આયાત કરેલા લેબલ્સ સોંપો,
|
||||||
Attachment,સંપેતરું,
|
Attachment,સંપેતરું,
|
||||||
August,ઓગસ્ટ,
|
August,ઓગસ્ટ,
|
||||||
Back Reference,ઊલટ સંદર્ભ,
|
Back Reference,ઊલટ સંદર્ભ,
|
||||||
Bad import data.,ખરાબ આયાત ડેટા.,
|
Bad import data.,ખરાબ આયાત ડેટા.,
|
||||||
Balance,સિલક,
|
Balance,સિલક,
|
||||||
Balance Sheet,પાકું સરવૈયું,
|
Balance Sheet,પાકું સરવૈયું,
|
||||||
@ -79,7 +80,8 @@ Bank Accounts,બેંક ખાતા,
|
|||||||
Bank Entry,બેંક ખતવણી,
|
Bank Entry,બેંક ખતવણી,
|
||||||
Bank Name,બેંકનું નામ,
|
Bank Name,બેંકનું નામ,
|
||||||
Bank Overdraft Account,બેંક ઓવરડ્રાફ્ટ ખાતું,
|
Bank Overdraft Account,બેંક ઓવરડ્રાફ્ટ ખાતું,
|
||||||
Base Grand Total,Base કુલ રકમ,
|
Barcode,બારકોડ,
|
||||||
|
Base Grand Total,પ્રમુખ કુલ રકમ,
|
||||||
Based On,આના આધારે,
|
Based On,આના આધારે,
|
||||||
Basic,Basic,
|
Basic,Basic,
|
||||||
Bill Created,બિલ નિર્માણ,
|
Bill Created,બિલ નિર્માણ,
|
||||||
@ -88,13 +90,13 @@ Blue,Blue,
|
|||||||
Both,બંને,
|
Both,બંને,
|
||||||
Both From and To Location cannot be undefined,બન્ને થકી અને પ્રત્યે સ્થળ ખાલી હોય શકે નહીં,
|
Both From and To Location cannot be undefined,બન્ને થકી અને પ્રત્યે સ્થળ ખાલી હોય શકે નહીં,
|
||||||
Buildings,મકાનો,
|
Buildings,મકાનો,
|
||||||
Business,વ્યવસાય,
|
Business,,
|
||||||
Cancel,ફોક,
|
Cancel,ફોક,
|
||||||
Cancel ${0} ${1}?,${0} ${1} ફોક કરો ?,
|
Cancel ${0} ${1}?,${0} ${1} ફોક કરો ?,
|
||||||
Cancelled,ફોક કરાયેલ,
|
Cancelled,ફોક કરાયેલ,
|
||||||
Cannot Commit Error,,
|
Cannot Commit Error,,
|
||||||
Cannot Delete,કાઢી શકાય તેમ નથી,
|
Cannot Delete,કાઢી શકાય તેમ નથી,
|
||||||
Cannot cancel ${0} ${1} because of the following ${2}: ${3},${2} ના કારણે ${0} {1} ફોક થાઈ તેમ નથી : ${3},
|
Cannot cancel ${0} ${1} because of the following ${2}: ${3},${2} ના કારણે ${0} {1} ફોક થાઈ તેમ નથી : ${3},
|
||||||
Cannot delete ${0} ${1} because of linked entries.,${0} ${1} સંકળાયેલા લેવડદેવડ ના કારણે કાઢી શકાય તેમ નથી.,
|
Cannot delete ${0} ${1} because of linked entries.,${0} ${1} સંકળાયેલા લેવડદેવડ ના કારણે કાઢી શકાય તેમ નથી.,
|
||||||
Cannot perform operation.,પ્રક્રિયા નહીં થઈ શકે.,
|
Cannot perform operation.,પ્રક્રિયા નહીં થઈ શકે.,
|
||||||
Capital Equipments,મૂડીનાં સાધનો,
|
Capital Equipments,મૂડીનાં સાધનો,
|
||||||
@ -102,11 +104,11 @@ Capital Stock,મૂડીનો હિસ્સો,
|
|||||||
Cash,રોકડ,
|
Cash,રોકડ,
|
||||||
Cash Entry,રોકડ ખતવણી,
|
Cash Entry,રોકડ ખતવણી,
|
||||||
Cash In Hand,હાથ પર રોકડ,
|
Cash In Hand,હાથ પર રોકડ,
|
||||||
Cashflow,કેશફલૉ,
|
Cashflow,રોકડ પ્રવાહ,
|
||||||
Central Tax,કેન્દ્રીય કર,
|
Central Tax,કેન્દ્રીય કર,
|
||||||
Change DB,ડીબી બદલો,
|
Change DB,ડીબી બદલો,
|
||||||
Change File,ફાઈલ બદલો,
|
Change File,ફાઈલ બદલો,
|
||||||
Change Ref Type,સંદર્ભ પ્રકાર બદલો ,
|
Change Ref Type,સંદર્ભ પ્રકાર બદલો,
|
||||||
Chargeable,તહોમતપાત્ર,
|
Chargeable,તહોમતપાત્ર,
|
||||||
Chart Of Accounts Reviewed,હિસાબી આલેખ સમીક્ષા,
|
Chart Of Accounts Reviewed,હિસાબી આલેખ સમીક્ષા,
|
||||||
Chart of Accounts,હિસાબી આલેખ,
|
Chart of Accounts,હિસાબી આલેખ,
|
||||||
@ -117,9 +119,9 @@ Clearance Date,ક્લિઅરન્સ તારીખ,
|
|||||||
Click to create,નિર્માણ માટે ક્લિક કરો,
|
Click to create,નિર્માણ માટે ક્લિક કરો,
|
||||||
Close,બંધ,
|
Close,બંધ,
|
||||||
Close Frappe Books and try manually,Frappe Books બંધ કરી ને જાતે પ્રયાસ કરો,
|
Close Frappe Books and try manually,Frappe Books બંધ કરી ને જાતે પ્રયાસ કરો,
|
||||||
Closing,બંધ થાય છે,
|
Closing,આખર,
|
||||||
Closing (Cr),આખર સિલક (Cr),
|
Closing (Cr),આખર સિલક (Cr),
|
||||||
Closing (Dr),આખર સિલક (Dr),
|
Closing (Dr),આખર સિલક (Dr),
|
||||||
Collapse,સંકોચો,
|
Collapse,સંકોચો,
|
||||||
Color,રંગ,
|
Color,રંગ,
|
||||||
Commission on Sales,વેચાણ પર આડત,
|
Commission on Sales,વેચાણ પર આડત,
|
||||||
@ -143,7 +145,7 @@ Country,દેશ,
|
|||||||
Country Code,દેશનો કોડ,
|
Country Code,દેશનો કોડ,
|
||||||
Country code used to initialize regional settings.,પ્રાદેશિક નિયમન પ્રારંભ કરવા માટે દેશ કોડ વપરાય છે.,
|
Country code used to initialize regional settings.,પ્રાદેશિક નિયમન પ્રારંભ કરવા માટે દેશ કોડ વપરાય છે.,
|
||||||
Courier,Courier,
|
Courier,Courier,
|
||||||
Cr.,જ.,
|
Cr.,,
|
||||||
Create,નિર્માણ કરો,
|
Create,નિર્માણ કરો,
|
||||||
Create Demo,ડેમો નિર્માણ,
|
Create Demo,ડેમો નિર્માણ,
|
||||||
Create Purchase,ખરીદી નિર્માણ,
|
Create Purchase,ખરીદી નિર્માણ,
|
||||||
@ -156,9 +158,9 @@ Create your first purchase invoice from the created supplier,મોજૂદ વ
|
|||||||
Create your first sales invoice for the created customer,મોજૂદ ગ્રાહક પરથી પ્રથમ વેચાણ ભરતિયું નિર્માણ કરો,
|
Create your first sales invoice for the created customer,મોજૂદ ગ્રાહક પરથી પ્રથમ વેચાણ ભરતિયું નિર્માણ કરો,
|
||||||
Created,નિર્માણ થયું,
|
Created,નિર્માણ થયું,
|
||||||
Created By,નિર્માણ પ્રમાણે,
|
Created By,નિર્માણ પ્રમાણે,
|
||||||
Creating Items and Parties,વસ્તુઓ અને પક્ષૉ નિર્માણ થઈ રહ્યા છે,
|
Creating Items and Parties,ચીજવસ્તુઓ અને પક્ષૉ નિર્માણ થઈ રહ્યા છે,
|
||||||
Creating Journal Entries,આમનોંધ લેવડદેવડ નિર્માણ થઈ રહી છે ,
|
Creating Journal Entries,આમનોંધ લેવડદેવડ નિર્માણ થઈ રહી છે,
|
||||||
Creating Purchase Invoices,ખરીદી ભરતિયું નિર્માણ થઈ રહ્યું છે ,
|
Creating Purchase Invoices,ખરીદી ભરતિયું નિર્માણ થઈ રહ્યું છે,
|
||||||
Credit,જમા,
|
Credit,જમા,
|
||||||
Credit Card Entry,ક્રેડિટ કાર્ડ ખતવણી,
|
Credit Card Entry,ક્રેડિટ કાર્ડ ખતવણી,
|
||||||
Credit Note,જમા-ચિઠ્ઠી,
|
Credit Note,જમા-ચિઠ્ઠી,
|
||||||
@ -174,15 +176,15 @@ Customer Created,ગ્રાહક નિર્માણ,
|
|||||||
Customer Currency,ગ્રાહક ચલણ,
|
Customer Currency,ગ્રાહક ચલણ,
|
||||||
Customers,ગ્રાહકો,
|
Customers,ગ્રાહકો,
|
||||||
Customise,રૂપરેખા,
|
Customise,રૂપરેખા,
|
||||||
Customize your invoices by adding a logo and address details,પ્રતીક અને સરનામાંની વિગતો ઉમેરીને તમારા ભરતીયા ની રૂપરેખા બદલો,
|
Customize your invoices by adding a logo and address details,પ્રતીક અને સરનામાંની વિગતો ઉમેરીને તમારા ભરતીયા ની રૂપરેખા બદલો,
|
||||||
Dashboard,મુખ્ય પૃષ્ઠ,
|
Dashboard,મુખ્ય પૃષ્ઠ,
|
||||||
Data Import,ડેટા આયાત ,
|
Data Import,ડેટા આયાત,
|
||||||
Database Error,ડેટાબેઝ ચૂક,
|
Database Error,ડેટાબેઝ ચૂક,
|
||||||
Database file: ${0},ડેટાબેઝ ફાઇલ: ${0},
|
Database file: ${0},ડેટાબેઝ ફાઇલ: ${0},
|
||||||
Date,તારીખ,
|
Date,તારીખ,
|
||||||
Date Format,તારીખ બંધારણ,
|
Date Format,તારીખ બંધારણ,
|
||||||
Day,દિવસ,
|
Day,દિવસ,
|
||||||
Debit,ઉધાર ,
|
Debit,ઉધાર,
|
||||||
Debit Note,ઉધાર-ચિઠ્ઠી,
|
Debit Note,ઉધાર-ચિઠ્ઠી,
|
||||||
Debtors,દેવાદાર,
|
Debtors,દેવાદાર,
|
||||||
December,ડીસેમ્બર,
|
December,ડીસેમ્બર,
|
||||||
@ -219,7 +221,7 @@ Dividends Paid,ચૂકવેલ ડિવિડન,
|
|||||||
Docs,દસ્તાવેજ,
|
Docs,દસ્તાવેજ,
|
||||||
Documentation,દસ્તાવેજીકરણ,
|
Documentation,દસ્તાવેજીકરણ,
|
||||||
Does Not Contain,સમાવિષ્ટ નથી,
|
Does Not Contain,સમાવિષ્ટ નથી,
|
||||||
Dr.,ઉ.,
|
Dr.,,
|
||||||
Draft,મુસદ્દો,
|
Draft,મુસદ્દો,
|
||||||
Duplicate,નકલ બનાવો,
|
Duplicate,નકલ બનાવો,
|
||||||
Duplicate ${0} ${1}?,નકલ ${0} ${1}?,
|
Duplicate ${0} ${1}?,નકલ ${0} ${1}?,
|
||||||
@ -230,10 +232,12 @@ Electronic Equipments,વિદ્યુત સાધન,
|
|||||||
Email,ઇ-મેઇલ,
|
Email,ઇ-મેઇલ,
|
||||||
Email Address,ઈ - મેઈલ સરનામું,
|
Email Address,ઈ - મેઈલ સરનામું,
|
||||||
Empty,ખાલી,
|
Empty,ખાલી,
|
||||||
|
Enable Barcodes,બારકોડ સક્ષમ કરો,
|
||||||
Enable Discount Accounting,વટાવ હિસાબ સક્ષમ કરો,
|
Enable Discount Accounting,વટાવ હિસાબ સક્ષમ કરો,
|
||||||
Enable Inventory,માલસૂચિ સક્ષમ કરો,
|
Enable Inventory,માલસૂચિ સક્ષમ કરો,
|
||||||
Enter Country to load States,રાજ્યો લોડ કરવા માટે દેશ દાખલ કરો,
|
Enter Country to load States,રાજ્યો લોડ કરવા માટે દેશ દાખલ કરો,
|
||||||
Enter State,રાજ્ય દાખલ કરો,
|
Enter State,રાજ્ય દાખલ કરો,
|
||||||
|
Enter barcode,બારકોડ ઉમેરો,
|
||||||
Entertainment Expenses,મનોરંજન ખર્ચ,
|
Entertainment Expenses,મનોરંજન ખર્ચ,
|
||||||
Entry Currency,ખતવણી ચલણ,
|
Entry Currency,ખતવણી ચલણ,
|
||||||
Entry No,ખતવણી ક્રમ,
|
Entry No,ખતવણી ક્રમ,
|
||||||
@ -266,18 +270,18 @@ Fiscal Year,નાણાકીય વર્ષ,
|
|||||||
Fiscal Year End Date,નાણાકીય વર્ષની અંતિમ તારીખ,
|
Fiscal Year End Date,નાણાકીય વર્ષની અંતિમ તારીખ,
|
||||||
Fiscal Year Start Date,નાણાકીય વર્ષની શરૂઆતની તારીખ,
|
Fiscal Year Start Date,નાણાકીય વર્ષની શરૂઆતની તારીખ,
|
||||||
Fixed Asset,નિયત રોકાણ,
|
Fixed Asset,નિયત રોકાણ,
|
||||||
Fixed Assets,નિયત અસ્કયામતો
|
Fixed Assets,નિયત અસ્કયામતો,
|
||||||
Font,અક્ષર વર્ણ,
|
Font,અક્ષર વર્ણ,
|
||||||
For,ને માટે,
|
For,ને માટે,
|
||||||
Forbidden Error,પ્રતિબંધિત ચૂક,
|
Forbidden Error,પ્રતિબંધિત ચૂક,
|
||||||
Fr,,
|
Fr,શુક્ર,
|
||||||
Fraction,દશાંશ,
|
Fraction,દશાંશ,
|
||||||
Fraction Units,દશાંશ એકમો,
|
Fraction Units,દશાંશ એકમો,
|
||||||
Freight and Forwarding Charges,ભાડા અને આગળના ખર્ચ,
|
Freight and Forwarding Charges,ભાડા અને આગળના ખર્ચ,
|
||||||
From,થકી,
|
From,થકી,
|
||||||
From Account,ખાતા થકી,
|
From Account,ખાતા થકી,
|
||||||
From Date,આ તારીખ થી,
|
From Date,આ તારીખ થી,
|
||||||
From Loc.,સ્થાન થકી,
|
From Loc.,સ્થાન થકી,
|
||||||
From Year,વર્ષ થકી,
|
From Year,વર્ષ થકી,
|
||||||
Full Name,પૂરું નામ,
|
Full Name,પૂરું નામ,
|
||||||
Furnitures and Fixtures,ફર્નિચર અને ફિક્સર,
|
Furnitures and Fixtures,ફર્નિચર અને ફિક્સર,
|
||||||
@ -285,7 +289,7 @@ GST,,
|
|||||||
GSTIN No.,GSTIN ક્રમ,
|
GSTIN No.,GSTIN ક્રમ,
|
||||||
GSTR1,GSTR 1,
|
GSTR1,GSTR 1,
|
||||||
GSTR2,GSTR 2,
|
GSTR2,GSTR 2,
|
||||||
Gain/Loss on Asset Disposal,રોકાણ પતાવટ પર લાભ/ખોટ,
|
Gain/Loss on Asset Disposal,રોકાણ પતાવટ પર લાભ/ખોટ,
|
||||||
General,સામાન્ય વિગતો,
|
General,સામાન્ય વિગતો,
|
||||||
General Ledger,સામાન્ય ખાતાવહી,
|
General Ledger,સામાન્ય ખાતાવહી,
|
||||||
Get Started,અહીં શરૂઆત,
|
Get Started,અહીં શરૂઆત,
|
||||||
@ -296,7 +300,7 @@ Green,Green,
|
|||||||
Group By,સમૂહ પ્રમાણે,
|
Group By,સમૂહ પ્રમાણે,
|
||||||
HSN/SAC,HSN/SAC,
|
HSN/SAC,HSN/SAC,
|
||||||
HSN/SAC Code,HSN/SAC Code,
|
HSN/SAC Code,HSN/SAC Code,
|
||||||
Half Yearly,અર્ધવાર્ષિક ,
|
Half Yearly,અર્ધવાર્ષિક,
|
||||||
Half Years,અર્ધ વર્ષ,
|
Half Years,અર્ધ વર્ષ,
|
||||||
Help,મદદ,
|
Help,મદદ,
|
||||||
Hex Value,Hex Value,
|
Hex Value,Hex Value,
|
||||||
@ -325,7 +329,8 @@ Instance Id,દાખલો,
|
|||||||
Insufficient Quantity.,અપૂર્ણ માત્રા,
|
Insufficient Quantity.,અપૂર્ણ માત્રા,
|
||||||
Intergrated Tax,આંતરગ્રાહી કર,
|
Intergrated Tax,આંતરગ્રાહી કર,
|
||||||
Internal Precision,આંતરિક ખરાપણું,
|
Internal Precision,આંતરિક ખરાપણું,
|
||||||
Invalid value ${0} for ${1},${0} માટે ${1} અમાન્ય મૂલ્ય,
|
Invalid barcode value ${0}.,બારકોડનું મૂલ્ય ${0} અમાન્ય છે,
|
||||||
|
Invalid value ${0} for ${1},${0} માટે ${1} અમાન્ય મૂલ્ય,
|
||||||
Inventory,માલસૂચિ,
|
Inventory,માલસૂચિ,
|
||||||
Inventory Settings,માલસૂચિ સંયોજન,
|
Inventory Settings,માલસૂચિ સંયોજન,
|
||||||
Investments,રોકાણ,
|
Investments,રોકાણ,
|
||||||
@ -341,17 +346,18 @@ Invoice Value,ભરતિયું મૂલ્ય,
|
|||||||
Invoices,ભરતિયું,
|
Invoices,ભરતિયું,
|
||||||
Is,,
|
Is,,
|
||||||
Is Empty,,
|
Is Empty,,
|
||||||
Is Group,,
|
Is Group,સમૂહ છે,
|
||||||
Is Not,,
|
Is Not,,
|
||||||
Is Not Empty,,
|
Is Not Empty,,
|
||||||
Is Whole,શું પરિપૂર્ણ છે,
|
Is Whole,શું પરિપૂર્ણ છે,
|
||||||
Item,વિગત,
|
Item,વિગત,
|
||||||
Item Description,વર્ણન,
|
Item Description,વર્ણન,
|
||||||
Item Name,નામ,
|
Item Name,ચીજવસ્તુનું નામ,
|
||||||
Items,વસ્તુઓ ,
|
Item with barcode ${0} not found.,બારકોડ ${0} વાળી ચીજવસ્તુ હજાર નથી.,
|
||||||
|
Items,ચીજવસ્તુઓ,
|
||||||
January,જાન્યુઆરી,
|
January,જાન્યુઆરી,
|
||||||
John Doe,John Doe,
|
John Doe,John Doe,
|
||||||
Journal Entries,આમનોંધ લેવડદેવડ,
|
Journal Entries,આમનોંધ લેવડદેવડ,
|
||||||
Journal Entry,આમનોંધ ખતવણી,
|
Journal Entry,આમનોંધ ખતવણી,
|
||||||
Journal Entry Account,આમનોંધ ખતવણી ખાતું,
|
Journal Entry Account,આમનોંધ ખતવણી ખાતું,
|
||||||
Journal Entry Number Series,આમનોંધ ખતવણી ક્રમાંકન,
|
Journal Entry Number Series,આમનોંધ ખતવણી ક્રમાંકન,
|
||||||
@ -369,19 +375,19 @@ Limit,મર્યાદા,
|
|||||||
Link Validation Error,લિંક માન્યતા ચૂક,
|
Link Validation Error,લિંક માન્યતા ચૂક,
|
||||||
List,યાદી,
|
List,યાદી,
|
||||||
Load an existing .db file from your computer.,તમારા કમ્પ્યુટરથી અસ્તિત્વમાં હોય એવી .db ફાઇલ લોડ કરો.,
|
Load an existing .db file from your computer.,તમારા કમ્પ્યુટરથી અસ્તિત્વમાં હોય એવી .db ફાઇલ લોડ કરો.,
|
||||||
Loading Report...,લોડિંગ રિપોર્ટ ...,
|
Loading Report...,અહેવાલ રજૂ થાય છે...,
|
||||||
Loading...,લોડ કરી રહ્યું છે ...,
|
Loading...,રજૂ થાય છે...,
|
||||||
Loans (Liabilities),લોન (દેવું),
|
Loans (Liabilities),લોન (દેવું),
|
||||||
Loans and Advances (Assets),કરજ અને ધિરાણ (રોકાણ),
|
Loans and Advances (Assets),કરજ અને ધિરાણ (રોકાણ),
|
||||||
Locale,પ્રાદેશિક નિયમન,
|
Locale,પ્રાદેશિક નિયમન,
|
||||||
Location,સ્થાન,
|
Location,સ્થાન,
|
||||||
Location Name,સ્થાનનું નામ ,
|
Location Name,સ્થાનનું નામ,
|
||||||
Logo,પ્રતીક,
|
Logo,પ્રતીક,
|
||||||
Make Entry,લેવડદેવડ ઉમેરો,
|
Make Entry,લેવડદેવડ ઉમેરો,
|
||||||
Mandatory Error,અનિવાર્ય ચૂક,
|
Mandatory Error,અનિવાર્ય ચૂક,
|
||||||
"Mar 23, 2022","23 માર્ચ, 2022",
|
"Mar 23, 2022","23 માર્ચ, 2022",
|
||||||
March,માર્ચ,
|
March,માર્ચ,
|
||||||
Marketing Expenses,વિજ્ઞાપન ખર્ચ,
|
Marketing Expenses,વિજ્ઞાપન ખર્ચ,
|
||||||
Material Issue,,
|
Material Issue,,
|
||||||
Material Receipt,,
|
Material Receipt,,
|
||||||
Material Transfer,,
|
Material Transfer,,
|
||||||
@ -390,36 +396,36 @@ Meter,મિટર,
|
|||||||
Minimal,Minimal,
|
Minimal,Minimal,
|
||||||
Misc,વગેરે,
|
Misc,વગેરે,
|
||||||
Miscellaneous Expenses,પરચૂરણ ખર્ચ,
|
Miscellaneous Expenses,પરચૂરણ ખર્ચ,
|
||||||
Mo,,
|
Mo,સોમ,
|
||||||
Modified,સુધારેલું,
|
Modified,સુધારેલું,
|
||||||
Modified By,સંશોધિત પ્રમાણે,
|
Modified By,સંશોધિત પ્રમાણે,
|
||||||
Monthly,માસિક,
|
Monthly,માસિક,
|
||||||
Months,મહિના,
|
Months,મહિના,
|
||||||
More Filters,વધુ તપાસો,
|
More Filters,વધુ તપાસો,
|
||||||
Movement Type,સ્થળાંતર પ્રકાર,
|
Movement Type,સ્થળાંતર પ્રકાર,
|
||||||
Moving Average,,
|
Moving Average,ચાલક સરેરાંશ,
|
||||||
Name,નામ,
|
Name,નામ,
|
||||||
Navigate,શોધખોળ,
|
Navigate,સંચાલન,
|
||||||
Net Total,ચોખ્ખો સરવાળો,
|
Net Total,કુલ સરવાળો,
|
||||||
New ${0},નવો ${0},
|
New ${0},નવુ ${0},
|
||||||
New Account,નવું ખાતું,
|
New Account,નવું ખાતું,
|
||||||
New Entry,નવી ખતવણી,
|
New Entry,નવી ખતવણી,
|
||||||
New File,નવી ફાઈલ,
|
New File,નવી ફાઈલ,
|
||||||
No,ના ,
|
No,ના,
|
||||||
No Data to Import,આયાત કરવા માટે કોઈ ડેટા નથી,
|
No Data to Import,આયાત કરવા માટે કોઈ ડેટા નથી,
|
||||||
No Values to be Displayed,પ્રદર્શિત કરવા માટે કોઈ મૂલ્યો નથી,
|
No Values to be Displayed,પ્રદર્શિત કરવા માટે કોઈ મૂલ્યો નથી,
|
||||||
No entries found,કોઈ લેવડદેવડ મળ્યા નથી,
|
No entries found,કોઈ લેવડદેવડ મળ્યા નથી,
|
||||||
No expenses in this period,આ સમયગાળામાં કોઈ ખર્ચ નથી,
|
No expenses in this period,આ સમયગાળામાં કોઈ ખર્ચ નથી,
|
||||||
No filters selected,તપાસ ઊમેરાય નથી,
|
No filters selected,તપાસ ઊમેરાય નથી,
|
||||||
No labels have been assigned.,કોઈ લેબલ સોંપવામાં આવ્યા નથી.,
|
No labels have been assigned.,કોઈ લેબલ સોંપવામાં આવ્યા નથી.,
|
||||||
No results found,કોઈ પરિણામો મળ્યા નથી,
|
No results found,કોઈ પરિણામો મળ્યા નથી,
|
||||||
No transactions yet,હજી સુધી કોઈ વ્યવહાર નથી,
|
No transactions yet,હજી સુધી કોઈ વ્યવહાર નથી,
|
||||||
None,,
|
None,કોઈ નહીં,
|
||||||
Not Found,મળ્યા નથી,
|
Not Found,મળ્યા નથી,
|
||||||
Not Saved,સંગ્રહ થયો નથી,
|
Not Saved,સંગ્રહ થયો નથી,
|
||||||
Notes,નોંધ,
|
Notes,નોંધ,
|
||||||
November,નવેમ્બર,
|
November,નવેમ્બર,
|
||||||
Number Series,સંખ્યા ક્રમાંકન,
|
Number Series,ક્રમાંકન,
|
||||||
Number of ${0},${0} ની સંખ્યા,
|
Number of ${0},${0} ની સંખ્યા,
|
||||||
October,ઓક્ટોબર,
|
October,ઓક્ટોબર,
|
||||||
Office Equipments,કચેરી સાધન,
|
Office Equipments,કચેરી સાધન,
|
||||||
@ -428,9 +434,9 @@ Office Rent,કચેરી ભાડા,
|
|||||||
Onboarding Complete,ઓનબોર્ડિંગ પૂર્ણ,
|
Onboarding Complete,ઓનબોર્ડિંગ પૂર્ણ,
|
||||||
Open Count,ખુલ્લી ગણતરી,
|
Open Count,ખુલ્લી ગણતરી,
|
||||||
Open Folder,ખુલ્લું ફોલ્ડર,
|
Open Folder,ખુલ્લું ફોલ્ડર,
|
||||||
Opening (Cr),ખૂલતી સિલક (જમા.),
|
Opening (Cr),ખૂલતી સિલક (Cr),
|
||||||
Opening (Dr),ખૂલતી સિલક (ઉધાર.),
|
Opening (Dr),ખૂલતી સિલક (Dr),
|
||||||
Opening Balance Equity,ખૂલતી ઇક્વિટી,
|
Opening Balance Equity,ખૂલતી ઇક્વિટી,
|
||||||
Opening Balances,શરૂઆતી મૂડી,
|
Opening Balances,શરૂઆતી મૂડી,
|
||||||
Opening Entry,શરૂઆતી ખતવણી,
|
Opening Entry,શરૂઆતી ખતવણી,
|
||||||
Orange,Orange,
|
Orange,Orange,
|
||||||
@ -441,16 +447,16 @@ Pad Zeros,દર્શાવા ખાતર શૂન્ય,
|
|||||||
Page,પાનાં,
|
Page,પાનાં,
|
||||||
Paid,ચુકવેલ,
|
Paid,ચુકવેલ,
|
||||||
Parent,પ્રધાન,
|
Parent,પ્રધાન,
|
||||||
Parent Account,પ્રધાન ખાતું,
|
Parent Account,પ્રધાન ખાતું,
|
||||||
Party,પેઢી,
|
Party,પેઢી,
|
||||||
Patch Run,,
|
Patch Run,મરામતી ક્રિયા,
|
||||||
Pay,ચૂકવણી,
|
Pay,ચૂકવણી,
|
||||||
Payable,ચૂકવવાપાત્ર,
|
Payable,ચૂકવવાપાત્ર,
|
||||||
Payment,પેમેન્ટ,
|
Payment,પેમેન્ટ,
|
||||||
Payment For,માટે પેમેન્ટ,
|
Payment For,માટે પેમેન્ટ,
|
||||||
Payment Method,પેમેન્ટ પદ્ધતિ,
|
Payment Method,પેમેન્ટ પદ્ધતિ,
|
||||||
Payment No,પેમેન્ટ ક્રમ,
|
Payment No,પેમેન્ટ ક્રમ,
|
||||||
Payment Number Series,ચુકવણી ક્રમાંકન,
|
Payment Number Series,પેમેન્ટ ક્રમાંકન,
|
||||||
Payment Reference,પેમેન્ટ સંદર્ભ,
|
Payment Reference,પેમેન્ટ સંદર્ભ,
|
||||||
Payment Type,પેમેન્ટનો પ્રકાર,
|
Payment Type,પેમેન્ટનો પ્રકાર,
|
||||||
Payment amount cannot be ${0}.,પેમેન્ટની રકમ ${0} હોઈ શકતી નથી.,
|
Payment amount cannot be ${0}.,પેમેન્ટની રકમ ${0} હોઈ શકતી નથી.,
|
||||||
@ -492,33 +498,33 @@ Purchase Invoice Number Series,ખરીદ ભરતિયું ક્રમ
|
|||||||
Purchase Invoice Terms,ખરીદી ભરતિયું શરતો,
|
Purchase Invoice Terms,ખરીદી ભરતિયું શરતો,
|
||||||
Purchase Invoices,ખરીદી ભરતિયા,
|
Purchase Invoices,ખરીદી ભરતિયા,
|
||||||
Purchase Item Created,ખરીદ વસ્તુ નિર્માણ,
|
Purchase Item Created,ખરીદ વસ્તુ નિર્માણ,
|
||||||
Purchase Items,ખરીદ માલયાદી,
|
Purchase Items,ખરીદ ચીજવસ્તુઓ,
|
||||||
Purchase Payments,ખરીદી ચૂકવણીઓ,
|
Purchase Payments,ખરીદી ચૂકવણીઓ,
|
||||||
Purchase Receipt,ખરીદ પહોંચ,
|
Purchase Receipt,ખરીદ પહોંચ,
|
||||||
Purchase Receipt Item,ખરીદ પહોંચ વસ્તુ,
|
Purchase Receipt Item,ખરીદ પહોંચ વસ્તુ,
|
||||||
Purchase Receipt Number Series,ખરીદ પહોંચ ક્રમાંકન,
|
Purchase Receipt Number Series,ખરીદ પહોંચ ક્રમાંકન,
|
||||||
Purchase Receipt Terms,ખરીદ પહોંચ શરતો,
|
Purchase Receipt Terms,ખરીદ પહોંચ શરતો,
|
||||||
Purchase Receipts,ખરીદ પહોંચ,
|
Purchase Receipts,ખરીદ પહોંચ,
|
||||||
Purchases,ખરીદી,
|
Purchases,ખરીદી,
|
||||||
Purple,Purple,
|
Purple,Purple,
|
||||||
Qty. ${0},માત્રા. ${0},
|
Qty. ${0},માત્રા. ${0},
|
||||||
Quantity,માત્રા,
|
Quantity,માત્રા,
|
||||||
Quantity (${0}) has to be greater than zero,માત્રા (${0}) શૂન્ય થી વધારે હોવી જોઈએ,
|
Quantity (${0}) has to be greater than zero,માત્રા (${0}) શૂન્ય થી વધારે હોવી જોઈએ,
|
||||||
Quantity needs to be set,માત્રા ,
|
Quantity needs to be set,માત્રા સૂચવવી જરૂરી છે,
|
||||||
Quarterly,ત્રિમાસિક,
|
Quarterly,ત્રિમાસિક,
|
||||||
Quarters,ત્રિમાસિક,
|
Quarters,ત્રિમાસિક,
|
||||||
Rate,ભાવ,
|
Rate,ભાવ,
|
||||||
Rate (${0}) cannot be less zero.,સંખ્યા (${0}) શૂન્ય થી ઓછી હોઈ શકતી નથી.,
|
Rate (${0}) cannot be less zero.,સંખ્યા (${0}) શૂન્ય થી ઓછી હોઈ શકતી નથી.,
|
||||||
Rate (${0}) has to be greater than zero,સંખ્યા (${0}) શૂન્ય થી વધારે હોવી જોઈએ,
|
Rate (${0}) has to be greater than zero,સંખ્યા (${0}) શૂન્ય થી વધારે હોવી જોઈએ,
|
||||||
Rate can't be negative.,સંખ્યા ઋણમાં હોઈ શકે નથી.,
|
Rate can't be negative.,સંખ્યા ઋણમાં હોઈ શકે નથી.,
|
||||||
Rate needs to be set,દર સુયોજિત કરવા જરૂરી છે,
|
Rate needs to be set,દર સુયોજિત કરવા જરૂરી છે,
|
||||||
Receivable,મળવાપાત્ર ,
|
Receivable,મળવાપાત્ર,
|
||||||
Receive,સ્વીકાર,
|
Receive,સ્વીકાર,
|
||||||
Recent Invoices,તાજેતરના Invoice,
|
Recent Invoices,તાજેતરના Invoice,
|
||||||
Red,Red,
|
Red,Red,
|
||||||
Ref Name,સંદર્ભ નામ ,
|
Ref Name,સંદર્ભ નામ,
|
||||||
Ref Type,સંદર્ભ પ્રકાર,
|
Ref Type,સંદર્ભ પ્રકાર,
|
||||||
Ref. / Cheque No.,સંદર્ભ. / ચેક નંબર. ,
|
Ref. / Cheque No.,સંદર્ભ. / ચેક નંબર.,
|
||||||
Ref. Date,સંદર્ભ. તારીખ,
|
Ref. Date,સંદર્ભ. તારીખ,
|
||||||
Ref. Name,સંદર્ભ. નામ,
|
Ref. Name,સંદર્ભ. નામ,
|
||||||
Ref. Type,સંદર્ભ. પ્રકાર,
|
Ref. Type,સંદર્ભ. પ્રકાર,
|
||||||
@ -526,18 +532,18 @@ Reference,સંદર્ભ,
|
|||||||
Reference Date,સંદર્ભ તારીખ,
|
Reference Date,સંદર્ભ તારીખ,
|
||||||
Reference Number,સંદર્ભ ક્રમ,
|
Reference Number,સંદર્ભ ક્રમ,
|
||||||
Reference Type,સંદર્ભ પ્રકાર,
|
Reference Type,સંદર્ભ પ્રકાર,
|
||||||
Reload App,એપ પુનઃપ્રારંભ,
|
Reload App,એપ પુનઃપ્રારંભ,
|
||||||
Report,અહેવાલ,
|
Report,અહેવાલ,
|
||||||
Report Error,ચૂક ઉલ્લેખ,
|
Report Error,ચૂક ઉલ્લેખ,
|
||||||
Report Issue,ખામી ઉલ્લેખ,
|
Report Issue,ખામી ઉલ્લેખ,
|
||||||
Reports,અહેવાલો,
|
Reports,અહેવાલો,
|
||||||
Required Fields not Assigned,નિર્દેશિત કોઠા ફાળવાયા નથી ,
|
Required Fields not Assigned,નિર્દેશિત કોઠા ફાળવાયા નથી,
|
||||||
Reset,Reset,
|
Reset,Reset,
|
||||||
Retained Earnings,જાળવેલ કમાણી,
|
Retained Earnings,જાળવેલ કમાણી,
|
||||||
Reverse Chrg.,Reverse Chrg.,
|
Reverse Chrg.,Reverse Chrg.,
|
||||||
Reverted,Reverted,
|
Reverted,Reverted,
|
||||||
Reverts,Reverts,
|
Reverts,Reverts,
|
||||||
Review Accounts,ખાતા નિરીક્ષણ,
|
Review Accounts,ખાતા નિરીક્ષણ,
|
||||||
"Review your chart of accounts, add any account or tax heads as needed","તમારા હિસાબી આલેખની સમીક્ષા કરો, જરૂર મુજબ કોઈપણ ખાતા અથવા કરવેરા ઉમેરો",
|
"Review your chart of accounts, add any account or tax heads as needed","તમારા હિસાબી આલેખની સમીક્ષા કરો, જરૂર મુજબ કોઈપણ ખાતા અથવા કરવેરા ઉમેરો",
|
||||||
Right Index,,
|
Right Index,,
|
||||||
Role,ભૂમિકા,
|
Role,ભૂમિકા,
|
||||||
@ -546,7 +552,7 @@ Round Off,પરચુરણ બાકાત,
|
|||||||
Round Off Account,પરચુરણ બાકાત ખાતું,
|
Round Off Account,પરચુરણ બાકાત ખાતું,
|
||||||
Round Off Account Not Found,પરચુરણ બાકાત ખાતું હાજર નથી,
|
Round Off Account Not Found,પરચુરણ બાકાત ખાતું હાજર નથી,
|
||||||
Rounded Off,બાકાત,
|
Rounded Off,બાકાત,
|
||||||
Sa,,
|
Sa,શનિ,
|
||||||
Salary,પગાર,
|
Salary,પગાર,
|
||||||
Sales,વેચાણ,
|
Sales,વેચાણ,
|
||||||
Sales Acc.,વેચાણ ખાતું,
|
Sales Acc.,વેચાણ ખાતું,
|
||||||
@ -555,9 +561,9 @@ Sales Invoice,વેચાણ ભરતિયું,
|
|||||||
Sales Invoice Item,વેચાણ ભરતિયું વસ્તુ,
|
Sales Invoice Item,વેચાણ ભરતિયું વસ્તુ,
|
||||||
Sales Invoice Number Series,વેચાણ ભરતિયું ક્રમાંકન,
|
Sales Invoice Number Series,વેચાણ ભરતિયું ક્રમાંકન,
|
||||||
Sales Invoice Terms,વેચાણ ભરતિયું શરતો,
|
Sales Invoice Terms,વેચાણ ભરતિયું શરતો,
|
||||||
Sales Invoices,વેચાણ ભરતીયા ,
|
Sales Invoices,વેચાણ ભરતીયા,
|
||||||
Sales Item Created,વેચાણની વસ્તુ નિર્માણ,
|
Sales Item Created,વેચાણની વસ્તુ નિર્માણ,
|
||||||
Sales Items,વેચાણ માલયાદી,
|
Sales Items,વેચાણ ચીજવસ્તુઓ,
|
||||||
Sales Payments,વેચાણ ચૂકવણીઓ,
|
Sales Payments,વેચાણ ચૂકવણીઓ,
|
||||||
Save,સંઘરો,
|
Save,સંઘરો,
|
||||||
Save Template,નમૂનો સંઘરો,
|
Save Template,નમૂનો સંઘરો,
|
||||||
@ -565,7 +571,7 @@ Save as PDF,PDF રીતે સંઘરો,
|
|||||||
Save as PDF Successful,PDF રીતે સંગ્રહ સફળકારક,
|
Save as PDF Successful,PDF રીતે સંગ્રહ સફળકારક,
|
||||||
Saved,સંગ્રહિત,
|
Saved,સંગ્રહિત,
|
||||||
Saving,સંગ્રહ થાય છે,
|
Saving,સંગ્રહ થાય છે,
|
||||||
Schema Name or Name not passed to Open Quick Edit,,
|
Schema Name or Name not passed to Open Quick Edit,સ્કીમા નામ અથવા નામ અપાયું નથી Quick Edit ખોલવા માટે,
|
||||||
Search,શોધ,
|
Search,શોધ,
|
||||||
Secured Loans,સુરક્ષિત લોન,
|
Secured Loans,સુરક્ષિત લોન,
|
||||||
Securities and Deposits,સિક્યોરિટીઝ અને થાપણો,
|
Securities and Deposits,સિક્યોરિટીઝ અને થાપણો,
|
||||||
@ -584,8 +590,8 @@ Selected file,પસંદ કરેલી ફાઇલ,
|
|||||||
September,સેપ્ટેમ્બર,
|
September,સેપ્ટેમ્બર,
|
||||||
Service,સેવાઓ,
|
Service,સેવાઓ,
|
||||||
Set Discount Amount,સુયોજિત વટાવ રકમ,
|
Set Discount Amount,સુયોજિત વટાવ રકમ,
|
||||||
Set Period,,
|
Set Period,સમયગાળો સુયોજિત કરો,
|
||||||
Set Up,સુયોજિત કરો,
|
Set Up,સુયોજિત કરો,
|
||||||
Set Up Your Workspace,વ્યવસાય સુયોજિત કરો,
|
Set Up Your Workspace,વ્યવસાય સુયોજિત કરો,
|
||||||
Set an Import Type,આયાત પ્રકાર સહેજો,
|
Set an Import Type,આયાત પ્રકાર સહેજો,
|
||||||
Set the display language.,પ્રદર્શન ભાષા સહેજો.,
|
Set the display language.,પ્રદર્શન ભાષા સહેજો.,
|
||||||
@ -598,7 +604,7 @@ Sets how many digits are shown after the decimal point.,દશાંશ બિ
|
|||||||
Sets the app-wide date display format.,એપ્લિકેશન-વ્યાપક તારીખ બંધારણ સ્થાપન કરે છે.,
|
Sets the app-wide date display format.,એપ્લિકેશન-વ્યાપક તારીખ બંધારણ સ્થાપન કરે છે.,
|
||||||
Sets the internal precision used for monetary calculations. Above 6 should be sufficient for most currencies.,નાણાકીય ગણતરીઓ માટે વપરાયેલી આંતરિક ચોકસાઇ સ્થાપન કરે છે. મોટાભાગના ચલણો માટે 6 થી ઉપર પૂરતું હોવું જોઈએ.,
|
Sets the internal precision used for monetary calculations. Above 6 should be sufficient for most currencies.,નાણાકીય ગણતરીઓ માટે વપરાયેલી આંતરિક ચોકસાઇ સ્થાપન કરે છે. મોટાભાગના ચલણો માટે 6 થી ઉપર પૂરતું હોવું જોઈએ.,
|
||||||
Setting Up Instance,દાખલો સુયોજિત કરી રહ્યા છીએ,
|
Setting Up Instance,દાખલો સુયોજિત કરી રહ્યા છીએ,
|
||||||
Setting up...,સુયોજિત થઈ રહ્યું છે,
|
Setting up...,સુયોજિત થઈ રહ્યું છે...,
|
||||||
Settings,પરિસ્થિતિ,
|
Settings,પરિસ્થિતિ,
|
||||||
Settings changes will be visible on reload,પરિસ્થિતિ ફેરફારો પુનઃપ્રારંભ પર દેખાશે,
|
Settings changes will be visible on reload,પરિસ્થિતિ ફેરફારો પુનઃપ્રારંભ પર દેખાશે,
|
||||||
Setup,સ્થાપન,
|
Setup,સ્થાપન,
|
||||||
@ -615,7 +621,7 @@ Show Month/Year,મહિના/વર્ષ દર્શાવો,
|
|||||||
Single Value,એકમો મૂલ્ય,
|
Single Value,એકમો મૂલ્ય,
|
||||||
Skip Child Tables,બાળ કોષ્ટકો અવગણો,
|
Skip Child Tables,બાળ કોષ્ટકો અવગણો,
|
||||||
Skip Transactions,વ્યવહાર અવગણો,
|
Skip Transactions,વ્યવહાર અવગણો,
|
||||||
Smallest Currency Fraction Value,સૌથી ઓછી ચલણ મૂલ્ય,
|
Smallest Currency Fraction Value,સૌથી ઓછુ ચલણ મૂલ્ય,
|
||||||
Softwares,Softwares,
|
Softwares,Softwares,
|
||||||
Something has gone terribly wrong. Please check the console and raise an issue.,કંઈક ભયંકર રીતે ખોટું થયું છે. કૃપા કરીને કોનસોલ તપાસો અને કોઈ મુદ્દો ઉભો કરો.,
|
Something has gone terribly wrong. Please check the console and raise an issue.,કંઈક ભયંકર રીતે ખોટું થયું છે. કૃપા કરીને કોનસોલ તપાસો અને કોઈ મુદ્દો ઉભો કરો.,
|
||||||
Source of Funds (Liabilities),ભંડોળનો સ્રોત (દેવું),
|
Source of Funds (Liabilities),ભંડોળનો સ્રોત (દેવું),
|
||||||
@ -632,12 +638,12 @@ Stock Entries,માલ લેવડદેવડ,
|
|||||||
Stock Expenses,માલ ખર્ચ,
|
Stock Expenses,માલ ખર્ચ,
|
||||||
Stock In Hand,હાથ પર માલ,
|
Stock In Hand,હાથ પર માલ,
|
||||||
Stock In Hand Acc.,હાથ પર માલ ખાતું,
|
Stock In Hand Acc.,હાથ પર માલ ખાતું,
|
||||||
Stock Ledger,માલ હિસાબ ,
|
Stock Ledger,માલ હિસાબ,
|
||||||
Stock Ledger Entry,માલ હિસાબ ખતવણી,
|
Stock Ledger Entry,માલ હિસાબ ખતવણી,
|
||||||
Stock Liabilities,માલ દેવું,
|
Stock Liabilities,માલ દેવું,
|
||||||
Stock Movement,માલ સંચાલન,
|
Stock Movement,માલ સંચાલન,
|
||||||
Stock Movement Item,માલ સંચાલન વસ્તુ,
|
Stock Movement Item,માલ સંચાલન વસ્તુ,
|
||||||
Stock Movement No.,માલ સંચાલન ક્રમ,
|
Stock Movement No.,માલ સંચાલન ક્રમ,
|
||||||
Stock Movement Number Series,માલ સંચાલન ક્રમાંકન,
|
Stock Movement Number Series,માલ સંચાલન ક્રમાંકન,
|
||||||
Stock Movements,માલ સંચાલન,
|
Stock Movements,માલ સંચાલન,
|
||||||
Stock Not Transferred,માલ સ્થળાંતર થયો નથી,
|
Stock Not Transferred,માલ સ્થળાંતર થયો નથી,
|
||||||
@ -645,10 +651,10 @@ Stock Received But Not Billed,પ્રાપ્ત માલ બિનઃ બ
|
|||||||
Stock Received But Not Billed Acc.,પ્રાપ્ત માલ બિનઃ બિલ ખાતું,
|
Stock Received But Not Billed Acc.,પ્રાપ્ત માલ બિનઃ બિલ ખાતું,
|
||||||
Stock Transfer Item,માલ સ્થળાંતર વસ્તુ,
|
Stock Transfer Item,માલ સ્થળાંતર વસ્તુ,
|
||||||
Stock has been transferred,માલ સ્થળાંતર થયો,
|
Stock has been transferred,માલ સ્થળાંતર થયો,
|
||||||
Stock qty. ${0} out of ${1} left to transfer,માલની માત્રા. ${1} માંથી ${0} સ્થળાંતર માટે બાકી,
|
Stock qty. ${0} out of ${1} left to transfer,માલની માત્રા. ${1} માંથી ${0} સ્થળાંતર માટે બાકી,
|
||||||
StockTransfer,,
|
StockTransfer,,
|
||||||
Stores,દુકાન/સંગ્રહ,
|
Stores,દુકાન/સંગ્રહ,
|
||||||
Su,,
|
Su,રવિ,
|
||||||
Submit,રજૂ કરો,
|
Submit,રજૂ કરો,
|
||||||
Submit ${0},${0} રજૂ કરો,
|
Submit ${0},${0} રજૂ કરો,
|
||||||
Submit Journal Entry?,આમનોંધ ખતવણી રજૂ કરો?,
|
Submit Journal Entry?,આમનોંધ ખતવણી રજૂ કરો?,
|
||||||
@ -657,7 +663,7 @@ Submit Sales Invoice?,વેચાણ ભરતિયું રજૂ કરો?
|
|||||||
Submit on Import,આયાત થતાં રજૂ કરો,
|
Submit on Import,આયાત થતાં રજૂ કરો,
|
||||||
Submitted,રજૂ,
|
Submitted,રજૂ,
|
||||||
Submitting,રજૂ થાય છે,
|
Submitting,રજૂ થાય છે,
|
||||||
Subtotal,પેટા-સરવાળો ,
|
Subtotal,પેટા-સરવાળો,
|
||||||
Successfully created the following ${0} entries:,સફળતાપૂર્વક નીચેની ${0} લેવડદેવડ બનાવી:,
|
Successfully created the following ${0} entries:,સફળતાપૂર્વક નીચેની ${0} લેવડદેવડ બનાવી:,
|
||||||
Supplier,વિક્રેતા,
|
Supplier,વિક્રેતા,
|
||||||
Supplier Created,વિક્રેતા નિર્માણ,
|
Supplier Created,વિક્રેતા નિર્માણ,
|
||||||
@ -667,8 +673,8 @@ System,પ્રણાલિ,
|
|||||||
System Settings,પ્રણાલિ પરિસ્થિતિ,
|
System Settings,પ્રણાલિ પરિસ્થિતિ,
|
||||||
System Setup,પ્રણાલિ સ્થાપન,
|
System Setup,પ્રણાલિ સ્થાપન,
|
||||||
Tax,કરવેરો,
|
Tax,કરવેરો,
|
||||||
Tax Account,કરવેરા હિસાબ,
|
Tax Account,કરવેરા ખાતું,
|
||||||
Tax Assets,કરવેરા રોકાણ ,
|
Tax Assets,કરવેરા રોકાણ,
|
||||||
Tax Detail,કર વિગત,
|
Tax Detail,કર વિગત,
|
||||||
Tax ID,કર ID,
|
Tax ID,કર ID,
|
||||||
Tax Invoice,,
|
Tax Invoice,,
|
||||||
@ -682,7 +688,7 @@ Template,નમૂનો,
|
|||||||
Temporary,કામચલાઉ,
|
Temporary,કામચલાઉ,
|
||||||
Temporary Accounts,હંગામી ખાતાઓ,
|
Temporary Accounts,હંગામી ખાતાઓ,
|
||||||
Temporary Opening,હંગામી આરંભ,
|
Temporary Opening,હંગામી આરંભ,
|
||||||
Th,,
|
Th,ગુરુ,
|
||||||
The following ${0} entries were created: ${1},નીચેની ${0} લેવડદેવડ બનાવવામાં આવી હતી: ${1},
|
The following ${0} entries were created: ${1},નીચેની ${0} લેવડદેવડ બનાવવામાં આવી હતી: ${1},
|
||||||
This Month,આ મહિને,
|
This Month,આ મહિને,
|
||||||
This Quarter,આ ત્રિમાસીક,
|
This Quarter,આ ત્રિમાસીક,
|
||||||
@ -692,10 +698,10 @@ This action is permanent and will cancel the following payment: ${0},આ ક્
|
|||||||
This action is permanent and will cancel the following payments: ${0},આ ક્રિયા કાયમી છે અને નીચેની ચુકવણીઓ ફોક થશે: ${0},
|
This action is permanent and will cancel the following payments: ${0},આ ક્રિયા કાયમી છે અને નીચેની ચુકવણીઓ ફોક થશે: ${0},
|
||||||
This action is permanent and will delete associated ledger entries.,આ ક્રિયા કાયમી છે અને સંકળાયેલ ખાતાવહી લેવડદેવડ છેકાશે.,
|
This action is permanent and will delete associated ledger entries.,આ ક્રિયા કાયમી છે અને સંકળાયેલ ખાતાવહી લેવડદેવડ છેકાશે.,
|
||||||
This action is permanent.,આ ક્રિયા કાયમી છે.,
|
This action is permanent.,આ ક્રિયા કાયમી છે.,
|
||||||
Times New Roman,Times New Roman,
|
Times New Roman,,
|
||||||
To,પ્રત્યે,
|
To,પ્રત્યે,
|
||||||
To Account,ખાતા પ્રત્યે,
|
To Account,ખાતા પ્રત્યે,
|
||||||
To Account and From Account can't be the same: ${0},થકી ખાતું અને પ્રત્યે ખાતું સરખા હોય શકે નહીં : ${0},
|
To Account and From Account can't be the same: ${0},થકી ખાતું અને પ્રત્યે ખાતું સરખા હોય શકે નહીં : ${0},
|
||||||
To Date,આ તારીખ સુધી,
|
To Date,આ તારીખ સુધી,
|
||||||
To Loc.,સ્થાન સુધી,
|
To Loc.,સ્થાન સુધી,
|
||||||
To Year,વર્ષ સુધી,
|
To Year,વર્ષ સુધી,
|
||||||
@ -719,11 +725,11 @@ Transfer Type,વ્યવહારનો પ્રકાર,
|
|||||||
Transfer will cause future entries to have negative stock.,સ્થળાંતર થી ભવિષ્ય ના લેવડદેવડો માં જથ્થો ઋણ માં જશે,
|
Transfer will cause future entries to have negative stock.,સ્થળાંતર થી ભવિષ્ય ના લેવડદેવડો માં જથ્થો ઋણ માં જશે,
|
||||||
Travel Expenses,મુસાફરી ખર્ચ,
|
Travel Expenses,મુસાફરી ખર્ચ,
|
||||||
Trial Balance,કાચું સરવૈયું,
|
Trial Balance,કાચું સરવૈયું,
|
||||||
Tu,,
|
Tu,મંગળ,
|
||||||
Type,પ્રકાર,
|
Type,પ્રકાર,
|
||||||
Type to search...,શોધવા માટે વર્ણન કરો ...,
|
Type to search...,શોધવા માટે વર્ણન કરો ...,
|
||||||
UOM,UOM,
|
UOM,માપણી,
|
||||||
Unit,એકમ,
|
Unit,,
|
||||||
Unit Type,એકમ પ્રકાર,
|
Unit Type,એકમ પ્રકાર,
|
||||||
Unpaid,અણચુકવેલ,
|
Unpaid,અણચુકવેલ,
|
||||||
Unsecured Loans,અસુરક્ષિત લોન,
|
Unsecured Loans,અસુરક્ષિત લોન,
|
||||||
@ -739,12 +745,12 @@ Version,Version,
|
|||||||
View,નિરીક્ષણ,
|
View,નિરીક્ષણ,
|
||||||
View Purchases,ખરીદી જુઓ,
|
View Purchases,ખરીદી જુઓ,
|
||||||
View Sales,વેચાણ જુઓ,
|
View Sales,વેચાણ જુઓ,
|
||||||
We,અમે,
|
We,બુધ,
|
||||||
Welcome to Frappe Books,Frappe Books પર આપનું સ્વાગત છે,
|
Welcome to Frappe Books,Frappe Books પર આપનું સ્વાગત છે,
|
||||||
Write Off,ખારીજ,
|
Write Off,ખારીજ,
|
||||||
Write Off Account,ખારીજ ખાતું,
|
Write Off Account,ખારીજ ખાતું,
|
||||||
Write Off Account ${0} does not exist. Please set Write Off Account in General Settings,ખારીજ ખાતું ${0} અસ્તિત્વ માં નથી,ખારીજ ખાતું સામાન્ય વિગતો માં ઉમેરો,
|
Write Off Account ${0} does not exist. Please set Write Off Account in General Settings,ખારીજ ખાતું ${0} અસ્તિત્વ માં નથી. ખારીજ ખાતું સામાન્ય વિગતો માં ઉમેરો,
|
||||||
Write Off Account not set. Please set Write Off Account in General Settings,ખારીજ ખાતું અસ્તિત્વ માં નથી,ખારીજ ખાતું સામાન્ય વિગતો માં ઉમેરો,
|
Write Off Account not set. Please set Write Off Account in General Settings,ખારીજ ખાતું અસ્તિત્વ માં નથી. ખારીજ ખાતું સામાન્ય વિગતો માં ઉમેરો,
|
||||||
Write Off Entry,ખારીજ ખાતું ખતવણી,
|
Write Off Entry,ખારીજ ખાતું ખતવણી,
|
||||||
Yearly,વાર્ષિક,
|
Yearly,વાર્ષિક,
|
||||||
Years,વર્ષ,
|
Years,વર્ષ,
|
||||||
@ -752,4 +758,4 @@ Yellow,Yellow,
|
|||||||
Yes,હા,
|
Yes,હા,
|
||||||
Your Name,તમારું નામ,
|
Your Name,તમારું નામ,
|
||||||
john@doe.com,john@doe.com,
|
john@doe.com,john@doe.com,
|
||||||
verify the imported data and click on,આયાત કરેલા ડેટાને ચકાસો અને ક્લિક કરો,
|
verify the imported data and click on,આયાત કરેલા ડેટાને ચકાસો અને ક્લિક કરો,
|
Can't render this file because it has a wrong number of fields in line 269.
|
@ -6,7 +6,7 @@ export function getValueMapFromList<T, K extends keyof T, V extends keyof T>(
|
|||||||
key: K,
|
key: K,
|
||||||
valueKey: V,
|
valueKey: V,
|
||||||
filterUndefined: boolean = true
|
filterUndefined: boolean = true
|
||||||
): Record<string, unknown> {
|
): Record<string, T[V]> {
|
||||||
if (filterUndefined) {
|
if (filterUndefined) {
|
||||||
list = list.filter(
|
list = list.filter(
|
||||||
(f) =>
|
(f) =>
|
||||||
@ -20,7 +20,7 @@ export function getValueMapFromList<T, K extends keyof T, V extends keyof T>(
|
|||||||
const value = f[valueKey];
|
const value = f[valueKey];
|
||||||
acc[keyValue] = value;
|
acc[keyValue] = value;
|
||||||
return acc;
|
return acc;
|
||||||
}, {} as Record<string, unknown>);
|
}, {} as Record<string, T[V]>);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getRandomString(): string {
|
export function getRandomString(): string {
|
||||||
|
@ -43,4 +43,8 @@ export interface SelectFileReturn {
|
|||||||
success: boolean;
|
success: boolean;
|
||||||
data: Buffer;
|
data: Buffer;
|
||||||
canceled: boolean;
|
canceled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type PropertyEnum<T extends Record<string, any>> = {
|
||||||
|
[key in keyof Required<T>]: key;
|
||||||
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user