2
0
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:
Akshay 2023-02-17 10:58:05 +05:30 committed by GitHub
commit 915c1d2e5c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
70 changed files with 3205 additions and 2004 deletions

View File

@ -10,6 +10,7 @@ module.exports = {
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'arrow-body-style': 'off', 'arrow-body-style': 'off',
'prefer-arrow-callback': 'off', 'prefer-arrow-callback': 'off',
'vue/no-mutating-props': 'off',
'vue/multi-word-component-names': 'off', 'vue/multi-word-component-names': 'off',
'vue/no-useless-template-attributes': 'off', 'vue/no-useless-template-attributes': 'off',
}, },

View File

@ -24,11 +24,8 @@ jobs:
- name: Install Dependencies - name: Install Dependencies
run: yarn run: yarn
- name: Install RPM
run: HOMEBREW_NO_AUTO_UPDATE=1 brew install rpm
- name: Run build - name: Run build
env: env:
CSC_IDENTITY_AUTO_DISCOVERY: false CSC_IDENTITY_AUTO_DISCOVERY: false
APPLE_NOTARIZE: 0 APPLE_NOTARIZE: 0
run: yarn electron:build -mwl --publish never run: yarn electron:build -mw --publish never

View File

@ -34,13 +34,12 @@ jobs:
- name: Run build - name: Run build
env: env:
APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }} APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
CSC_LINK: ${{ secrets.CSC_LINK }} CSC_LINK: ${{ secrets.CSC_LINK }}
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }} CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
CSC_IDENTITY_AUTO_DISCOVERY: true CSC_IDENTITY_AUTO_DISCOVERY: true
GH_TOKEN: ${{ secrets.GH_TOKEN }} GH_TOKEN: ${{ secrets.GH_TOKEN }}
APPLE_NOTARIZE: 1
run: | run: |
yarn set version 1.22.18 yarn set version 1.22.18
yarn electron:build --mac --publish always yarn electron:build --mac --publish always

View File

@ -121,8 +121,7 @@ If you want to contribute code then you can fork this repo, make changes and rai
## Links ## Links
- [Telegram Group](https://t.me/frappebooks): Used for discussions regarding features, issues, changes, etc. This group is also be used to make decisions regarding project direction. - [Telegram Group](https://t.me/frappebooks): Used for discussions and decisions regarding everything Frappe Books.
- [Project Board](https://github.com/frappe/books/projects/1): Roadmap that is updated with acceptable latency.
- [GitHub Discussions](https://github.com/frappe/books/discussions): Used for discussions around a specific topic. - [GitHub Discussions](https://github.com/frappe/books/discussions): Used for discussions around a specific topic.
- [Frappe Books Blog](https://tech.frappebooks.com/): Sporadically updated dev blog regarding the development of this project. - [Frappe Books Blog](https://tech.frappebooks.com/): Sporadically updated dev blog regarding the development of this project.

View File

@ -2,9 +2,19 @@ import { getDefaultMetaFieldValueMap } from '../../backend/helpers';
import { DatabaseManager } from '../database/manager'; import { DatabaseManager } from '../database/manager';
async function execute(dm: DatabaseManager) { async function execute(dm: DatabaseManager) {
const s = (await dm.db?.getAll('SingleValue', {
fields: ['value'],
filters: { fieldname: 'setupComplete' },
})) as { value: string }[];
if (!Number(s?.[0]?.value ?? '0')) {
return;
}
const names: Record<string, string> = { const names: Record<string, string> = {
StockMovement: 'SMOV-', StockMovement: 'SMOV-',
Shipment: 'SHP-', PurchaseReceipt: 'PREC-',
Shipment: 'SHPM-',
}; };
for (const referenceType in names) { for (const referenceType in names) {

View File

@ -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,
});
};

View File

@ -1,6 +1,5 @@
productName: Frappe Books productName: Frappe Books
appId: io.frappe.books appId: io.frappe.books
afterSign: build/notarize.js
asarUnpack: '**/*.node' asarUnpack: '**/*.node'
extraResources: extraResources:
[ [
@ -11,6 +10,8 @@ mac:
type: distribution type: distribution
category: public.app-category.finance category: public.app-category.finance
icon: build/icon.icns icon: build/icon.icns
notarize:
appBundleId: io.frappe.books
hardenedRuntime: true hardenedRuntime: true
gatekeeperAssess: false gatekeeperAssess: false
darkModeSupport: false darkModeSupport: false

View File

@ -3,7 +3,6 @@ import { Doc } from 'fyo/model/doc';
import { isPesa } from 'fyo/utils'; import { isPesa } from 'fyo/utils';
import { ValueError } from 'fyo/utils/errors'; import { ValueError } from 'fyo/utils/errors';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { Money } from 'pesa';
import { Field, FieldTypeEnum, RawValue, TargetField } from 'schemas/types'; import { Field, FieldTypeEnum, RawValue, TargetField } from 'schemas/types';
import { getIsNullOrUndef, safeParseFloat, safeParseInt } from 'utils'; import { getIsNullOrUndef, safeParseFloat, safeParseInt } from 'utils';
import { DatabaseHandler } from './dbHandler'; import { DatabaseHandler } from './dbHandler';

View File

@ -5,7 +5,6 @@ import { Verb } from 'fyo/telemetry/types';
import { DEFAULT_USER } from 'fyo/utils/consts'; import { DEFAULT_USER } from 'fyo/utils/consts';
import { ConflictError, MandatoryError, NotFoundError } from 'fyo/utils/errors'; import { ConflictError, MandatoryError, NotFoundError } from 'fyo/utils/errors';
import Observable from 'fyo/utils/observable'; import Observable from 'fyo/utils/observable';
import { Money } from 'pesa';
import { import {
DynamicLinkField, DynamicLinkField,
Field, Field,
@ -142,6 +141,10 @@ export class Doc extends Observable<DocValue | Doc[]> {
return false; return false;
} }
if (this.schema.isChild) {
return false;
}
if (!this.schema.isSubmittable) { if (!this.schema.isSubmittable) {
return true; return true;
} }
@ -186,6 +189,14 @@ export class Doc extends Observable<DocValue | Doc[]> {
return false; return false;
} }
if (this.schema.isSingle) {
return false;
}
if (this.schema.isChild) {
return false;
}
return true; return true;
} }

View File

@ -188,7 +188,7 @@ function getField(df: string | Field): Field {
label: '', label: '',
fieldname: '', fieldname: '',
fieldtype: df as FieldType, fieldtype: df as FieldType,
}; } as Field;
} }
return df; return df;

View File

@ -1,8 +1,9 @@
import { Fyo } from 'fyo'; import { Fyo } from 'fyo';
import { DocValue } from 'fyo/core/types';
import { Doc } from 'fyo/model/doc'; import { Doc } from 'fyo/model/doc';
import { Action } from 'fyo/model/types'; import { Action } from 'fyo/model/types';
import { Money } from 'pesa'; import { Money } from 'pesa';
import { Field, OptionField, SelectOption } from 'schemas/types'; import { Field, FieldType, OptionField, SelectOption } from 'schemas/types';
import { getIsNullOrUndef, safeParseInt } from 'utils'; import { getIsNullOrUndef, safeParseInt } from 'utils';
export function slug(str: string) { export function slug(str: string) {
@ -109,3 +110,34 @@ function getRawOptionList(field: Field, doc: Doc | undefined | null) {
return getList(doc!); return getList(doc!);
} }
export function getEmptyValuesByFieldTypes(
fieldtype: FieldType,
fyo: Fyo
): DocValue {
switch (fieldtype) {
case 'Date':
case 'Datetime':
return new Date();
case 'Float':
case 'Int':
return 0;
case 'Currency':
return fyo.pesa(0);
case 'Check':
return false;
case 'DynamicLink':
case 'Link':
case 'Select':
case 'AutoComplete':
case 'Text':
case 'Data':
case 'Color':
return null;
case 'Table':
case 'Attachment':
case 'AttachImage':
default:
return null;
}
}

View File

@ -6,7 +6,12 @@ import {
FormulaMap, FormulaMap,
ListViewSettings, ListViewSettings,
} from 'fyo/model/types'; } from 'fyo/model/types';
import { addItem, getDocStatusListColumn, getLedgerLinkAction } from 'models/helpers'; import { ValidationError } from 'fyo/utils/errors';
import {
addItem,
getDocStatusListColumn,
getLedgerLinkAction,
} from 'models/helpers';
import { LedgerPosting } from 'models/Transactional/LedgerPosting'; import { LedgerPosting } from 'models/Transactional/LedgerPosting';
import { ModelNameEnum } from 'models/types'; import { ModelNameEnum } from 'models/types';
import { Money } from 'pesa'; import { Money } from 'pesa';
@ -42,6 +47,24 @@ export class StockMovement extends Transfer {
}, },
}; };
async validate() {
await super.validate();
if (this.movementType !== MovementType.Manufacture) {
return;
}
const hasFrom = this.items?.findIndex((f) => f.fromLocation) !== -1;
const hasTo = this.items?.findIndex((f) => f.toLocation) !== -1;
if (!hasFrom) {
throw new ValidationError(this.fyo.t`Item with From location not found`);
}
if (!hasTo) {
throw new ValidationError(this.fyo.t`Item with To location not found`);
}
}
static filters: FiltersMap = { static filters: FiltersMap = {
numberSeries: () => ({ referenceType: ModelNameEnum.StockMovement }), numberSeries: () => ({ referenceType: ModelNameEnum.StockMovement }),
}; };
@ -68,6 +91,7 @@ export class StockMovement extends Transfer {
[MovementType.MaterialIssue]: fyo.t`Material Issue`, [MovementType.MaterialIssue]: fyo.t`Material Issue`,
[MovementType.MaterialReceipt]: fyo.t`Material Receipt`, [MovementType.MaterialReceipt]: fyo.t`Material Receipt`,
[MovementType.MaterialTransfer]: fyo.t`Material Transfer`, [MovementType.MaterialTransfer]: fyo.t`Material Transfer`,
[MovementType.Manufacture]: fyo.t`Manufacture`,
}[movementType] ?? ''; }[movementType] ?? '';
return { return {

View File

@ -4,7 +4,9 @@ import {
FormulaMap, FormulaMap,
ReadOnlyMap, ReadOnlyMap,
RequiredMap, RequiredMap,
ValidationMap,
} from 'fyo/model/types'; } from 'fyo/model/types';
import { ValidationError } from 'fyo/utils/errors';
import { ModelNameEnum } from 'models/types'; import { ModelNameEnum } from 'models/types';
import { Money } from 'pesa'; import { Money } from 'pesa';
import { StockMovement } from './StockMovement'; import { StockMovement } from './StockMovement';
@ -33,6 +35,10 @@ export class StockMovementItem extends Doc {
return this.parentdoc?.movementType === MovementType.MaterialTransfer; return this.parentdoc?.movementType === MovementType.MaterialTransfer;
} }
get isManufacture() {
return this.parentdoc?.movementType === MovementType.Manufacture;
}
static filters: FiltersMap = { static filters: FiltersMap = {
item: () => ({ trackItem: true }), item: () => ({ trackItem: true }),
}; };
@ -53,14 +59,14 @@ export class StockMovementItem extends Doc {
dependsOn: ['item', 'rate', 'quantity'], dependsOn: ['item', 'rate', 'quantity'],
}, },
fromLocation: { fromLocation: {
formula: (fn) => { formula: () => {
if (this.isReceipt || this.isTransfer) { if (this.isReceipt || this.isTransfer || this.isManufacture) {
return null; return null;
} }
const defaultLocation = this.fyo.singles.InventorySettings const defaultLocation = this.fyo.singles.InventorySettings
?.defaultLocation as string | undefined; ?.defaultLocation as string | undefined;
if (defaultLocation && !this.location && this.isIssue) { if (defaultLocation && !this.fromLocation && this.isIssue) {
return defaultLocation; return defaultLocation;
} }
@ -69,14 +75,14 @@ export class StockMovementItem extends Doc {
dependsOn: ['movementType'], dependsOn: ['movementType'],
}, },
toLocation: { toLocation: {
formula: (fn) => { formula: () => {
if (this.isIssue || this.isTransfer) { if (this.isIssue || this.isTransfer || this.isManufacture) {
return null; return null;
} }
const defaultLocation = this.fyo.singles.InventorySettings const defaultLocation = this.fyo.singles.InventorySettings
?.defaultLocation as string | undefined; ?.defaultLocation as string | undefined;
if (defaultLocation && !this.location && this.isReceipt) { if (defaultLocation && !this.toLocation && this.isReceipt) {
return defaultLocation; return defaultLocation;
} }
@ -86,6 +92,31 @@ export class StockMovementItem extends Doc {
}, },
}; };
validations: ValidationMap = {
fromLocation: (value) => {
if (!this.isManufacture) {
return;
}
if (value && this.toLocation) {
throw new ValidationError(
this.fyo.t`Only From or To can be set for Manucature`
);
}
},
toLocation: (value) => {
if (!this.isManufacture) {
return;
}
if (value && this.fromLocation) {
throw new ValidationError(
this.fyo.t`Only From or To can be set for Manufacture`
);
}
},
};
required: RequiredMap = { required: RequiredMap = {
fromLocation: () => this.isIssue || this.isTransfer, fromLocation: () => this.isIssue || this.isTransfer,
toLocation: () => this.isReceipt || this.isTransfer, toLocation: () => this.isReceipt || this.isTransfer,

View 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);

View File

@ -9,6 +9,7 @@ export enum MovementType {
'MaterialIssue' = 'MaterialIssue', 'MaterialIssue' = 'MaterialIssue',
'MaterialReceipt' = 'MaterialReceipt', 'MaterialReceipt' = 'MaterialReceipt',
'MaterialTransfer' = 'MaterialTransfer', 'MaterialTransfer' = 'MaterialTransfer',
'Manufacture' = 'Manufacture',
} }
export interface SMDetails { export interface SMDetails {

View File

@ -1,6 +1,6 @@
{ {
"name": "frappe-books", "name": "frappe-books",
"version": "0.8.0", "version": "0.9.0",
"description": "Simple book-keeping app for everyone", "description": "Simple book-keeping app for everyone",
"main": "background.js", "main": "background.js",
"author": { "author": {
@ -55,10 +55,9 @@
"autoprefixer": "^9", "autoprefixer": "^9",
"babel-loader": "^8.2.3", "babel-loader": "^8.2.3",
"dotenv": "^16.0.0", "dotenv": "^16.0.0",
"electron": "^18.3.7", "electron": "18.3.7",
"electron-builder": "^23.0.3", "electron-builder": "24.0.0-alpha.12",
"electron-devtools-installer": "^3.2.0", "electron-devtools-installer": "^3.2.0",
"electron-notarize": "^1.1.1",
"electron-rebuild": "^3.2.9", "electron-rebuild": "^3.2.9",
"electron-updater": "^5.2.1", "electron-updater": "^5.2.1",
"eslint": "^7.32.0", "eslint": "^7.32.0",
@ -81,7 +80,7 @@
"webpack": "^5.66.0" "webpack": "^5.66.0"
}, },
"resolutions": { "resolutions": {
"electron-builder": "^23.3.3" "electron-builder": "24.0.0-alpha.12"
}, },
"prettier": { "prettier": {
"semi": true, "semi": true,

View File

@ -1,6 +1,6 @@
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { AccountRootType } from 'models/baseModels/Account/types'; import { AccountRootType } from 'models/baseModels/Account/types';
import { BaseField, RawValue } from 'schemas/types'; import { BaseField, FieldType, RawValue } from 'schemas/types';
export type ExportExtention = 'csv' | 'json'; export type ExportExtention = 'csv' | 'json';
@ -24,7 +24,8 @@ export interface ReportRow {
foldedBelow?: boolean; foldedBelow?: boolean;
} }
export type ReportData = ReportRow[]; export type ReportData = ReportRow[];
export interface ColumnField extends BaseField { export interface ColumnField extends Omit<BaseField, 'fieldtype'> {
fieldtype: FieldType;
align?: 'left' | 'right' | 'center'; align?: 'left' | 'right' | 'center';
width?: number; width?: number;
} }

View File

@ -73,7 +73,6 @@
"label": "Address", "label": "Address",
"fieldtype": "Link", "fieldtype": "Link",
"target": "Address", "target": "Address",
"placeholder": "Click to create",
"inline": true "inline": true
}, },
{ {

View File

@ -40,7 +40,6 @@
"label": "Address", "label": "Address",
"fieldtype": "Link", "fieldtype": "Link",
"target": "Address", "target": "Address",
"placeholder": "Click to create",
"inline": true "inline": true
}, },
{ {

View File

@ -16,7 +16,6 @@
"label": "Address", "label": "Address",
"fieldtype": "Link", "fieldtype": "Link",
"target": "Address", "target": "Address",
"placeholder": "Click to create",
"inline": true "inline": true
} }
], ],

View File

@ -35,6 +35,10 @@
{ {
"value": "MaterialTransfer", "value": "MaterialTransfer",
"label": "Material Transfer" "label": "Material Transfer"
},
{
"value": "Manufacture",
"label": "Manufacture"
} }
], ],
"required": true "required": true

View File

@ -73,7 +73,6 @@
"label": "Address", "label": "Address",
"fieldtype": "Link", "fieldtype": "Link",
"target": "Address", "target": "Address",
"placeholder": "Click to create",
"inline": true "inline": true
} }
], ],

View File

