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',
|
||||
'arrow-body-style': 'off',
|
||||
'prefer-arrow-callback': 'off',
|
||||
'vue/no-mutating-props': 'off',
|
||||
'vue/multi-word-component-names': '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
|
||||
run: yarn
|
||||
|
||||
- name: Install RPM
|
||||
run: HOMEBREW_NO_AUTO_UPDATE=1 brew install rpm
|
||||
|
||||
- name: Run build
|
||||
env:
|
||||
CSC_IDENTITY_AUTO_DISCOVERY: false
|
||||
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
|
||||
env:
|
||||
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 }}
|
||||
CSC_LINK: ${{ secrets.CSC_LINK }}
|
||||
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
|
||||
CSC_IDENTITY_AUTO_DISCOVERY: true
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
APPLE_NOTARIZE: 1
|
||||
run: |
|
||||
yarn set version 1.22.18
|
||||
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
|
||||
|
||||
- [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.
|
||||
- [Project Board](https://github.com/frappe/books/projects/1): Roadmap that is updated with acceptable latency.
|
||||
- [Telegram Group](https://t.me/frappebooks): Used for discussions and decisions regarding everything Frappe Books.
|
||||
- [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.
|
||||
|
||||
|
@ -2,9 +2,19 @@ import { getDefaultMetaFieldValueMap } from '../../backend/helpers';
|
||||
import { DatabaseManager } from '../database/manager';
|
||||
|
||||
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> = {
|
||||
StockMovement: 'SMOV-',
|
||||
Shipment: 'SHP-',
|
||||
PurchaseReceipt: 'PREC-',
|
||||
Shipment: 'SHPM-',
|
||||
};
|
||||
|
||||
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
|
||||
appId: io.frappe.books
|
||||
afterSign: build/notarize.js
|
||||
asarUnpack: '**/*.node'
|
||||
extraResources:
|
||||
[
|
||||
@ -11,6 +10,8 @@ mac:
|
||||
type: distribution
|
||||
category: public.app-category.finance
|
||||
icon: build/icon.icns
|
||||
notarize:
|
||||
appBundleId: io.frappe.books
|
||||
hardenedRuntime: true
|
||||
gatekeeperAssess: false
|
||||
darkModeSupport: false
|
||||
|
@ -3,7 +3,6 @@ import { Doc } from 'fyo/model/doc';
|
||||
import { isPesa } from 'fyo/utils';
|
||||
import { ValueError } from 'fyo/utils/errors';
|
||||
import { DateTime } from 'luxon';
|
||||
import { Money } from 'pesa';
|
||||
import { Field, FieldTypeEnum, RawValue, TargetField } from 'schemas/types';
|
||||
import { getIsNullOrUndef, safeParseFloat, safeParseInt } from 'utils';
|
||||
import { DatabaseHandler } from './dbHandler';
|
||||
|
@ -5,7 +5,6 @@ import { Verb } from 'fyo/telemetry/types';
|
||||
import { DEFAULT_USER } from 'fyo/utils/consts';
|
||||
import { ConflictError, MandatoryError, NotFoundError } from 'fyo/utils/errors';
|
||||
import Observable from 'fyo/utils/observable';
|
||||
import { Money } from 'pesa';
|
||||
import {
|
||||
DynamicLinkField,
|
||||
Field,
|
||||
@ -142,6 +141,10 @@ export class Doc extends Observable<DocValue | Doc[]> {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.schema.isChild) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.schema.isSubmittable) {
|
||||
return true;
|
||||
}
|
||||
@ -186,6 +189,14 @@ export class Doc extends Observable<DocValue | Doc[]> {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.schema.isSingle) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.schema.isChild) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -188,7 +188,7 @@ function getField(df: string | Field): Field {
|
||||
label: '',
|
||||
fieldname: '',
|
||||
fieldtype: df as FieldType,
|
||||
};
|
||||
} as Field;
|
||||
}
|
||||
|
||||
return df;
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { Fyo } from 'fyo';
|
||||
import { DocValue } from 'fyo/core/types';
|
||||
import { Doc } from 'fyo/model/doc';
|
||||
import { Action } from 'fyo/model/types';
|
||||
import { Money } from 'pesa';
|
||||
import { Field, OptionField, SelectOption } from 'schemas/types';
|
||||
import { Field, FieldType, OptionField, SelectOption } from 'schemas/types';
|
||||
import { getIsNullOrUndef, safeParseInt } from 'utils';
|
||||
|
||||
export function slug(str: string) {
|
||||
@ -109,3 +110,34 @@ function getRawOptionList(field: Field, doc: Doc | undefined | null) {
|
||||
|
||||
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,
|
||||
ListViewSettings,
|
||||
} 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 { ModelNameEnum } from 'models/types';
|
||||
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 = {
|
||||
numberSeries: () => ({ referenceType: ModelNameEnum.StockMovement }),
|
||||
};
|
||||
@ -68,6 +91,7 @@ export class StockMovement extends Transfer {
|
||||
[MovementType.MaterialIssue]: fyo.t`Material Issue`,
|
||||
[MovementType.MaterialReceipt]: fyo.t`Material Receipt`,
|
||||
[MovementType.MaterialTransfer]: fyo.t`Material Transfer`,
|
||||
[MovementType.Manufacture]: fyo.t`Manufacture`,
|
||||
}[movementType] ?? '';
|
||||
|
||||
return {
|
||||
|
@ -4,7 +4,9 @@ import {
|
||||
FormulaMap,
|
||||
ReadOnlyMap,
|
||||
RequiredMap,
|
||||
ValidationMap,
|
||||
} from 'fyo/model/types';
|
||||
import { ValidationError } from 'fyo/utils/errors';
|
||||
import { ModelNameEnum } from 'models/types';
|
||||
import { Money } from 'pesa';
|
||||
import { StockMovement } from './StockMovement';
|
||||
@ -33,6 +35,10 @@ export class StockMovementItem extends Doc {
|
||||
return this.parentdoc?.movementType === MovementType.MaterialTransfer;
|
||||
}
|
||||
|
||||
get isManufacture() {
|
||||
return this.parentdoc?.movementType === MovementType.Manufacture;
|
||||
}
|
||||
|
||||
static filters: FiltersMap = {
|
||||
item: () => ({ trackItem: true }),
|
||||
};
|
||||
@ -53,14 +59,14 @@ export class StockMovementItem extends Doc {
|
||||
dependsOn: ['item', 'rate', 'quantity'],
|
||||
},
|
||||
fromLocation: {
|
||||
formula: (fn) => {
|
||||
if (this.isReceipt || this.isTransfer) {
|
||||
formula: () => {
|
||||
if (this.isReceipt || this.isTransfer || this.isManufacture) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const defaultLocation = this.fyo.singles.InventorySettings
|
||||
?.defaultLocation as string | undefined;
|
||||
if (defaultLocation && !this.location && this.isIssue) {
|
||||
if (defaultLocation && !this.fromLocation && this.isIssue) {
|
||||
return defaultLocation;
|
||||
}
|
||||
|
||||
@ -69,14 +75,14 @@ export class StockMovementItem extends Doc {
|
||||
dependsOn: ['movementType'],
|
||||
},
|
||||
toLocation: {
|
||||
formula: (fn) => {
|
||||
if (this.isIssue || this.isTransfer) {
|
||||
formula: () => {
|
||||
if (this.isIssue || this.isTransfer || this.isManufacture) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const defaultLocation = this.fyo.singles.InventorySettings
|
||||
?.defaultLocation as string | undefined;
|
||||
if (defaultLocation && !this.location && this.isReceipt) {
|
||||
if (defaultLocation && !this.toLocation && this.isReceipt) {
|
||||
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 = {
|
||||
fromLocation: () => this.isIssue || 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',
|
||||
'MaterialReceipt' = 'MaterialReceipt',
|
||||
'MaterialTransfer' = 'MaterialTransfer',
|
||||
'Manufacture' = 'Manufacture',
|
||||
}
|
||||
|
||||
export interface SMDetails {
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "frappe-books",
|
||||
"version": "0.8.0",
|
||||
"version": "0.9.0",
|
||||
"description": "Simple book-keeping app for everyone",
|
||||
"main": "background.js",
|
||||
"author": {
|
||||
@ -55,10 +55,9 @@
|
||||
"autoprefixer": "^9",
|
||||
"babel-loader": "^8.2.3",
|
||||
"dotenv": "^16.0.0",
|
||||
"electron": "^18.3.7",
|
||||
"electron-builder": "^23.0.3",
|
||||
"electron": "18.3.7",
|
||||
"electron-builder": "24.0.0-alpha.12",
|
||||
"electron-devtools-installer": "^3.2.0",
|
||||
"electron-notarize": "^1.1.1",
|
||||
"electron-rebuild": "^3.2.9",
|
||||
"electron-updater": "^5.2.1",
|
||||
"eslint": "^7.32.0",
|
||||
@ -81,7 +80,7 @@
|
||||
"webpack": "^5.66.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"electron-builder": "^23.3.3"
|
||||
"electron-builder": "24.0.0-alpha.12"
|
||||
},
|
||||
"prettier": {
|
||||
"semi": true,
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { DateTime } from 'luxon';
|
||||
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';
|
||||
|
||||
@ -24,7 +24,8 @@ export interface ReportRow {
|
||||
foldedBelow?: boolean;
|
||||
}
|
||||
export type ReportData = ReportRow[];
|
||||
export interface ColumnField extends BaseField {
|
||||
export interface ColumnField extends Omit<BaseField, 'fieldtype'> {
|
||||
fieldtype: FieldType;
|
||||
align?: 'left' | 'right' | 'center';
|
||||
width?: number;
|
||||
}
|
||||
|
@ -73,7 +73,6 @@
|
||||
"label": "Address",
|
||||
"fieldtype": "Link",
|
||||
"target": "Address",
|
||||
"placeholder": "Click to create",
|
||||
"inline": true
|
||||
},
|
||||
{
|
||||
|
@ -40,7 +40,6 @@
|
||||
"label": "Address",
|
||||
"fieldtype": "Link",
|
||||
"target": "Address",
|
||||
"placeholder": "Click to create",
|
||||
"inline": true
|
||||
},
|
||||
{
|
||||
|
@ -16,7 +16,6 @@
|
||||
"label": "Address",
|
||||
"fieldtype": "Link",
|
||||
"target": "Address",
|
||||
"placeholder": "Click to create",
|
||||
"inline": true
|
||||
}
|
||||
],
|
||||
|
@ -35,6 +35,10 @@
|
||||
{
|
||||
"value": "MaterialTransfer",
|
||||
"label": "Material Transfer"
|
||||
},
|
||||
{
|
||||
"value": "Manufacture",
|
||||
"label": "Manufacture"
|
||||
}
|
||||
],
|
||||
"required": true
|
||||
|
@ -73,7 +73,6 @@
|
||||
"label": "Address",
|
||||
"fieldtype": "Link",
|
||||
"target": "Address",
|
||||
"placeholder": "Click to create",
|
||||
"inline": true
|
||||
}
|
||||
],
|
||||
|
@ -1,28 +1,56 @@
|
||||
export enum FieldTypeEnum {
|
||||
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',
|
||||
}
|
||||
import { PropertyEnum } from "utils/types";
|
||||
|
||||
export type FieldType =
|
||||
| 'Data'
|
||||
| 'Select'
|
||||
| 'Link'
|
||||
| 'Date'
|
||||
| 'Datetime'
|
||||
| 'Table'
|
||||
| 'AutoComplete'
|
||||
| 'Check'
|
||||
| 'AttachImage'
|
||||
| 'DynamicLink'
|
||||
| 'Int'
|
||||
| 'Float'
|
||||
| 'Currency'
|
||||
| '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 interface BaseField {
|
||||
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
|
||||
schemaName?: string; // Convenient access to schemaName incase just the field is passed
|
||||
required?: boolean; // Implies Not Null
|
||||
@ -39,31 +67,28 @@ export interface BaseField {
|
||||
}
|
||||
|
||||
export type SelectOption = { value: string; label: string };
|
||||
export interface OptionField extends BaseField {
|
||||
fieldtype:
|
||||
| FieldTypeEnum.Select
|
||||
| FieldTypeEnum.AutoComplete
|
||||
| FieldTypeEnum.Color;
|
||||
export interface OptionField extends Omit<BaseField, 'fieldtype'> {
|
||||
fieldtype: OptionFieldType;
|
||||
options: SelectOption[];
|
||||
emptyMessage?: string;
|
||||
allowCustom?: boolean;
|
||||
}
|
||||
|
||||
export interface TargetField extends BaseField {
|
||||
fieldtype: FieldTypeEnum.Table | FieldTypeEnum.Link;
|
||||
export interface TargetField extends Omit<BaseField, 'fieldtype'> {
|
||||
fieldtype: TargetFieldType;
|
||||
target: string; // Name of the table or group of tables to fetch values
|
||||
create?: boolean; // Whether to show Create in the dropdown
|
||||
edit?: boolean; // Whether the Table has quick editable columns
|
||||
}
|
||||
|
||||
export interface DynamicLinkField extends BaseField {
|
||||
fieldtype: FieldTypeEnum.DynamicLink;
|
||||
export interface DynamicLinkField extends Omit<BaseField, 'fieldtype'> {
|
||||
fieldtype: DynamicLinkFieldType;
|
||||
emptyMessage?: string;
|
||||
references: string; // Reference to an option field that links to schema
|
||||
}
|
||||
|
||||
export interface NumberField extends BaseField {
|
||||
fieldtype: FieldTypeEnum.Float | FieldTypeEnum.Int;
|
||||
export interface NumberField extends Omit<BaseField, 'fieldtype'> {
|
||||
fieldtype: NumberFieldType;
|
||||
minvalue?: number; // UI Facing used to restrict lower bound
|
||||
maxvalue?: number; // UI Facing used to restrict upper bound
|
||||
}
|
||||
|
21
src/App.vue
21
src/App.vue
@ -41,6 +41,7 @@
|
||||
import { ConfigKeys } from 'fyo/core/types';
|
||||
import { RTL_LANGUAGES } from 'fyo/utils/consts';
|
||||
import { ModelNameEnum } from 'models/types';
|
||||
import { systemLanguageRef } from 'src/utils/refs';
|
||||
import { computed } from 'vue';
|
||||
import WindowsTitleBar from './components/WindowsTitleBar.vue';
|
||||
import { handleErrorWithDialog } from './errorHandling';
|
||||
@ -54,6 +55,7 @@ import { initializeInstance } from './utils/initialization';
|
||||
import { checkForUpdates } from './utils/ipcCalls';
|
||||
import { updateConfigFiles } from './utils/misc';
|
||||
import { Search } from './utils/search';
|
||||
import { setGlobalShortcuts } from './utils/shortcuts';
|
||||
import { routeTo } from './utils/ui';
|
||||
import { Shortcuts, useKeys } from './utils/vueUtils';
|
||||
|
||||
@ -69,8 +71,6 @@ export default {
|
||||
companyName: '',
|
||||
searcher: null,
|
||||
shortcuts: null,
|
||||
languageDirection: 'ltr',
|
||||
language: '',
|
||||
};
|
||||
},
|
||||
provide() {
|
||||
@ -88,11 +88,8 @@ export default {
|
||||
WindowsTitleBar,
|
||||
},
|
||||
async mounted() {
|
||||
this.language = fyo.config.get('language');
|
||||
this.languageDirection = RTL_LANGUAGES.includes(this.language)
|
||||
? 'rtl'
|
||||
: 'ltr';
|
||||
this.shortcuts = new Shortcuts(this.keys);
|
||||
const shortcuts = new Shortcuts(this.keys);
|
||||
this.shortcuts = shortcuts;
|
||||
const lastSelectedFilePath = fyo.config.get(
|
||||
ConfigKeys.LastSelectedFilePath,
|
||||
null
|
||||
@ -108,6 +105,16 @@ export default {
|
||||
await handleErrorWithDialog(err, undefined, true, true);
|
||||
await this.showDbSelector();
|
||||
}
|
||||
|
||||
setGlobalShortcuts(shortcuts);
|
||||
},
|
||||
computed: {
|
||||
language() {
|
||||
return systemLanguageRef.value;
|
||||
},
|
||||
languageDirection() {
|
||||
return RTL_LANGUAGES.includes(this.language) ? 'rtl' : 'ltr';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async setDesk(filePath) {
|
||||
|
@ -108,6 +108,10 @@ export default {
|
||||
option = this.options.find((o) => o.label === value);
|
||||
}
|
||||
|
||||
if (!value && option === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return option?.label ?? oldValue;
|
||||
},
|
||||
async updateSuggestions(keyword) {
|
||||
|
@ -23,7 +23,7 @@
|
||||
@change="(e) => triggerChange(e.target.value)"
|
||||
@focus="(e) => $emit('focus', e)"
|
||||
>
|
||||
<option value="" disabled selected>
|
||||
<option value="" disabled selected v-if="inputPlaceholder">
|
||||
{{ inputPlaceholder }}
|
||||
</option>
|
||||
<option
|
||||
|
@ -62,6 +62,7 @@
|
||||
import { Doc } from 'fyo/model/doc';
|
||||
import Row from 'src/components/Row.vue';
|
||||
import { getErrorMessage } from 'src/utils';
|
||||
import { nextTick } from 'vue';
|
||||
import Button from '../Button.vue';
|
||||
import FormControl from './FormControl.vue';
|
||||
|
||||
@ -102,15 +103,18 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onChange(df, value) {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
async onChange(df, value) {
|
||||
const fieldname = df.fieldname;
|
||||
this.errors[fieldname] = null;
|
||||
const oldValue = this.row[fieldname];
|
||||
|
||||
this.errors[df.fieldname] = null;
|
||||
this.row.set(df.fieldname, value).catch((e) => {
|
||||
this.errors[df.fieldname] = getErrorMessage(e, this.row);
|
||||
});
|
||||
try {
|
||||
await this.row.set(fieldname, value);
|
||||
} catch (e) {
|
||||
this.errors[fieldname] = getErrorMessage(e, this.row);
|
||||
this.row[fieldname] = '';
|
||||
nextTick(() => (this.row[fieldname] = oldValue));
|
||||
}
|
||||
},
|
||||
getErrorString() {
|
||||
return Object.values(this.errors).filter(Boolean).join(' ');
|
||||
|
@ -6,7 +6,7 @@
|
||||
<div :class="showMandatory ? 'show-mandatory' : ''">
|
||||
<textarea
|
||||
ref="input"
|
||||
rows="3"
|
||||
:rows="rows"
|
||||
:class="['resize-none', inputClasses, containerClasses]"
|
||||
:value="value"
|
||||
:placeholder="inputPlaceholder"
|
||||
@ -27,5 +27,6 @@ export default {
|
||||
name: 'Text',
|
||||
extends: Base,
|
||||
emits: ['focus', 'input'],
|
||||
props: { rows: { type: Number, default: 3 } },
|
||||
};
|
||||
</script>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="flex bg-gray-25">
|
||||
<div class="flex bg-gray-25 overflow-x-auto">
|
||||
<div class="flex flex-1 flex-col">
|
||||
<!-- Page Header (Title, Buttons, etc) -->
|
||||
<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="
|
||||
fixed
|
||||
top-0
|
||||
left-0
|
||||
start-0
|
||||
w-screen
|
||||
h-screen
|
||||
z-20
|
||||
@ -12,19 +12,16 @@
|
||||
justify-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')"
|
||||
v-if="openModal"
|
||||
>
|
||||
<div
|
||||
class="
|
||||
bg-white
|
||||
rounded-lg
|
||||
shadow-2xl
|
||||
border
|
||||
overflow-hidden
|
||||
inner
|
||||
"
|
||||
class="bg-white rounded-lg shadow-2xl border overflow-hidden inner"
|
||||
v-bind="$attrs"
|
||||
@click.stop
|
||||
>
|
||||
@ -43,6 +40,10 @@ export default defineComponent({
|
||||
default: false,
|
||||
type: Boolean,
|
||||
},
|
||||
useBackdrop: {
|
||||
default: true,
|
||||
type: Boolean,
|
||||
},
|
||||
},
|
||||
emits: ['closemodal'],
|
||||
watch: {
|
||||
|
@ -8,7 +8,7 @@
|
||||
>
|
||||
<Transition name="spacer">
|
||||
<div
|
||||
v-if="!sidebar && platform === 'Mac'"
|
||||
v-if="!sidebar && platform === 'Mac' && languageDirection !== 'rtl'"
|
||||
class="h-full"
|
||||
:class="sidebar ? '' : 'w-tl me-4 border-e'"
|
||||
/>
|
||||
@ -16,7 +16,10 @@
|
||||
<h1 class="text-xl font-semibold select-none" v-if="title">
|
||||
{{ title }}
|
||||
</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 />
|
||||
<div class="border-e" v-if="showBorder" />
|
||||
<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';
|
||||
|
||||
export default {
|
||||
inject: ['sidebar'],
|
||||
inject: ['sidebar', 'languageDirection'],
|
||||
props: {
|
||||
title: { type: String, default: '' },
|
||||
backLink: { type: Boolean, default: true },
|
||||
|
@ -4,8 +4,8 @@
|
||||
<Button @click="open" class="px-2" :padding="false">
|
||||
<feather-icon name="search" class="w-4 h-4 me-1 text-gray-800" />
|
||||
<p>{{ t`Search` }}</p>
|
||||
<div class="text-gray-500 px-1 ms-4 text-sm">
|
||||
{{ modKey('k') }}
|
||||
<div class="text-gray-500 px-1 ms-4 text-sm whitespace-nowrap">
|
||||
{{ modKeyText('k') }}
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
@ -16,8 +16,9 @@
|
||||
@closemodal="close"
|
||||
:set-close-listener="false"
|
||||
>
|
||||
<div class="w-form">
|
||||
<!-- Search Input -->
|
||||
<div class="p-1 w-form">
|
||||
<div class="p-1">
|
||||
<input
|
||||
ref="input"
|
||||
type="search"
|
||||
@ -75,7 +76,9 @@
|
||||
class="text-sm text-end justify-self-end"
|
||||
:class="`text-${groupColorMap[si.group]}-500`"
|
||||
>
|
||||
{{ si.group === 'Docs' ? si.schemaLabel : groupLabelMap[si.group] }}
|
||||
{{
|
||||
si.group === 'Docs' ? si.schemaLabel : groupLabelMap[si.group]
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@ -137,9 +140,14 @@
|
||||
border-blue-100
|
||||
whitespace-nowrap
|
||||
"
|
||||
:class="{ 'bg-blue-100': searcher.filters.schemaFilters[sf.value] }"
|
||||
:class="{
|
||||
'bg-blue-100': searcher.filters.schemaFilters[sf.value],
|
||||
}"
|
||||
@click="
|
||||
searcher.set(sf.value, !searcher.filters.schemaFilters[sf.value])
|
||||
searcher.set(
|
||||
sf.value,
|
||||
!searcher.filters.schemaFilters[sf.value]
|
||||
)
|
||||
"
|
||||
>
|
||||
{{ sf.label }}
|
||||
@ -187,6 +195,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
<script>
|
||||
@ -195,7 +204,6 @@ import { getBgTextColorClass } from 'src/utils/colors';
|
||||
import { openLink } from 'src/utils/ipcCalls';
|
||||
import { docsPathMap } from 'src/utils/misc';
|
||||
import { getGroupLabelMap, searchGroups } from 'src/utils/search';
|
||||
import { getModKeyCode } from 'src/utils/vueUtils';
|
||||
import { nextTick } from 'vue';
|
||||
import Button from './Button.vue';
|
||||
import Modal from './Modal.vue';
|
||||
@ -233,18 +241,19 @@ export default {
|
||||
openLink('https://docs.frappebooks.com/' + docsPathMap.Search);
|
||||
},
|
||||
getShortcuts() {
|
||||
const modKey = getModKeyCode(this.platform);
|
||||
const ifOpen = (cb) => () => this.openModal && cb();
|
||||
const ifClose = (cb) => () => !this.openModal && cb();
|
||||
|
||||
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) {
|
||||
shortcuts.push({
|
||||
shortcut: [modKey, `Digit${Number(i) + 1}`],
|
||||
shortcut: `Digit${Number(i) + 1}`,
|
||||
callback: ifOpen(() => {
|
||||
const group = searchGroups[i];
|
||||
const value = this.searcher.filters.groupFilters[group];
|
||||
@ -261,15 +270,15 @@ export default {
|
||||
},
|
||||
setShortcuts() {
|
||||
for (const { shortcut, callback } of this.getShortcuts()) {
|
||||
this.shortcuts.set(shortcut, callback);
|
||||
this.shortcuts.pmod.set([shortcut], callback);
|
||||
}
|
||||
},
|
||||
deleteShortcuts() {
|
||||
for (const { shortcut } of this.getShortcuts()) {
|
||||
this.shortcuts.delete(shortcut);
|
||||
this.shortcuts.pmod.delete([shortcut]);
|
||||
}
|
||||
},
|
||||
modKey(key) {
|
||||
modKeyText(key) {
|
||||
key = key.toUpperCase();
|
||||
if (this.platform === 'Mac') {
|
||||
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 -->
|
||||
<div
|
||||
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
|
||||
class="
|
||||
@ -98,6 +100,20 @@
|
||||
</p>
|
||||
</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
|
||||
class="
|
||||
flex
|
||||
@ -153,6 +169,10 @@
|
||||
>
|
||||
<feather-icon name="chevrons-left" class="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
<Modal :open-modal="viewShortcuts" @closemodal="viewShortcuts = false">
|
||||
<ShortcutsHelper class="w-form" />
|
||||
</Modal>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
@ -160,18 +180,23 @@ import Button from 'src/components/Button.vue';
|
||||
import { reportIssue } from 'src/errorHandling';
|
||||
import { fyo } from 'src/initFyo';
|
||||
import { openLink } from 'src/utils/ipcCalls';
|
||||
import { docsPathRef } from 'src/utils/refs';
|
||||
import { getSidebarConfig } from 'src/utils/sidebarConfig';
|
||||
import { docsPath, routeTo } from 'src/utils/ui';
|
||||
import { routeTo } from 'src/utils/ui';
|
||||
import router from '../router';
|
||||
import Icon from './Icon.vue';
|
||||
import Modal from './Modal.vue';
|
||||
import ShortcutsHelper from './ShortcutsHelper.vue';
|
||||
|
||||
export default {
|
||||
components: [Button],
|
||||
inject: ['languageDirection', 'shortcuts'],
|
||||
emits: ['change-db-file', 'toggle-sidebar'],
|
||||
data() {
|
||||
return {
|
||||
companyName: '',
|
||||
groups: [],
|
||||
viewShortcuts: false,
|
||||
activeGroup: null,
|
||||
};
|
||||
},
|
||||
@ -182,6 +207,8 @@ export default {
|
||||
},
|
||||
components: {
|
||||
Icon,
|
||||
Modal,
|
||||
ShortcutsHelper,
|
||||
},
|
||||
async mounted() {
|
||||
const { companyName } = await fyo.doc.getDoc('AccountingSettings');
|
||||
@ -192,12 +219,23 @@ export default {
|
||||
router.afterEach(() => {
|
||||
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: {
|
||||
routeTo,
|
||||
reportIssue,
|
||||
openDocumentation() {
|
||||
openLink('https://docs.frappebooks.com/' + docsPath.value);
|
||||
openLink('https://docs.frappebooks.com/' + docsPathRef.value);
|
||||
},
|
||||
setActiveGroup() {
|
||||
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 { fyo } from 'src/initFyo';
|
||||
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 { nextTick } from 'vue';
|
||||
import Button from '../components/Button.vue';
|
||||
@ -184,7 +185,7 @@ export default {
|
||||
window.coa = this;
|
||||
}
|
||||
|
||||
docsPath.value = docsPathMap.ChartOfAccounts;
|
||||
docsPathRef.value = docsPathMap.ChartOfAccounts;
|
||||
|
||||
if (this.refetchTotals) {
|
||||
await this.setTotalDebitAndCredit();
|
||||
@ -192,7 +193,7 @@ export default {
|
||||
}
|
||||
},
|
||||
deactivated() {
|
||||
docsPath.value = '';
|
||||
docsPathRef.value = '';
|
||||
},
|
||||
methods: {
|
||||
async expand() {
|
||||
|
@ -65,12 +65,12 @@
|
||||
|
||||
<script>
|
||||
import PageHeader from 'src/components/PageHeader.vue';
|
||||
import { docsPath } from 'src/utils/ui';
|
||||
import UnpaidInvoices from './UnpaidInvoices.vue';
|
||||
import Cashflow from './Cashflow.vue';
|
||||
import Expenses from './Expenses.vue';
|
||||
import PeriodSelector from './PeriodSelector.vue';
|
||||
import ProfitAndLoss from './ProfitAndLoss.vue';
|
||||
import { docsPathRef } from 'src/utils/refs';
|
||||
|
||||
export default {
|
||||
name: 'Dashboard',
|
||||
@ -86,10 +86,10 @@ export default {
|
||||
return { period: 'This Year' };
|
||||
},
|
||||
activated() {
|
||||
docsPath.value = 'analytics/dashboard';
|
||||
docsPathRef.value = 'analytics/dashboard';
|
||||
},
|
||||
deactivated() {
|
||||
docsPath.value = '';
|
||||
docsPathRef.value = '';
|
||||
},
|
||||
methods: {
|
||||
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="
|
||||
absolute
|
||||
bottom-0
|
||||
left-0
|
||||
start-0
|
||||
text-gray-600
|
||||
bg-gray-100
|
||||
rounded
|
||||
rtl-rotate-180
|
||||
p-1
|
||||
m-4
|
||||
opacity-0
|
||||
hover:opacity-100 hover:shadow-md
|
||||
"
|
||||
|
||||
@click="sidebar = !sidebar"
|
||||
>
|
||||
<feather-icon name="chevrons-right" class="w-4 h-4" />
|
||||
@ -76,6 +77,11 @@ export default {
|
||||
transform: translateX(calc(-1 * var(--w-sidebar)));
|
||||
width: 0px;
|
||||
}
|
||||
[dir='rtl'] .sidebar-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(calc(1 * var(--w-sidebar)));
|
||||
width: 0px;
|
||||
}
|
||||
|
||||
.sidebar-enter-to,
|
||||
.sidebar-leave-from {
|
||||
|
@ -130,7 +130,6 @@
|
||||
|
||||
<template #quickedit v-if="quickEditDoc">
|
||||
<QuickEditForm
|
||||
class="w-quick-edit"
|
||||
:name="quickEditDoc.name"
|
||||
:show-name="false"
|
||||
:show-save="false"
|
||||
@ -160,8 +159,8 @@ import FormHeader from 'src/components/FormHeader.vue';
|
||||
import StatusBadge from 'src/components/StatusBadge.vue';
|
||||
import { fyo } from 'src/initFyo';
|
||||
import { docsPathMap } from 'src/utils/misc';
|
||||
import { docsPathRef, focusedDocsRef } from 'src/utils/refs';
|
||||
import {
|
||||
docsPath,
|
||||
getGroupedActionsForDoc,
|
||||
routeTo,
|
||||
showMessageDialog,
|
||||
@ -232,14 +231,17 @@ export default {
|
||||
},
|
||||
},
|
||||
activated() {
|
||||
docsPath.value = docsPathMap[this.schemaName];
|
||||
docsPathRef.value = docsPathMap[this.schemaName];
|
||||
focusedDocsRef.add(this.doc);
|
||||
},
|
||||
deactivated() {
|
||||
docsPath.value = '';
|
||||
docsPathRef.value = '';
|
||||
focusedDocsRef.delete(this.doc);
|
||||
},
|
||||
async mounted() {
|
||||
try {
|
||||
this.doc = await fyo.doc.getDoc(this.schemaName, this.name);
|
||||
focusedDocsRef.add(this.doc);
|
||||
} catch (error) {
|
||||
if (error instanceof fyo.errors.NotFoundError) {
|
||||
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">
|
||||
<QuickEditForm
|
||||
v-if="quickEditDoc && !linked"
|
||||
class="w-quick-edit"
|
||||
:name="quickEditDoc.name"
|
||||
:show-name="false"
|
||||
:show-save="false"
|
||||
@ -313,8 +312,8 @@ import StatusBadge from 'src/components/StatusBadge.vue';
|
||||
import LinkedEntryWidget from 'src/components/Widgets/LinkedEntryWidget.vue';
|
||||
import { fyo } from 'src/initFyo';
|
||||
import { docsPathMap } from 'src/utils/misc';
|
||||
import { docsPathRef, focusedDocsRef } from 'src/utils/refs';
|
||||
import {
|
||||
docsPath,
|
||||
getGroupedActionsForDoc,
|
||||
routeTo,
|
||||
showMessageDialog,
|
||||
@ -339,6 +338,7 @@ export default {
|
||||
LinkedEntryWidget,
|
||||
Barcode,
|
||||
},
|
||||
inject: ['shortcuts'],
|
||||
provide() {
|
||||
return {
|
||||
schemaName: this.schemaName,
|
||||
@ -455,14 +455,17 @@ export default {
|
||||
},
|
||||
},
|
||||
activated() {
|
||||
docsPath.value = docsPathMap[this.schemaName];
|
||||
docsPathRef.value = docsPathMap[this.schemaName];
|
||||
focusedDocsRef.add(this.doc);
|
||||
},
|
||||
deactivated() {
|
||||
docsPath.value = '';
|
||||
docsPathRef.value = '';
|
||||
focusedDocsRef.delete(this.doc);
|
||||
},
|
||||
async mounted() {
|
||||
try {
|
||||
this.doc = await fyo.doc.getDoc(this.schemaName, this.name);
|
||||
focusedDocsRef.add(this.doc);
|
||||
} catch (error) {
|
||||
if (error instanceof fyo.errors.NotFoundError) {
|
||||
routeTo(`/list/${this.schemaName}`);
|
||||
|
@ -148,8 +148,8 @@ import FormHeader from 'src/components/FormHeader.vue';
|
||||
import StatusBadge from 'src/components/StatusBadge.vue';
|
||||
import { fyo } from 'src/initFyo';
|
||||
import { docsPathMap } from 'src/utils/misc';
|
||||
import { docsPathRef, focusedDocsRef } from 'src/utils/refs';
|
||||
import {
|
||||
docsPath,
|
||||
getGroupedActionsForDoc,
|
||||
routeTo,
|
||||
showMessageDialog,
|
||||
@ -182,14 +182,17 @@ export default {
|
||||
};
|
||||
},
|
||||
activated() {
|
||||
docsPath.value = docsPathMap.JournalEntry;
|
||||
docsPathRef.value = docsPathMap.JournalEntry;
|
||||
focusedDocsRef.add(this.doc);
|
||||
},
|
||||
deactivated() {
|
||||
docsPath.value = '';
|
||||
docsPathRef.value = '';
|
||||
focusedDocsRef.delete(this.doc);
|
||||
},
|
||||
async mounted() {
|
||||
try {
|
||||
this.doc = await fyo.doc.getDoc(this.schemaName, this.name);
|
||||
focusedDocsRef.add(this.doc);
|
||||
} catch (error) {
|
||||
if (error instanceof fyo.errors.NotFoundError) {
|
||||
routeTo(`/list/${this.schemaName}`);
|
||||
|
@ -51,7 +51,8 @@ import {
|
||||
docsPathMap,
|
||||
getCreateFiltersFromListViewFilters,
|
||||
} 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';
|
||||
|
||||
export default {
|
||||
@ -82,14 +83,14 @@ export default {
|
||||
}
|
||||
|
||||
this.listConfig = getListConfig(this.schemaName);
|
||||
docsPath.value = docsPathMap[this.schemaName] ?? docsPathMap.Entries;
|
||||
docsPathRef.value = docsPathMap[this.schemaName] ?? docsPathMap.Entries;
|
||||
|
||||
if (this.fyo.store.isDevelopment) {
|
||||
window.lv = this;
|
||||
}
|
||||
},
|
||||
deactivated() {
|
||||
docsPath.value = '';
|
||||
docsPathRef.value = '';
|
||||
},
|
||||
methods: {
|
||||
updatedData(listFilters) {
|
||||
|
@ -105,6 +105,7 @@ import StatusBadge from 'src/components/StatusBadge.vue';
|
||||
import TwoColumnForm from 'src/components/TwoColumnForm.vue';
|
||||
import { fyo } from 'src/initFyo';
|
||||
import { getQuickEditWidget } from 'src/utils/quickEditWidgets';
|
||||
import { focusedDocsRef } from 'src/utils/refs';
|
||||
import { getActionsForDoc, openQuickEdit } from 'src/utils/ui';
|
||||
|
||||
export default {
|
||||
@ -131,6 +132,7 @@ export default {
|
||||
DropdownWithActions,
|
||||
},
|
||||
emits: ['close'],
|
||||
inject: ['shortcuts'],
|
||||
provide() {
|
||||
return {
|
||||
schemaName: this.schemaName,
|
||||
@ -147,17 +149,26 @@ export default {
|
||||
statusText: null,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
async mounted() {
|
||||
if (this.defaults) {
|
||||
this.values = JSON.parse(this.defaults);
|
||||
}
|
||||
|
||||
await this.fetchFieldsAndDoc();
|
||||
focusedDocsRef.add(this.doc);
|
||||
|
||||
if (fyo.store.isDevelopment) {
|
||||
window.qef = this;
|
||||
}
|
||||
},
|
||||
async created() {
|
||||
await this.fetchFieldsAndDoc();
|
||||
activated() {
|
||||
focusedDocsRef.add(this.doc);
|
||||
},
|
||||
deactivated() {
|
||||
focusedDocsRef.delete(this.doc);
|
||||
},
|
||||
unmounted() {
|
||||
focusedDocsRef.delete(this.doc);
|
||||
},
|
||||
computed: {
|
||||
isChild() {
|
||||
|
@ -46,7 +46,7 @@ import PageHeader from 'src/components/PageHeader.vue';
|
||||
import ListReport from 'src/components/Report/ListReport.vue';
|
||||
import { fyo } from 'src/initFyo';
|
||||
import { docsPathMap } from 'src/utils/misc';
|
||||
import { docsPath } from 'src/utils/ui';
|
||||
import { docsPathRef } from 'src/utils/refs';
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
@ -70,7 +70,7 @@ export default defineComponent({
|
||||
},
|
||||
components: { PageHeader, FormControl, ListReport, DropdownWithActions },
|
||||
async activated() {
|
||||
docsPath.value = docsPathMap[this.reportClassName] ?? docsPathMap.Reports;
|
||||
docsPathRef.value = docsPathMap[this.reportClassName] ?? docsPathMap.Reports;
|
||||
await this.setReportData();
|
||||
|
||||
const filters = JSON.parse(this.defaultFilters);
|
||||
@ -88,7 +88,7 @@ export default defineComponent({
|
||||
}
|
||||
},
|
||||
deactivated() {
|
||||
docsPath.value = '';
|
||||
docsPathRef.value = '';
|
||||
},
|
||||
computed: {
|
||||
title() {
|
||||
|
@ -49,7 +49,8 @@ import Row from 'src/components/Row.vue';
|
||||
import StatusBadge from 'src/components/StatusBadge.vue';
|
||||
import { fyo } from 'src/initFyo';
|
||||
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 { h, markRaw } from 'vue';
|
||||
import TabBase from './TabBase.vue';
|
||||
@ -112,10 +113,10 @@ export default {
|
||||
},
|
||||
activated() {
|
||||
this.setActiveTab();
|
||||
docsPath.value = docsPathMap.Settings;
|
||||
docsPathRef.value = docsPathMap.Settings;
|
||||
},
|
||||
deactivated() {
|
||||
docsPath.value = '';
|
||||
docsPathRef.value = '';
|
||||
if (this.fieldsChanged.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { ModelNameEnum } from 'models/types';
|
||||
import ChartOfAccounts from 'src/pages/ChartOfAccounts.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 GetStarted from 'src/pages/GetStarted.vue';
|
||||
import InvoiceForm from 'src/pages/InvoiceForm.vue';
|
||||
@ -138,9 +138,9 @@ const routes: RouteRecordRaw[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/data-import',
|
||||
name: 'Data Import',
|
||||
component: DataImport,
|
||||
path: '/import-wizard',
|
||||
name: 'Import Wizard',
|
||||
component: ImportWizard,
|
||||
},
|
||||
{
|
||||
path: '/settings',
|
||||
|
@ -82,6 +82,7 @@ input[type='number']::-webkit-inner-spin-button {
|
||||
|
||||
.w-quick-edit {
|
||||
width: var(--w-quick-edit);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.h-form {
|
||||
|
@ -1,12 +1,18 @@
|
||||
import { Fyo } from 'fyo';
|
||||
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 { GetAllOptions, QueryFilter } from 'utils/db/types';
|
||||
import { getMapFromList, safeParseFloat } from 'utils/index';
|
||||
import { ExportField, ExportTableField } from './types';
|
||||
|
||||
const excludedFieldTypes = [
|
||||
const excludedFieldTypes: FieldType[] = [
|
||||
FieldTypeEnum.AttachImage,
|
||||
FieldTypeEnum.Attachment,
|
||||
];
|
||||
@ -26,7 +32,7 @@ export function getExportFields(
|
||||
.filter((f) => !f.computed && f.label && !exclude.includes(f.fieldname))
|
||||
.map((field) => {
|
||||
const { fieldname, label } = field;
|
||||
const fieldtype = field.fieldtype as FieldTypeEnum;
|
||||
const fieldtype = field.fieldtype as FieldType;
|
||||
return {
|
||||
fieldname,
|
||||
fieldtype,
|
||||
@ -323,7 +329,7 @@ async function getChildTableData(
|
||||
return data;
|
||||
}
|
||||
|
||||
function convertRawPesaToFloat(data: RawValueMap[], fields: Field[]) {
|
||||
function convertRawPesaToFloat(data: RawValueMap[], fields: ExportField[]) {
|
||||
const currencyFields = fields.filter(
|
||||
(f) => f.fieldtype === FieldTypeEnum.Currency
|
||||
);
|
||||
|
@ -3,6 +3,7 @@ import { DEFAULT_LANGUAGE } from 'fyo/utils/consts';
|
||||
import { setLanguageMapOnTranslationString } from 'fyo/utils/translation';
|
||||
import { fyo } from 'src/initFyo';
|
||||
import { IPC_ACTIONS, IPC_MESSAGES } from 'utils/messages';
|
||||
import { systemLanguageRef } from './refs';
|
||||
import { showToast } from './ui';
|
||||
|
||||
// Language: Language Code in books/translations
|
||||
@ -42,6 +43,7 @@ export async function setLanguageMap(
|
||||
|
||||
if (success && !usingDefault) {
|
||||
fyo.config.set('language', language);
|
||||
systemLanguageRef.value = language;
|
||||
}
|
||||
|
||||
if (!dontReload && success && initLanguage !== oldLanguage) {
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { Fyo } from 'fyo';
|
||||
import { ConfigFile, ConfigKeys } from 'fyo/core/types';
|
||||
import { Doc } from 'fyo/model/doc';
|
||||
import { DateTime } from 'luxon';
|
||||
import { SetupWizard } from 'models/baseModels/SetupWizard/SetupWizard';
|
||||
import { ModelNameEnum } from 'models/types';
|
||||
@ -117,7 +118,7 @@ export const docsPathMap: Record<string, string | undefined> = {
|
||||
// Miscellaneous
|
||||
Search: 'miscellaneous/search',
|
||||
NumberSeries: 'miscellaneous/number-series',
|
||||
DataImport: 'miscellaneous/data-import',
|
||||
ImportWizard: 'miscellaneous/import-wizard',
|
||||
Settings: 'miscellaneous/settings',
|
||||
ChartOfAccounts: 'miscellaneous/chart-of-accounts',
|
||||
};
|
||||
@ -160,3 +161,45 @@ export function getCreateFiltersFromListViewFilters(filters: QueryFilter) {
|
||||
|
||||
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',
|
||||
},
|
||||
{
|
||||
label: t`Data Import`,
|
||||
route: '/data-import',
|
||||
label: t`Import Wizard`,
|
||||
route: '/import-wizard',
|
||||
group: 'Page',
|
||||
},
|
||||
{
|
||||
@ -594,10 +594,7 @@ export class Search {
|
||||
keys.sort((a, b) => safeParseFloat(b) - safeParseFloat(a));
|
||||
const array: SearchItems = [];
|
||||
for (const key of keys) {
|
||||
const keywords = groupedKeywords[key];
|
||||
if (!keywords?.length) {
|
||||
continue;
|
||||
}
|
||||
const keywords = groupedKeywords[key] ?? [];
|
||||
|
||||
this._pushDocSearchItems(keywords, array, input);
|
||||
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',
|
||||
},
|
||||
{
|
||||
label: t`Data Import`,
|
||||
name: 'data-import',
|
||||
route: '/data-import',
|
||||
label: t`Import Wizard`,
|
||||
name: 'import-wizard',
|
||||
route: '/import-wizard',
|
||||
},
|
||||
{
|
||||
label: t`Settings`,
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Doc } from "fyo/model/doc";
|
||||
import { FieldTypeEnum } from "schemas/types";
|
||||
import { FieldType } from "schemas/types";
|
||||
import { QueryFilter } from "utils/db/types";
|
||||
|
||||
export interface MessageDialogButton {
|
||||
@ -58,7 +58,7 @@ export interface SidebarItem {
|
||||
|
||||
export interface ExportField {
|
||||
fieldname: string;
|
||||
fieldtype: FieldTypeEnum;
|
||||
fieldtype: FieldType;
|
||||
label: string;
|
||||
export: boolean;
|
||||
}
|
||||
|
@ -4,7 +4,7 @@
|
||||
*/
|
||||
import { ipcRenderer } from 'electron';
|
||||
import { t } from 'fyo';
|
||||
import { Doc } from 'fyo/model/doc';
|
||||
import type { Doc } from 'fyo/model/doc';
|
||||
import { Action } from 'fyo/model/types';
|
||||
import { getActions } from 'fyo/utils';
|
||||
import { getDbError, LinkValidationError, ValueError } from 'fyo/utils/errors';
|
||||
@ -23,8 +23,6 @@ import {
|
||||
ToastOptions,
|
||||
} from './types';
|
||||
|
||||
export const docsPath = ref('');
|
||||
|
||||
export async function openQuickEdit({
|
||||
doc,
|
||||
schemaName,
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { onMounted, onUnmounted, Ref, ref, watch } from 'vue';
|
||||
import { onMounted, onUnmounted, reactive, ref, watch } from 'vue';
|
||||
|
||||
interface Keys {
|
||||
pressed: Set<string>;
|
||||
interface ModMap {
|
||||
alt: boolean;
|
||||
ctrl: boolean;
|
||||
meta: boolean;
|
||||
@ -9,13 +8,27 @@ interface Keys {
|
||||
repeat: boolean;
|
||||
}
|
||||
|
||||
export class Shortcuts {
|
||||
keys: Ref<Keys>;
|
||||
shortcuts: Map<string, Function>;
|
||||
type Mod = keyof ModMap;
|
||||
|
||||
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.shortcuts = new Map();
|
||||
this.isMac = getIsMac();
|
||||
|
||||
watch(this.keys, (keys) => {
|
||||
this.#trigger(keys);
|
||||
@ -23,17 +36,22 @@ export class Shortcuts {
|
||||
}
|
||||
|
||||
#trigger(keys: Keys) {
|
||||
const key = Array.from(keys.pressed).sort().join('+');
|
||||
const key = this.getKey(Array.from(keys.pressed), keys);
|
||||
this.shortcuts.get(key)?.();
|
||||
}
|
||||
|
||||
has(shortcut: string[]) {
|
||||
const key = shortcut.sort().join('+');
|
||||
const key = this.getKey(shortcut);
|
||||
return this.shortcuts.has(key);
|
||||
}
|
||||
|
||||
set(shortcut: string[], callback: Function, removeIfSet: boolean = true) {
|
||||
const key = shortcut.sort().join('+');
|
||||
set(
|
||||
shortcut: string[],
|
||||
callback: ShortcutFunction,
|
||||
removeIfSet: boolean = true
|
||||
) {
|
||||
const key = this.getKey(shortcut);
|
||||
|
||||
if (removeIfSet) {
|
||||
this.shortcuts.delete(key);
|
||||
}
|
||||
@ -46,13 +64,68 @@ export class Shortcuts {
|
||||
}
|
||||
|
||||
delete(shortcut: string[]) {
|
||||
const key = shortcut.sort().join('+');
|
||||
const key = this.getKey(shortcut);
|
||||
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() {
|
||||
const keys: Ref<Keys> = ref({
|
||||
const isMac = getIsMac();
|
||||
const keys: Keys = reactive({
|
||||
pressed: new Set<string>(),
|
||||
alt: false,
|
||||
ctrl: false,
|
||||
@ -62,21 +135,32 @@ export function useKeys() {
|
||||
});
|
||||
|
||||
const keydownListener = (e: KeyboardEvent) => {
|
||||
keys.value.pressed.add(e.code);
|
||||
keys.value.alt = e.altKey;
|
||||
keys.value.ctrl = e.ctrlKey;
|
||||
keys.value.meta = e.metaKey;
|
||||
keys.value.shift = e.shiftKey;
|
||||
keys.value.repeat = e.repeat;
|
||||
keys.alt = e.altKey;
|
||||
keys.ctrl = e.ctrlKey;
|
||||
keys.meta = e.metaKey;
|
||||
keys.shift = e.shiftKey;
|
||||
keys.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) => {
|
||||
keys.value.pressed.delete(e.code);
|
||||
|
||||
// Key up won't trigger on macOS for other keys.
|
||||
if (e.code === 'MetaLeft') {
|
||||
keys.value.pressed.clear();
|
||||
const { code } = e;
|
||||
if (code.startsWith('Meta') && isMac) {
|
||||
return keys.pressed.clear();
|
||||
}
|
||||
|
||||
keys.pressed.delete(code);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
@ -110,10 +194,6 @@ export function useMouseLocation() {
|
||||
return loc;
|
||||
}
|
||||
|
||||
export function getModKeyCode(platform: 'Windows' | 'Linux' | 'Mac') {
|
||||
if (platform === 'Mac') {
|
||||
return 'MetaLeft';
|
||||
}
|
||||
|
||||
return 'CtrlLeft';
|
||||
function getIsMac() {
|
||||
return navigator.userAgent.indexOf('Mac') !== -1;
|
||||
}
|
||||
|
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 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 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 Display","Affichage de l'adresse",
|
||||
"Address Line 1","Ligne d'adresse 1",
|
||||
@ -76,7 +76,7 @@ Buildings,Bâtiments,
|
||||
Business,Entreprise,
|
||||
Cancel,Annuler,
|
||||
"Cancel ${0} ${1}?","Annuler ${0} ${1} ?",
|
||||
Cancelled,Anulé,
|
||||
Cancelled,Annulé,
|
||||
"Cannot Delete","Impossible à supprimer",
|
||||
"Cannot delete ${0} ${1} because of linked entries.","Impossible de supprimer ${0} ${1} à cause des entrées liées.",
|
||||
"Capital Equipments","Biens d'équipement",
|
||||
@ -102,6 +102,7 @@ Close,Fermer,
|
||||
Closing,Fermeture,
|
||||
"Closing (Cr)","Fermeture (Cr)",
|
||||
"Closing (Dr)","Fermeture (Dr)",
|
||||
Collapse,Réduire,
|
||||
Color,Couleur,
|
||||
"Commission on Sales","Commission sur les ventes",
|
||||
Common,Autres,
|
||||
@ -143,8 +144,8 @@ Credit,Crédit,
|
||||
"Credit Card Entry","Entrée Carte de Crédit",
|
||||
"Credit Note",Avoir,
|
||||
Creditors,Créanciers,
|
||||
Currency,Monnaie,
|
||||
"Currency Name","Nom de la monnaie",
|
||||
Currency,Devise,
|
||||
"Currency Name","Nom de la devise",
|
||||
Current,Actuel,
|
||||
"Current Assets","Actifs courants",
|
||||
"Current Liabilities","Passifs courants",
|
||||
@ -153,11 +154,11 @@ Customer,Client,
|
||||
"Customer Created","Client créé",
|
||||
"Customer Currency","Monnaie du client",
|
||||
Customers,Clients,
|
||||
Customise,Personnalisez,
|
||||
"Customize your invoices by adding a logo and address details","Customisez vos factures en y ajoutant votre logo et adresse",
|
||||
Customise,Personnaliser,
|
||||
"Customize your invoices by adding a logo and address details",Personnalisez vos factures en y ajoutant votre logo et adresse,
|
||||
Dashboard,"Tableau de bord",
|
||||
"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 Format","Format de la date",
|
||||
Debit,Débit,
|
||||
@ -176,7 +177,7 @@ Details,Détails,
|
||||
"Direct Income","Revenu direct",
|
||||
Discount,Réduction,
|
||||
"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 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}).",
|
||||
@ -189,7 +190,7 @@ Discounts,"Réductions",
|
||||
"Display Logo in Invoice","Afficher le logo sur la facture",
|
||||
"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.",
|
||||
"Dividends Paid","Dividendes versés",
|
||||
"Dividends Paid","Dividendes versées",
|
||||
Docs,Documents,
|
||||
Documentation,Documentation,
|
||||
"Does Not Contain","Ne contient pas",
|
||||
@ -205,6 +206,7 @@ Email,,
|
||||
"Email Address","Adresse électronique",
|
||||
Empty,Vide,
|
||||
"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 State","Entrez le département",
|
||||
"Entertainment Expenses","Dépenses liées au divertissement",
|
||||
@ -216,13 +218,14 @@ Error,Erreur,
|
||||
"Exchange Rate","Taux de change",
|
||||
"Excise Entry","Entrée d'acquis",
|
||||
"Existing File","Fichier existant",
|
||||
Expand,Développer,
|
||||
Expense,Dépenses,
|
||||
"Expense Account","Compte de dépenses",
|
||||
Expenses,Dépenses,
|
||||
"Expenses Included In Valuation","Dépenses incluses dans la valorisation ",
|
||||
Export,Exporter,
|
||||
"Export Failed","Export Échoué",
|
||||
"Export Successful","Export réussie",
|
||||
"Export Successful","Export réussi",
|
||||
Fax,Fax,
|
||||
Field,Champ,
|
||||
Fieldname,"Nom du champ",
|
||||
@ -383,7 +386,7 @@ Paid,Payé,
|
||||
Parent,,
|
||||
"Parent Account","Compte parent",
|
||||
Party,Partie,
|
||||
"Patch Run","Éxecuter les correctifs",
|
||||
"Patch Run","Exécuter les correctifs",
|
||||
Pay,Payer,
|
||||
Payable,Payable,
|
||||
Payment,Paiement,
|
||||
@ -434,7 +437,7 @@ Purchases,Achats,
|
||||
Purple,Violet,
|
||||
Quantity,Quantité,
|
||||
Quarterly,Trimestriel,
|
||||
Quarters,Trimestre,
|
||||
Quarters,Trimestres,
|
||||
Rate,Tarif,
|
||||
"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é)",
|
||||
@ -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 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.",
|
||||
"Set Up","Configurer",
|
||||
"Setting Up Instance","Paramétrage de l'Instance",
|
||||
"Setting Up...",Paramétrage...,
|
||||
Settings,Paramètres,
|
||||
@ -584,7 +588,7 @@ Terms,Conditions,
|
||||
"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 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",
|
||||
"Times New Roman",,
|
||||
"To Account","Au compte",
|
||||
|
|
@ -1,14 +1,15 @@
|
||||
${0} ${1} already exists.,${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} account not set in Inventory Settings.,${0} ખાતું માલસૂચી સંયોજન માં સક્ષમ નથી,
|
||||
${0} already exists.,${0} પહેલેથી જ અસ્તિત્વમાં છે.,
|
||||
${0} fields selected,${0} ખાનાઓ લાગુ,
|
||||
${0} filters applied,${0} તપાસો લાગુ,
|
||||
${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} quantity 1 added.,${0} માત્રા 1 ઉમેરાણી,
|
||||
${0} rows,${0} પંક્તિઓ,
|
||||
${0} value ${1} does not exist.,{0} મૂલ્ય ${1} અસ્તિત્વમાં નથી,
|
||||
* required fields,* જરૂરી કોઠા,
|
||||
@ -16,11 +17,11 @@ ${0} value ${1} does not exist.,{0} મૂલ્ય ${1} અસ્તિત્
|
||||
03-23-2022,03-23-2022,
|
||||
03/23/2022,03/23/2022,
|
||||
1 filter applied,1 તપાસ લાગુ,
|
||||
2022-03-23,,
|
||||
"23 Mar, 2022",,
|
||||
23-03-2022,23-03-2022,
|
||||
23-Mar-22,"23 માર્ચ, 2022",
|
||||
23-03-2022,23-03-2022,
|
||||
0.983819444,0.983819444,
|
||||
23-03-2022,23-03-2022,
|
||||
23.03.2022,,
|
||||
23/03/2022,,
|
||||
9888900000,9888900000,
|
||||
Account,ખાતું,
|
||||
Account ${0} does not exist.,${0} ખાતું અસ્તિત્વ માં નથી,
|
||||
@ -79,7 +80,8 @@ Bank Accounts,બેંક ખાતા,
|
||||
Bank Entry,બેંક ખતવણી,
|
||||
Bank Name,બેંકનું નામ,
|
||||
Bank Overdraft Account,બેંક ઓવરડ્રાફ્ટ ખાતું,
|
||||
Base Grand Total,Base કુલ રકમ,
|
||||
Barcode,બારકોડ,
|
||||
Base Grand Total,પ્રમુખ કુલ રકમ,
|
||||
Based On,આના આધારે,
|
||||
Basic,Basic,
|
||||
Bill Created,બિલ નિર્માણ,
|
||||
@ -88,7 +90,7 @@ Blue,Blue,
|
||||
Both,બંને,
|
||||
Both From and To Location cannot be undefined,બન્ને થકી અને પ્રત્યે સ્થળ ખાલી હોય શકે નહીં,
|
||||
Buildings,મકાનો,
|
||||
Business,વ્યવસાય,
|
||||
Business,,
|
||||
Cancel,ફોક,
|
||||
Cancel ${0} ${1}?,${0} ${1} ફોક કરો ?,
|
||||
Cancelled,ફોક કરાયેલ,
|
||||
@ -102,11 +104,11 @@ Capital Stock,મૂડીનો હિસ્સો,
|
||||
Cash,રોકડ,
|
||||
Cash Entry,રોકડ ખતવણી,
|
||||
Cash In Hand,હાથ પર રોકડ,
|
||||
Cashflow,કેશફલૉ,
|
||||
Cashflow,રોકડ પ્રવાહ,
|
||||
Central Tax,કેન્દ્રીય કર,
|
||||
Change DB,ડીબી બદલો,
|
||||
Change File,ફાઈલ બદલો,
|
||||
Change Ref Type,સંદર્ભ પ્રકાર બદલો ,
|
||||
Change Ref Type,સંદર્ભ પ્રકાર બદલો,
|
||||
Chargeable,તહોમતપાત્ર,
|
||||
Chart Of Accounts Reviewed,હિસાબી આલેખ સમીક્ષા,
|
||||
Chart of Accounts,હિસાબી આલેખ,
|
||||
@ -117,7 +119,7 @@ Clearance Date,ક્લિઅરન્સ તારીખ,
|
||||
Click to create,નિર્માણ માટે ક્લિક કરો,
|
||||
Close,બંધ,
|
||||
Close Frappe Books and try manually,Frappe Books બંધ કરી ને જાતે પ્રયાસ કરો,
|
||||
Closing,બંધ થાય છે,
|
||||
Closing,આખર,
|
||||
Closing (Cr),આખર સિલક (Cr),
|
||||
Closing (Dr),આખર સિલક (Dr),
|
||||
Collapse,સંકોચો,
|
||||
@ -143,7 +145,7 @@ Country,દેશ,
|
||||
Country Code,દેશનો કોડ,
|
||||
Country code used to initialize regional settings.,પ્રાદેશિક નિયમન પ્રારંભ કરવા માટે દેશ કોડ વપરાય છે.,
|
||||
Courier,Courier,
|
||||
Cr.,જ.,
|
||||
Cr.,,
|
||||
Create,નિર્માણ કરો,
|
||||
Create Demo,ડેમો નિર્માણ,
|
||||
Create Purchase,ખરીદી નિર્માણ,
|
||||
@ -156,9 +158,9 @@ Create your first purchase invoice from the created supplier,મોજૂદ વ
|
||||
Create your first sales invoice for the created customer,મોજૂદ ગ્રાહક પરથી પ્રથમ વેચાણ ભરતિયું નિર્માણ કરો,
|
||||
Created,નિર્માણ થયું,
|
||||
Created By,નિર્માણ પ્રમાણે,
|
||||
Creating Items and Parties,વસ્તુઓ અને પક્ષૉ નિર્માણ થઈ રહ્યા છે,
|
||||
Creating Journal Entries,આમનોંધ લેવડદેવડ નિર્માણ થઈ રહી છે ,
|
||||
Creating Purchase Invoices,ખરીદી ભરતિયું નિર્માણ થઈ રહ્યું છે ,
|
||||
Creating Items and Parties,ચીજવસ્તુઓ અને પક્ષૉ નિર્માણ થઈ રહ્યા છે,
|
||||
Creating Journal Entries,આમનોંધ લેવડદેવડ નિર્માણ થઈ રહી છે,
|
||||
Creating Purchase Invoices,ખરીદી ભરતિયું નિર્માણ થઈ રહ્યું છે,
|
||||
Credit,જમા,
|
||||
Credit Card Entry,ક્રેડિટ કાર્ડ ખતવણી,
|
||||
Credit Note,જમા-ચિઠ્ઠી,
|
||||
@ -176,13 +178,13 @@ Customers,ગ્રાહકો,
|
||||
Customise,રૂપરેખા,
|
||||
Customize your invoices by adding a logo and address details,પ્રતીક અને સરનામાંની વિગતો ઉમેરીને તમારા ભરતીયા ની રૂપરેખા બદલો,
|
||||
Dashboard,મુખ્ય પૃષ્ઠ,
|
||||
Data Import,ડેટા આયાત ,
|
||||
Data Import,ડેટા આયાત,
|
||||
Database Error,ડેટાબેઝ ચૂક,
|
||||
Database file: ${0},ડેટાબેઝ ફાઇલ: ${0},
|
||||
Date,તારીખ,
|
||||
Date Format,તારીખ બંધારણ,
|
||||
Day,દિવસ,
|
||||
Debit,ઉધાર ,
|
||||
Debit,ઉધાર,
|
||||
Debit Note,ઉધાર-ચિઠ્ઠી,
|
||||
Debtors,દેવાદાર,
|
||||
December,ડીસેમ્બર,
|
||||
@ -219,7 +221,7 @@ Dividends Paid,ચૂકવેલ ડિવિડન,
|
||||
Docs,દસ્તાવેજ,
|
||||
Documentation,દસ્તાવેજીકરણ,
|
||||
Does Not Contain,સમાવિષ્ટ નથી,
|
||||
Dr.,ઉ.,
|
||||
Dr.,,
|
||||
Draft,મુસદ્દો,
|
||||
Duplicate,નકલ બનાવો,
|
||||
Duplicate ${0} ${1}?,નકલ ${0} ${1}?,
|
||||
@ -230,10 +232,12 @@ Electronic Equipments,વિદ્યુત સાધન,
|
||||
Email,ઇ-મેઇલ,
|
||||
Email Address,ઈ - મેઈલ સરનામું,
|
||||
Empty,ખાલી,
|
||||
Enable Barcodes,બારકોડ સક્ષમ કરો,
|
||||
Enable Discount Accounting,વટાવ હિસાબ સક્ષમ કરો,
|
||||
Enable Inventory,માલસૂચિ સક્ષમ કરો,
|
||||
Enter Country to load States,રાજ્યો લોડ કરવા માટે દેશ દાખલ કરો,
|
||||
Enter State,રાજ્ય દાખલ કરો,
|
||||
Enter barcode,બારકોડ ઉમેરો,
|
||||
Entertainment Expenses,મનોરંજન ખર્ચ,
|
||||
Entry Currency,ખતવણી ચલણ,
|
||||
Entry No,ખતવણી ક્રમ,
|
||||
@ -266,11 +270,11 @@ Fiscal Year,નાણાકીય વર્ષ,
|
||||
Fiscal Year End Date,નાણાકીય વર્ષની અંતિમ તારીખ,
|
||||
Fiscal Year Start Date,નાણાકીય વર્ષની શરૂઆતની તારીખ,
|
||||
Fixed Asset,નિયત રોકાણ,
|
||||
Fixed Assets,નિયત અસ્કયામતો
|
||||
Fixed Assets,નિયત અસ્કયામતો,
|
||||
Font,અક્ષર વર્ણ,
|
||||
For,ને માટે,
|
||||
Forbidden Error,પ્રતિબંધિત ચૂક,
|
||||
Fr,,
|
||||
Fr,શુક્ર,
|
||||
Fraction,દશાંશ,
|
||||
Fraction Units,દશાંશ એકમો,
|
||||
Freight and Forwarding Charges,ભાડા અને આગળના ખર્ચ,
|
||||
@ -296,7 +300,7 @@ Green,Green,
|
||||
Group By,સમૂહ પ્રમાણે,
|
||||
HSN/SAC,HSN/SAC,
|
||||
HSN/SAC Code,HSN/SAC Code,
|
||||
Half Yearly,અર્ધવાર્ષિક ,
|
||||
Half Yearly,અર્ધવાર્ષિક,
|
||||
Half Years,અર્ધ વર્ષ,
|
||||
Help,મદદ,
|
||||
Hex Value,Hex Value,
|
||||
@ -325,6 +329,7 @@ Instance Id,દાખલો,
|
||||
Insufficient Quantity.,અપૂર્ણ માત્રા,
|
||||
Intergrated Tax,આંતરગ્રાહી કર,
|
||||
Internal Precision,આંતરિક ખરાપણું,
|
||||
Invalid barcode value ${0}.,બારકોડનું મૂલ્ય ${0} અમાન્ય છે,
|
||||
Invalid value ${0} for ${1},${0} માટે ${1} અમાન્ય મૂલ્ય,
|
||||
Inventory,માલસૂચિ,
|
||||
Inventory Settings,માલસૂચિ સંયોજન,
|
||||
@ -341,14 +346,15 @@ Invoice Value,ભરતિયું મૂલ્ય,
|
||||
Invoices,ભરતિયું,
|
||||
Is,,
|
||||
Is Empty,,
|
||||
Is Group,,
|
||||
Is Group,સમૂહ છે,
|
||||
Is Not,,
|
||||
Is Not Empty,,
|
||||
Is Whole,શું પરિપૂર્ણ છે,
|
||||
Item,વિગત,
|
||||
Item Description,વર્ણન,
|
||||
Item Name,નામ,
|
||||
Items,વસ્તુઓ ,
|
||||
Item Name,ચીજવસ્તુનું નામ,
|
||||
Item with barcode ${0} not found.,બારકોડ ${0} વાળી ચીજવસ્તુ હજાર નથી.,
|
||||
Items,ચીજવસ્તુઓ,
|
||||
January,જાન્યુઆરી,
|
||||
John Doe,John Doe,
|
||||
Journal Entries,આમનોંધ લેવડદેવડ,
|
||||
@ -369,13 +375,13 @@ Limit,મર્યાદા,
|
||||
Link Validation Error,લિંક માન્યતા ચૂક,
|
||||
List,યાદી,
|
||||
Load an existing .db file from your computer.,તમારા કમ્પ્યુટરથી અસ્તિત્વમાં હોય એવી .db ફાઇલ લોડ કરો.,
|
||||
Loading Report...,લોડિંગ રિપોર્ટ ...,
|
||||
Loading...,લોડ કરી રહ્યું છે ...,
|
||||
Loading Report...,અહેવાલ રજૂ થાય છે...,
|
||||
Loading...,રજૂ થાય છે...,
|
||||
Loans (Liabilities),લોન (દેવું),
|
||||
Loans and Advances (Assets),કરજ અને ધિરાણ (રોકાણ),
|
||||
Locale,પ્રાદેશિક નિયમન,
|
||||
Location,સ્થાન,
|
||||
Location Name,સ્થાનનું નામ ,
|
||||
Location Name,સ્થાનનું નામ,
|
||||
Logo,પ્રતીક,
|
||||
Make Entry,લેવડદેવડ ઉમેરો,
|
||||
Mandatory Error,અનિવાર્ય ચૂક,
|
||||
@ -390,22 +396,22 @@ Meter,મિટર,
|
||||
Minimal,Minimal,
|
||||
Misc,વગેરે,
|
||||
Miscellaneous Expenses,પરચૂરણ ખર્ચ,
|
||||
Mo,,
|
||||
Mo,સોમ,
|
||||
Modified,સુધારેલું,
|
||||
Modified By,સંશોધિત પ્રમાણે,
|
||||
Monthly,માસિક,
|
||||
Months,મહિના,
|
||||
More Filters,વધુ તપાસો,
|
||||
Movement Type,સ્થળાંતર પ્રકાર,
|
||||
Moving Average,,
|
||||
Moving Average,ચાલક સરેરાંશ,
|
||||
Name,નામ,
|
||||
Navigate,શોધખોળ,
|
||||
Net Total,ચોખ્ખો સરવાળો,
|
||||
New ${0},નવો ${0},
|
||||
Navigate,સંચાલન,
|
||||
Net Total,કુલ સરવાળો,
|
||||
New ${0},નવુ ${0},
|
||||
New Account,નવું ખાતું,
|
||||
New Entry,નવી ખતવણી,
|
||||
New File,નવી ફાઈલ,
|
||||
No,ના ,
|
||||
No,ના,
|
||||
No Data to Import,આયાત કરવા માટે કોઈ ડેટા નથી,
|
||||
No Values to be Displayed,પ્રદર્શિત કરવા માટે કોઈ મૂલ્યો નથી,
|
||||
No entries found,કોઈ લેવડદેવડ મળ્યા નથી,
|
||||
@ -414,12 +420,12 @@ No filters selected,તપાસ ઊમેરાય નથી,
|
||||
No labels have been assigned.,કોઈ લેબલ સોંપવામાં આવ્યા નથી.,
|
||||
No results found,કોઈ પરિણામો મળ્યા નથી,
|
||||
No transactions yet,હજી સુધી કોઈ વ્યવહાર નથી,
|
||||
None,,
|
||||
None,કોઈ નહીં,
|
||||
Not Found,મળ્યા નથી,
|
||||
Not Saved,સંગ્રહ થયો નથી,
|
||||
Notes,નોંધ,
|
||||
November,નવેમ્બર,
|
||||
Number Series,સંખ્યા ક્રમાંકન,
|
||||
Number Series,ક્રમાંકન,
|
||||
Number of ${0},${0} ની સંખ્યા,
|
||||
October,ઓક્ટોબર,
|
||||
Office Equipments,કચેરી સાધન,
|
||||
@ -428,8 +434,8 @@ Office Rent,કચેરી ભાડા,
|
||||
Onboarding Complete,ઓનબોર્ડિંગ પૂર્ણ,
|
||||
Open Count,ખુલ્લી ગણતરી,
|
||||
Open Folder,ખુલ્લું ફોલ્ડર,
|
||||
Opening (Cr),ખૂલતી સિલક (જમા.),
|
||||
Opening (Dr),ખૂલતી સિલક (ઉધાર.),
|
||||
Opening (Cr),ખૂલતી સિલક (Cr),
|
||||
Opening (Dr),ખૂલતી સિલક (Dr),
|
||||
Opening Balance Equity,ખૂલતી ઇક્વિટી,
|
||||
Opening Balances,શરૂઆતી મૂડી,
|
||||
Opening Entry,શરૂઆતી ખતવણી,
|
||||
@ -443,14 +449,14 @@ Paid,ચુકવેલ,
|
||||
Parent,પ્રધાન,
|
||||
Parent Account,પ્રધાન ખાતું,
|
||||
Party,પેઢી,
|
||||
Patch Run,,
|
||||
Patch Run,મરામતી ક્રિયા,
|
||||
Pay,ચૂકવણી,
|
||||
Payable,ચૂકવવાપાત્ર,
|
||||
Payment,પેમેન્ટ,
|
||||
Payment For,માટે પેમેન્ટ,
|
||||
Payment Method,પેમેન્ટ પદ્ધતિ,
|
||||
Payment No,પેમેન્ટ ક્રમ,
|
||||
Payment Number Series,ચુકવણી ક્રમાંકન,
|
||||
Payment Number Series,પેમેન્ટ ક્રમાંકન,
|
||||
Payment Reference,પેમેન્ટ સંદર્ભ,
|
||||
Payment Type,પેમેન્ટનો પ્રકાર,
|
||||
Payment amount cannot be ${0}.,પેમેન્ટની રકમ ${0} હોઈ શકતી નથી.,
|
||||
@ -492,7 +498,7 @@ Purchase Invoice Number Series,ખરીદ ભરતિયું ક્રમ
|
||||
Purchase Invoice Terms,ખરીદી ભરતિયું શરતો,
|
||||
Purchase Invoices,ખરીદી ભરતિયા,
|
||||
Purchase Item Created,ખરીદ વસ્તુ નિર્માણ,
|
||||
Purchase Items,ખરીદ માલયાદી,
|
||||
Purchase Items,ખરીદ ચીજવસ્તુઓ,
|
||||
Purchase Payments,ખરીદી ચૂકવણીઓ,
|
||||
Purchase Receipt,ખરીદ પહોંચ,
|
||||
Purchase Receipt Item,ખરીદ પહોંચ વસ્તુ,
|
||||
@ -504,7 +510,7 @@ Purple,Purple,
|
||||
Qty. ${0},માત્રા. ${0},
|
||||
Quantity,માત્રા,
|
||||
Quantity (${0}) has to be greater than zero,માત્રા (${0}) શૂન્ય થી વધારે હોવી જોઈએ,
|
||||
Quantity needs to be set,માત્રા ,
|
||||
Quantity needs to be set,માત્રા સૂચવવી જરૂરી છે,
|
||||
Quarterly,ત્રિમાસિક,
|
||||
Quarters,ત્રિમાસિક,
|
||||
Rate,ભાવ,
|
||||
@ -512,13 +518,13 @@ Rate (${0}) cannot be less zero.,સંખ્યા (${0}) શૂન્ય થ
|
||||
Rate (${0}) has to be greater than zero,સંખ્યા (${0}) શૂન્ય થી વધારે હોવી જોઈએ,
|
||||
Rate can't be negative.,સંખ્યા ઋણમાં હોઈ શકે નથી.,
|
||||
Rate needs to be set,દર સુયોજિત કરવા જરૂરી છે,
|
||||
Receivable,મળવાપાત્ર ,
|
||||
Receivable,મળવાપાત્ર,
|
||||
Receive,સ્વીકાર,
|
||||
Recent Invoices,તાજેતરના Invoice,
|
||||
Red,Red,
|
||||
Ref Name,સંદર્ભ નામ ,
|
||||
Ref Name,સંદર્ભ નામ,
|
||||
Ref Type,સંદર્ભ પ્રકાર,
|
||||
Ref. / Cheque No.,સંદર્ભ. / ચેક નંબર. ,
|
||||
Ref. / Cheque No.,સંદર્ભ. / ચેક નંબર.,
|
||||
Ref. Date,સંદર્ભ. તારીખ,
|
||||
Ref. Name,સંદર્ભ. નામ,
|
||||
Ref. Type,સંદર્ભ. પ્રકાર,
|
||||
@ -531,7 +537,7 @@ Report,અહેવાલ,
|
||||
Report Error,ચૂક ઉલ્લેખ,
|
||||
Report Issue,ખામી ઉલ્લેખ,
|
||||
Reports,અહેવાલો,
|
||||
Required Fields not Assigned,નિર્દેશિત કોઠા ફાળવાયા નથી ,
|
||||
Required Fields not Assigned,નિર્દેશિત કોઠા ફાળવાયા નથી,
|
||||
Reset,Reset,
|
||||
Retained Earnings,જાળવેલ કમાણી,
|
||||
Reverse Chrg.,Reverse Chrg.,
|
||||
@ -546,7 +552,7 @@ Round Off,પરચુરણ બાકાત,
|
||||
Round Off Account,પરચુરણ બાકાત ખાતું,
|
||||
Round Off Account Not Found,પરચુરણ બાકાત ખાતું હાજર નથી,
|
||||
Rounded Off,બાકાત,
|
||||
Sa,,
|
||||
Sa,શનિ,
|
||||
Salary,પગાર,
|
||||
Sales,વેચાણ,
|
||||
Sales Acc.,વેચાણ ખાતું,
|
||||
@ -555,9 +561,9 @@ Sales Invoice,વેચાણ ભરતિયું,
|
||||
Sales Invoice Item,વેચાણ ભરતિયું વસ્તુ,
|
||||
Sales Invoice Number Series,વેચાણ ભરતિયું ક્રમાંકન,
|
||||
Sales Invoice Terms,વેચાણ ભરતિયું શરતો,
|
||||
Sales Invoices,વેચાણ ભરતીયા ,
|
||||
Sales Invoices,વેચાણ ભરતીયા,
|
||||
Sales Item Created,વેચાણની વસ્તુ નિર્માણ,
|
||||
Sales Items,વેચાણ માલયાદી,
|
||||
Sales Items,વેચાણ ચીજવસ્તુઓ,
|
||||
Sales Payments,વેચાણ ચૂકવણીઓ,
|
||||
Save,સંઘરો,
|
||||
Save Template,નમૂનો સંઘરો,
|
||||
@ -565,7 +571,7 @@ Save as PDF,PDF રીતે સંઘરો,
|
||||
Save as PDF Successful,PDF રીતે સંગ્રહ સફળકારક,
|
||||
Saved,સંગ્રહિત,
|
||||
Saving,સંગ્રહ થાય છે,
|
||||
Schema Name or Name not passed to Open Quick Edit,,
|
||||
Schema Name or Name not passed to Open Quick Edit,સ્કીમા નામ અથવા નામ અપાયું નથી Quick Edit ખોલવા માટે,
|
||||
Search,શોધ,
|
||||
Secured Loans,સુરક્ષિત લોન,
|
||||
Securities and Deposits,સિક્યોરિટીઝ અને થાપણો,
|
||||
@ -584,7 +590,7 @@ Selected file,પસંદ કરેલી ફાઇલ,
|
||||
September,સેપ્ટેમ્બર,
|
||||
Service,સેવાઓ,
|
||||
Set Discount Amount,સુયોજિત વટાવ રકમ,
|
||||
Set Period,,
|
||||
Set Period,સમયગાળો સુયોજિત કરો,
|
||||
Set Up,સુયોજિત કરો,
|
||||
Set Up Your Workspace,વ્યવસાય સુયોજિત કરો,
|
||||
Set an Import Type,આયાત પ્રકાર સહેજો,
|
||||
@ -598,7 +604,7 @@ Sets how many digits are shown after the decimal point.,દશાંશ બિ
|
||||
Sets the app-wide date display format.,એપ્લિકેશન-વ્યાપક તારીખ બંધારણ સ્થાપન કરે છે.,
|
||||
Sets the internal precision used for monetary calculations. Above 6 should be sufficient for most currencies.,નાણાકીય ગણતરીઓ માટે વપરાયેલી આંતરિક ચોકસાઇ સ્થાપન કરે છે. મોટાભાગના ચલણો માટે 6 થી ઉપર પૂરતું હોવું જોઈએ.,
|
||||
Setting Up Instance,દાખલો સુયોજિત કરી રહ્યા છીએ,
|
||||
Setting up...,સુયોજિત થઈ રહ્યું છે,
|
||||
Setting up...,સુયોજિત થઈ રહ્યું છે...,
|
||||
Settings,પરિસ્થિતિ,
|
||||
Settings changes will be visible on reload,પરિસ્થિતિ ફેરફારો પુનઃપ્રારંભ પર દેખાશે,
|
||||
Setup,સ્થાપન,
|
||||
@ -615,7 +621,7 @@ Show Month/Year,મહિના/વર્ષ દર્શાવો,
|
||||
Single Value,એકમો મૂલ્ય,
|
||||
Skip Child Tables,બાળ કોષ્ટકો અવગણો,
|
||||
Skip Transactions,વ્યવહાર અવગણો,
|
||||
Smallest Currency Fraction Value,સૌથી ઓછી ચલણ મૂલ્ય,
|
||||
Smallest Currency Fraction Value,સૌથી ઓછુ ચલણ મૂલ્ય,
|
||||
Softwares,Softwares,
|
||||
Something has gone terribly wrong. Please check the console and raise an issue.,કંઈક ભયંકર રીતે ખોટું થયું છે. કૃપા કરીને કોનસોલ તપાસો અને કોઈ મુદ્દો ઉભો કરો.,
|
||||
Source of Funds (Liabilities),ભંડોળનો સ્રોત (દેવું),
|
||||
@ -632,7 +638,7 @@ Stock Entries,માલ લેવડદેવડ,
|
||||
Stock Expenses,માલ ખર્ચ,
|
||||
Stock In Hand,હાથ પર માલ,
|
||||
Stock In Hand Acc.,હાથ પર માલ ખાતું,
|
||||
Stock Ledger,માલ હિસાબ ,
|
||||
Stock Ledger,માલ હિસાબ,
|
||||
Stock Ledger Entry,માલ હિસાબ ખતવણી,
|
||||
Stock Liabilities,માલ દેવું,
|
||||
Stock Movement,માલ સંચાલન,
|
||||
@ -648,7 +654,7 @@ Stock has been transferred,માલ સ્થળાંતર થયો,
|
||||
Stock qty. ${0} out of ${1} left to transfer,માલની માત્રા. ${1} માંથી ${0} સ્થળાંતર માટે બાકી,
|
||||
StockTransfer,,
|
||||
Stores,દુકાન/સંગ્રહ,
|
||||
Su,,
|
||||
Su,રવિ,
|
||||
Submit,રજૂ કરો,
|
||||
Submit ${0},${0} રજૂ કરો,
|
||||
Submit Journal Entry?,આમનોંધ ખતવણી રજૂ કરો?,
|
||||
@ -657,7 +663,7 @@ Submit Sales Invoice?,વેચાણ ભરતિયું રજૂ કરો?
|
||||
Submit on Import,આયાત થતાં રજૂ કરો,
|
||||
Submitted,રજૂ,
|
||||
Submitting,રજૂ થાય છે,
|
||||
Subtotal,પેટા-સરવાળો ,
|
||||
Subtotal,પેટા-સરવાળો,
|
||||
Successfully created the following ${0} entries:,સફળતાપૂર્વક નીચેની ${0} લેવડદેવડ બનાવી:,
|
||||
Supplier,વિક્રેતા,
|
||||
Supplier Created,વિક્રેતા નિર્માણ,
|
||||
@ -667,8 +673,8 @@ System,પ્રણાલિ,
|
||||
System Settings,પ્રણાલિ પરિસ્થિતિ,
|
||||
System Setup,પ્રણાલિ સ્થાપન,
|
||||
Tax,કરવેરો,
|
||||
Tax Account,કરવેરા હિસાબ,
|
||||
Tax Assets,કરવેરા રોકાણ ,
|
||||
Tax Account,કરવેરા ખાતું,
|
||||
Tax Assets,કરવેરા રોકાણ,
|
||||
Tax Detail,કર વિગત,
|
||||
Tax ID,કર ID,
|
||||
Tax Invoice,,
|
||||
@ -682,7 +688,7 @@ Template,નમૂનો,
|
||||
Temporary,કામચલાઉ,
|
||||
Temporary Accounts,હંગામી ખાતાઓ,
|
||||
Temporary Opening,હંગામી આરંભ,
|
||||
Th,,
|
||||
Th,ગુરુ,
|
||||
The following ${0} entries were created: ${1},નીચેની ${0} લેવડદેવડ બનાવવામાં આવી હતી: ${1},
|
||||
This Month,આ મહિને,
|
||||
This Quarter,આ ત્રિમાસીક,
|
||||
@ -692,7 +698,7 @@ 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 delete associated ledger entries.,આ ક્રિયા કાયમી છે અને સંકળાયેલ ખાતાવહી લેવડદેવડ છેકાશે.,
|
||||
This action is permanent.,આ ક્રિયા કાયમી છે.,
|
||||
Times New Roman,Times New Roman,
|
||||
Times New Roman,,
|
||||
To,પ્રત્યે,
|
||||
To Account,ખાતા પ્રત્યે,
|
||||
To Account and From Account can't be the same: ${0},થકી ખાતું અને પ્રત્યે ખાતું સરખા હોય શકે નહીં : ${0},
|
||||
@ -719,11 +725,11 @@ Transfer Type,વ્યવહારનો પ્રકાર,
|
||||
Transfer will cause future entries to have negative stock.,સ્થળાંતર થી ભવિષ્ય ના લેવડદેવડો માં જથ્થો ઋણ માં જશે,
|
||||
Travel Expenses,મુસાફરી ખર્ચ,
|
||||
Trial Balance,કાચું સરવૈયું,
|
||||
Tu,,
|
||||
Tu,મંગળ,
|
||||
Type,પ્રકાર,
|
||||
Type to search...,શોધવા માટે વર્ણન કરો ...,
|
||||
UOM,UOM,
|
||||
Unit,એકમ,
|
||||
UOM,માપણી,
|
||||
Unit,,
|
||||
Unit Type,એકમ પ્રકાર,
|
||||
Unpaid,અણચુકવેલ,
|
||||
Unsecured Loans,અસુરક્ષિત લોન,
|
||||
@ -739,12 +745,12 @@ Version,Version,
|
||||
View,નિરીક્ષણ,
|
||||
View Purchases,ખરીદી જુઓ,
|
||||
View Sales,વેચાણ જુઓ,
|
||||
We,અમે,
|
||||
We,બુધ,
|
||||
Welcome to Frappe Books,Frappe Books પર આપનું સ્વાગત છે,
|
||||
Write Off,ખારીજ,
|
||||
Write Off Account,ખારીજ ખાતું,
|
||||
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 ${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 Entry,ખારીજ ખાતું ખતવણી,
|
||||
Yearly,વાર્ષિક,
|
||||
Years,વર્ષ,
|
||||
|
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,
|
||||
valueKey: V,
|
||||
filterUndefined: boolean = true
|
||||
): Record<string, unknown> {
|
||||
): Record<string, T[V]> {
|
||||
if (filterUndefined) {
|
||||
list = list.filter(
|
||||
(f) =>
|
||||
@ -20,7 +20,7 @@ export function getValueMapFromList<T, K extends keyof T, V extends keyof T>(
|
||||
const value = f[valueKey];
|
||||
acc[keyValue] = value;
|
||||
return acc;
|
||||
}, {} as Record<string, unknown>);
|
||||
}, {} as Record<string, T[V]>);
|
||||
}
|
||||
|
||||
export function getRandomString(): string {
|
||||
|
@ -44,3 +44,7 @@ export interface SelectFileReturn {
|
||||
data: Buffer;
|
||||
canceled: boolean;
|
||||
}
|
||||
|
||||
export type PropertyEnum<T extends Record<string, any>> = {
|
||||
[key in keyof Required<T>]: key;
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user