@ -1,28 +1,56 @@
export enum FieldTypeEnum { import { PropertyEnum } from "utils/types";
Data = 'Data',
Select = 'Select', export type FieldType =
Link = 'Link', | 'Data'
Date = 'Date', | 'Select'
Datetime = 'Datetime', | 'Link'
Table = 'Table', | 'Date'
AutoComplete = 'AutoComplete', | 'Datetime'
Check = 'Check', | 'Table'
AttachImage = 'AttachImage', | 'AutoComplete'
DynamicLink = 'DynamicLink', | 'Check'
Int = 'Int', | 'AttachImage'
Float = 'Float', | 'DynamicLink'
Currency = 'Currency', | 'Int'
Text = 'Text', | 'Float'
Color = 'Color', | 'Currency'
Attachment = 'Attachment', | 'Text'
} | 'Color'
| 'Attachment';
export const FieldTypeEnum: PropertyEnum<Record<FieldType, FieldType>> = {
Data: 'Data',
Select: 'Select',
Link: 'Link',
Date: 'Date',
Datetime: 'Datetime',
Table: 'Table',
AutoComplete: 'AutoComplete',
Check: 'Check',
AttachImage: 'AttachImage',
DynamicLink: 'DynamicLink',
Int: 'Int',
Float: 'Float',
Currency: 'Currency',
Text: 'Text',
Color: 'Color',
Attachment: 'Attachment',
};
type OptionFieldType = 'Select' | 'AutoComplete' | 'Color';
type TargetFieldType = 'Table' | 'Link';
type NumberFieldType = 'Int' | 'Float';
type DynamicLinkFieldType = 'DynamicLink';
type BaseFieldType = Exclude<
FieldType,
TargetFieldType | DynamicLinkFieldType | OptionFieldType | NumberFieldType
>;
export type FieldType = keyof typeof FieldTypeEnum;
export type RawValue = string | number | boolean | null; export type RawValue = string | number | boolean | null;
export interface BaseField { export interface BaseField {
fieldname: string; // Column name in the db fieldname: string; // Column name in the db
fieldtype: FieldType; // UI Descriptive field types that map to column types fieldtype: BaseFieldType; // UI Descriptive field types that map to column types
label: string; // Translateable UI facing name label: string; // Translateable UI facing name
schemaName?: string; // Convenient access to schemaName incase just the field is passed schemaName?: string; // Convenient access to schemaName incase just the field is passed
required?: boolean; // Implies Not Null required?: boolean; // Implies Not Null
@ -39,31 +67,28 @@ export interface BaseField {
} }
export type SelectOption = { value: string; label: string }; export type SelectOption = { value: string; label: string };
export interface OptionField extends BaseField { export interface OptionField extends Omit<BaseField, 'fieldtype'> {
fieldtype: fieldtype: OptionFieldType;
| FieldTypeEnum.Select
| FieldTypeEnum.AutoComplete
| FieldTypeEnum.Color;
options: SelectOption[]; options: SelectOption[];
emptyMessage?: string; emptyMessage?: string;
allowCustom?: boolean; allowCustom?: boolean;
} }
export interface TargetField extends BaseField { export interface TargetField extends Omit<BaseField, 'fieldtype'> {
fieldtype: FieldTypeEnum.Table | FieldTypeEnum.Link; fieldtype: TargetFieldType;
target: string; // Name of the table or group of tables to fetch values target: string; // Name of the table or group of tables to fetch values
create?: boolean; // Whether to show Create in the dropdown create?: boolean; // Whether to show Create in the dropdown
edit?: boolean; // Whether the Table has quick editable columns edit?: boolean; // Whether the Table has quick editable columns
} }
export interface DynamicLinkField extends BaseField { export interface DynamicLinkField extends Omit<BaseField, 'fieldtype'> {
fieldtype: FieldTypeEnum.DynamicLink; fieldtype: DynamicLinkFieldType;
emptyMessage?: string; emptyMessage?: string;
references: string; // Reference to an option field that links to schema references: string; // Reference to an option field that links to schema
} }
export interface NumberField extends BaseField { export interface NumberField extends Omit<BaseField, 'fieldtype'> {
fieldtype: FieldTypeEnum.Float | FieldTypeEnum.Int; fieldtype: NumberFieldType;
minvalue?: number; // UI Facing used to restrict lower bound minvalue?: number; // UI Facing used to restrict lower bound
maxvalue?: number; // UI Facing used to restrict upper bound maxvalue?: number; // UI Facing used to restrict upper bound
} }
@ -80,7 +105,7 @@ export type Naming = 'autoincrement' | 'random' | 'numberSeries' | 'manual';
export interface Schema { export interface Schema {
name: string; // Table name name: string; // Table name
label: string; // Translateable UI facing name label: string; // Translateable UI facing name
fields: Field[]; // Maps to database columns fields: Field[]; // Maps to database columns
isTree?: boolean; // Used for nested set, eg for Chart of Accounts isTree?: boolean; // Used for nested set, eg for Chart of Accounts
extends?: string; // Value points to an Abstract schema. Indicates Subclass schema extends?: string; // Value points to an Abstract schema. Indicates Subclass schema
isChild?: boolean; // Indicates a child table, i.e table with "parent" FK column isChild?: boolean; // Indicates a child table, i.e table with "parent" FK column

View File

@ -41,6 +41,7 @@
import { ConfigKeys } from 'fyo/core/types'; import { ConfigKeys } from 'fyo/core/types';
import { RTL_LANGUAGES } from 'fyo/utils/consts'; import { RTL_LANGUAGES } from 'fyo/utils/consts';
import { ModelNameEnum } from 'models/types'; import { ModelNameEnum } from 'models/types';
import { systemLanguageRef } from 'src/utils/refs';
import { computed } from 'vue'; import { computed } from 'vue';
import WindowsTitleBar from './components/WindowsTitleBar.vue'; import WindowsTitleBar from './components/WindowsTitleBar.vue';
import { handleErrorWithDialog } from './errorHandling'; import { handleErrorWithDialog } from './errorHandling';
@ -54,6 +55,7 @@ import { initializeInstance } from './utils/initialization';
import { checkForUpdates } from './utils/ipcCalls'; import { checkForUpdates } from './utils/ipcCalls';
import { updateConfigFiles } from './utils/misc'; import { updateConfigFiles } from './utils/misc';
import { Search } from './utils/search'; import { Search } from './utils/search';
import { setGlobalShortcuts } from './utils/shortcuts';
import { routeTo } from './utils/ui'; import { routeTo } from './utils/ui';
import { Shortcuts, useKeys } from './utils/vueUtils'; import { Shortcuts, useKeys } from './utils/vueUtils';
@ -69,8 +71,6 @@ export default {
companyName: '', companyName: '',
searcher: null, searcher: null,
shortcuts: null, shortcuts: null,
languageDirection: 'ltr',
language: '',
}; };
}, },
provide() { provide() {
@ -88,11 +88,8 @@ export default {
WindowsTitleBar, WindowsTitleBar,
}, },
async mounted() { async mounted() {
this.language = fyo.config.get('language'); const shortcuts = new Shortcuts(this.keys);
this.languageDirection = RTL_LANGUAGES.includes(this.language) this.shortcuts = shortcuts;
? 'rtl'
: 'ltr';
this.shortcuts = new Shortcuts(this.keys);
const lastSelectedFilePath = fyo.config.get( const lastSelectedFilePath = fyo.config.get(
ConfigKeys.LastSelectedFilePath, ConfigKeys.LastSelectedFilePath,
null null
@ -108,6 +105,16 @@ export default {
await handleErrorWithDialog(err, undefined, true, true); await handleErrorWithDialog(err, undefined, true, true);
await this.showDbSelector(); await this.showDbSelector();
} }
setGlobalShortcuts(shortcuts);
},
computed: {
language() {
return systemLanguageRef.value;
},
languageDirection() {
return RTL_LANGUAGES.includes(this.language) ? 'rtl' : 'ltr';
},
}, },
methods: { methods: {
async setDesk(filePath) { async setDesk(filePath) {

View File

@ -108,6 +108,10 @@ export default {
option = this.options.find((o) => o.label === value); option = this.options.find((o) => o.label === value);
} }
if (!value && option === undefined) {
return null;
}
return option?.label ?? oldValue; return option?.label ?? oldValue;
}, },
async updateSuggestions(keyword) { async updateSuggestions(keyword) {

View File

@ -23,7 +23,7 @@
@change="(e) => triggerChange(e.target.value)" @change="(e) => triggerChange(e.target.value)"
@focus="(e) => $emit('focus', e)" @focus="(e) => $emit('focus', e)"
> >
<option value="" disabled selected> <option value="" disabled selected v-if="inputPlaceholder">
{{ inputPlaceholder }} {{ inputPlaceholder }}
</option> </option>
<option <option

View File

@ -62,6 +62,7 @@
import { Doc } from 'fyo/model/doc'; import { Doc } from 'fyo/model/doc';
import Row from 'src/components/Row.vue'; import Row from 'src/components/Row.vue';
import { getErrorMessage } from 'src/utils'; import { getErrorMessage } from 'src/utils';
import { nextTick } from 'vue';
import Button from '../Button.vue'; import Button from '../Button.vue';
import FormControl from './FormControl.vue'; import FormControl from './FormControl.vue';
@ -102,15 +103,18 @@ export default {
}, },
}, },
methods: { methods: {
onChange(df, value) { async onChange(df, value) {
if (value == null) { const fieldname = df.fieldname;
return; this.errors[fieldname] = null;
} const oldValue = this.row[fieldname];
this.errors[df.fieldname] = null; try {
this.row.set(df.fieldname, value).catch((e) => { await this.row.set(fieldname, value);
this.errors[df.fieldname] = getErrorMessage(e, this.row); } catch (e) {
}); this.errors[fieldname] = getErrorMessage(e, this.row);
this.row[fieldname] = '';
nextTick(() => (this.row[fieldname] = oldValue));
}
}, },
getErrorString() { getErrorString() {
return Object.values(this.errors).filter(Boolean).join(' '); return Object.values(this.errors).filter(Boolean).join(' ');

View File

@ -6,7 +6,7 @@
<div :class="showMandatory ? 'show-mandatory' : ''"> <div :class="showMandatory ? 'show-mandatory' : ''">
<textarea <textarea
ref="input" ref="input"
rows="3" :rows="rows"
:class="['resize-none', inputClasses, containerClasses]" :class="['resize-none', inputClasses, containerClasses]"
:value="value" :value="value"
:placeholder="inputPlaceholder" :placeholder="inputPlaceholder"
@ -27,5 +27,6 @@ export default {
name: 'Text', name: 'Text',
extends: Base, extends: Base,
emits: ['focus', 'input'], emits: ['focus', 'input'],
props: { rows: { type: Number, default: 3 } },
}; };
</script> </script>

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="flex bg-gray-25"> <div class="flex bg-gray-25 overflow-x-auto">
<div class="flex flex-1 flex-col"> <div class="flex flex-1 flex-col">
<!-- Page Header (Title, Buttons, etc) --> <!-- Page Header (Title, Buttons, etc) -->
<PageHeader :title="title" :border="false" :searchborder="searchborder"> <PageHeader :title="title" :border="false" :searchborder="searchborder">

View File

@ -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>

View File

@ -4,7 +4,7 @@
class=" class="
fixed fixed
top-0 top-0
left-0 start-0
w-screen w-screen
h-screen h-screen
z-20 z-20
@ -12,19 +12,16 @@
justify-center justify-center
items-center items-center
" "
style="background: rgba(0, 0, 0, 0.2); backdrop-filter: blur(4px)" :style="
useBackdrop
? 'background: rgba(0, 0, 0, 0.1); backdrop-filter: blur(2px)'
: ''
"
@click="$emit('closemodal')" @click="$emit('closemodal')"
v-if="openModal" v-if="openModal"
> >
<div <div
class=" class="bg-white rounded-lg shadow-2xl border overflow-hidden inner"
bg-white
rounded-lg
shadow-2xl
border
overflow-hidden
inner
"
v-bind="$attrs" v-bind="$attrs"
@click.stop @click.stop
> >
@ -43,6 +40,10 @@ export default defineComponent({
default: false, default: false,
type: Boolean, type: Boolean,
}, },
useBackdrop: {
default: true,
type: Boolean,
},
}, },
emits: ['closemodal'], emits: ['closemodal'],
watch: { watch: {

View File

@ -8,7 +8,7 @@
> >
<Transition name="spacer"> <Transition name="spacer">
<div <div
v-if="!sidebar && platform === 'Mac'" v-if="!sidebar && platform === 'Mac' && languageDirection !== 'rtl'"
class="h-full" class="h-full"
:class="sidebar ? '' : 'w-tl me-4 border-e'" :class="sidebar ? '' : 'w-tl me-4 border-e'"
/> />
@ -16,7 +16,10 @@
<h1 class="text-xl font-semibold select-none" v-if="title"> <h1 class="text-xl font-semibold select-none" v-if="title">
{{ title }} {{ title }}
</h1> </h1>
<div class="flex items-stretch window-no-drag gap-2 ms-auto"> <div
class="flex items-stretch window-no-drag gap-2 ms-auto"
:class="platform === 'Mac' && languageDirection === 'rtl' ? 'me-18' : ''"
>
<slot /> <slot />
<div class="border-e" v-if="showBorder" /> <div class="border-e" v-if="showBorder" />
<BackLink v-if="backLink" class="window-no-drag rtl-rotate-180" /> <BackLink v-if="backLink" class="window-no-drag rtl-rotate-180" />
@ -30,7 +33,7 @@ import BackLink from './BackLink.vue';
import SearchBar from './SearchBar.vue'; import SearchBar from './SearchBar.vue';
export default { export default {
inject: ['sidebar'], inject: ['sidebar', 'languageDirection'],
props: { props: {
title: { type: String, default: '' }, title: { type: String, default: '' },
backLink: { type: Boolean, default: true }, backLink: { type: Boolean, default: true },

View File

@ -4,8 +4,8 @@
<Button @click="open" class="px-2" :padding="false"> <Button @click="open" class="px-2" :padding="false">
<feather-icon name="search" class="w-4 h-4 me-1 text-gray-800" /> <feather-icon name="search" class="w-4 h-4 me-1 text-gray-800" />
<p>{{ t`Search` }}</p> <p>{{ t`Search` }}</p>
<div class="text-gray-500 px-1 ms-4 text-sm"> <div class="text-gray-500 px-1 ms-4 text-sm whitespace-nowrap">
{{ modKey('k') }} {{ modKeyText('k') }}
</div> </div>
</Button> </Button>
</div> </div>
@ -16,174 +16,183 @@
@closemodal="close" @closemodal="close"
:set-close-listener="false" :set-close-listener="false"
> >
<!-- Search Input --> <div class="w-form">
<div class="p-1 w-form"> <!-- Search Input -->
<input <div class="p-1">
ref="input" <input
type="search" ref="input"
autocomplete="off" type="search"
spellcheck="false" autocomplete="off"
:placeholder="t`Type to search...`" spellcheck="false"
v-model="inputValue" :placeholder="t`Type to search...`"
@focus="search" v-model="inputValue"
@input="search" @focus="search"
@keydown.up="up" @input="search"
@keydown.down="down" @keydown.up="up"
@keydown.enter="() => select()" @keydown.down="down"
@keydown.esc="close" @keydown.enter="() => select()"
class=" @keydown.esc="close"
bg-gray-100 class="
text-2xl bg-gray-100
focus:outline-none text-2xl
w-full focus:outline-none
placeholder-gray-500 w-full
text-gray-900 placeholder-gray-500
rounded-md text-gray-900
p-3 rounded-md
" p-3
/> "
</div> />
<hr v-if="suggestions.length" /> </div>
<hr v-if="suggestions.length" />
<!-- Search List --> <!-- Search List -->
<div :style="`max-height: ${49 * 6 - 1}px`" class="overflow-auto"> <div :style="`max-height: ${49 * 6 - 1}px`" class="overflow-auto">
<div
v-for="(si, i) in suggestions"
:key="`${i}-${si.key}`"
ref="suggestions"
class="hover:bg-gray-50 cursor-pointer"
:class="idx === i ? 'border-blue-500 bg-gray-50 border-s-4' : ''"
@click="select(i)"
>
<!-- Search List Item -->
<div <div
class="flex w-full justify-between px-3 items-center" v-for="(si, i) in suggestions"
style="height: var(--h-row-mid)" :key="`${i}-${si.key}`"
ref="suggestions"
class="hover:bg-gray-50 cursor-pointer"
:class="idx === i ? 'border-blue-500 bg-gray-50 border-s-4' : ''"
@click="select(i)"
> >
<div class="flex items-center"> <!-- Search List Item -->
<div
class="flex w-full justify-between px-3 items-center"
style="height: var(--h-row-mid)"
>
<div class="flex items-center">
<p
:class="idx === i ? 'text-blue-600' : 'text-gray-900'"
:style="idx === i ? 'margin-left: -4px' : ''"
>
{{ si.label }}
</p>
<p class="text-gray-600 text-sm ms-3" v-if="si.group === 'Docs'">
{{ si.more.filter(Boolean).join(', ') }}
</p>
</div>
<p <p
:class="idx === i ? 'text-blue-600' : 'text-gray-900'" class="text-sm text-end justify-self-end"
:style="idx === i ? 'margin-left: -4px' : ''" :class="`text-${groupColorMap[si.group]}-500`"
> >
{{ si.label }} {{
</p> si.group === 'Docs' ? si.schemaLabel : groupLabelMap[si.group]
<p class="text-gray-600 text-sm ms-3" v-if="si.group === 'Docs'"> }}
{{ si.more.filter(Boolean).join(', ') }}
</p> </p>
</div> </div>
<p
class="text-sm text-end justify-self-end"
:class="`text-${groupColorMap[si.group]}-500`"
>
{{ si.group === 'Docs' ? si.schemaLabel : groupLabelMap[si.group] }}
</p>
</div>
<hr v-if="i !== suggestions.length - 1" /> <hr v-if="i !== suggestions.length - 1" />
</div>
</div>
<!-- Footer -->
<hr />
<div class="m-1 flex justify-between flex-col gap-2 text-sm select-none">
<!-- Group Filters -->
<div class="flex justify-between">
<div class="flex gap-1">
<button
v-for="g in searchGroups"
:key="g"
class="border px-1 py-0.5 rounded-lg"
:class="getGroupFilterButtonClass(g)"
@click="searcher.set(g, !searcher.filters.groupFilters[g])"
>
{{ groupLabelMap[g] }}
</button>
</div>
<button
class="hover:text-gray-900 py-0.5 rounded text-gray-700"
@click="showMore = !showMore"
>
{{ showMore ? t`Less Filters` : t`More Filters` }}
</button>
</div>
<!-- Additional Filters -->
<div v-if="showMore" class="-mt-1">
<!-- Group Skip Filters -->
<div class="flex gap-1 text-gray-800">
<button
v-for="s in ['skipTables', 'skipTransactions']"
:key="s"
class="border px-1 py-0.5 rounded-lg"
:class="{ 'bg-gray-200': searcher.filters[s] }"
@click="searcher.set(s, !searcher.filters[s])"
>
{{
s === 'skipTables' ? t`Skip Child Tables` : t`Skip Transactions`
}}
</button>
</div>
<!-- Schema Name Filters -->
<div class="flex mt-1 gap-1 text-blue-500 flex-wrap">
<button
v-for="sf in schemaFilters"
:key="sf.value"
class="
border
px-1
py-0.5
rounded-lg
border-blue-100
whitespace-nowrap
"
:class="{ 'bg-blue-100': searcher.filters.schemaFilters[sf.value] }"
@click="
searcher.set(sf.value, !searcher.filters.schemaFilters[sf.value])
"
>
{{ sf.label }}
</button>
</div> </div>
</div> </div>
<!-- Keybindings Help --> <!-- Footer -->
<div class="flex text-sm text-gray-500 justify-between items-baseline"> <hr />
<div class="flex gap-4"> <div class="m-1 flex justify-between flex-col gap-2 text-sm select-none">
<p> {{ t`Navigate` }}</p> <!-- Group Filters -->
<p> {{ t`Select` }}</p> <div class="flex justify-between">
<p><span class="tracking-tighter">esc</span> {{ t`Close` }}</p> <div class="flex gap-1">
<button
class="flex items-center hover:text-gray-800"
@click="openDocs"
>
<feather-icon name="help-circle" class="w-4 h-4 me-1" />
{{ t`Help` }}
</button>
</div>
<p v-if="searcher?.numSearches" class="ms-auto">
{{ t`${suggestions.length} out of ${searcher.numSearches}` }}
</p>
<div
class="border border-gray-100 rounded flex justify-self-end ms-2"
v-if="(searcher?.numSearches ?? 0) > 50"
>
<template
v-for="c in allowedLimits.filter(
(c) => c < searcher.numSearches || c === -1
)"
:key="c + '-count'"
>
<button <button
@click="limit = parseInt(c)" v-for="g in searchGroups"
class="w-9" :key="g"
:class="limit === c ? 'bg-gray-100' : ''" class="border px-1 py-0.5 rounded-lg"
:class="getGroupFilterButtonClass(g)"
@click="searcher.set(g, !searcher.filters.groupFilters[g])"
> >
{{ c === -1 ? t`All` : c }} {{ groupLabelMap[g] }}
</button> </button>
</template> </div>
<button
class="hover:text-gray-900 py-0.5 rounded text-gray-700"
@click="showMore = !showMore"
>
{{ showMore ? t`Less Filters` : t`More Filters` }}
</button>
</div>
<!-- Additional Filters -->
<div v-if="showMore" class="-mt-1">
<!-- Group Skip Filters -->
<div class="flex gap-1 text-gray-800">
<button
v-for="s in ['skipTables', 'skipTransactions']"
:key="s"
class="border px-1 py-0.5 rounded-lg"
:class="{ 'bg-gray-200': searcher.filters[s] }"
@click="searcher.set(s, !searcher.filters[s])"
>
{{
s === 'skipTables' ? t`Skip Child Tables` : t`Skip Transactions`
}}
</button>
</div>
<!-- Schema Name Filters -->
<div class="flex mt-1 gap-1 text-blue-500 flex-wrap">
<button
v-for="sf in schemaFilters"
:key="sf.value"
class="
border
px-1
py-0.5
rounded-lg
border-blue-100
whitespace-nowrap
"
:class="{
'bg-blue-100': searcher.filters.schemaFilters[sf.value],
}"
@click="
searcher.set(
sf.value,
!searcher.filters.schemaFilters[sf.value]
)
"
>
{{ sf.label }}
</button>
</div>
</div>
<!-- Keybindings Help -->
<div class="flex text-sm text-gray-500 justify-between items-baseline">
<div class="flex gap-4">
<p> {{ t`Navigate` }}</p>
<p> {{ t`Select` }}</p>
<p><span class="tracking-tighter">esc</span> {{ t`Close` }}</p>
<button
class="flex items-center hover:text-gray-800"
@click="openDocs"
>
<feather-icon name="help-circle" class="w-4 h-4 me-1" />
{{ t`Help` }}
</button>
</div>
<p v-if="searcher?.numSearches" class="ms-auto">
{{ t`${suggestions.length} out of ${searcher.numSearches}` }}
</p>
<div
class="border border-gray-100 rounded flex justify-self-end ms-2"
v-if="(searcher?.numSearches ?? 0) > 50"
>
<template
v-for="c in allowedLimits.filter(
(c) => c < searcher.numSearches || c === -1
)"
:key="c + '-count'"
>
<button
@click="limit = parseInt(c)"
class="w-9"
:class="limit === c ? 'bg-gray-100' : ''"
>
{{ c === -1 ? t`All` : c }}
</button>
</template>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -195,7 +204,6 @@ import { getBgTextColorClass } from 'src/utils/colors';
import { openLink } from 'src/utils/ipcCalls'; import { openLink } from 'src/utils/ipcCalls';
import { docsPathMap } from 'src/utils/misc'; import { docsPathMap } from 'src/utils/misc';
import { getGroupLabelMap, searchGroups } from 'src/utils/search'; import { getGroupLabelMap, searchGroups } from 'src/utils/search';
import { getModKeyCode } from 'src/utils/vueUtils';
import { nextTick } from 'vue'; import { nextTick } from 'vue';
import Button from './Button.vue'; import Button from './Button.vue';
import Modal from './Modal.vue'; import Modal from './Modal.vue';
@ -233,18 +241,19 @@ export default {
openLink('https://docs.frappebooks.com/' + docsPathMap.Search); openLink('https://docs.frappebooks.com/' + docsPathMap.Search);
}, },
getShortcuts() { getShortcuts() {
const modKey = getModKeyCode(this.platform);
const ifOpen = (cb) => () => this.openModal && cb(); const ifOpen = (cb) => () => this.openModal && cb();
const ifClose = (cb) => () => !this.openModal && cb(); const ifClose = (cb) => () => !this.openModal && cb();
const shortcuts = [ const shortcuts = [
{ shortcut: ['KeyK', modKey], callback: ifClose(() => this.open()) }, {
{ shortcut: ['Escape'], callback: ifOpen(() => this.close()) }, shortcut: 'KeyK',
callback: ifClose(() => this.open()),
},
]; ];
for (const i in searchGroups) { for (const i in searchGroups) {
shortcuts.push({ shortcuts.push({
shortcut: [modKey, `Digit${Number(i) + 1}`], shortcut: `Digit${Number(i) + 1}`,
callback: ifOpen(() => { callback: ifOpen(() => {
const group = searchGroups[i]; const group = searchGroups[i];
const value = this.searcher.filters.groupFilters[group]; const value = this.searcher.filters.groupFilters[group];
@ -261,15 +270,15 @@ export default {
}, },
setShortcuts() { setShortcuts() {
for (const { shortcut, callback } of this.getShortcuts()) { for (const { shortcut, callback } of this.getShortcuts()) {
this.shortcuts.set(shortcut, callback); this.shortcuts.pmod.set([shortcut], callback);
} }
}, },
deleteShortcuts() { deleteShortcuts() {
for (const { shortcut } of this.getShortcuts()) { for (const { shortcut } of this.getShortcuts()) {
this.shortcuts.delete(shortcut); this.shortcuts.pmod.delete([shortcut]);
} }
}, },
modKey(key) { modKeyText(key) {
key = key.toUpperCase(); key = key.toUpperCase();
if (this.platform === 'Mac') { if (this.platform === 'Mac') {
return `${key}`; return `${key}`;

View 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>

View File

@ -9,7 +9,9 @@
<!-- Company name and DB Switcher --> <!-- Company name and DB Switcher -->
<div <div
class="px-4 flex flex-row items-center justify-between mb-4" class="px-4 flex flex-row items-center justify-between mb-4"
:class="platform === 'Mac' ? 'mt-10' : 'mt-2'" :class="
platform === 'Mac' && languageDirection === 'ltr' ? 'mt-10' : 'mt-2'
"
> >
<h6 <h6
class=" class="
@ -98,6 +100,20 @@
</p> </p>
</button> </button>
<button
class="
flex
text-sm text-gray-600
hover:text-gray-800
gap-1
items-center
"
@click="viewShortcuts = true"
>
<feather-icon name="command" class="h-4 w-4 flex-shrink-0" />
<p>{{ t`Shortcuts` }}</p>
</button>
<button <button
class=" class="
flex flex
@ -153,6 +169,10 @@
> >
<feather-icon name="chevrons-left" class="w-4 h-4" /> <feather-icon name="chevrons-left" class="w-4 h-4" />
</button> </button>
<Modal :open-modal="viewShortcuts" @closemodal="viewShortcuts = false">
<ShortcutsHelper class="w-form" />
</Modal>
</div> </div>
</template> </template>
<script> <script>
@ -160,18 +180,23 @@ import Button from 'src/components/Button.vue';
import { reportIssue } from 'src/errorHandling'; import { reportIssue } from 'src/errorHandling';
import { fyo } from 'src/initFyo'; import { fyo } from 'src/initFyo';
import { openLink } from 'src/utils/ipcCalls'; import { openLink } from 'src/utils/ipcCalls';
import { docsPathRef } from 'src/utils/refs';
import { getSidebarConfig } from 'src/utils/sidebarConfig'; import { getSidebarConfig } from 'src/utils/sidebarConfig';
import { docsPath, routeTo } from 'src/utils/ui'; import { routeTo } from 'src/utils/ui';
import router from '../router'; import router from '../router';
import Icon from './Icon.vue'; import Icon from './Icon.vue';
import Modal from './Modal.vue';
import ShortcutsHelper from './ShortcutsHelper.vue';
export default { export default {
components: [Button], components: [Button],
inject: ['languageDirection', 'shortcuts'],
emits: ['change-db-file', 'toggle-sidebar'], emits: ['change-db-file', 'toggle-sidebar'],
data() { data() {
return { return {
companyName: '', companyName: '',
groups: [], groups: [],
viewShortcuts: false,
activeGroup: null, activeGroup: null,
}; };
}, },
@ -182,6 +207,8 @@ export default {
}, },
components: { components: {
Icon, Icon,
Modal,
ShortcutsHelper,
}, },
async mounted() { async mounted() {
const { companyName } = await fyo.doc.getDoc('AccountingSettings'); const { companyName } = await fyo.doc.getDoc('AccountingSettings');
@ -192,12 +219,23 @@ export default {
router.afterEach(() => { router.afterEach(() => {
this.setActiveGroup(); this.setActiveGroup();
}); });
this.shortcuts.shift.set(['KeyH'], () => {
if (document.body === document.activeElement) {
this.$emit('toggle-sidebar');
}
});
this.shortcuts.set(['F1'], () => this.openDocumentation());
},
unmounted() {
this.shortcuts.alt.delete(['KeyH']);
this.shortcuts.delete(['F1']);
}, },
methods: { methods: {
routeTo, routeTo,
reportIssue, reportIssue,
openDocumentation() { openDocumentation() {
openLink('https://docs.frappebooks.com/' + docsPath.value); openLink('https://docs.frappebooks.com/' + docsPathRef.value);
}, },
setActiveGroup() { setActiveGroup() {
const { fullPath } = this.$router.currentRoute.value; const { fullPath } = this.$router.currentRoute.value;

View File

@ -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
View 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;
}

View File

@ -147,7 +147,8 @@ import { ModelNameEnum } from 'models/types';
import PageHeader from 'src/components/PageHeader.vue'; import PageHeader from 'src/components/PageHeader.vue';
import { fyo } from 'src/initFyo'; import { fyo } from 'src/initFyo';
import { docsPathMap } from 'src/utils/misc'; import { docsPathMap } from 'src/utils/misc';
import { docsPath, openQuickEdit } from 'src/utils/ui'; import { docsPathRef } from 'src/utils/refs';
import { openQuickEdit } from 'src/utils/ui';
import { getMapFromList, removeAtIndex } from 'utils/index'; import { getMapFromList, removeAtIndex } from 'utils/index';
import { nextTick } from 'vue'; import { nextTick } from 'vue';
import Button from '../components/Button.vue'; import Button from '../components/Button.vue';
@ -184,7 +185,7 @@ export default {
window.coa = this; window.coa = this;
} }
docsPath.value = docsPathMap.ChartOfAccounts; docsPathRef.value = docsPathMap.ChartOfAccounts;
if (this.refetchTotals) { if (this.refetchTotals) {
await this.setTotalDebitAndCredit(); await this.setTotalDebitAndCredit();
@ -192,7 +193,7 @@ export default {
} }
}, },
deactivated() { deactivated() {
docsPath.value = ''; docsPathRef.value = '';
}, },
methods: { methods: {
async expand() { async expand() {

View File

@ -65,12 +65,12 @@
<script> <script>
import PageHeader from 'src/components/PageHeader.vue'; import PageHeader from 'src/components/PageHeader.vue';
import { docsPath } from 'src/utils/ui';
import UnpaidInvoices from './UnpaidInvoices.vue'; import UnpaidInvoices from './UnpaidInvoices.vue';
import Cashflow from './Cashflow.vue'; import Cashflow from './Cashflow.vue';
import Expenses from './Expenses.vue'; import Expenses from './Expenses.vue';
import PeriodSelector from './PeriodSelector.vue'; import PeriodSelector from './PeriodSelector.vue';
import ProfitAndLoss from './ProfitAndLoss.vue'; import ProfitAndLoss from './ProfitAndLoss.vue';
import { docsPathRef } from 'src/utils/refs';
export default { export default {
name: 'Dashboard', name: 'Dashboard',
@ -86,10 +86,10 @@ export default {
return { period: 'This Year' }; return { period: 'This Year' };
}, },
activated() { activated() {
docsPath.value = 'analytics/dashboard'; docsPathRef.value = 'analytics/dashboard';
}, },
deactivated() { deactivated() {
docsPath.value = ''; docsPathRef.value = '';
}, },
methods: { methods: {
handlePeriodChange(period) { handlePeriodChange(period) {

View File

@ -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>

View File

@ -36,15 +36,16 @@
class=" class="
absolute absolute
bottom-0 bottom-0
left-0 start-0
text-gray-600 text-gray-600
bg-gray-100 bg-gray-100
rounded rounded
rtl-rotate-180
p-1 p-1
m-4 m-4
opacity-0
hover:opacity-100 hover:shadow-md hover:opacity-100 hover:shadow-md
" "
@click="sidebar = !sidebar" @click="sidebar = !sidebar"
> >
<feather-icon name="chevrons-right" class="w-4 h-4" /> <feather-icon name="chevrons-right" class="w-4 h-4" />
@ -76,6 +77,11 @@ export default {
transform: translateX(calc(-1 * var(--w-sidebar))); transform: translateX(calc(-1 * var(--w-sidebar)));
width: 0px; width: 0px;
} }
[dir='rtl'] .sidebar-leave-to {
opacity: 0;
transform: translateX(calc(1 * var(--w-sidebar)));
width: 0px;
}
.sidebar-enter-to, .sidebar-enter-to,
.sidebar-leave-from { .sidebar-leave-from {

View File

@ -130,7 +130,6 @@
<template #quickedit v-if="quickEditDoc"> <template #quickedit v-if="quickEditDoc">
<QuickEditForm <QuickEditForm
class="w-quick-edit"
:name="quickEditDoc.name" :name="quickEditDoc.name"
:show-name="false" :show-name="false"
:show-save="false" :show-save="false"
@ -160,8 +159,8 @@ import FormHeader from 'src/components/FormHeader.vue';
import StatusBadge from 'src/components/StatusBadge.vue'; import StatusBadge from 'src/components/StatusBadge.vue';
import { fyo } from 'src/initFyo'; import { fyo } from 'src/initFyo';
import { docsPathMap } from 'src/utils/misc'; import { docsPathMap } from 'src/utils/misc';
import { docsPathRef, focusedDocsRef } from 'src/utils/refs';
import { import {
docsPath,
getGroupedActionsForDoc, getGroupedActionsForDoc,
routeTo, routeTo,
showMessageDialog, showMessageDialog,
@ -232,14 +231,17 @@ export default {
}, },
}, },
activated() { activated() {
docsPath.value = docsPathMap[this.schemaName]; docsPathRef.value = docsPathMap[this.schemaName];
focusedDocsRef.add(this.doc);
}, },
deactivated() { deactivated() {
docsPath.value = ''; docsPathRef.value = '';
focusedDocsRef.delete(this.doc);
}, },
async mounted() { async mounted() {
try { try {
this.doc = await fyo.doc.getDoc(this.schemaName, this.name); this.doc = await fyo.doc.getDoc(this.schemaName, this.name);
focusedDocsRef.add(this.doc);
} catch (error) { } catch (error) {
if (error instanceof fyo.errors.NotFoundError) { if (error instanceof fyo.errors.NotFoundError) {
routeTo(`/list/${this.schemaName}`); routeTo(`/list/${this.schemaName}`);

952
src/pages/ImportWizard.vue Normal file
View 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>

View File

@ -273,7 +273,6 @@
<Transition name="quickedit"> <Transition name="quickedit">
<QuickEditForm <QuickEditForm
v-if="quickEditDoc && !linked" v-if="quickEditDoc && !linked"
class="w-quick-edit"
:name="quickEditDoc.name" :name="quickEditDoc.name"
:show-name="false" :show-name="false"
:show-save="false" :show-save="false"
@ -313,8 +312,8 @@ import StatusBadge from 'src/components/StatusBadge.vue';
import LinkedEntryWidget from 'src/components/Widgets/LinkedEntryWidget.vue'; import LinkedEntryWidget from 'src/components/Widgets/LinkedEntryWidget.vue';
import { fyo } from 'src/initFyo'; import { fyo } from 'src/initFyo';
import { docsPathMap } from 'src/utils/misc'; import { docsPathMap } from 'src/utils/misc';
import { docsPathRef, focusedDocsRef } from 'src/utils/refs';
import { import {
docsPath,
getGroupedActionsForDoc, getGroupedActionsForDoc,
routeTo, routeTo,
showMessageDialog, showMessageDialog,
@ -339,6 +338,7 @@ export default {
LinkedEntryWidget, LinkedEntryWidget,
Barcode, Barcode,
}, },
inject: ['shortcuts'],
provide() { provide() {
return { return {
schemaName: this.schemaName, schemaName: this.schemaName,
@ -455,14 +455,17 @@ export default {
}, },
}, },
activated() { activated() {
docsPath.value = docsPathMap[this.schemaName]; docsPathRef.value = docsPathMap[this.schemaName];
focusedDocsRef.add(this.doc);
}, },
deactivated() { deactivated() {
docsPath.value = ''; docsPathRef.value = '';
focusedDocsRef.delete(this.doc);
}, },
async mounted() { async mounted() {
try { try {
this.doc = await fyo.doc.getDoc(this.schemaName, this.name); this.doc = await fyo.doc.getDoc(this.schemaName, this.name);
focusedDocsRef.add(this.doc);
} catch (error) { } catch (error) {
if (error instanceof fyo.errors.NotFoundError) { if (error instanceof fyo.errors.NotFoundError) {
routeTo(`/list/${this.schemaName}`); routeTo(`/list/${this.schemaName}`);

View File

@ -148,8 +148,8 @@ import FormHeader from 'src/components/FormHeader.vue';
import StatusBadge from 'src/components/StatusBadge.vue'; import StatusBadge from 'src/components/StatusBadge.vue';
import { fyo } from 'src/initFyo'; import { fyo } from 'src/initFyo';
import { docsPathMap } from 'src/utils/misc'; import { docsPathMap } from 'src/utils/misc';
import { docsPathRef, focusedDocsRef } from 'src/utils/refs';
import { import {
docsPath,
getGroupedActionsForDoc, getGroupedActionsForDoc,
routeTo, routeTo,
showMessageDialog, showMessageDialog,
@ -182,14 +182,17 @@ export default {
}; };
}, },
activated() { activated() {
docsPath.value = docsPathMap.JournalEntry; docsPathRef.value = docsPathMap.JournalEntry;
focusedDocsRef.add(this.doc);
}, },
deactivated() { deactivated() {
docsPath.value = ''; docsPathRef.value = '';
focusedDocsRef.delete(this.doc);
}, },
async mounted() { async mounted() {
try { try {
this.doc = await fyo.doc.getDoc(this.schemaName, this.name); this.doc = await fyo.doc.getDoc(this.schemaName, this.name);
focusedDocsRef.add(this.doc);
} catch (error) { } catch (error) {
if (error instanceof fyo.errors.NotFoundError) { if (error instanceof fyo.errors.NotFoundError) {
routeTo(`/list/${this.schemaName}`); routeTo(`/list/${this.schemaName}`);

View File

@ -51,7 +51,8 @@ import {
docsPathMap, docsPathMap,
getCreateFiltersFromListViewFilters, getCreateFiltersFromListViewFilters,
} from 'src/utils/misc'; } from 'src/utils/misc';
import { docsPath, openQuickEdit, routeTo } from 'src/utils/ui'; import { docsPathRef } from 'src/utils/refs';
import { openQuickEdit, routeTo } from 'src/utils/ui';
import List from './List.vue'; import List from './List.vue';
export default { export default {
@ -82,14 +83,14 @@ export default {
} }
this.listConfig = getListConfig(this.schemaName); this.listConfig = getListConfig(this.schemaName);
docsPath.value = docsPathMap[this.schemaName] ?? docsPathMap.Entries; docsPathRef.value = docsPathMap[this.schemaName] ?? docsPathMap.Entries;
if (this.fyo.store.isDevelopment) { if (this.fyo.store.isDevelopment) {
window.lv = this; window.lv = this;
} }
}, },
deactivated() { deactivated() {
docsPath.value = ''; docsPathRef.value = '';
}, },
methods: { methods: {
updatedData(listFilters) { updatedData(listFilters) {

View File

@ -105,6 +105,7 @@ import StatusBadge from 'src/components/StatusBadge.vue';
import TwoColumnForm from 'src/components/TwoColumnForm.vue'; import TwoColumnForm from 'src/components/TwoColumnForm.vue';
import { fyo } from 'src/initFyo'; import { fyo } from 'src/initFyo';
import { getQuickEditWidget } from 'src/utils/quickEditWidgets'; import { getQuickEditWidget } from 'src/utils/quickEditWidgets';
import { focusedDocsRef } from 'src/utils/refs';
import { getActionsForDoc, openQuickEdit } from 'src/utils/ui'; import { getActionsForDoc, openQuickEdit } from 'src/utils/ui';
export default { export default {
@ -131,6 +132,7 @@ export default {
DropdownWithActions, DropdownWithActions,
}, },
emits: ['close'], emits: ['close'],
inject: ['shortcuts'],
provide() { provide() {
return { return {
schemaName: this.schemaName, schemaName: this.schemaName,
@ -147,17 +149,26 @@ export default {
statusText: null, statusText: null,
}; };
}, },
mounted() { async mounted() {
if (this.defaults) { if (this.defaults) {
this.values = JSON.parse(this.defaults); this.values = JSON.parse(this.defaults);
} }
await this.fetchFieldsAndDoc();
focusedDocsRef.add(this.doc);
if (fyo.store.isDevelopment) { if (fyo.store.isDevelopment) {
window.qef = this; window.qef = this;
} }
}, },
async created() { activated() {
await this.fetchFieldsAndDoc(); focusedDocsRef.add(this.doc);
},
deactivated() {
focusedDocsRef.delete(this.doc);
},
unmounted() {
focusedDocsRef.delete(this.doc);
}, },
computed: { computed: {
isChild() { isChild() {

View File

@ -46,7 +46,7 @@ import PageHeader from 'src/components/PageHeader.vue';
import ListReport from 'src/components/Report/ListReport.vue'; import ListReport from 'src/components/Report/ListReport.vue';
import { fyo } from 'src/initFyo'; import { fyo } from 'src/initFyo';
import { docsPathMap } from 'src/utils/misc'; import { docsPathMap } from 'src/utils/misc';
import { docsPath } from 'src/utils/ui'; import { docsPathRef } from 'src/utils/refs';
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
export default defineComponent({ export default defineComponent({
@ -70,7 +70,7 @@ export default defineComponent({
}, },
components: { PageHeader, FormControl, ListReport, DropdownWithActions }, components: { PageHeader, FormControl, ListReport, DropdownWithActions },
async activated() { async activated() {
docsPath.value = docsPathMap[this.reportClassName] ?? docsPathMap.Reports; docsPathRef.value = docsPathMap[this.reportClassName] ?? docsPathMap.Reports;
await this.setReportData(); await this.setReportData();
const filters = JSON.parse(this.defaultFilters); const filters = JSON.parse(this.defaultFilters);
@ -88,7 +88,7 @@ export default defineComponent({
} }
}, },
deactivated() { deactivated() {
docsPath.value = ''; docsPathRef.value = '';
}, },
computed: { computed: {
title() { title() {

View File

@ -49,7 +49,8 @@ import Row from 'src/components/Row.vue';
import StatusBadge from 'src/components/StatusBadge.vue'; import StatusBadge from 'src/components/StatusBadge.vue';
import { fyo } from 'src/initFyo'; import { fyo } from 'src/initFyo';
import { docsPathMap } from 'src/utils/misc'; import { docsPathMap } from 'src/utils/misc';
import { docsPath, showToast } from 'src/utils/ui'; import { docsPathRef } from 'src/utils/refs';
import { showToast } from 'src/utils/ui';
import { IPC_MESSAGES } from 'utils/messages'; import { IPC_MESSAGES } from 'utils/messages';
import { h, markRaw } from 'vue'; import { h, markRaw } from 'vue';
import TabBase from './TabBase.vue'; import TabBase from './TabBase.vue';
@ -112,10 +113,10 @@ export default {
}, },
activated() { activated() {
this.setActiveTab(); this.setActiveTab();
docsPath.value = docsPathMap.Settings; docsPathRef.value = docsPathMap.Settings;
}, },
deactivated() { deactivated() {
docsPath.value = ''; docsPathRef.value = '';
if (this.fieldsChanged.length === 0) { if (this.fieldsChanged.length === 0) {
return; return;
} }

View File

@ -1,7 +1,7 @@
import { ModelNameEnum } from 'models/types'; import { ModelNameEnum } from 'models/types';
import ChartOfAccounts from 'src/pages/ChartOfAccounts.vue'; import ChartOfAccounts from 'src/pages/ChartOfAccounts.vue';
import Dashboard from 'src/pages/Dashboard/Dashboard.vue'; import Dashboard from 'src/pages/Dashboard/Dashboard.vue';
import DataImport from 'src/pages/DataImport.vue'; import ImportWizard from 'src/pages/ImportWizard.vue';
import GeneralForm from 'src/pages/GeneralForm.vue'; import GeneralForm from 'src/pages/GeneralForm.vue';
import GetStarted from 'src/pages/GetStarted.vue'; import GetStarted from 'src/pages/GetStarted.vue';
import InvoiceForm from 'src/pages/InvoiceForm.vue'; import InvoiceForm from 'src/pages/InvoiceForm.vue';
@ -138,9 +138,9 @@ const routes: RouteRecordRaw[] = [
}, },
}, },
{ {
path: '/data-import', path: '/import-wizard',
name: 'Data Import', name: 'Import Wizard',
component: DataImport, component: ImportWizard,
}, },
{ {
path: '/settings', path: '/settings',

View File

@ -82,6 +82,7 @@ input[type='number']::-webkit-inner-spin-button {
.w-quick-edit { .w-quick-edit {
width: var(--w-quick-edit); width: var(--w-quick-edit);
flex-shrink: 0;
} }
.h-form { .h-form {

View File

@ -1,12 +1,18 @@
import { Fyo } from 'fyo'; import { Fyo } from 'fyo';
import { RawValueMap } from 'fyo/core/types'; import { RawValueMap } from 'fyo/core/types';
import { Field, FieldTypeEnum, RawValue, TargetField } from 'schemas/types'; import {
Field,
FieldType,
FieldTypeEnum,
RawValue,
TargetField,
} from 'schemas/types';
import { generateCSV } from 'utils/csvParser'; import { generateCSV } from 'utils/csvParser';
import { GetAllOptions, QueryFilter } from 'utils/db/types'; import { GetAllOptions, QueryFilter } from 'utils/db/types';
import { getMapFromList, safeParseFloat } from 'utils/index'; import { getMapFromList, safeParseFloat } from 'utils/index';
import { ExportField, ExportTableField } from './types'; import { ExportField, ExportTableField } from './types';
const excludedFieldTypes = [ const excludedFieldTypes: FieldType[] = [
FieldTypeEnum.AttachImage, FieldTypeEnum.AttachImage,
FieldTypeEnum.Attachment, FieldTypeEnum.Attachment,
]; ];
@ -26,7 +32,7 @@ export function getExportFields(
.filter((f) => !f.computed && f.label && !exclude.includes(f.fieldname)) .filter((f) => !f.computed && f.label && !exclude.includes(f.fieldname))
.map((field) => { .map((field) => {
const { fieldname, label } = field; const { fieldname, label } = field;
const fieldtype = field.fieldtype as FieldTypeEnum; const fieldtype = field.fieldtype as FieldType;
return { return {
fieldname, fieldname,
fieldtype, fieldtype,
@ -323,7 +329,7 @@ async function getChildTableData(
return data; return data;
} }
function convertRawPesaToFloat(data: RawValueMap[], fields: Field[]) { function convertRawPesaToFloat(data: RawValueMap[], fields: ExportField[]) {
const currencyFields = fields.filter( const currencyFields = fields.filter(
(f) => f.fieldtype === FieldTypeEnum.Currency (f) => f.fieldtype === FieldTypeEnum.Currency
); );

View File

@ -3,6 +3,7 @@ import { DEFAULT_LANGUAGE } from 'fyo/utils/consts';
import { setLanguageMapOnTranslationString } from 'fyo/utils/translation'; import { setLanguageMapOnTranslationString } from 'fyo/utils/translation';
import { fyo } from 'src/initFyo'; import { fyo } from 'src/initFyo';
import { IPC_ACTIONS, IPC_MESSAGES } from 'utils/messages'; import { IPC_ACTIONS, IPC_MESSAGES } from 'utils/messages';
import { systemLanguageRef } from './refs';
import { showToast } from './ui'; import { showToast } from './ui';
// Language: Language Code in books/translations // Language: Language Code in books/translations
@ -42,6 +43,7 @@ export async function setLanguageMap(
if (success && !usingDefault) { if (success && !usingDefault) {
fyo.config.set('language', language); fyo.config.set('language', language);
systemLanguageRef.value = language;
} }
if (!dontReload && success && initLanguage !== oldLanguage) { if (!dontReload && success && initLanguage !== oldLanguage) {

View File

@ -1,5 +1,6 @@
import { Fyo } from 'fyo'; import { Fyo } from 'fyo';
import { ConfigFile, ConfigKeys } from 'fyo/core/types'; import { ConfigFile, ConfigKeys } from 'fyo/core/types';
import { Doc } from 'fyo/model/doc';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { SetupWizard } from 'models/baseModels/SetupWizard/SetupWizard'; import { SetupWizard } from 'models/baseModels/SetupWizard/SetupWizard';
import { ModelNameEnum } from 'models/types'; import { ModelNameEnum } from 'models/types';
@ -117,7 +118,7 @@ export const docsPathMap: Record<string, string | undefined> = {
// Miscellaneous // Miscellaneous
Search: 'miscellaneous/search', Search: 'miscellaneous/search',
NumberSeries: 'miscellaneous/number-series', NumberSeries: 'miscellaneous/number-series',
DataImport: 'miscellaneous/data-import', ImportWizard: 'miscellaneous/import-wizard',
Settings: 'miscellaneous/settings', Settings: 'miscellaneous/settings',
ChartOfAccounts: 'miscellaneous/chart-of-accounts', ChartOfAccounts: 'miscellaneous/chart-of-accounts',
}; };
@ -160,3 +161,45 @@ export function getCreateFiltersFromListViewFilters(filters: QueryFilter) {
return createFilters; return createFilters;
} }
export class FocusedDocContextSet {
set: Doc[];
constructor() {
this.set = [];
}
add(doc: unknown) {
if (!(doc instanceof Doc)) {
return;
}
const index = this.findIndex(doc);
if (index !== -1) {
this.delete(index);
}
return this.set.push(doc);
}
delete(index: Doc | number) {
if (typeof index !== 'number') {
index = this.findIndex(index);
}
if (index === -1) {
return;
}
this.set = this.set.filter((_, i) => i !== index);
}
last() {
return this.set.at(-1);
}
findIndex(doc: Doc) {
return this.set.findIndex(
(d) => d.name === doc.name && d.schemaName === doc.schemaName
);
}
}

8
src/utils/refs.ts Normal file
View 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()
);

View File

@ -323,8 +323,8 @@ function getSetupList(): SearchItem[] {
group: 'Page', group: 'Page',
}, },
{ {
label: t`Data Import`, label: t`Import Wizard`,
route: '/data-import', route: '/import-wizard',
group: 'Page', group: 'Page',
}, },
{ {
@ -594,10 +594,7 @@ export class Search {
keys.sort((a, b) => safeParseFloat(b) - safeParseFloat(a)); keys.sort((a, b) => safeParseFloat(b) - safeParseFloat(a));
const array: SearchItems = []; const array: SearchItems = [];
for (const key of keys) { for (const key of keys) {
const keywords = groupedKeywords[key]; const keywords = groupedKeywords[key] ?? [];
if (!keywords?.length) {
continue;
}
this._pushDocSearchItems(keywords, array, input); this._pushDocSearchItems(keywords, array, input);
if (key === '0') { if (key === '0') {

78
src/utils/shortcuts.ts Normal file
View 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() {},
},
],
});
}

View File

@ -268,9 +268,9 @@ async function getCompleteSidebar(): Promise<SidebarConfig> {
schemaName: 'Tax', schemaName: 'Tax',
}, },
{ {
label: t`Data Import`, label: t`Import Wizard`,
name: 'data-import', name: 'import-wizard',
route: '/data-import', route: '/import-wizard',
}, },
{ {
label: t`Settings`, label: t`Settings`,

View File

@ -1,5 +1,5 @@
import { Doc } from "fyo/model/doc"; import { Doc } from "fyo/model/doc";
import { FieldTypeEnum } from "schemas/types"; import { FieldType } from "schemas/types";
import { QueryFilter } from "utils/db/types"; import { QueryFilter } from "utils/db/types";
export interface MessageDialogButton { export interface MessageDialogButton {
@ -58,7 +58,7 @@ export interface SidebarItem {
export interface ExportField { export interface ExportField {
fieldname: string; fieldname: string;
fieldtype: FieldTypeEnum; fieldtype: FieldType;
label: string; label: string;
export: boolean; export: boolean;
} }

View File

@ -4,7 +4,7 @@
*/ */
import { ipcRenderer } from 'electron'; import { ipcRenderer } from 'electron';
import { t } from 'fyo'; import { t } from 'fyo';
import { Doc } from 'fyo/model/doc'; import type { Doc } from 'fyo/model/doc';
import { Action } from 'fyo/model/types'; import { Action } from 'fyo/model/types';
import { getActions } from 'fyo/utils'; import { getActions } from 'fyo/utils';
import { getDbError, LinkValidationError, ValueError } from 'fyo/utils/errors'; import { getDbError, LinkValidationError, ValueError } from 'fyo/utils/errors';
@ -23,8 +23,6 @@ import {
ToastOptions, ToastOptions,
} from './types'; } from './types';
export const docsPath = ref('');
export async function openQuickEdit({ export async function openQuickEdit({
doc, doc,
schemaName, schemaName,

View File

@ -1,7 +1,6 @@
import { onMounted, onUnmounted, Ref, ref, watch } from 'vue'; import { onMounted, onUnmounted, reactive, ref, watch } from 'vue';
interface Keys { interface ModMap {
pressed: Set<string>;
alt: boolean; alt: boolean;
ctrl: boolean; ctrl: boolean;
meta: boolean; meta: boolean;
@ -9,13 +8,27 @@ interface Keys {
repeat: boolean; repeat: boolean;
} }
export class Shortcuts { type Mod = keyof ModMap;
keys: Ref<Keys>;
shortcuts: Map<string, Function>;
constructor(keys?: Ref<Keys>) { interface Keys extends ModMap {
pressed: Set<string>;
}
type ShortcutFunction = () => void;
const mods: Readonly<Mod[]> = ['alt', 'ctrl', 'meta', 'repeat', 'shift'];
export class Shortcuts {
keys: Keys;
isMac: boolean;
shortcuts: Map<string, ShortcutFunction>;
modMap: Partial<Record<Mod, boolean>>;
constructor(keys?: Keys) {
this.modMap = {};
this.keys = keys ?? useKeys(); this.keys = keys ?? useKeys();
this.shortcuts = new Map(); this.shortcuts = new Map();
this.isMac = getIsMac();
watch(this.keys, (keys) => { watch(this.keys, (keys) => {
this.#trigger(keys); this.#trigger(keys);
@ -23,17 +36,22 @@ export class Shortcuts {
} }
#trigger(keys: Keys) { #trigger(keys: Keys) {
const key = Array.from(keys.pressed).sort().join('+'); const key = this.getKey(Array.from(keys.pressed), keys);
this.shortcuts.get(key)?.(); this.shortcuts.get(key)?.();
} }
has(shortcut: string[]) { has(shortcut: string[]) {
const key = shortcut.sort().join('+'); const key = this.getKey(shortcut);
return this.shortcuts.has(key); return this.shortcuts.has(key);
} }
set(shortcut: string[], callback: Function, removeIfSet: boolean = true) { set(
const key = shortcut.sort().join('+'); shortcut: string[],
callback: ShortcutFunction,
removeIfSet: boolean = true
) {
const key = this.getKey(shortcut);
if (removeIfSet) { if (removeIfSet) {
this.shortcuts.delete(key); this.shortcuts.delete(key);
} }
@ -46,13 +64,68 @@ export class Shortcuts {
} }
delete(shortcut: string[]) { delete(shortcut: string[]) {
const key = shortcut.sort().join('+'); const key = this.getKey(shortcut);
this.shortcuts.delete(key); this.shortcuts.delete(key);
} }
getKey(shortcut: string[], modMap?: Partial<ModMap>): string {
const _modMap = modMap || this.modMap;
this.modMap = {};
const shortcutString = shortcut.sort().join('+');
const modString = mods.filter((k) => _modMap[k]).join('+');
if (shortcutString && modString) {
return modString + '+' + shortcutString;
}
if (!modString) {
return shortcutString;
}
if (!shortcutString) {
return modString;
}
return '';
}
get alt() {
this.modMap['alt'] = true;
return this;
}
get ctrl() {
this.modMap['ctrl'] = true;
return this;
}
get meta() {
this.modMap['meta'] = true;
return this;
}
get shift() {
this.modMap['shift'] = true;
return this;
}
get repeat() {
this.modMap['repeat'] = true;
return this;
}
get pmod() {
if (this.isMac) {
return this.meta;
} else {
return this.ctrl;
}
}
} }
export function useKeys() { export function useKeys() {
const keys: Ref<Keys> = ref({ const isMac = getIsMac();
const keys: Keys = reactive({
pressed: new Set<string>(), pressed: new Set<string>(),
alt: false, alt: false,
ctrl: false, ctrl: false,
@ -62,21 +135,32 @@ export function useKeys() {
}); });
const keydownListener = (e: KeyboardEvent) => { const keydownListener = (e: KeyboardEvent) => {
keys.value.pressed.add(e.code); keys.alt = e.altKey;
keys.value.alt = e.altKey; keys.ctrl = e.ctrlKey;
keys.value.ctrl = e.ctrlKey; keys.meta = e.metaKey;
keys.value.meta = e.metaKey; keys.shift = e.shiftKey;
keys.value.shift = e.shiftKey; keys.repeat = e.repeat;
keys.value.repeat = e.repeat;
const { code } = e;
if (
code.startsWith('Alt') ||
code.startsWith('Control') ||
code.startsWith('Meta') ||
code.startsWith('Shift')
) {
return;
}
keys.pressed.add(code);
}; };
const keyupListener = (e: KeyboardEvent) => { const keyupListener = (e: KeyboardEvent) => {
keys.value.pressed.delete(e.code); const { code } = e;
if (code.startsWith('Meta') && isMac) {
// Key up won't trigger on macOS for other keys. return keys.pressed.clear();
if (e.code === 'MetaLeft') {
keys.value.pressed.clear();
} }
keys.pressed.delete(code);
}; };
onMounted(() => { onMounted(() => {
@ -110,10 +194,6 @@ export function useMouseLocation() {
return loc; return loc;
} }
export function getModKeyCode(platform: 'Windows' | 'Linux' | 'Mac') { function getIsMac() {
if (platform === 'Mac') { return navigator.userAgent.indexOf('Mac') !== -1;
return 'MetaLeft';
}
return 'CtrlLeft';
} }

13
tests/items.csv Normal file
View 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
1 Item Name Description Unit Type Type For Sales Acc. Purchase Acc. Tax Rate HSN/SAC Barcode Track Item Created By Modified By Created Modified
2 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
3 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
4 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
5 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
6 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
7 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
8 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
9 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
10 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
11 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
12 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
13 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
View 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
1 Name Role Default Account Outstanding Amount Currency Email Phone Address GSTIN No. GST Registration Created By Modified By Created Modified
2 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
3 Randoe Both 259.6 INR Unregistered lin@to.co lin@to.co 2023-01-09T04:58:16.050Z 2023-01-09T10:46:46.128Z
4 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
5 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
6 Lyn Customer Debtors 100 INR lyn@to.co Consumer Administrator Administrator 2022-02-04T06:21:19.069Z 2022-02-28T05:18:32.743Z
7 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
8 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
View 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,,,
1 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
2 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
3 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
4 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
5 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
6 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
7 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
8 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
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
10 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
11 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
12 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
13 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
14 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
15 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
16 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
17 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
18 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
19 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
20 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
21 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
22 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
23 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
24 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
25 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
26 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
27 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
28 1001 2021-12-16 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
29 1001 2021-12-16 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
30 1001 2021-12-16 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

View 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);

View File

@ -38,7 +38,7 @@ Accounts,Comptes,
"Add a remark","Ajouter une remarque", "Add a remark","Ajouter une remarque",
"Add invoice terms","Ajouter votre politique de vente", "Add invoice terms","Ajouter votre politique de vente",
"Add products or services that you buy from your suppliers","Ajoutez des produits ou services que vous achetez à vos fournisseurs", "Add products or services that you buy from your suppliers","Ajoutez des produits ou services que vous achetez à vos fournisseurs",
"Add products or services that you sell to your customers","Ajouter les produits ou services que vous vendez à vos clients.", "Add products or services that you sell to your customers","Ajouter les produits ou services que vous vendez à vos clients",
Address,Adresse, Address,Adresse,
"Address Display","Affichage de l'adresse", "Address Display","Affichage de l'adresse",
"Address Line 1","Ligne d'adresse 1", "Address Line 1","Ligne d'adresse 1",
@ -76,7 +76,7 @@ Buildings,Bâtiments,
Business,Entreprise, Business,Entreprise,
Cancel,Annuler, Cancel,Annuler,
"Cancel ${0} ${1}?","Annuler ${0} ${1} ?", "Cancel ${0} ${1}?","Annuler ${0} ${1} ?",
Cancelled,Anulé, Cancelled,Annulé,
"Cannot Delete","Impossible à supprimer", "Cannot Delete","Impossible à supprimer",
"Cannot delete ${0} ${1} because of linked entries.","Impossible de supprimer ${0} ${1} à cause des entrées liées.", "Cannot delete ${0} ${1} because of linked entries.","Impossible de supprimer ${0} ${1} à cause des entrées liées.",
"Capital Equipments","Biens d'équipement", "Capital Equipments","Biens d'équipement",
@ -102,6 +102,7 @@ Close,Fermer,
Closing,Fermeture, Closing,Fermeture,
"Closing (Cr)","Fermeture (Cr)", "Closing (Cr)","Fermeture (Cr)",
"Closing (Dr)","Fermeture (Dr)", "Closing (Dr)","Fermeture (Dr)",
Collapse,Réduire,
Color,Couleur, Color,Couleur,
"Commission on Sales","Commission sur les ventes", "Commission on Sales","Commission sur les ventes",
Common,Autres, Common,Autres,
@ -143,8 +144,8 @@ Credit,Crédit,
"Credit Card Entry","Entrée Carte de Crédit", "Credit Card Entry","Entrée Carte de Crédit",
"Credit Note",Avoir, "Credit Note",Avoir,
Creditors,Créanciers, Creditors,Créanciers,
Currency,Monnaie, Currency,Devise,
"Currency Name","Nom de la monnaie", "Currency Name","Nom de la devise",
Current,Actuel, Current,Actuel,
"Current Assets","Actifs courants", "Current Assets","Actifs courants",
"Current Liabilities","Passifs courants", "Current Liabilities","Passifs courants",
@ -153,11 +154,11 @@ Customer,Client,
"Customer Created","Client créé", "Customer Created","Client créé",
"Customer Currency","Monnaie du client", "Customer Currency","Monnaie du client",
Customers,Clients, Customers,Clients,
Customise,Personnalisez, Customise,Personnaliser,
"Customize your invoices by adding a logo and address details","Customisez vos factures en y ajoutant votre logo et adresse", "Customize your invoices by adding a logo and address details",Personnalisez vos factures en y ajoutant votre logo et adresse,
Dashboard,"Tableau de bord", Dashboard,"Tableau de bord",
"Data Import","Importation de données", "Data Import","Importation de données",
"Database file: ${0}","Fichier de base de donnée : ${0}", "Database file: ${0}","Fichier de base de données : ${0}",
Date,, Date,,
"Date Format","Format de la date", "Date Format","Format de la date",
Debit,Débit, Debit,Débit,
@ -176,7 +177,7 @@ Details,Détails,
"Direct Income","Revenu direct", "Direct Income","Revenu direct",
Discount,Réduction, Discount,Réduction,
"Discount Account","Compte des réductions", "Discount Account","Compte des réductions",
"Discount Account is not set.","Le compte des réductions n'est pas défini", "Discount Account is not set.","Le compte des réductions n'est pas défini.",
"Discount After Tax","Réduction après taxes", "Discount After Tax","Réduction après taxes",
"Discount Amount","Montant de la réduction", "Discount Amount","Montant de la réduction",
"Discount Amount (${0}) cannot be greated than Amount (${1}).","Le montant de la réduction (${0}) ne peut pas être plus grand que le montant (${1}).", "Discount Amount (${0}) cannot be greated than Amount (${1}).","Le montant de la réduction (${0}) ne peut pas être plus grand que le montant (${1}).",
@ -189,7 +190,7 @@ Discounts,"Réductions",
"Display Logo in Invoice","Afficher le logo sur la facture", "Display Logo in Invoice","Afficher le logo sur la facture",
"Display Precision","Précision de l'affichage", "Display Precision","Précision de l'affichage",
"Display Precision should have a value between 0 and 9.","La précision de l'affichage doit avoir une valeur comprise entre 0 et 9.", "Display Precision should have a value between 0 and 9.","La précision de l'affichage doit avoir une valeur comprise entre 0 et 9.",
"Dividends Paid","Dividendes versés", "Dividends Paid","Dividendes versées",
Docs,Documents, Docs,Documents,
Documentation,Documentation, Documentation,Documentation,
"Does Not Contain","Ne contient pas", "Does Not Contain","Ne contient pas",
@ -205,6 +206,7 @@ Email,,
"Email Address","Adresse électronique", "Email Address","Adresse électronique",
Empty,Vide, Empty,Vide,
"Enable Discount Accounting","Activer la gestion des réductions", "Enable Discount Accounting","Activer la gestion des réductions",
"Enable Inventory","Activer l'inventaire",
"Enter Country to load States","Entrez le Pays pour charger le département", "Enter Country to load States","Entrez le Pays pour charger le département",
"Enter State","Entrez le département", "Enter State","Entrez le département",
"Entertainment Expenses","Dépenses liées au divertissement", "Entertainment Expenses","Dépenses liées au divertissement",
@ -216,13 +218,14 @@ Error,Erreur,
"Exchange Rate","Taux de change", "Exchange Rate","Taux de change",
"Excise Entry","Entrée d'acquis", "Excise Entry","Entrée d'acquis",
"Existing File","Fichier existant", "Existing File","Fichier existant",
Expand,Développer,
Expense,Dépenses, Expense,Dépenses,
"Expense Account","Compte de dépenses", "Expense Account","Compte de dépenses",
Expenses,Dépenses, Expenses,Dépenses,
"Expenses Included In Valuation","Dépenses incluses dans la valorisation ", "Expenses Included In Valuation","Dépenses incluses dans la valorisation ",
Export,Exporter, Export,Exporter,
"Export Failed","Export Échoué", "Export Failed","Export Échoué",
"Export Successful","Export réussie", "Export Successful","Export réussi",
Fax,Fax, Fax,Fax,
Field,Champ, Field,Champ,
Fieldname,"Nom du champ", Fieldname,"Nom du champ",
@ -377,13 +380,13 @@ Orange,,
Organisation,Organisation, Organisation,Organisation,
Outflow,Dépenses, Outflow,Dépenses,
"Outstanding Amount","Montant impayé", "Outstanding Amount","Montant impayé",
"Pad Zeros","Remplir de zéros", "Pad Zeros","Remplir de zéros",
Page,, Page,,
Paid,Payé, Paid,Payé,
Parent,, Parent,,
"Parent Account","Compte parent", "Parent Account","Compte parent",
Party,Partie, Party,Partie,
"Patch Run","Éxecuter les correctifs", "Patch Run","Exécuter les correctifs",
Pay,Payer, Pay,Payer,
Payable,Payable, Payable,Payable,
Payment,Paiement, Payment,Paiement,
@ -434,7 +437,7 @@ Purchases,Achats,
Purple,Violet, Purple,Violet,
Quantity,Quantité, Quantity,Quantité,
Quarterly,Trimestriel, Quarterly,Trimestriel,
Quarters,Trimestre, Quarters,Trimestres,
Rate,Tarif, Rate,Tarif,
"Rate (${0}) cannot be less zero.","Le Tarif (${0}) ne peut pas être inférieur à zéro.", "Rate (${0}) cannot be less zero.","Le Tarif (${0}) ne peut pas être inférieur à zéro.",
"Rate (Company Currency)","Tarif (devise utilisée par la société)", "Rate (Company Currency)","Tarif (devise utilisée par la société)",
@ -511,6 +514,7 @@ Service,,
"Sets how many digits are shown after the decimal point.","Définit le nombre de chiffres affichés après le point décimal.", "Sets how many digits are shown after the decimal point.","Définit le nombre de chiffres affichés après le point décimal.",
"Sets the app-wide date display format.","Définit le format d'affichage de la date pour l'application.", "Sets the app-wide date display format.","Définit le format d'affichage de la date pour l'application.",
"Sets the internal precision used for monetary calculations. Above 6 should be sufficient for most currencies.","Définit la précision interne utilisée pour les calculs monétaires. Une précision supérieure à 6 devrait être suffisante pour la plupart des monnaies.", "Sets the internal precision used for monetary calculations. Above 6 should be sufficient for most currencies.","Définit la précision interne utilisée pour les calculs monétaires. Une précision supérieure à 6 devrait être suffisante pour la plupart des monnaies.",
"Set Up","Configurer",
"Setting Up Instance","Paramétrage de l'Instance", "Setting Up Instance","Paramétrage de l'Instance",
"Setting Up...",Paramétrage..., "Setting Up...",Paramétrage...,
Settings,Paramètres, Settings,Paramètres,
@ -584,7 +588,7 @@ Terms,Conditions,
"This action is permanent","Cette action est permanente", "This action is permanent","Cette action est permanente",
"This action is permanent and will cancel the following payment: ${0}","Cette action est permanente et annulera le paiement suivant : ${0}", "This action is permanent and will cancel the following payment: ${0}","Cette action est permanente et annulera le paiement suivant : ${0}",
"This action is permanent and will cancel the following payments: ${0}","Cette action est permanente et annulera les paiements suivants : ${0}", "This action is permanent and will cancel the following payments: ${0}","Cette action est permanente et annulera les paiements suivants : ${0}",
"This action is permanent and will delete associated ledger entries.","Cette action est permanente et supprimera les écritures dans le registre associées.", "This action is permanent and will delete associated ledger entries.","Cette action est permanente et supprimera les écritures dans le registre associés.",
"This action is permanent.","Cette action est permanente", "This action is permanent.","Cette action est permanente",
"Times New Roman",, "Times New Roman",,
"To Account","Au compte", "To Account","Au compte",

1 ${0} ${1} already exists. ${0} ${1} existe déjà.
38 Add a remark Ajouter une remarque
39 Add invoice terms Ajouter votre politique de vente
40 Add products or services that you buy from your suppliers Ajoutez des produits ou services que vous achetez à vos fournisseurs
41 Add products or services that you sell to your customers Ajouter les produits ou services que vous vendez à vos clients. Ajouter les produits ou services que vous vendez à vos clients
42 Address Adresse
43 Address Display Affichage de l'adresse
44 Address Line 1 Ligne d'adresse 1
76 Business Entreprise
77 Cancel Annuler
78 Cancel ${0} ${1}? Annuler ${0} ${1} ?
79 Cancelled Anulé Annulé
80 Cannot Delete Impossible à supprimer
81 Cannot delete ${0} ${1} because of linked entries. Impossible de supprimer ${0} ${1} à cause des entrées liées.
82 Capital Equipments Biens d'équipement
102 Closing Fermeture
103 Closing (Cr) Fermeture (Cr)
104 Closing (Dr) Fermeture (Dr)
105 Collapse Réduire
106 Color Couleur
107 Commission on Sales Commission sur les ventes
108 Common Autres
144 Credit Card Entry Entrée Carte de Crédit
145 Credit Note Avoir
146 Creditors Créanciers
147 Currency Monnaie Devise
148 Currency Name Nom de la monnaie Nom de la devise
149 Current Actuel
150 Current Assets Actifs courants
151 Current Liabilities Passifs courants
154 Customer Created Client créé
155 Customer Currency Monnaie du client
156 Customers Clients
157 Customise Personnalisez Personnaliser
158 Customize your invoices by adding a logo and address details Customisez vos factures en y ajoutant votre logo et adresse Personnalisez vos factures en y ajoutant votre logo et adresse
159 Dashboard Tableau de bord
160 Data Import Importation de données
161 Database file: ${0} Fichier de base de donnée : ${0} Fichier de base de données : ${0}
162 Date
163 Date Format Format de la date
164 Debit Débit
177 Direct Income Revenu direct
178 Discount Réduction
179 Discount Account Compte des réductions
180 Discount Account is not set. Le compte des réductions n'est pas défini Le compte des réductions n'est pas défini.
181 Discount After Tax Réduction après taxes
182 Discount Amount Montant de la réduction
183 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}).
190 Display Logo in Invoice Afficher le logo sur la facture
191 Display Precision Précision de l'affichage
192 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.
193 Dividends Paid Dividendes versés Dividendes versées
194 Docs Documents
195 Documentation Documentation
196 Does Not Contain Ne contient pas
206 Email Address Adresse électronique
207 Empty Vide
208 Enable Discount Accounting Activer la gestion des réductions
209 Enable Inventory Activer l'inventaire
210 Enter Country to load States Entrez le Pays pour charger le département
211 Enter State Entrez le département
212 Entertainment Expenses Dépenses liées au divertissement
218 Exchange Rate Taux de change
219 Excise Entry Entrée d'acquis
220 Existing File Fichier existant
221 Expand Développer
222 Expense Dépenses
223 Expense Account Compte de dépenses
224 Expenses Dépenses
225 Expenses Included In Valuation Dépenses incluses dans la valorisation
226 Export Exporter
227 Export Failed Export Échoué
228 Export Successful Export réussie Export réussi
229 Fax Fax
230 Field Champ
231 Fieldname Nom du champ
380 Organisation Organisation
381 Outflow Dépenses
382 Outstanding Amount Montant impayé
383 Pad Zeros Remplir de zéros Remplir de zéros
384 Page
385 Paid Payé
386 Parent
387 Parent Account Compte parent
388 Party Partie
389 Patch Run Éxecuter les correctifs Exécuter les correctifs
390 Pay Payer
391 Payable Payable
392 Payment Paiement
437 Purple Violet
438 Quantity Quantité
439 Quarterly Trimestriel
440 Quarters Trimestre Trimestres
441 Rate Tarif
442 Rate (${0}) cannot be less zero. Le Tarif (${0}) ne peut pas être inférieur à zéro.
443 Rate (Company Currency) Tarif (devise utilisée par la société)
514 Sets how many digits are shown after the decimal point. Définit le nombre de chiffres affichés après le point décimal.
515 Sets the app-wide date display format. Définit le format d'affichage de la date pour l'application.
516 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.
517 Set Up Configurer
518 Setting Up Instance Paramétrage de l'Instance
519 Setting Up... Paramétrage...
520 Settings Paramètres
588 This action is permanent Cette action est permanente
589 This action is permanent and will cancel the following payment: ${0} Cette action est permanente et annulera le paiement suivant : ${0}
590 This action is permanent and will cancel the following payments: ${0} Cette action est permanente et annulera les paiements suivants : ${0}
591 This action is permanent and will delete associated ledger entries. Cette action est permanente et supprimera les écritures dans le registre associées. Cette action est permanente et supprimera les écritures dans le registre associés.
592 This action is permanent. Cette action est permanente
593 Times New Roman
594 To Account Au compte

View File

@ -1,35 +1,36 @@
${0} ${1} already exists.,${0} ${1} પહેલાથી અસ્તિત્વમાં છે., ${0} ${1} already exists.,${0} ${1} પહેલાથી અસ્તિત્વમાં છે.,
${0} ${1} does not exist,${0} ${1} અસ્તિત્વમાં નથી, ${0} ${1} does not exist,${0} ${1} અસ્તિત્વમાં નથી,
${0} ${1} has been modified after loading,${0} ${1} લોડિંગ પછી ફેરબદલ કરવામાં આવેલ છે, ${0} ${1} has been modified after loading,${0} ${1} લોડિંગ પછી ફેરબદલ થયેલ છે,
${0} ${1} is linked with existing records.,${0} ${1} હાલમાં રેકોર્ડ્સ સાથે જોડાયેલ છે., ${0} ${1} is linked with existing records.,${0} ${1} હાલમાં રેકોર્ડ્સ સાથે જોડાયેલ છે.,
${0} account not set in Inventory Settings.,${0} ખાતું માલસૂચી સંયોજન માં સક્ષમ નથી, ${0} account not set in Inventory Settings.,${0} ખાતું માલસૂચી સંયોજન માં સક્ષમ નથી,
${0} already exists.,${0} પહેલેથી જ અસ્તિત્વમાં છે., ${0} already exists.,${0} પહેલેથી જ અસ્તિત્વમાં છે.,
${0} fields selected,${0} ખાનાઓ લાગુ, ${0} fields selected,${0} ખાનાઓ લાગુ,
${0} filters applied,${0} તપાસો લાગુ, ${0} filters applied,${0} તપાસો લાગુ,
${0} of type ${1} does not exist,${0}નો પ્રકાર ${1} અસ્તિત્વ માં નથી, ${0} of type ${1} does not exist,${0}નો પ્રકાર ${1} અસ્તિત્વ માં નથી,
${0} out of ${1},${0} માંથી ${1}, ${0} out of ${1},${1} માંથી ${0},
${0} party ${1} is different from ${2},${0} {1} પેઢી ${2} થી અલગ છે, ${0} party ${1} is different from ${2},${0} {1} પેઢી ${2} થી અલગ છે,
${0} quantity 1 added.,${0} માત્રા 1 ઉમેરાણી,
${0} rows,${0} પંક્તિઓ, ${0} rows,${0} પંક્તિઓ,
${0} value ${1} does not exist.,{0} મૂલ્ય ${1} અસ્તિત્વમાં નથી, ${0} value ${1} does not exist.,{0} મૂલ્ય ${1} અસ્તિત્વમાં નથી,
* required fields,* જરૂરી કોઠા, * required fields,* જરૂરી કોઠા,
0%,0%, 0%,0%,
03-23-2022,03-23-2022, 03-23-2022,03-23-2022,
03/23/2022,03/23/2022, 03/23/2022,03/23/2022,
1 filter applied,1 તપાસ લાગુ, 1 filter applied,1 તપાસ લાગુ,
2022-03-23,,
"23 Mar, 2022",,
23-03-2022,23-03-2022, 23-03-2022,23-03-2022,
23-Mar-22,"23 માર્ચ, 2022", 23.03.2022,,
23-03-2022,23-03-2022, 23/03/2022,,
0.983819444,0.983819444,
23-03-2022,23-03-2022,
9888900000,9888900000, 9888900000,9888900000,
Account,ખાતું, Account,ખાતું,
Account ${0} does not exist.,${0} ખાતું અસ્તિત્વ માં નથી, Account ${0} does not exist.,${0} ખાતું અસ્તિત્વ માં નથી,
Account Entries,ખાતા લેવડદેવડ, Account Entries,ખાતા લેવડદેવડ,
Account Name,ખાતા નું નામ, Account Name,ખાતા નું નામ,
Account Type,ખાતાના પ્રકાર, Account Type,ખાતાના પ્રકાર,
Accounting Entries,હિસાબી લેવડદેવડ, Accounting Entries,હિસાબી લેવડદેવડ,
Accounting Ledger Entry,ખાતાવહી ખતવણી, Accounting Ledger Entry,ખાતાવહી ખતવણી,
Accounting Settings,હિસાબી સંયોજન, Accounting Settings,હિસાબી સંયોજન,
Accounts,હિસાબ, Accounts,હિસાબ,
Accounts Payable,ચુકવવાપાત્ર ખાતાઓ, Accounts Payable,ચુકવવાપાત્ર ખાતાઓ,
Accounts Receivable,મળવાપાત્ર હિસાબ, Accounts Receivable,મળવાપાત્ર હિસાબ,
@ -40,17 +41,17 @@ Add Group,સમૂહ ઉમેરો,
Add Items,ઉત્પાદન ઉમેરો, Add Items,ઉત્પાદન ઉમેરો,
Add Row,વિગત ઉમેરો, Add Row,વિગત ઉમેરો,
Add Suppliers,વિક્રેતાઓ ઉમેરો, Add Suppliers,વિક્રેતાઓ ઉમેરો,
Add Taxes,કરવેરા ઉમેરો, Add Taxes,કરવેરા ઉમેરો,
Add a few customers to create your first sales invoice,પ્રથમ વેચાણ ભરતિયું નિર્માણ કરવા પહેલા થોડાક ગ્રાહક ઉમેરો, Add a few customers to create your first sales invoice,પ્રથમ વેચાણ ભરતિયું નિર્માણ કરવા પહેલા થોડાક ગ્રાહક ઉમેરો,
Add a few suppliers to create your first purchase invoice,પ્રથમ ખરીદી ભરતિયું નિર્માણ કરવા પહેલા થોડાક વિક્રેતા ઉમેરો, Add a few suppliers to create your first purchase invoice,પ્રથમ ખરીદી ભરતિયું નિર્માણ કરવા પહેલા થોડાક વિક્રેતા ઉમેરો,
Add a filter,તપાસ ઉમેરો, Add a filter,તપાસ ઉમેરો,
Add a remark,એક ટિપ્પણી ઉમેરો, Add a remark,એક ટિપ્પણી ઉમેરો,
Add attachment,ટાંચ ઉમેરો, Add attachment,ટાંચ ઉમેરો,
Add invoice terms,બિલ શરતો ઉમેરો, Add invoice terms,બિલ શરતો ઉમેરો,
Add products or services that you buy from your suppliers,તમે તમારા વિક્રેતા પાસેથી ખરીદેલા ઉત્પાદનો અથવા સેવાઓ ઉમેરો, Add products or services that you buy from your suppliers,તમે તમારા વિક્રેતા પાસેથી ખરીદેલા ઉત્પાદનો અથવા સેવાઓ ઉમેરો,
Add products or services that you sell to your customers,તમે તમારા ગ્રાહકોને વેચતા ઉત્પાદનો અથવા સેવાઓ ઉમેરો, Add products or services that you sell to your customers,તમે તમારા ગ્રાહકોને વેચતા ઉત્પાદનો અથવા સેવાઓ ઉમેરો,
Add transfer terms,સ્થળાંતર શરતો ઉમેરો, Add transfer terms,સ્થળાંતર શરતો ઉમેરો,
Additional quantity (${0}) required to make outward transfer of item ${1} from ${2} on ${3},વધારાની (${0}) માત્રા જરૂરી છે બાહ્ય સ્થળાંતર માટે વસ્તુ ${2} માંથી ${1} પર ${3}, Additional quantity (${0}) required to make outward transfer of item ${1} from ${2} on ${3},વધારાની (${0}) માત્રા જરૂરી છે બાહ્ય સ્થળાંતર માટે વસ્તુ ${2} માંથી ${1} પર ${3},
Address,સરનામું, Address,સરનામું,
Address Display,પ્રદર્શિત સરનામુ, Address Display,પ્રદર્શિત સરનામુ,
Address Line 1,સરનામું કડી 1, Address Line 1,સરનામું કડી 1,
@ -70,7 +71,7 @@ Asset,રોકાણ,
Assign Imported Labels,આયાત કરેલા લેબલ્સ સોંપો, Assign Imported Labels,આયાત કરેલા લેબલ્સ સોંપો,
Attachment,સંપેતરું, Attachment,સંપેતરું,
August,ઓગસ્ટ, August,ઓગસ્ટ,
Back Reference,ઊલટ સંદર્ભ, Back Reference,ઊલટ સંદર્ભ,
Bad import data.,ખરાબ આયાત ડેટા., Bad import data.,ખરાબ આયાત ડેટા.,
Balance,સિલક, Balance,સિલક,
Balance Sheet,પાકું સરવૈયું, Balance Sheet,પાકું સરવૈયું,
@ -79,7 +80,8 @@ Bank Accounts,બેંક ખાતા,
Bank Entry,બેંક ખતવણી, Bank Entry,બેંક ખતવણી,
Bank Name,બેંકનું નામ, Bank Name,બેંકનું નામ,
Bank Overdraft Account,બેંક ઓવરડ્રાફ્ટ ખાતું, Bank Overdraft Account,બેંક ઓવરડ્રાફ્ટ ખાતું,
Base Grand Total,Base કુલ રકમ, Barcode,બારકોડ,
Base Grand Total,પ્રમુખ કુલ રકમ,
Based On,આના આધારે, Based On,આના આધારે,
Basic,Basic, Basic,Basic,
Bill Created,બિલ નિર્માણ, Bill Created,બિલ નિર્માણ,
@ -88,13 +90,13 @@ Blue,Blue,
Both,બંને, Both,બંને,
Both From and To Location cannot be undefined,બન્ને થકી અને પ્રત્યે સ્થળ ખાલી હોય શકે નહીં, Both From and To Location cannot be undefined,બન્ને થકી અને પ્રત્યે સ્થળ ખાલી હોય શકે નહીં,
Buildings,મકાનો, Buildings,મકાનો,
Business,વ્યવસાય, Business,,
Cancel,ફોક, Cancel,ફોક,
Cancel ${0} ${1}?,${0} ${1} ફોક કરો ?, Cancel ${0} ${1}?,${0} ${1} ફોક કરો ?,
Cancelled,ફોક કરાયેલ, Cancelled,ફોક કરાયેલ,
Cannot Commit Error,, Cannot Commit Error,,
Cannot Delete,કાઢી શકાય તેમ નથી, Cannot Delete,કાઢી શકાય તેમ નથી,
Cannot cancel ${0} ${1} because of the following ${2}: ${3},${2} ના કારણે ${0} {1} ફોક થાઈ તેમ નથી : ${3}, Cannot cancel ${0} ${1} because of the following ${2}: ${3},${2} ના કારણે ${0} {1} ફોક થાઈ તેમ નથી : ${3},
Cannot delete ${0} ${1} because of linked entries.,${0} ${1} સંકળાયેલા લેવડદેવડ ના કારણે કાઢી શકાય તેમ નથી., Cannot delete ${0} ${1} because of linked entries.,${0} ${1} સંકળાયેલા લેવડદેવડ ના કારણે કાઢી શકાય તેમ નથી.,
Cannot perform operation.,પ્રક્રિયા નહીં થઈ શકે., Cannot perform operation.,પ્રક્રિયા નહીં થઈ શકે.,
Capital Equipments,મૂડીનાં સાધનો, Capital Equipments,મૂડીનાં સાધનો,
@ -102,11 +104,11 @@ Capital Stock,મૂડીનો હિસ્સો,
Cash,રોકડ, Cash,રોકડ,
Cash Entry,રોકડ ખતવણી, Cash Entry,રોકડ ખતવણી,
Cash In Hand,હાથ પર રોકડ, Cash In Hand,હાથ પર રોકડ,
Cashflow,કેશફલૉ, Cashflow,રોકડ પ્રવાહ,
Central Tax,કેન્દ્રીય કર, Central Tax,કેન્દ્રીય કર,
Change DB,ડીબી બદલો, Change DB,ડીબી બદલો,
Change File,ફાઈલ બદલો, Change File,ફાઈલ બદલો,
Change Ref Type,સંદર્ભ પ્રકાર બદલો , Change Ref Type,સંદર્ભ પ્રકાર બદલો,
Chargeable,તહોમતપાત્ર, Chargeable,તહોમતપાત્ર,
Chart Of Accounts Reviewed,હિસાબી આલેખ સમીક્ષા, Chart Of Accounts Reviewed,હિસાબી આલેખ સમીક્ષા,
Chart of Accounts,હિસાબી આલેખ, Chart of Accounts,હિસાબી આલેખ,
@ -117,9 +119,9 @@ Clearance Date,ક્લિઅરન્સ તારીખ,
Click to create,નિર્માણ માટે ક્લિક કરો, Click to create,નિર્માણ માટે ક્લિક કરો,
Close,બંધ, Close,બંધ,
Close Frappe Books and try manually,Frappe Books બંધ કરી ને જાતે પ્રયાસ કરો, Close Frappe Books and try manually,Frappe Books બંધ કરી ને જાતે પ્રયાસ કરો,
Closing,બંધ થાય છે, Closing,આખર,
Closing (Cr),આખર સિલક (Cr), Closing (Cr),આખર સિલક (Cr),
Closing (Dr),આખર સિલક (Dr), Closing (Dr),આખર સિલક (Dr),
Collapse,સંકોચો, Collapse,સંકોચો,
Color,રંગ, Color,રંગ,
Commission on Sales,વેચાણ પર આડત, Commission on Sales,વેચાણ પર આડત,
@ -143,7 +145,7 @@ Country,દેશ,
Country Code,દેશનો કોડ, Country Code,દેશનો કોડ,
Country code used to initialize regional settings.,પ્રાદેશિક નિયમન પ્રારંભ કરવા માટે દેશ કોડ વપરાય છે., Country code used to initialize regional settings.,પ્રાદેશિક નિયમન પ્રારંભ કરવા માટે દેશ કોડ વપરાય છે.,
Courier,Courier, Courier,Courier,
Cr.,જ., Cr.,,
Create,નિર્માણ કરો, Create,નિર્માણ કરો,
Create Demo,ડેમો નિર્માણ, Create Demo,ડેમો નિર્માણ,
Create Purchase,ખરીદી નિર્માણ, Create Purchase,ખરીદી નિર્માણ,
@ -156,9 +158,9 @@ Create your first purchase invoice from the created supplier,મોજૂદ વ
Create your first sales invoice for the created customer,મોજૂદ ગ્રાહક પરથી પ્રથમ વેચાણ ભરતિયું નિર્માણ કરો, Create your first sales invoice for the created customer,મોજૂદ ગ્રાહક પરથી પ્રથમ વેચાણ ભરતિયું નિર્માણ કરો,
Created,નિર્માણ થયું, Created,નિર્માણ થયું,
Created By,નિર્માણ પ્રમાણે, Created By,નિર્માણ પ્રમાણે,
Creating Items and Parties,વસ્તુઓ અને પક્ષૉ નિર્માણ થઈ રહ્યા છે, Creating Items and Parties,ચીજવસ્તુઓ અને પક્ષૉ નિર્માણ થઈ રહ્યા છે,
Creating Journal Entries,આમનોંધ લેવડદેવડ નિર્માણ થઈ રહી છે , Creating Journal Entries,આમનોંધ લેવડદેવડ નિર્માણ થઈ રહી છે,
Creating Purchase Invoices,ખરીદી ભરતિયું નિર્માણ થઈ રહ્યું છે , Creating Purchase Invoices,ખરીદી ભરતિયું નિર્માણ થઈ રહ્યું છે,
Credit,જમા, Credit,જમા,
Credit Card Entry,ક્રેડિટ કાર્ડ ખતવણી, Credit Card Entry,ક્રેડિટ કાર્ડ ખતવણી,
Credit Note,જમા-ચિઠ્ઠી, Credit Note,જમા-ચિઠ્ઠી,
@ -174,15 +176,15 @@ Customer Created,ગ્રાહક નિર્માણ,
Customer Currency,ગ્રાહક ચલણ, Customer Currency,ગ્રાહક ચલણ,
Customers,ગ્રાહકો, Customers,ગ્રાહકો,
Customise,રૂપરેખા, Customise,રૂપરેખા,
Customize your invoices by adding a logo and address details,પ્રતીક અને સરનામાંની વિગતો ઉમેરીને તમારા ભરતીયા ની રૂપરેખા બદલો, Customize your invoices by adding a logo and address details,પ્રતીક અને સરનામાંની વિગતો ઉમેરીને તમારા ભરતીયા ની રૂપરેખા બદલો,
Dashboard,મુખ્ય પૃષ્ઠ, Dashboard,મુખ્ય પૃષ્ઠ,
Data Import,ડેટા આયાત , Data Import,ડેટા આયાત,
Database Error,ડેટાબેઝ ચૂક, Database Error,ડેટાબેઝ ચૂક,
Database file: ${0},ડેટાબેઝ ફાઇલ: ${0}, Database file: ${0},ડેટાબેઝ ફાઇલ: ${0},
Date,તારીખ, Date,તારીખ,
Date Format,તારીખ બંધારણ, Date Format,તારીખ બંધારણ,
Day,દિવસ, Day,દિવસ,
Debit,ઉધાર , Debit,ઉધાર,
Debit Note,ઉધાર-ચિઠ્ઠી, Debit Note,ઉધાર-ચિઠ્ઠી,
Debtors,દેવાદાર, Debtors,દેવાદાર,
December,ડીસેમ્બર, December,ડીસેમ્બર,
@ -219,7 +221,7 @@ Dividends Paid,ચૂકવેલ ડિવિડન,
Docs,દસ્તાવેજ, Docs,દસ્તાવેજ,
Documentation,દસ્તાવેજીકરણ, Documentation,દસ્તાવેજીકરણ,
Does Not Contain,સમાવિષ્ટ નથી, Does Not Contain,સમાવિષ્ટ નથી,
Dr.,ઉ., Dr.,,
Draft,મુસદ્દો, Draft,મુસદ્દો,
Duplicate,નકલ બનાવો, Duplicate,નકલ બનાવો,
Duplicate ${0} ${1}?,નકલ ${0} ${1}?, Duplicate ${0} ${1}?,નકલ ${0} ${1}?,
@ -230,10 +232,12 @@ Electronic Equipments,વિદ્યુત સાધન,
Email,ઇ-મેઇલ, Email,ઇ-મેઇલ,
Email Address,ઈ - મેઈલ સરનામું, Email Address,ઈ - મેઈલ સરનામું,
Empty,ખાલી, Empty,ખાલી,
Enable Barcodes,બારકોડ સક્ષમ કરો,
Enable Discount Accounting,વટાવ હિસાબ સક્ષમ કરો, Enable Discount Accounting,વટાવ હિસાબ સક્ષમ કરો,
Enable Inventory,માલસૂચિ સક્ષમ કરો, Enable Inventory,માલસૂચિ સક્ષમ કરો,
Enter Country to load States,રાજ્યો લોડ કરવા માટે દેશ દાખલ કરો, Enter Country to load States,રાજ્યો લોડ કરવા માટે દેશ દાખલ કરો,
Enter State,રાજ્ય દાખલ કરો, Enter State,રાજ્ય દાખલ કરો,
Enter barcode,બારકોડ ઉમેરો,
Entertainment Expenses,મનોરંજન ખર્ચ, Entertainment Expenses,મનોરંજન ખર્ચ,
Entry Currency,ખતવણી ચલણ, Entry Currency,ખતવણી ચલણ,
Entry No,ખતવણી ક્રમ, Entry No,ખતવણી ક્રમ,
@ -266,18 +270,18 @@ Fiscal Year,નાણાકીય વર્ષ,
Fiscal Year End Date,નાણાકીય વર્ષની અંતિમ તારીખ, Fiscal Year End Date,નાણાકીય વર્ષની અંતિમ તારીખ,
Fiscal Year Start Date,નાણાકીય વર્ષની શરૂઆતની તારીખ, Fiscal Year Start Date,નાણાકીય વર્ષની શરૂઆતની તારીખ,
Fixed Asset,નિયત રોકાણ, Fixed Asset,નિયત રોકાણ,
Fixed Assets,નિયત અસ્કયામતો Fixed Assets,નિયત અસ્કયામતો,
Font,અક્ષર વર્ણ, Font,અક્ષર વર્ણ,
For,ને માટે, For,ને માટે,
Forbidden Error,પ્રતિબંધિત ચૂક, Forbidden Error,પ્રતિબંધિત ચૂક,
Fr,, Fr,શુક્ર,
Fraction,દશાંશ, Fraction,દશાંશ,
Fraction Units,દશાંશ એકમો, Fraction Units,દશાંશ એકમો,
Freight and Forwarding Charges,ભાડા અને આગળના ખર્ચ, Freight and Forwarding Charges,ભાડા અને આગળના ખર્ચ,
From,થકી, From,થકી,
From Account,ખાતા થકી, From Account,ખાતા થકી,
From Date,આ તારીખ થી, From Date,આ તારીખ થી,
From Loc.,સ્થાન થકી, From Loc.,સ્થાન થકી,
From Year,વર્ષ થકી, From Year,વર્ષ થકી,
Full Name,પૂરું નામ, Full Name,પૂરું નામ,
Furnitures and Fixtures,ફર્નિચર અને ફિક્સર, Furnitures and Fixtures,ફર્નિચર અને ફિક્સર,
@ -285,7 +289,7 @@ GST,,
GSTIN No.,GSTIN ક્રમ, GSTIN No.,GSTIN ક્રમ,
GSTR1,GSTR 1, GSTR1,GSTR 1,
GSTR2,GSTR 2, GSTR2,GSTR 2,
Gain/Loss on Asset Disposal,રોકાણ પતાવટ પર લાભ/ખોટ, Gain/Loss on Asset Disposal,રોકાણ પતાવટ પર લાભ/ખોટ,
General,સામાન્ય વિગતો, General,સામાન્ય વિગતો,
General Ledger,સામાન્ય ખાતાવહી, General Ledger,સામાન્ય ખાતાવહી,
Get Started,અહીં શરૂઆત, Get Started,અહીં શરૂઆત,
@ -296,7 +300,7 @@ Green,Green,
Group By,સમૂહ પ્રમાણે, Group By,સમૂહ પ્રમાણે,
HSN/SAC,HSN/SAC, HSN/SAC,HSN/SAC,
HSN/SAC Code,HSN/SAC Code, HSN/SAC Code,HSN/SAC Code,
Half Yearly,અર્ધવાર્ષિક , Half Yearly,અર્ધવાર્ષિક,
Half Years,અર્ધ વર્ષ, Half Years,અર્ધ વર્ષ,
Help,મદદ, Help,મદદ,
Hex Value,Hex Value, Hex Value,Hex Value,
@ -325,7 +329,8 @@ Instance Id,દાખલો,
Insufficient Quantity.,અપૂર્ણ માત્રા, Insufficient Quantity.,અપૂર્ણ માત્રા,
Intergrated Tax,આંતરગ્રાહી કર, Intergrated Tax,આંતરગ્રાહી કર,
Internal Precision,આંતરિક ખરાપણું, Internal Precision,આંતરિક ખરાપણું,
Invalid value ${0} for ${1},${0} માટે ${1} અમાન્ય મૂલ્ય, Invalid barcode value ${0}.,બારકોડનું મૂલ્ય ${0} અમાન્ય છે,
Invalid value ${0} for ${1},${0} માટે ${1} અમાન્ય મૂલ્ય,
Inventory,માલસૂચિ, Inventory,માલસૂચિ,
Inventory Settings,માલસૂચિ સંયોજન, Inventory Settings,માલસૂચિ સંયોજન,
Investments,રોકાણ, Investments,રોકાણ,
@ -341,17 +346,18 @@ Invoice Value,ભરતિયું મૂલ્ય,
Invoices,ભરતિયું, Invoices,ભરતિયું,
Is,, Is,,
Is Empty,, Is Empty,,
Is Group,, Is Group,સમૂહ છે,
Is Not,, Is Not,,
Is Not Empty,, Is Not Empty,,
Is Whole,શું પરિપૂર્ણ છે, Is Whole,શું પરિપૂર્ણ છે,
Item,વિગત, Item,વિગત,
Item Description,વર્ણન, Item Description,વર્ણન,
Item Name,નામ, Item Name,ચીજવસ્તુનું નામ,
Items,વસ્તુઓ , Item with barcode ${0} not found.,બારકોડ ${0} વાળી ચીજવસ્તુ હજાર નથી.,
Items,ચીજવસ્તુઓ,
January,જાન્યુઆરી, January,જાન્યુઆરી,
John Doe,John Doe, John Doe,John Doe,
Journal Entries,આમનોંધ લેવડદેવડ, Journal Entries,આમનોંધ લેવડદેવડ,
Journal Entry,આમનોંધ ખતવણી, Journal Entry,આમનોંધ ખતવણી,
Journal Entry Account,આમનોંધ ખતવણી ખાતું, Journal Entry Account,આમનોંધ ખતવણી ખાતું,
Journal Entry Number Series,આમનોંધ ખતવણી ક્રમાંકન, Journal Entry Number Series,આમનોંધ ખતવણી ક્રમાંકન,
@ -369,19 +375,19 @@ Limit,મર્યાદા,
Link Validation Error,લિંક માન્યતા ચૂક, Link Validation Error,લિંક માન્યતા ચૂક,
List,યાદી, List,યાદી,
Load an existing .db file from your computer.,તમારા કમ્પ્યુટરથી અસ્તિત્વમાં હોય એવી .db ફાઇલ લોડ કરો., Load an existing .db file from your computer.,તમારા કમ્પ્યુટરથી અસ્તિત્વમાં હોય એવી .db ફાઇલ લોડ કરો.,
Loading Report...,લોડિંગ રિપોર્ટ ..., Loading Report...,અહેવાલ રજૂ થાય છે...,
Loading...,લોડ કરી રહ્યું છે ..., Loading...,રજૂ થાય છે...,
Loans (Liabilities),લોન (દેવું), Loans (Liabilities),લોન (દેવું),
Loans and Advances (Assets),કરજ અને ધિરાણ (રોકાણ), Loans and Advances (Assets),કરજ અને ધિરાણ (રોકાણ),
Locale,પ્રાદેશિક નિયમન, Locale,પ્રાદેશિક નિયમન,
Location,સ્થાન, Location,સ્થાન,
Location Name,સ્થાનનું નામ , Location Name,સ્થાનનું નામ,
Logo,પ્રતીક, Logo,પ્રતીક,
Make Entry,લેવડદેવડ ઉમેરો, Make Entry,લેવડદેવડ ઉમેરો,
Mandatory Error,અનિવાર્ય ચૂક, Mandatory Error,અનિવાર્ય ચૂક,
"Mar 23, 2022","23 માર્ચ, 2022", "Mar 23, 2022","23 માર્ચ, 2022",
March,માર્ચ, March,માર્ચ,
Marketing Expenses,વિજ્ઞાપન ખર્ચ, Marketing Expenses,વિજ્ઞાપન ખર્ચ,
Material Issue,, Material Issue,,
Material Receipt,, Material Receipt,,
Material Transfer,, Material Transfer,,
@ -390,36 +396,36 @@ Meter,મિટર,
Minimal,Minimal, Minimal,Minimal,
Misc,વગેરે, Misc,વગેરે,
Miscellaneous Expenses,પરચૂરણ ખર્ચ, Miscellaneous Expenses,પરચૂરણ ખર્ચ,
Mo,, Mo,સોમ,
Modified,સુધારેલું, Modified,સુધારેલું,
Modified By,સંશોધિત પ્રમાણે, Modified By,સંશોધિત પ્રમાણે,
Monthly,માસિક, Monthly,માસિક,
Months,મહિના, Months,મહિના,
More Filters,વધુ તપાસો, More Filters,વધુ તપાસો,
Movement Type,સ્થળાંતર પ્રકાર, Movement Type,સ્થળાંતર પ્રકાર,
Moving Average,, Moving Average,ચાલક સરેરાંશ,
Name,નામ, Name,નામ,
Navigate,શોધખોળ, Navigate,સંચાલન,
Net Total,ચોખ્ખો સરવાળો, Net Total,કુલ સરવાળો,
New ${0},નવ ${0}, New ${0},નવ ${0},
New Account,નવું ખાતું, New Account,નવું ખાતું,
New Entry,નવી ખતવણી, New Entry,નવી ખતવણી,
New File,નવી ફાઈલ, New File,નવી ફાઈલ,
No,ના , No,ના,
No Data to Import,આયાત કરવા માટે કોઈ ડેટા નથી, No Data to Import,આયાત કરવા માટે કોઈ ડેટા નથી,
No Values to be Displayed,પ્રદર્શિત કરવા માટે કોઈ મૂલ્યો નથી, No Values to be Displayed,પ્રદર્શિત કરવા માટે કોઈ મૂલ્યો નથી,
No entries found,કોઈ લેવડદેવડ મળ્યા નથી, No entries found,કોઈ લેવડદેવડ મળ્યા નથી,
No expenses in this period,આ સમયગાળામાં કોઈ ખર્ચ નથી, No expenses in this period,આ સમયગાળામાં કોઈ ખર્ચ નથી,
No filters selected,તપાસ ઊમેરાય નથી, No filters selected,તપાસ ઊમેરાય નથી,
No labels have been assigned.,કોઈ લેબલ સોંપવામાં આવ્યા નથી., No labels have been assigned.,કોઈ લેબલ સોંપવામાં આવ્યા નથી.,
No results found,કોઈ પરિણામો મળ્યા નથી, No results found,કોઈ પરિણામો મળ્યા નથી,
No transactions yet,હજી સુધી કોઈ વ્યવહાર નથી, No transactions yet,હજી સુધી કોઈ વ્યવહાર નથી,
None,, None,કોઈ નહીં,
Not Found,મળ્યા નથી, Not Found,મળ્યા નથી,
Not Saved,સંગ્રહ થયો નથી, Not Saved,સંગ્રહ થયો નથી,
Notes,નોંધ, Notes,નોંધ,
November,નવેમ્બર, November,નવેમ્બર,
Number Series,સંખ્યા ક્રમાંકન, Number Series,ક્રમાંકન,
Number of ${0},${0} ની સંખ્યા, Number of ${0},${0} ની સંખ્યા,
October,ઓક્ટોબર, October,ઓક્ટોબર,
Office Equipments,કચેરી સાધન, Office Equipments,કચેરી સાધન,
@ -428,9 +434,9 @@ Office Rent,કચેરી ભાડા,
Onboarding Complete,ઓનબોર્ડિંગ પૂર્ણ, Onboarding Complete,ઓનબોર્ડિંગ પૂર્ણ,
Open Count,ખુલ્લી ગણતરી, Open Count,ખુલ્લી ગણતરી,
Open Folder,ખુલ્લું ફોલ્ડર, Open Folder,ખુલ્લું ફોલ્ડર,
Opening (Cr),ખૂલતી સિલક (જમા.), Opening (Cr),ખૂલતી સિલક (Cr),
Opening (Dr),ખૂલતી સિલક (ઉધાર.), Opening (Dr),ખૂલતી સિલક (Dr),
Opening Balance Equity,ખૂલતી ઇક્વિટી, Opening Balance Equity,ખૂલતી ઇક્વિટી,
Opening Balances,શરૂઆતી મૂડી, Opening Balances,શરૂઆતી મૂડી,
Opening Entry,શરૂઆતી ખતવણી, Opening Entry,શરૂઆતી ખતવણી,
Orange,Orange, Orange,Orange,
@ -441,16 +447,16 @@ Pad Zeros,દર્શાવા ખાતર શૂન્ય,
Page,પાનાં, Page,પાનાં,
Paid,ચુકવેલ, Paid,ચુકવેલ,
Parent,પ્રધાન, Parent,પ્રધાન,
Parent Account,પ્રધાન ખાતું, Parent Account,પ્રધાન ખાતું,
Party,પેઢી, Party,પેઢી,
Patch Run,, Patch Run,મરામતી ક્રિયા,
Pay,ચૂકવણી, Pay,ચૂકવણી,
Payable,ચૂકવવાપાત્ર, Payable,ચૂકવવાપાત્ર,
Payment,પેમેન્ટ, Payment,પેમેન્ટ,
Payment For,માટે પેમેન્ટ, Payment For,માટે પેમેન્ટ,
Payment Method,પેમેન્ટ પદ્ધતિ, Payment Method,પેમેન્ટ પદ્ધતિ,
Payment No,પેમેન્ટ ક્રમ, Payment No,પેમેન્ટ ક્રમ,
Payment Number Series,ચુકવણી ક્રમાંકન, Payment Number Series,પેમેન્ટ ક્રમાંકન,
Payment Reference,પેમેન્ટ સંદર્ભ, Payment Reference,પેમેન્ટ સંદર્ભ,
Payment Type,પેમેન્ટનો પ્રકાર, Payment Type,પેમેન્ટનો પ્રકાર,
Payment amount cannot be ${0}.,પેમેન્ટની રકમ ${0} હોઈ શકતી નથી., Payment amount cannot be ${0}.,પેમેન્ટની રકમ ${0} હોઈ શકતી નથી.,
@ -492,33 +498,33 @@ Purchase Invoice Number Series,ખરીદ ભરતિયું ક્રમ
Purchase Invoice Terms,ખરીદી ભરતિયું શરતો, Purchase Invoice Terms,ખરીદી ભરતિયું શરતો,
Purchase Invoices,ખરીદી ભરતિયા, Purchase Invoices,ખરીદી ભરતિયા,
Purchase Item Created,ખરીદ વસ્તુ નિર્માણ, Purchase Item Created,ખરીદ વસ્તુ નિર્માણ,
Purchase Items,ખરીદ માલયાદી, Purchase Items,ખરીદ ચીજવસ્તુઓ,
Purchase Payments,ખરીદી ચૂકવણીઓ, Purchase Payments,ખરીદી ચૂકવણીઓ,
Purchase Receipt,ખરીદ પહોંચ, Purchase Receipt,ખરીદ પહોંચ,
Purchase Receipt Item,ખરીદ પહોંચ વસ્તુ, Purchase Receipt Item,ખરીદ પહોંચ વસ્તુ,
Purchase Receipt Number Series,ખરીદ પહોંચ ક્રમાંકન, Purchase Receipt Number Series,ખરીદ પહોંચ ક્રમાંકન,
Purchase Receipt Terms,ખરીદ પહોંચ શરતો, Purchase Receipt Terms,ખરીદ પહોંચ શરતો,
Purchase Receipts,ખરીદ પહોંચ, Purchase Receipts,ખરીદ પહોંચ,
Purchases,ખરીદી, Purchases,ખરીદી,
Purple,Purple, Purple,Purple,
Qty. ${0},માત્રા. ${0}, Qty. ${0},માત્રા. ${0},
Quantity,માત્રા, Quantity,માત્રા,
Quantity (${0}) has to be greater than zero,માત્રા (${0}) શૂન્ય થી વધારે હોવી જોઈએ, Quantity (${0}) has to be greater than zero,માત્રા (${0}) શૂન્ય થી વધારે હોવી જોઈએ,
Quantity needs to be set,માત્રા , Quantity needs to be set,માત્રા સૂચવવી જરૂરી છે,
Quarterly,ત્રિમાસિક, Quarterly,ત્રિમાસિક,
Quarters,ત્રિમાસિક, Quarters,ત્રિમાસિક,
Rate,ભાવ, Rate,ભાવ,
Rate (${0}) cannot be less zero.,સંખ્યા (${0}) શૂન્ય થી ઓછી હોઈ શકતી નથી., Rate (${0}) cannot be less zero.,સંખ્યા (${0}) શૂન્ય થી ઓછી હોઈ શકતી નથી.,
Rate (${0}) has to be greater than zero,સંખ્યા (${0}) શૂન્ય થી વધારે હોવી જોઈએ, Rate (${0}) has to be greater than zero,સંખ્યા (${0}) શૂન્ય થી વધારે હોવી જોઈએ,
Rate can't be negative.,સંખ્યા ઋણમાં હોઈ શકે નથી., Rate can't be negative.,સંખ્યા ઋણમાં હોઈ શકે નથી.,
Rate needs to be set,દર સુયોજિત કરવા જરૂરી છે, Rate needs to be set,દર સુયોજિત કરવા જરૂરી છે,
Receivable,મળવાપાત્ર , Receivable,મળવાપાત્ર,
Receive,સ્વીકાર, Receive,સ્વીકાર,
Recent Invoices,તાજેતરના Invoice, Recent Invoices,તાજેતરના Invoice,
Red,Red, Red,Red,
Ref Name,સંદર્ભ નામ , Ref Name,સંદર્ભ નામ,
Ref Type,સંદર્ભ પ્રકાર, Ref Type,સંદર્ભ પ્રકાર,
Ref. / Cheque No.,સંદર્ભ. / ચેક નંબર. , Ref. / Cheque No.,સંદર્ભ. / ચેક નંબર.,
Ref. Date,સંદર્ભ. તારીખ, Ref. Date,સંદર્ભ. તારીખ,
Ref. Name,સંદર્ભ. નામ, Ref. Name,સંદર્ભ. નામ,
Ref. Type,સંદર્ભ. પ્રકાર, Ref. Type,સંદર્ભ. પ્રકાર,
@ -526,18 +532,18 @@ Reference,સંદર્ભ,
Reference Date,સંદર્ભ તારીખ, Reference Date,સંદર્ભ તારીખ,
Reference Number,સંદર્ભ ક્રમ, Reference Number,સંદર્ભ ક્રમ,
Reference Type,સંદર્ભ પ્રકાર, Reference Type,સંદર્ભ પ્રકાર,
Reload App,એપ પુનઃપ્રારંભ, Reload App,એપ પુનઃપ્રારંભ,
Report,અહેવાલ, Report,અહેવાલ,
Report Error,ચૂક ઉલ્લેખ, Report Error,ચૂક ઉલ્લેખ,
Report Issue,ખામી ઉલ્લેખ, Report Issue,ખામી ઉલ્લેખ,
Reports,અહેવાલો, Reports,અહેવાલો,
Required Fields not Assigned,નિર્દેશિત કોઠા ફાળવાયા નથી , Required Fields not Assigned,નિર્દેશિત કોઠા ફાળવાયા નથી,
Reset,Reset, Reset,Reset,
Retained Earnings,જાળવેલ કમાણી, Retained Earnings,જાળવેલ કમાણી,
Reverse Chrg.,Reverse Chrg., Reverse Chrg.,Reverse Chrg.,
Reverted,Reverted, Reverted,Reverted,
Reverts,Reverts, Reverts,Reverts,
Review Accounts,ખાતા નિરીક્ષણ, Review Accounts,ખાતા નિરીક્ષણ,
"Review your chart of accounts, add any account or tax heads as needed","તમારા હિસાબી આલેખની સમીક્ષા કરો, જરૂર મુજબ કોઈપણ ખાતા અથવા કરવેરા ઉમેરો", "Review your chart of accounts, add any account or tax heads as needed","તમારા હિસાબી આલેખની સમીક્ષા કરો, જરૂર મુજબ કોઈપણ ખાતા અથવા કરવેરા ઉમેરો",
Right Index,, Right Index,,
Role,ભૂમિકા, Role,ભૂમિકા,
@ -546,7 +552,7 @@ Round Off,પરચુરણ બાકાત,
Round Off Account,પરચુરણ બાકાત ખાતું, Round Off Account,પરચુરણ બાકાત ખાતું,
Round Off Account Not Found,પરચુરણ બાકાત ખાતું હાજર નથી, Round Off Account Not Found,પરચુરણ બાકાત ખાતું હાજર નથી,
Rounded Off,બાકાત, Rounded Off,બાકાત,
Sa,, Sa,શનિ,
Salary,પગાર, Salary,પગાર,
Sales,વેચાણ, Sales,વેચાણ,
Sales Acc.,વેચાણ ખાતું, Sales Acc.,વેચાણ ખાતું,
@ -555,9 +561,9 @@ Sales Invoice,વેચાણ ભરતિયું,
Sales Invoice Item,વેચાણ ભરતિયું વસ્તુ, Sales Invoice Item,વેચાણ ભરતિયું વસ્તુ,
Sales Invoice Number Series,વેચાણ ભરતિયું ક્રમાંકન, Sales Invoice Number Series,વેચાણ ભરતિયું ક્રમાંકન,
Sales Invoice Terms,વેચાણ ભરતિયું શરતો, Sales Invoice Terms,વેચાણ ભરતિયું શરતો,
Sales Invoices,વેચાણ ભરતીયા , Sales Invoices,વેચાણ ભરતીયા,
Sales Item Created,વેચાણની વસ્તુ નિર્માણ, Sales Item Created,વેચાણની વસ્તુ નિર્માણ,
Sales Items,વેચાણ માલયાદી, Sales Items,વેચાણ ચીજવસ્તુઓ,
Sales Payments,વેચાણ ચૂકવણીઓ, Sales Payments,વેચાણ ચૂકવણીઓ,
Save,સંઘરો, Save,સંઘરો,
Save Template,નમૂનો સંઘરો, Save Template,નમૂનો સંઘરો,
@ -565,7 +571,7 @@ Save as PDF,PDF રીતે સંઘરો,
Save as PDF Successful,PDF રીતે સંગ્રહ સફળકારક, Save as PDF Successful,PDF રીતે સંગ્રહ સફળકારક,
Saved,સંગ્રહિત, Saved,સંગ્રહિત,
Saving,સંગ્રહ થાય છે, Saving,સંગ્રહ થાય છે,
Schema Name or Name not passed to Open Quick Edit,, Schema Name or Name not passed to Open Quick Edit,સ્કીમા નામ અથવા નામ અપાયું નથી Quick Edit ખોલવા માટે,
Search,શોધ, Search,શોધ,
Secured Loans,સુરક્ષિત લોન, Secured Loans,સુરક્ષિત લોન,
Securities and Deposits,સિક્યોરિટીઝ અને થાપણો, Securities and Deposits,સિક્યોરિટીઝ અને થાપણો,
@ -584,8 +590,8 @@ Selected file,પસંદ કરેલી ફાઇલ,
September,સેપ્ટેમ્બર, September,સેપ્ટેમ્બર,
Service,સેવાઓ, Service,સેવાઓ,
Set Discount Amount,સુયોજિત વટાવ રકમ, Set Discount Amount,સુયોજિત વટાવ રકમ,
Set Period,, Set Period,સમયગાળો સુયોજિત કરો,
Set Up,સુયોજિત કરો, Set Up,સુયોજિત કરો,
Set Up Your Workspace,વ્યવસાય સુયોજિત કરો, Set Up Your Workspace,વ્યવસાય સુયોજિત કરો,
Set an Import Type,આયાત પ્રકાર સહેજો, Set an Import Type,આયાત પ્રકાર સહેજો,
Set the display language.,પ્રદર્શન ભાષા સહેજો., Set the display language.,પ્રદર્શન ભાષા સહેજો.,
@ -598,7 +604,7 @@ Sets how many digits are shown after the decimal point.,દશાંશ બિ
Sets the app-wide date display format.,એપ્લિકેશન-વ્યાપક તારીખ બંધારણ સ્થાપન કરે છે., Sets the app-wide date display format.,એપ્લિકેશન-વ્યાપક તારીખ બંધારણ સ્થાપન કરે છે.,
Sets the internal precision used for monetary calculations. Above 6 should be sufficient for most currencies.,નાણાકીય ગણતરીઓ માટે વપરાયેલી આંતરિક ચોકસાઇ સ્થાપન કરે છે. મોટાભાગના ચલણો માટે 6 થી ઉપર પૂરતું હોવું જોઈએ., Sets the internal precision used for monetary calculations. Above 6 should be sufficient for most currencies.,નાણાકીય ગણતરીઓ માટે વપરાયેલી આંતરિક ચોકસાઇ સ્થાપન કરે છે. મોટાભાગના ચલણો માટે 6 થી ઉપર પૂરતું હોવું જોઈએ.,
Setting Up Instance,દાખલો સુયોજિત કરી રહ્યા છીએ, Setting Up Instance,દાખલો સુયોજિત કરી રહ્યા છીએ,
Setting up...,સુયોજિત થઈ રહ્યું છે, Setting up...,સુયોજિત થઈ રહ્યું છે...,
Settings,પરિસ્થિતિ, Settings,પરિસ્થિતિ,
Settings changes will be visible on reload,પરિસ્થિતિ ફેરફારો પુનઃપ્રારંભ પર દેખાશે, Settings changes will be visible on reload,પરિસ્થિતિ ફેરફારો પુનઃપ્રારંભ પર દેખાશે,
Setup,સ્થાપન, Setup,સ્થાપન,
@ -615,7 +621,7 @@ Show Month/Year,મહિના/વર્ષ દર્શાવો,
Single Value,એકમો મૂલ્ય, Single Value,એકમો મૂલ્ય,
Skip Child Tables,બાળ કોષ્ટકો અવગણો, Skip Child Tables,બાળ કોષ્ટકો અવગણો,
Skip Transactions,વ્યવહાર અવગણો, Skip Transactions,વ્યવહાર અવગણો,
Smallest Currency Fraction Value,સૌથી ઓછ ચલણ મૂલ્ય, Smallest Currency Fraction Value,સૌથી ઓછ ચલણ મૂલ્ય,
Softwares,Softwares, Softwares,Softwares,
Something has gone terribly wrong. Please check the console and raise an issue.,કંઈક ભયંકર રીતે ખોટું થયું છે. કૃપા કરીને કોનસોલ તપાસો અને કોઈ મુદ્દો ઉભો કરો., Something has gone terribly wrong. Please check the console and raise an issue.,કંઈક ભયંકર રીતે ખોટું થયું છે. કૃપા કરીને કોનસોલ તપાસો અને કોઈ મુદ્દો ઉભો કરો.,
Source of Funds (Liabilities),ભંડોળનો સ્રોત (દેવું), Source of Funds (Liabilities),ભંડોળનો સ્રોત (દેવું),
@ -632,12 +638,12 @@ Stock Entries,માલ લેવડદેવડ,
Stock Expenses,માલ ખર્ચ, Stock Expenses,માલ ખર્ચ,
Stock In Hand,હાથ પર માલ, Stock In Hand,હાથ પર માલ,
Stock In Hand Acc.,હાથ પર માલ ખાતું, Stock In Hand Acc.,હાથ પર માલ ખાતું,
Stock Ledger,માલ હિસાબ , Stock Ledger,માલ હિસાબ,
Stock Ledger Entry,માલ હિસાબ ખતવણી, Stock Ledger Entry,માલ હિસાબ ખતવણી,
Stock Liabilities,માલ દેવું, Stock Liabilities,માલ દેવું,
Stock Movement,માલ સંચાલન, Stock Movement,માલ સંચાલન,
Stock Movement Item,માલ સંચાલન વસ્તુ, Stock Movement Item,માલ સંચાલન વસ્તુ,
Stock Movement No.,માલ સંચાલન ક્રમ, Stock Movement No.,માલ સંચાલન ક્રમ,
Stock Movement Number Series,માલ સંચાલન ક્રમાંકન, Stock Movement Number Series,માલ સંચાલન ક્રમાંકન,
Stock Movements,માલ સંચાલન, Stock Movements,માલ સંચાલન,
Stock Not Transferred,માલ સ્થળાંતર થયો નથી, Stock Not Transferred,માલ સ્થળાંતર થયો નથી,
@ -645,10 +651,10 @@ Stock Received But Not Billed,પ્રાપ્ત માલ બિનઃ બ
Stock Received But Not Billed Acc.,પ્રાપ્ત માલ બિનઃ બિલ ખાતું, Stock Received But Not Billed Acc.,પ્રાપ્ત માલ બિનઃ બિલ ખાતું,
Stock Transfer Item,માલ સ્થળાંતર વસ્તુ, Stock Transfer Item,માલ સ્થળાંતર વસ્તુ,
Stock has been transferred,માલ સ્થળાંતર થયો, Stock has been transferred,માલ સ્થળાંતર થયો,
Stock qty. ${0} out of ${1} left to transfer,માલની માત્રા. ${1} માંથી ${0} સ્થળાંતર માટે બાકી, Stock qty. ${0} out of ${1} left to transfer,માલની માત્રા. ${1} માંથી ${0} સ્થળાંતર માટે બાકી,
StockTransfer,, StockTransfer,,
Stores,દુકાન/સંગ્રહ, Stores,દુકાન/સંગ્રહ,
Su,, Su,રવિ,
Submit,રજૂ કરો, Submit,રજૂ કરો,
Submit ${0},${0} રજૂ કરો, Submit ${0},${0} રજૂ કરો,
Submit Journal Entry?,આમનોંધ ખતવણી રજૂ કરો?, Submit Journal Entry?,આમનોંધ ખતવણી રજૂ કરો?,
@ -657,7 +663,7 @@ Submit Sales Invoice?,વેચાણ ભરતિયું રજૂ કરો?
Submit on Import,આયાત થતાં રજૂ કરો, Submit on Import,આયાત થતાં રજૂ કરો,
Submitted,રજૂ, Submitted,રજૂ,
Submitting,રજૂ થાય છે, Submitting,રજૂ થાય છે,
Subtotal,પેટા-સરવાળો , Subtotal,પેટા-સરવાળો,
Successfully created the following ${0} entries:,સફળતાપૂર્વક નીચેની ${0} લેવડદેવડ બનાવી:, Successfully created the following ${0} entries:,સફળતાપૂર્વક નીચેની ${0} લેવડદેવડ બનાવી:,
Supplier,વિક્રેતા, Supplier,વિક્રેતા,
Supplier Created,વિક્રેતા નિર્માણ, Supplier Created,વિક્રેતા નિર્માણ,
@ -667,8 +673,8 @@ System,પ્રણાલિ,
System Settings,પ્રણાલિ પરિસ્થિતિ, System Settings,પ્રણાલિ પરિસ્થિતિ,
System Setup,પ્રણાલિ સ્થાપન, System Setup,પ્રણાલિ સ્થાપન,
Tax,કરવેરો, Tax,કરવેરો,
Tax Account,કરવેરા હિસાબ, Tax Account,કરવેરા ખાતું,
Tax Assets,કરવેરા રોકાણ , Tax Assets,કરવેરા રોકાણ,
Tax Detail,કર વિગત, Tax Detail,કર વિગત,
Tax ID,કર ID, Tax ID,કર ID,
Tax Invoice,, Tax Invoice,,
@ -682,7 +688,7 @@ Template,નમૂનો,
Temporary,કામચલાઉ, Temporary,કામચલાઉ,
Temporary Accounts,હંગામી ખાતાઓ, Temporary Accounts,હંગામી ખાતાઓ,
Temporary Opening,હંગામી આરંભ, Temporary Opening,હંગામી આરંભ,
Th,, Th,ગુરુ,
The following ${0} entries were created: ${1},નીચેની ${0} લેવડદેવડ બનાવવામાં આવી હતી: ${1}, The following ${0} entries were created: ${1},નીચેની ${0} લેવડદેવડ બનાવવામાં આવી હતી: ${1},
This Month,આ મહિને, This Month,આ મહિને,
This Quarter,આ ત્રિમાસીક, This Quarter,આ ત્રિમાસીક,
@ -692,10 +698,10 @@ This action is permanent and will cancel the following payment: ${0},આ ક્
This action is permanent and will cancel the following payments: ${0},આ ક્રિયા કાયમી છે અને નીચેની ચુકવણીઓ ફોક થશે: ${0}, This action is permanent and will cancel the following payments: ${0},આ ક્રિયા કાયમી છે અને નીચેની ચુકવણીઓ ફોક થશે: ${0},
This action is permanent and will delete associated ledger entries.,આ ક્રિયા કાયમી છે અને સંકળાયેલ ખાતાવહી લેવડદેવડ છેકાશે., This action is permanent and will delete associated ledger entries.,આ ક્રિયા કાયમી છે અને સંકળાયેલ ખાતાવહી લેવડદેવડ છેકાશે.,
This action is permanent.,આ ક્રિયા કાયમી છે., This action is permanent.,આ ક્રિયા કાયમી છે.,
Times New Roman,Times New Roman, Times New Roman,,
To,પ્રત્યે, To,પ્રત્યે,
To Account,ખાતા પ્રત્યે, To Account,ખાતા પ્રત્યે,
To Account and From Account can't be the same: ${0},થકી ખાતું અને પ્રત્યે ખાતું સરખા હોય શકે નહીં : ${0}, To Account and From Account can't be the same: ${0},થકી ખાતું અને પ્રત્યે ખાતું સરખા હોય શકે નહીં : ${0},
To Date,આ તારીખ સુધી, To Date,આ તારીખ સુધી,
To Loc.,સ્થાન સુધી, To Loc.,સ્થાન સુધી,
To Year,વર્ષ સુધી, To Year,વર્ષ સુધી,
@ -719,11 +725,11 @@ Transfer Type,વ્યવહારનો પ્રકાર,
Transfer will cause future entries to have negative stock.,સ્થળાંતર થી ભવિષ્ય ના લેવડદેવડો માં જથ્થો ઋણ માં જશે, Transfer will cause future entries to have negative stock.,સ્થળાંતર થી ભવિષ્ય ના લેવડદેવડો માં જથ્થો ઋણ માં જશે,
Travel Expenses,મુસાફરી ખર્ચ, Travel Expenses,મુસાફરી ખર્ચ,
Trial Balance,કાચું સરવૈયું, Trial Balance,કાચું સરવૈયું,
Tu,, Tu,મંગળ,
Type,પ્રકાર, Type,પ્રકાર,
Type to search...,શોધવા માટે વર્ણન કરો ..., Type to search...,શોધવા માટે વર્ણન કરો ...,
UOM,UOM, UOM,માપણી,
Unit,એકમ, Unit,,
Unit Type,એકમ પ્રકાર, Unit Type,એકમ પ્રકાર,
Unpaid,અણચુકવેલ, Unpaid,અણચુકવેલ,
Unsecured Loans,અસુરક્ષિત લોન, Unsecured Loans,અસુરક્ષિત લોન,
@ -739,12 +745,12 @@ Version,Version,
View,નિરીક્ષણ, View,નિરીક્ષણ,
View Purchases,ખરીદી જુઓ, View Purchases,ખરીદી જુઓ,
View Sales,વેચાણ જુઓ, View Sales,વેચાણ જુઓ,
We,અમે, We,બુધ,
Welcome to Frappe Books,Frappe Books પર આપનું સ્વાગત છે, Welcome to Frappe Books,Frappe Books પર આપનું સ્વાગત છે,
Write Off,ખારીજ, Write Off,ખારીજ,
Write Off Account,ખારીજ ખાતું, Write Off Account,ખારીજ ખાતું,
Write Off Account ${0} does not exist. Please set Write Off Account in General Settings,ખારીજ ખાતું ${0} અસ્તિત્વ માં નથી,ખારીજ ખાતું સામાન્ય વિગતો માં ઉમેરો, Write Off Account ${0} does not exist. Please set Write Off Account in General Settings,ખારીજ ખાતું ${0} અસ્તિત્વ માં નથી. ખારીજ ખાતું સામાન્ય વિગતો માં ઉમેરો,
Write Off Account not set. Please set Write Off Account in General Settings,ખારીજ ખાતું અસ્તિત્વ માં નથી,ખારીજ ખાતું સામાન્ય વિગતો માં ઉમેરો, Write Off Account not set. Please set Write Off Account in General Settings,ખારીજ ખાતું અસ્તિત્વ માં નથી. ખારીજ ખાતું સામાન્ય વિગતો માં ઉમેરો,
Write Off Entry,ખારીજ ખાતું ખતવણી, Write Off Entry,ખારીજ ખાતું ખતવણી,
Yearly,વાર્ષિક, Yearly,વાર્ષિક,
Years,વર્ષ, Years,વર્ષ,
@ -752,4 +758,4 @@ Yellow,Yellow,
Yes,હા, Yes,હા,
Your Name,તમારું નામ, Your Name,તમારું નામ,
john@doe.com,john@doe.com, john@doe.com,john@doe.com,
verify the imported data and click on,આયાત કરેલા ડેટાને ચકાસો અને ક્લિક કરો, verify the imported data and click on,આયાત કરેલા ડેટાને ચકાસો અને ક્લિક કરો,
Can't render this file because it has a wrong number of fields in line 269.

View File

@ -6,7 +6,7 @@ export function getValueMapFromList<T, K extends keyof T, V extends keyof T>(
key: K, key: K,
valueKey: V, valueKey: V,
filterUndefined: boolean = true filterUndefined: boolean = true
): Record<string, unknown> { ): Record<string, T[V]> {
if (filterUndefined) { if (filterUndefined) {
list = list.filter( list = list.filter(
(f) => (f) =>
@ -20,7 +20,7 @@ export function getValueMapFromList<T, K extends keyof T, V extends keyof T>(
const value = f[valueKey]; const value = f[valueKey];
acc[keyValue] = value; acc[keyValue] = value;
return acc; return acc;
}, {} as Record<string, unknown>); }, {} as Record<string, T[V]>);
} }
export function getRandomString(): string { export function getRandomString(): string {

View File

@ -43,4 +43,8 @@ export interface SelectFileReturn {
success: boolean; success: boolean;
data: Buffer; data: Buffer;
canceled: boolean; canceled: boolean;
} }
export type PropertyEnum<T extends Record<string, any>> = {
[key in keyof Required<T>]: key;
};

643
yarn.lock

File diff suppressed because it is too large Load Diff