2
0
mirror of https://github.com/frappe/books.git synced 2024-11-09 15:20:56 +00:00

Merge pull request #663 from frappe/use-vite

build: use vite
This commit is contained in:
Alan 2023-06-26 01:25:40 -07:00 committed by GitHub
commit 5f2cef7534
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
251 changed files with 4583 additions and 11035 deletions

View File

@ -1,23 +1,62 @@
module.exports = {
root: true,
env: {
node: true,
browser: true,
es2018: true,
},
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'no-console': 'warn',
'no-debugger': 'warn',
'arrow-body-style': 'off',
'prefer-arrow-callback': 'off',
'prettier/prettier': 'warn',
'prefer-arrow-callback': 'warn',
'vue/no-mutating-props': 'off',
'vue/multi-word-component-names': 'off',
'vue/no-useless-template-attributes': 'off',
'vue/one-component-per-file': 'off',
'vue/no-reserved-component-names': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/no-var-requires': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-floating-promises': 'warn',
'@typescript-eslint/no-misused-promises': 'warn',
},
parser: 'vue-eslint-parser',
parserOptions: {
parser: '@typescript-eslint/parser',
project: true,
tsconfigRootDir: __dirname,
sourceType: 'module',
extraFileExtensions: ['.vue'],
},
extends: ['plugin:vue/vue3-essential', '@vue/typescript'],
plugins: ['@typescript-eslint', 'prettier'],
extends: [
'plugin:vue/vue3-recommended',
'plugin:@typescript-eslint/recommended',
'plugin:@typescript-eslint/recommended-requiring-type-checking',
'plugin:prettier/recommended',
],
overrides: [
{
files: ['*.vue'],
rules: {
'@typescript-eslint/no-misused-promises': 'off',
'@typescript-eslint/no-unsafe-assignment': 'off',
'@typescript-eslint/no-unsafe-member-access': 'off',
'@typescript-eslint/no-unsafe-call': 'off',
},
},
],
ignorePatterns: [
'*.mjs',
'.eslintrc.js',
'tailwind.config.js',
'node_modules',
'dist_electron',
'*.spec.ts',
'vite.config.ts',
'postcss.config.js',
'src/components/**/*.vue', // Incrementally fix these
],
};

View File

@ -13,7 +13,7 @@ jobs:
- name: Setup node
uses: actions/setup-node@v2
with:
node-version: '16.13.1'
node-version: '16.14.0'
- name: Set yarn version
run: yarn set version 1.22.18
@ -28,4 +28,4 @@ jobs:
env:
CSC_IDENTITY_AUTO_DISCOVERY: false
APPLE_NOTARIZE: 0
run: yarn electron:build -mw --publish never
run: yarn build -mw --publish never

32
.github/workflows/lint.yml vendored Normal file
View File

@ -0,0 +1,32 @@
name: Lint
on:
push:
branches: [$default-branch]
paths-ignore:
- '**.md'
pull_request:
paths-ignore:
- '**.md'
workflow_dispatch:
jobs:
setup_and_lint:
runs-on: macos-latest
steps:
- name: Setup node
uses: actions/setup-node@v2
with:
node-version: '16.14.0'
- name: Set yarn version
run: yarn set version 1.22.18
- name: Checkout Books
uses: actions/checkout@v2
- name: Install Dependencies
run: yarn
- name: Lint
run: yarn lint

View File

@ -9,7 +9,7 @@ jobs:
- name: Setup node
uses: actions/setup-node@v2
with:
node-version: '16.13.1'
node-version: '16.14.0'
- name: Checkout Books
uses: actions/checkout@v2
@ -42,7 +42,7 @@ jobs:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
run: |
yarn set version 1.22.18
yarn electron:build --mac --publish always
yarn build --mac --publish always
- name: Tar files
run: tar -cvf dist-macOS.tar dist_electron
@ -59,7 +59,7 @@ jobs:
- name: Setup node
uses: actions/setup-node@v2
with:
node-version: '16.13.1'
node-version: '16.14.0'
- name: Checkout Books
uses: actions/checkout@v2
@ -86,7 +86,7 @@ jobs:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
run: |
yarn set version 1.22.18
yarn electron:build --linux --publish always
yarn build --linux --publish always
- name: Tar files
run: tar -cvf dist-linux.tar dist_electron
@ -107,7 +107,7 @@ jobs:
- name: Setup node
uses: actions/setup-node@v2
with:
node-version: '16.13.1'
node-version: '16.14.0'
- name: Checkout Books
uses: actions/checkout@v2
@ -132,7 +132,7 @@ jobs:
WIN_CSC_LINK: ${{ secrets.WIN_CSC_LINK }}
WIN_CSC_KEY_PASSWORD: ${{ secrets.WIN_CSC_KEY_PASSWORD }}
GH_TOKEN: ${{ secrets.GH_TOKEN }}
run: yarn electron:build --win --publish always
run: yarn build --win --publish always
- name: Tar files
run: tar -cvf dist-windows.tar dist_electron

View File

@ -17,7 +17,7 @@ jobs:
- name: Setup node
uses: actions/setup-node@v2
with:
node-version: '16.13.1'
node-version: '16.14.0'
- name: Set yarn version
run: yarn set version 1.22.18

View File

@ -60,7 +60,7 @@ a local SQLite file as the database.
### Pre-requisites
To get the dev environment up and running you need to first set up Node.js version
16.13.1 and npm. For this, we suggest using
16.14.0 and npm. For this, we suggest using
[nvm](https://github.com/nvm-sh/nvm#installing-and-updating).
Next, you will need to install [yarn](https://classic.yarnpkg.com/lang/en/docs/install/#mac-stable).
@ -87,25 +87,41 @@ To run Frappe Books in development mode (with hot reload, etc):
```bash
# start the electron app
yarn electron:serve
yarn dev
```
**Note: First Boot**
When you run `yarn dev` electron will run immediately but the UI will take a
couple of seconds to render this because of how dev mode works. Each file is
individually served by the dev server. And there are many files that have to be
sent.
**Note: Debug Electron Main Process**
When in dev mode electron runs with the `--inspect` flag which allows an
external debugger to connect to port 5858. You can use chrome for this by
visiting `chrome://inspect` while Frappe Books is running in dev mode.
See more [here](https://www.electronjs.org/docs/latest/tutorial/debugging-main-process#external-debuggers).
#### Build
To build Frappe Books and create an installer:
```bash
# start the electron app
yarn electron:build
yarn build
```
**Note**
**Note: Build Target**
By default the above command will build for your computer's operating system and
architecture. To build for other environments (example: for linux from a windows
computer) check the _Building_ section at
[electron.build/cli](https://www.electron.build/cli).
So to build for linux you could use the `--linux` flag like so: `yarn electron:build --linux`.
So to build for linux you could use the `--linux` flag like so: `yarn build --linux`.
## Contributions and Community

View File

@ -1,3 +0,0 @@
module.exports = {
presets: ['@vue/cli-plugin-babel/preset']
};

View File

@ -1,3 +1,10 @@
import {
Cashflow,
IncomeExpense,
TopExpenses,
TotalCreditAndDebit,
TotalOutstanding,
} from 'utils/db/types';
import { ModelNameEnum } from '../../models/types';
import DatabaseCore from './core';
import { BespokeFunction } from './types';
@ -43,7 +50,7 @@ export class BespokeQueries {
.groupBy('account')
.orderBy('total', 'desc')
.limit(5);
return topExpenses;
return topExpenses as TopExpenses;
}
static async getTotalOutstanding(
@ -52,13 +59,13 @@ export class BespokeQueries {
fromDate: string,
toDate: string
) {
return await db.knex!(schemaName)
return (await db.knex!(schemaName)
.sum({ total: 'baseGrandTotal' })
.sum({ outstanding: 'outstandingAmount' })
.where('submitted', true)
.where('cancelled', false)
.whereBetween('date', [fromDate, toDate])
.first();
.first()) as TotalOutstanding;
}
static async getCashflow(db: DatabaseCore, fromDate: string, toDate: string) {
@ -67,7 +74,7 @@ export class BespokeQueries {
.where('accountType', 'in', ['Cash', 'Bank'])
.andWhere('isGroup', false);
const dateAsMonthYear = db.knex!.raw(`strftime('%Y-%m', ??)`, 'date');
return await db.knex!('AccountingLedgerEntry')
return (await db.knex!('AccountingLedgerEntry')
.where('reverted', false)
.sum({
inflow: 'debit',
@ -78,7 +85,7 @@ export class BespokeQueries {
})
.where('account', 'in', cashAndBankAccounts)
.whereBetween('date', [fromDate, toDate])
.groupBy(dateAsMonthYear);
.groupBy(dateAsMonthYear)) as Cashflow;
}
static async getIncomeAndExpenses(
@ -86,7 +93,7 @@ export class BespokeQueries {
fromDate: string,
toDate: string
) {
const income = await db.knex!.raw(
const income = (await db.knex!.raw(
`
select sum(cast(credit as real) - cast(debit as real)) as balance, strftime('%Y-%m', date) as yearmonth
from AccountingLedgerEntry
@ -100,9 +107,9 @@ export class BespokeQueries {
)
group by yearmonth`,
[fromDate, toDate]
);
)) as IncomeExpense['income'];
const expense = await db.knex!.raw(
const expense = (await db.knex!.raw(
`
select sum(cast(debit as real) - cast(credit as real)) as balance, strftime('%Y-%m', date) as yearmonth
from AccountingLedgerEntry
@ -116,20 +123,20 @@ export class BespokeQueries {
)
group by yearmonth`,
[fromDate, toDate]
);
)) as IncomeExpense['expense'];
return { income, expense };
}
static async getTotalCreditAndDebit(db: DatabaseCore) {
return await db.knex!.raw(`
return (await db.knex!.raw(`
select
account,
sum(cast(credit as real)) as totalCredit,
sum(cast(debit as real)) as totalDebit
from AccountingLedgerEntry
group by account
`);
`)) as unknown as TotalCreditAndDebit;
}
static async getStockQuantity(
@ -141,6 +148,7 @@ export class BespokeQueries {
batch?: string,
serialNumbers?: string[]
): Promise<number | null> {
/* eslint-disable @typescript-eslint/no-floating-promises */
const query = db.knex!(ModelNameEnum.StockLedgerEntry)
.sum('quantity')
.where('item', item);

View File

@ -1,9 +1,4 @@
import {
CannotCommitError,
getDbError,
NotFoundError,
ValueError,
} from 'fyo/utils/errors';
import { getDbError, NotFoundError, ValueError } from 'fyo/utils/errors';
import { knex, Knex } from 'knex';
import {
Field,
@ -73,16 +68,16 @@ export default class DatabaseCore extends DatabaseBase {
let query: { value: string }[] = [];
try {
query = await db.knex!('SingleValue').where({
query = (await db.knex!('SingleValue').where({
fieldname: 'countryCode',
parent: 'SystemSettings',
});
})) as { value: string }[];
} catch {
// Database not inialized and no countryCode passed
}
if (query.length > 0) {
countryCode = query[0].value as string;
countryCode = query[0].value;
}
await db.close();
@ -95,9 +90,6 @@ export default class DatabaseCore extends DatabaseBase {
async connect() {
this.knex = knex(this.connectionParams);
this.knex.on('query-error', (error) => {
error.type = getDbError(error);
});
await this.knex.raw('PRAGMA foreign_keys=ON');
}
@ -105,22 +97,6 @@ export default class DatabaseCore extends DatabaseBase {
await this.knex!.destroy();
}
async commit() {
/**
* this auto commits, commit is not required
* will later wrap the outermost functions in
* transactions.
*/
try {
// await this.knex!.raw('commit');
} catch (err) {
const type = getDbError(err as Error);
if (type !== CannotCommitError) {
throw err;
}
}
}
async migrate() {
for (const schemaName in this.schemaMap) {
const schema = this.schemaMap[schemaName] as Schema;
@ -135,7 +111,6 @@ export default class DatabaseCore extends DatabaseBase {
}
}
await this.commit();
await this.#initializeSingles();
}
@ -149,6 +124,7 @@ export default class DatabaseCore extends DatabaseBase {
try {
const qb = this.knex!(schemaName);
if (name !== undefined) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
qb.where({ name });
}
row = await qb.limit(1);
@ -178,7 +154,7 @@ export default class DatabaseCore extends DatabaseBase {
async get(
schemaName: string,
name: string = '',
name = '',
fields?: string | string[]
): Promise<FieldValueMap> {
const schema = this.schemaMap[schemaName] as Schema;
@ -316,7 +292,6 @@ export default class DatabaseCore extends DatabaseBase {
await this.knex!(schemaName)
.update({ name: newName })
.where('name', oldName);
await this.commit();
}
async update(schemaName: string, fieldValueMap: FieldValueMap) {
@ -422,6 +397,7 @@ export default class DatabaseCore extends DatabaseBase {
filters: QueryFilter,
options: GetQueryBuilderOptions
): Knex.QueryBuilder {
/* eslint-disable @typescript-eslint/no-floating-promises */
const builder = this.knex!.select(fields).from(schemaName);
this.#applyFiltersToBuilder(builder, filters);
@ -464,12 +440,12 @@ export default class DatabaseCore extends DatabaseBase {
// => `date >= 2017-09-09 and date <= 2017-11-01`
const filtersArray = this.#getFiltersArray(filters);
for (const i in filtersArray) {
for (let i = 0; i < filtersArray.length; i++) {
const filter = filtersArray[i];
const field = filter[0] as string;
const operator = filter[1];
const comparisonValue = filter[2];
const type = i === '0' ? 'where' : 'andWhere';
const type = i === 0 ? 'where' : 'andWhere';
if (operator === '=') {
builder[type](field, comparisonValue);
@ -505,7 +481,8 @@ export default class DatabaseCore extends DatabaseBase {
if (
operator === 'like' &&
!(comparisonValue as (string | number)[]).includes('%')
typeof comparisonValue === 'string' &&
!comparisonValue.includes('%')
) {
comparisonValue = `%${comparisonValue}%`;
}
@ -595,11 +572,8 @@ export default class DatabaseCore extends DatabaseBase {
}
// link
if (
field.fieldtype === FieldTypeEnum.Link &&
(field as TargetField).target
) {
const targetSchemaName = (field as TargetField).target as string;
if (field.fieldtype === FieldTypeEnum.Link && field.target) {
const targetSchemaName = field.target;
const schema = this.schemaMap[targetSchemaName] as Schema;
table
.foreign(field.fieldname)
@ -630,7 +604,7 @@ export default class DatabaseCore extends DatabaseBase {
}
if (newForeignKeys.length) {
await this.#addForeignKeys(schemaName, newForeignKeys);
await this.#addForeignKeys(schemaName);
}
}
@ -652,15 +626,15 @@ export default class DatabaseCore extends DatabaseBase {
async #getNonExtantSingleValues(singleSchemaName: string) {
const existingFields = (
await this.knex!('SingleValue')
(await this.knex!('SingleValue')
.where({ parent: singleSchemaName })
.select('fieldname')
.select('fieldname')) as { fieldname: string }[]
).map(({ fieldname }) => fieldname);
return this.schemaMap[singleSchemaName]!.fields.map(
({ fieldname, default: value }) => ({
fieldname,
value: value as RawValue | undefined,
value: value,
})
).filter(
({ fieldname, value }) =>
@ -710,7 +684,7 @@ export default class DatabaseCore extends DatabaseBase {
child.idx ??= idx;
}
async #addForeignKeys(schemaName: string, newForeignKeys: Field[]) {
async #addForeignKeys(schemaName: string) {
const tableRows = await this.knex!.select().from(schemaName);
await this.prestigeTheTable(schemaName, tableRows);
}
@ -731,10 +705,10 @@ export default class DatabaseCore extends DatabaseBase {
}
async #getOne(schemaName: string, name: string, fields: string[]) {
const fieldValueMap: FieldValueMap = await this.knex!.select(fields)
const fieldValueMap = (await this.knex!.select(fields)
.from(schemaName)
.where('name', name)
.first();
.first()) as FieldValueMap;
return fieldValueMap;
}
@ -794,9 +768,9 @@ export default class DatabaseCore extends DatabaseBase {
fieldname,
};
const names: { name: string }[] = await this.knex!('SingleValue')
const names = (await this.knex!('SingleValue')
.select('name')
.where(updateKey);
.where(updateKey)) as { name: string }[];
if (!names?.length) {
this.#insertSingleValue(singleSchemaName, fieldname, value);
@ -899,7 +873,7 @@ export default class DatabaseCore extends DatabaseBase {
continue;
}
for (const child of tableFieldValue!) {
for (const child of tableFieldValue) {
this.#prepareChild(schemaName, parentName, child, field, added.length);
if (

View File

@ -204,7 +204,7 @@ export async function assertDoesNotThrow(
throw new assert.AssertionError({
message: `Got unwanted exception${
message ? `: ${message}` : ''
}\nError: ${(err as Error).message}\n${(err as Error).stack}`,
}\nError: ${(err as Error).message}\n${(err as Error).stack ?? ''}`,
});
}
}

View File

@ -55,7 +55,13 @@ export function emitMainProcessError(
error: unknown,
more?: Record<string, unknown>
) {
(process.emit as Function)(CUSTOM_EVENTS.MAIN_PROCESS_ERROR, error, more);
(
process.emit as (
event: string,
error: unknown,
more?: Record<string, unknown>
) => void
)(CUSTOM_EVENTS.MAIN_PROCESS_ERROR, error, more);
}
export async function checkFileAccess(filePath: string, mode?: number) {

View File

@ -1,5 +1,6 @@
import { DatabaseManager } from '../database/manager';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async function execute(dm: DatabaseManager) {
/**
* Execute function will receive the DatabaseManager which is to be used

View File

@ -30,9 +30,9 @@ async function execute(dm: DatabaseManager) {
const sourceKnex = dm.db!.knex!;
const version = (
await sourceKnex('SingleValue')
(await sourceKnex('SingleValue')
.select('value')
.where({ fieldname: 'version' })
.where({ fieldname: 'version' })) as { value: string }[]
)?.[0]?.value;
/**
@ -58,7 +58,7 @@ async function execute(dm: DatabaseManager) {
await copyData(sourceKnex, destDm);
} catch (err) {
const destPath = destDm.db!.dbPath;
destDm.db!.close();
await destDm.db!.close();
await fs.unlink(destPath);
throw err;
}
@ -94,7 +94,7 @@ async function replaceDatabaseCore(
async function copyData(sourceKnex: Knex, destDm: DatabaseManager) {
const destKnex = destDm.db!.knex!;
const schemaMap = destDm.getSchemaMap();
await destKnex!.raw('PRAGMA foreign_keys=OFF');
await destKnex.raw('PRAGMA foreign_keys=OFF');
await copySingleValues(sourceKnex, destKnex, schemaMap);
await copyParty(sourceKnex, destKnex, schemaMap[ModelNameEnum.Party]!);
await copyItem(sourceKnex, destKnex, schemaMap[ModelNameEnum.Item]!);
@ -111,7 +111,7 @@ async function copyData(sourceKnex: Knex, destDm: DatabaseManager) {
destKnex,
schemaMap[ModelNameEnum.NumberSeries]!
);
await destKnex!.raw('PRAGMA foreign_keys=ON');
await destKnex.raw('PRAGMA foreign_keys=ON');
}
async function copyNumberSeries(
@ -137,14 +137,14 @@ async function copyNumberSeries(
continue;
}
const indices = await sourceKnex.raw(
const indices = (await sourceKnex.raw(
`
select cast(substr(name, ??) as int) as idx
from ??
order by idx desc
limit 1`,
[name.length + 1, referenceType]
);
)) as { idx: number }[];
value.start = 1001;
value.current = indices[0]?.idx ?? value.current ?? value.start;
@ -358,7 +358,9 @@ async function getCountryCode(knex: Knex) {
* Need to account for schema changes, in 0.4.3-beta.0
*/
const country = (
await knex('SingleValue').select('value').where({ fieldname: 'country' })
(await knex('SingleValue')
.select('value')
.where({ fieldname: 'country' })) as { value: string }[]
)?.[0]?.value;
if (!country) {

166
build/scripts/build.mjs Normal file
View File

@ -0,0 +1,166 @@
import vue from '@vitejs/plugin-vue';
import builder from 'electron-builder';
import esbuild from 'esbuild';
import fs from 'fs-extra';
import path from 'path';
import { fileURLToPath } from 'url';
import * as vite from 'vite';
import { getMainProcessCommonConfig } from './helpers.mjs';
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
const dirname = path.dirname(fileURLToPath(import.meta.url));
const root = path.join(dirname, '..', '..');
const buildDirPath = path.join(root, 'dist_electron', 'build');
const packageDirPath = path.join(root, 'dist_electron', 'bundled');
const mainFileName = 'main.js';
const commonConfig = getMainProcessCommonConfig(root);
updatePaths();
await buildMainProcessSource();
await buildRendererProcessSource();
copyPackageJson();
await packageApp();
function updatePaths() {
fs.removeSync(buildDirPath);
fs.ensureDirSync(buildDirPath);
fs.removeSync(packageDirPath);
fs.ensureDirSync(packageDirPath);
fs.ensureDirSync(path.join(buildDirPath, 'node_modules'));
}
async function buildMainProcessSource() {
const result = await esbuild.build({
...commonConfig,
outfile: path.join(buildDirPath, mainFileName),
});
if (result.errors.length) {
console.error('app build failed due to main process source build');
result.errors.forEach((err) => console.error(err));
process.exit(1);
}
}
async function buildRendererProcessSource() {
const base = 'app://';
const outDir = path.join(buildDirPath, 'src');
await vite.build({
base: `/${base}`,
root: path.join(root, 'src'),
build: { outDir },
plugins: [vue()],
resolve: {
alias: {
vue: 'vue/dist/vue.esm-bundler.js',
fyo: path.join(root, 'fyo'),
src: path.join(root, 'src'),
schemas: path.join(root, 'schemas'),
backend: path.join(root, 'backend'),
models: path.join(root, 'models'),
utils: path.join(root, 'utils'),
regional: path.join(root, 'regional'),
reports: path.join(root, 'reports'),
dummy: path.join(root, 'dummy'),
fixtures: path.join(root, 'fixtures'),
},
},
});
removeBaseLeadingSlash(outDir, base);
}
/**
* Copies the package.json file to the build folder with the
* following changes:
* - Irrelevant fields are removed.
* - Non-external deps (those that are bundled) and devDeps are removed.
* - Main file is updated to the bundled main process JS file.
*/
function copyPackageJson() {
const packageJsonText = fs.readFileSync(path.join(root, 'package.json'), {
encoding: 'utf-8',
});
const packageJson = JSON.parse(packageJsonText);
const keys = [
'name',
'version',
'description',
'author',
'homepage',
'repository',
'license',
];
const modifiedPackageJson = {};
for (const key of keys) {
modifiedPackageJson[key] = packageJson[key];
}
modifiedPackageJson.main = mainFileName;
modifiedPackageJson.dependencies = {};
for (const dep of commonConfig.external) {
modifiedPackageJson.dependencies[dep] = packageJson.dependencies[dep];
}
fs.writeFileSync(
path.join(buildDirPath, 'package.json'),
JSON.stringify(modifiedPackageJson, null, 2),
{
encoding: 'utf-8',
}
);
}
/**
* Packages the app using electron builder.
*
* Note: this also handles signing and notarization if the
* appropriate flags are set.
*
* Electron builder cli [commands](https://www.electron.build/cli)
* are passed on as builderArgs.
*/
async function packageApp() {
const { configureBuildCommand } = await await import(
'electron-builder/out/builder.js'
);
const rawArgs = hideBin(process.argv);
const builderArgs = yargs(rawArgs)
.command(['build', '*'], 'Build', configureBuildCommand)
.parse();
const buildOptions = {
config: {
directories: { output: packageDirPath, app: buildDirPath },
files: ['**'],
extends: null,
},
...builderArgs,
};
await builder.build(buildOptions);
}
/**
* Removes leading slash from all renderer files
* electron uses a custom registered protocol to load the
* files: "app://"
*
* @param {string} dir
* @param {string} base
*/
function removeBaseLeadingSlash(dir, base) {
for (const file of fs.readdirSync(dir)) {
const filePath = path.join(dir, file);
if (fs.lstatSync(filePath).isDirectory()) {
removeBaseLeadingSlash(filePath, base);
continue;
}
const contents = fs.readFileSync(filePath).toString('utf-8');
fs.writeFileSync(filePath, contents.replaceAll('/' + base, base));
}
}

142
build/scripts/dev.mjs Normal file
View File

@ -0,0 +1,142 @@
import chokidar from 'chokidar';
import esbuild from 'esbuild';
import { $ } from 'execa';
import path from 'path';
import { fileURLToPath } from 'url';
import { getMainProcessCommonConfig } from './helpers.mjs';
process.env['ELECTRON_DISABLE_SECURITY_WARNINGS'] = 'true';
process.env['NODE_ENV'] = 'development';
process.env['VITE_HOST'] = '0.0.0.0';
process.env['VITE_PORT'] = 6969;
/**
* This script does several things:
* 1. Runs the vite server in dev mode `yarn vite` (unless --no-renderer is passed)
* 2. Runs a file watcher for the main processes
* 3. Builds the main process on file changes
* 4. Runs electron which loads renderer using vite server url
*/
/**
* @type {null | Function} global function used to stop dev
*/
const dirname = path.dirname(fileURLToPath(import.meta.url));
const root = path.join(dirname, '..', '..');
const $$ = $({ stdio: 'inherit' });
let isReload = false;
/**
* @type {null | import('execa').ExecaChildProcess<string>}
*/
let electronProcess = null;
console.log(`running Frappe Books in dev mode\nroot: ${root}`);
/**
* @type {import('execa').ExecaChildProcess<string>}
*/
const viteProcess = $$`yarn vite`;
/**
* Create esbuild context that is used
* to [re]build the main process code
*/
const ctx = await esbuild.context({
...getMainProcessCommonConfig(root),
outfile: path.join(root, 'dist_electron', 'dev', 'main.js'),
});
/**
* Create a file watcher so that rebuild
* can be triggered everytime a main process
* file changes.
*/
const fswatcher = chokidar.watch([
path.join(root, 'main.ts'),
path.join(root, 'main'),
path.join(root, 'backend'),
path.join(root, 'schemas'),
]);
/**
* Callback function to cleanly shut file watching
* and rebuilding objects.
*
* Called on CTRL+C and kill
*/
const terminate = async () => {
await fswatcher.close();
await ctx.dispose();
if (electronProcess) {
electronProcess.kill();
}
if (viteProcess) {
viteProcess.kill();
}
process.exit();
};
process.on('SIGINT', terminate);
process.on('SIGTERM', terminate);
if (viteProcess) {
viteProcess.on('close', terminate);
}
/**
* Build once and run electron before setting file watcher
*/
await handleResult(await ctx.rebuild());
electronProcess = runElectron();
/**
* On main process source files change
* - rebuild main process
* - restart electron
*/
fswatcher.on('change', async (path) => {
console.log(`change detected:\n\t${path}`);
const result = await ctx.rebuild();
await handleResult(result);
console.log(`main process source rebuilt\nrestarting electron`);
if (electronProcess) {
isReload = true;
electronProcess.kill();
electronProcess = runElectron();
}
});
/**
* @param {esbuild.BuildResult} result
*/
async function handleResult(result) {
if (!result.errors.length) {
return;
}
console.log('error on build');
for (const error of result.errors) {
console.log(error);
}
await terminate();
}
function runElectron() {
const electronProcess = $$`npx electron --inspect=5858 ${path.join(
root,
'dist_electron',
'dev',
'main.js'
)}`;
electronProcess.on('close', async () => {
if (isReload) {
return;
}
await terminate();
});
return electronProcess;
}

51
build/scripts/helpers.mjs Normal file
View File

@ -0,0 +1,51 @@
import fs from 'fs';
import path from 'path';
/**
* Common ESBuild config used for building main process source
* code for both dev and production.
*
* @param {string} root
* @returns {import('esbuild').BuildOptions}
*/
export function getMainProcessCommonConfig(root) {
return {
entryPoints: [path.join(root, 'main.ts')],
bundle: true,
sourcemap: true,
sourcesContent: false,
platform: 'node',
target: 'node16',
external: ['knex', 'electron', 'better-sqlite3', 'electron-store'],
plugins: [excludeVendorFromSourceMap],
write: true,
};
}
/**
* ESBuild plugin used to prevent source maps from being generated for
* packages inside node_modules, only first-party code source maps
* are to be included.
*
* Note, this is used only for the main process source code.
*
* source: https://github.com/evanw/esbuild/issues/1685#issuecomment-944916409
* @type {import('esbuild').Plugin}
*/
export const excludeVendorFromSourceMap = {
name: 'excludeVendorFromSourceMap',
setup(build) {
build.onLoad({ filter: /node_modules/ }, (args) => {
if (args.path.endsWith('.json')) {
return;
}
return {
contents:
fs.readFileSync(args.path, 'utf8') +
'\n//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIiJdLCJtYXBwaW5ncyI6IkEifQ==',
loader: 'default',
};
});
},
};

115
colors.json Normal file
View File

@ -0,0 +1,115 @@
{
"black": "#1E293B",
"gray": {
"25": "#fcfcfd",
"50": "#f8f9fc",
"100": "#f2f4f8",
"200": "#ebeff5",
"300": "#e2e8f0",
"400": "#cad5e2",
"500": "#9aa8bc",
"600": "#8493a9",
"700": "#64748b",
"800": "#475569",
"900": "#334155"
},
"red": {
"100": "#fff5f5",
"200": "#fed7d7",
"300": "#feb2b2",
"400": "#fc8181",
"500": "#f56565",
"600": "#e53e3e",
"700": "#c53030",
"800": "#9b2c2c",
"900": "#742a2a"
},
"orange": {
"100": "#fffaf0",
"200": "#feebc8",
"300": "#fbd38d",
"400": "#f6ad55",
"500": "#ed8936",
"600": "#dd6b20",
"700": "#c05621",
"800": "#9c4221",
"900": "#7b341e"
},
"yellow": {
"100": "#fffff0",
"200": "#fefcbf",
"300": "#faf089",
"400": "#f6e05e",
"500": "#ecc94b",
"600": "#d69e2e",
"700": "#b7791f",
"800": "#975a16",
"900": "#744210"
},
"green": {
"100": "#f0fff4",
"200": "#c6f6d5",
"300": "#9ae6b4",
"400": "#68d391",
"500": "#48bb78",
"600": "#38a169",
"700": "#2f855a",
"800": "#276749",
"900": "#22543d"
},
"teal": {
"100": "#e6fffa",
"200": "#b2f5ea",
"300": "#81e6d9",
"400": "#4fd1c5",
"500": "#38b2ac",
"600": "#319795",
"700": "#2c7a7b",
"800": "#285e61",
"900": "#234e52"
},
"blue": {
"100": "#e5f3ff",
"200": "#cce7ff",
"300": "#99d0ff",
"400": "#66b8ff",
"500": "#33a1ff",
"600": "#2490ef",
"700": "#006ecc",
"800": "#005299",
"900": "#003766"
},
"indigo": {
"100": "#ebf4ff",
"200": "#c3dafe",
"300": "#a3bffa",
"400": "#7f9cf5",
"500": "#667eea",
"600": "#5a67d8",
"700": "#4c51bf",
"800": "#434190",
"900": "#3c366b"
},
"purple": {
"100": "#faf5ff",
"200": "#e9d8fd",
"300": "#d6bcfa",
"400": "#b794f4",
"500": "#9f7aea",
"600": "#805ad5",
"700": "#6b46c1",
"800": "#553c9a",
"900": "#44337a"
},
"pink": {
"100": "#fdf7f8",
"200": "#fbeef1",
"300": "#f7dee5",
"400": "#eec3d2",
"500": "#df9eb8",
"600": "#cf82a7",
"700": "#ac688b",
"800": "#8f5b79",
"900": "#70485f"
}
}

View File

@ -25,8 +25,8 @@ type Notifier = (stage: string, percent: number) => void;
export async function setupDummyInstance(
dbPath: string,
fyo: Fyo,
years: number = 1,
baseCount: number = 1000,
years = 1,
baseCount = 1000,
notifier?: Notifier
) {
await fyo.purgeCache();
@ -251,7 +251,7 @@ async function getSalesInvoices(
* For each date create a Sales Invoice.
*/
for (const d in dates) {
for (let d = 0; d < dates.length; d++) {
const date = dates[d];
notifier?.(
@ -424,7 +424,7 @@ async function getSalesPurchaseInvoices(
for (const item of supplierGrouped[supplier]) {
await doc.append('items', {});
const quantity = purchaseQty[item];
doc.items!.at(-1)!.set({ item, quantity });
await doc.items!.at(-1)!.set({ item, quantity });
}
invoices.push(doc);
@ -527,7 +527,7 @@ async function syncAndSubmit(docs: Doc[], notifier?: Notifier) {
};
const total = docs.length;
for (const i in docs) {
for (let i = 0; i < docs.length; i++) {
const doc = docs[i];
notifier?.(
`Syncing ${nameMap[doc.schemaName]}, ${i} out of ${total}`,

View File

@ -58,59 +58,8 @@ export class AuthHandler {
return { ...this.#config };
}
init() {}
async login(email: string, password: string) {
if (email === 'Administrator') {
this.#session.user = 'Administrator';
return;
}
const response = await fetch(this.#getServerURL() + '/api/login', {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({ email, password }),
});
if (response.status === 200) {
const res = await response.json();
this.#session.user = email;
this.#session.token = res.token;
return res;
}
return response;
}
async signup(email: string, fullName: string, password: string) {
const response = await fetch(this.#getServerURL() + '/api/signup', {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({ email, fullName, password }),
});
if (response.status === 200) {
return await response.json();
}
return response;
}
async logout() {
// TODO: Implement this with auth flow
}
async purgeCache() {}
#getServerURL() {
return this.#config.serverURL || '';
init() {
return null;
}
async getCreds(): Promise<Creds> {

View File

@ -144,7 +144,7 @@ export class Converter {
return this.#toRawValueMap(parentSchemaName, value.getValidDict());
}
return this.#toRawValueMap(parentSchemaName, value as DocValueMap);
return this.#toRawValueMap(parentSchemaName, value);
});
} else {
rawValueMap[fieldname] = Converter.toRawValue(
@ -176,7 +176,7 @@ function toDocString(value: RawValue, field: Field) {
}
function toDocDate(value: RawValue, field: Field) {
if ((value as any) instanceof Date) {
if ((value as unknown) instanceof Date) {
return value;
}
@ -286,7 +286,7 @@ function toDocAttachment(value: RawValue, field: Field): null | Attachment {
}
try {
return JSON.parse(value) || null;
return (JSON.parse(value) as Attachment) || null;
} catch {
throwError(value, field, 'doc');
}
@ -322,7 +322,7 @@ function toRawInt(value: DocValue, field: Field): number {
}
if (typeof value === 'number') {
return Math.floor(value as number);
return Math.floor(value);
}
throwError(value, field, 'raw');
@ -363,7 +363,7 @@ function toRawDateTime(value: DocValue, field: Field): string | null {
}
if (value instanceof Date) {
return (value as Date).toISOString();
return value.toISOString();
}
if (value instanceof DateTime) {
@ -431,8 +431,8 @@ function toRawAttachment(value: DocValue, field: Field): null | string {
function throwError<T>(value: T, field: Field, type: 'raw' | 'doc'): never {
throw new ValueError(
`invalid ${type} conversion '${value}' of type ${typeof value} found, field: ${JSON.stringify(
field
)}`
`invalid ${type} conversion '${String(
value
)}' of type ${typeof value} found, field: ${JSON.stringify(field)}`
);
}

View File

@ -7,10 +7,15 @@ import { translateSchema } from 'fyo/utils/translation';
import { Field, RawValue, SchemaMap } from 'schemas/types';
import { getMapFromList } from 'utils';
import {
Cashflow,
DatabaseBase,
DatabaseDemuxBase,
GetAllOptions,
IncomeExpense,
QueryFilter,
TopExpenses,
TotalCreditAndDebit,
TotalOutstanding,
} from 'utils/db/types';
import { schemaTranslateables } from 'utils/translationHelpers';
import { LanguageMap } from 'utils/types';
@ -22,20 +27,10 @@ import {
RawValueMap,
} from './types';
// Return types of Bespoke Queries
type TopExpenses = { account: string; total: number }[];
type TotalOutstanding = { total: number; outstanding: number };
type Cashflow = { inflow: number; outflow: number; yearmonth: string }[];
type Balance = { balance: number; yearmonth: string }[];
type IncomeExpense = { income: Balance; expense: Balance };
type TotalCreditAndDebit = {
account: string;
totalCredit: number;
totalDebit: number;
};
type FieldMap = Record<string, Record<string, Field>>;
export class DatabaseHandler extends DatabaseBase {
/* eslint-disable @typescript-eslint/no-floating-promises */
#fyo: Fyo;
converter: Converter;
#demux: DatabaseDemuxBase;
@ -83,7 +78,7 @@ export class DatabaseHandler extends DatabaseBase {
}
async init() {
this.#schemaMap = (await this.#demux.getSchemaMap()) as SchemaMap;
this.#schemaMap = await this.#demux.getSchemaMap();
this.#setFieldMap();
this.observer = new Observable();
}
@ -92,7 +87,7 @@ export class DatabaseHandler extends DatabaseBase {
if (languageMap) {
translateSchema(this.#schemaMap, languageMap, schemaTranslateables);
} else {
this.#schemaMap = (await this.#demux.getSchemaMap()) as SchemaMap;
this.#schemaMap = await this.#demux.getSchemaMap();
this.#setFieldMap();
}
}
@ -142,6 +137,7 @@ export class DatabaseHandler extends DatabaseBase {
options: GetAllOptions = {}
): Promise<DocValueMap[]> {
const rawValueMap = await this.#getAll(schemaName, options);
this.observer.trigger(`getAll:${schemaName}`, options);
return this.converter.toDocValueMap(
schemaName,
@ -154,6 +150,7 @@ export class DatabaseHandler extends DatabaseBase {
options: GetAllOptions = {}
): Promise<RawValueMap[]> {
const all = await this.#getAll(schemaName, options);
this.observer.trigger(`getAllRaw:${schemaName}`, options);
return all;
}
@ -188,6 +185,7 @@ export class DatabaseHandler extends DatabaseBase {
): Promise<number> {
const rawValueMap = await this.#getAll(schemaName, options);
const count = rawValueMap.length;
this.observer.trigger(`count:${schemaName}`, options);
return count;
}
@ -199,18 +197,21 @@ export class DatabaseHandler extends DatabaseBase {
newName: string
): Promise<void> {
await this.#demux.call('rename', schemaName, oldName, newName);
this.observer.trigger(`rename:${schemaName}`, { oldName, newName });
}
async update(schemaName: string, docValueMap: DocValueMap): Promise<void> {
const rawValueMap = this.converter.toRawValueMap(schemaName, docValueMap);
await this.#demux.call('update', schemaName, rawValueMap);
this.observer.trigger(`update:${schemaName}`, docValueMap);
}
// Delete
async delete(schemaName: string, name: string): Promise<void> {
await this.#demux.call('delete', schemaName, name);
this.observer.trigger(`delete:${schemaName}`, name);
}
@ -220,6 +221,7 @@ export class DatabaseHandler extends DatabaseBase {
schemaName,
filters
)) as number;
this.observer.trigger(`deleteAll:${schemaName}`, filters);
return count;
}
@ -231,6 +233,7 @@ export class DatabaseHandler extends DatabaseBase {
schemaName,
name
)) as boolean;
this.observer.trigger(`exists:${schemaName}`, name);
return doesExist;
}
@ -302,7 +305,7 @@ export class DatabaseHandler extends DatabaseBase {
)) as IncomeExpense;
}
async getTotalCreditAndDebit(): Promise<unknown> {
async getTotalCreditAndDebit(): Promise<TotalCreditAndDebit[]> {
return (await this.#demux.callBespoke(
'getTotalCreditAndDebit'
)) as TotalCreditAndDebit[];

View File

@ -28,7 +28,7 @@ export class DocHandler {
this.observer = new Observable();
}
async purgeCache() {
purgeCache() {
this.init();
}
@ -82,10 +82,10 @@ export class DocHandler {
getNewDoc(
schemaName: string,
data: DocValueMap | RawValueMap = {},
cacheDoc: boolean = true,
cacheDoc = true,
schema?: Schema,
Model?: typeof Doc,
isRawValueMap: boolean = true
isRawValueMap = true
): Doc {
if (!this.models[schemaName] && Model) {
this.models[schemaName] = Model;
@ -153,7 +153,8 @@ export class DocHandler {
// propagate change to `docs`
doc.on('change', (params: unknown) => {
this.docs!.trigger('change', params);
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.docs.trigger('change', params);
});
doc.on('afterSync', () => {
@ -167,22 +168,24 @@ export class DocHandler {
}
#setCacheUpdationListeners(schemaName: string) {
this.fyo.db.observer.on(`delete:${schemaName}`, (name: string) => {
this.fyo.db.observer.on(`delete:${schemaName}`, (name) => {
if (typeof name !== 'string') {
return;
}
this.removeFromCache(schemaName, name);
});
this.fyo.db.observer.on(
`rename:${schemaName}`,
(names: { oldName: string; newName: string }) => {
const doc = this.#getFromCache(schemaName, names.oldName);
if (doc === undefined) {
return;
}
this.removeFromCache(schemaName, names.oldName);
this.#addToCache(doc);
this.fyo.db.observer.on(`rename:${schemaName}`, (names) => {
const { oldName } = names as { oldName: string };
const doc = this.#getFromCache(schemaName, oldName);
if (doc === undefined) {
return;
}
);
this.removeFromCache(schemaName, oldName);
this.#addToCache(doc);
});
}
removeFromCache(schemaName: string, name: string) {

View File

@ -1,8 +1,8 @@
import { Doc } from 'fyo/model/doc';
import { Money } from 'pesa';
import { RawValue } from 'schemas/types';
import { AuthDemuxBase } from 'utils/auth/types';
import { DatabaseDemuxBase } from 'utils/db/types';
import type { Doc } from 'fyo/model/doc';
import type { Money } from 'pesa';
import type { RawValue } from 'schemas/types';
import type { AuthDemuxBase } from 'utils/auth/types';
import type { DatabaseDemuxBase } from 'utils/db/types';
export type Attachment = { name: string; type: string; data: string };
export type DocValue =
@ -31,12 +31,12 @@ export type DatabaseDemuxConstructor = new (
export type AuthDemuxConstructor = new (isElectron?: boolean) => AuthDemuxBase;
export enum ConfigKeys {
Files = 'files',
LastSelectedFilePath = 'lastSelectedFilePath',
Language = 'language',
DeviceId = 'deviceId',
}
export type ConfigMap = {
files: ConfigFile[];
lastSelectedFilePath: null | string;
language: string
deviceId: string
};
export interface ConfigFile {
id: string;

View File

@ -1,10 +1,10 @@
import { ipcRenderer } from 'electron';
import { AuthDemuxBase } from 'utils/auth/types';
import { IPC_ACTIONS } from 'utils/messages';
import { Creds } from 'utils/types';
const { ipcRenderer } = require('electron');
export class AuthDemux extends AuthDemuxBase {
#isElectron: boolean = false;
#isElectron = false;
constructor(isElectron: boolean) {
super();
this.#isElectron = isElectron;

View File

@ -1,55 +1,46 @@
import config from 'utils/config';
import type Store from 'electron-store';
import { ConfigMap } from 'fyo/core/types';
export class Config {
#useElectronConfig: boolean;
fallback: Map<string, unknown> = new Map();
config: Map<string, unknown> | Store;
constructor(isElectron: boolean) {
this.#useElectronConfig = isElectron;
this.config = new Map();
if (isElectron) {
const Config = require('electron-store') as typeof Store;
this.config = new Config();
}
}
get store(): Record<string, unknown> {
if (this.#useElectronConfig) {
return config.store;
} else {
get store() {
if (this.config instanceof Map) {
const store: Record<string, unknown> = {};
for (const key of this.fallback.keys()) {
store[key] = this.fallback.get(key);
for (const key of this.config.keys()) {
store[key] = this.config.get(key);
}
return store;
} else {
return this.config;
}
}
get(key: string, defaultValue?: unknown): unknown {
if (this.#useElectronConfig) {
return config.get(key, defaultValue);
} else {
return this.fallback.get(key) ?? defaultValue;
}
get<K extends keyof ConfigMap>(
key: K,
defaultValue?: ConfigMap[K]
): ConfigMap[K] | undefined {
const value = this.config.get(key) as ConfigMap[K] | undefined;
return value ?? defaultValue;
}
set(key: string, value: unknown) {
if (this.#useElectronConfig) {
config.set(key, value);
} else {
this.fallback.set(key, value);
}
set<K extends keyof ConfigMap>(key: K, value: ConfigMap[K]) {
this.config.set(key, value);
}
delete(key: string) {
if (this.#useElectronConfig) {
config.delete(key);
} else {
this.fallback.delete(key);
}
delete(key: keyof ConfigMap) {
this.config.delete(key);
}
clear() {
if (this.#useElectronConfig) {
config.clear();
} else {
this.fallback.clear();
}
this.config.clear();
}
}

View File

@ -1,4 +1,4 @@
import { ipcRenderer } from 'electron';
const { ipcRenderer } = require('electron');
import { DatabaseError, NotImplemented } from 'fyo/utils/errors';
import { SchemaMap } from 'schemas/types';
import { DatabaseDemuxBase, DatabaseMethod } from 'utils/db/types';
@ -6,7 +6,7 @@ import { BackendResponse } from 'utils/ipc/types';
import { IPC_ACTIONS } from 'utils/messages';
export class DatabaseDemux extends DatabaseDemuxBase {
#isElectron: boolean = false;
#isElectron = false;
constructor(isElectron: boolean) {
super();
this.#isElectron = isElectron;
@ -27,70 +27,76 @@ export class DatabaseDemux extends DatabaseDemuxBase {
}
async getSchemaMap(): Promise<SchemaMap> {
if (this.#isElectron) {
return (await this.#handleDBCall(async function dbFunc() {
return await ipcRenderer.invoke(IPC_ACTIONS.DB_SCHEMA);
})) as SchemaMap;
if (!this.#isElectron) {
throw new NotImplemented();
}
throw new NotImplemented();
return (await this.#handleDBCall(async () => {
return (await ipcRenderer.invoke(
IPC_ACTIONS.DB_SCHEMA
)) as BackendResponse;
})) as SchemaMap;
}
async createNewDatabase(
dbPath: string,
countryCode?: string
): Promise<string> {
if (this.#isElectron) {
return (await this.#handleDBCall(async function dbFunc() {
return await ipcRenderer.invoke(
IPC_ACTIONS.DB_CREATE,
dbPath,
countryCode
);
})) as string;
if (!this.#isElectron) {
throw new NotImplemented();
}
throw new NotImplemented();
return (await this.#handleDBCall(async () => {
return (await ipcRenderer.invoke(
IPC_ACTIONS.DB_CREATE,
dbPath,
countryCode
)) as BackendResponse;
})) as string;
}
async connectToDatabase(
dbPath: string,
countryCode?: string
): Promise<string> {
if (this.#isElectron) {
return (await this.#handleDBCall(async function dbFunc() {
return await ipcRenderer.invoke(
IPC_ACTIONS.DB_CONNECT,
dbPath,
countryCode
);
})) as string;
if (!this.#isElectron) {
throw new NotImplemented();
}
throw new NotImplemented();
return (await this.#handleDBCall(async () => {
return (await ipcRenderer.invoke(
IPC_ACTIONS.DB_CONNECT,
dbPath,
countryCode
)) as BackendResponse;
})) as string;
}
async call(method: DatabaseMethod, ...args: unknown[]): Promise<unknown> {
if (this.#isElectron) {
return (await this.#handleDBCall(async function dbFunc() {
return await ipcRenderer.invoke(IPC_ACTIONS.DB_CALL, method, ...args);
})) as unknown;
if (!this.#isElectron) {
throw new NotImplemented();
}
throw new NotImplemented();
return await this.#handleDBCall(async () => {
return (await ipcRenderer.invoke(
IPC_ACTIONS.DB_CALL,
method,
...args
)) as BackendResponse;
});
}
async callBespoke(method: string, ...args: unknown[]): Promise<unknown> {
if (this.#isElectron) {
return (await this.#handleDBCall(async function dbFunc() {
return await ipcRenderer.invoke(
IPC_ACTIONS.DB_BESPOKE,
method,
...args
);
})) as unknown;
if (!this.#isElectron) {
throw new NotImplemented();
}
throw new NotImplemented();
return await this.#handleDBCall(async () => {
return (await ipcRenderer.invoke(
IPC_ACTIONS.DB_BESPOKE,
method,
...args
)) as BackendResponse;
});
}
}

View File

@ -35,7 +35,7 @@ export class Fyo {
doc: DocHandler;
db: DatabaseHandler;
_initialized: boolean = false;
_initialized = false;
errorLog: ErrorLog[] = [];
temp?: Record<string, unknown>;
@ -94,10 +94,9 @@ export class Fyo {
return format(value, field, doc ?? null, this);
}
async setIsElectron() {
setIsElectron() {
try {
const { ipcRenderer } = await import('electron');
this.isElectron = Boolean(ipcRenderer);
this.isElectron = Boolean(require('electron'));
} catch {
this.isElectron = false;
}
@ -106,7 +105,7 @@ export class Fyo {
async initializeAndRegister(
models: ModelMap = {},
regionalModels: ModelMap = {},
force: boolean = false
force = false
) {
if (this._initialized && !force) return;
@ -122,8 +121,8 @@ export class Fyo {
// temp params while calling routes
this.temp = {};
await this.doc.init();
await this.auth.init();
this.doc.init();
this.auth.init();
await this.db.init();
}
@ -165,7 +164,6 @@ export class Fyo {
async close() {
await this.db.close();
await this.auth.logout();
}
getField(schemaName: string, fieldname: string) {
@ -190,14 +188,14 @@ export class Fyo {
let value: DocValue | Doc[];
try {
doc = await this.doc.getDoc(schemaName, name);
value = doc.get(fieldname!);
value = doc.get(fieldname);
} catch (err) {
value = undefined;
}
if (value === undefined && schemaName === name) {
const sv = await this.db.getSingleValues({
fieldname: fieldname!,
fieldname: fieldname,
parent: schemaName,
});
@ -222,8 +220,7 @@ export class Fyo {
this.errorLog = [];
this.temp = {};
await this.db.purgeCache();
await this.auth.purgeCache();
await this.doc.purgeCache();
this.doc.purgeCache();
}
store = {

View File

@ -9,7 +9,6 @@ import {
DynamicLinkField,
Field,
FieldTypeEnum,
OptionField,
RawValue,
Schema,
TargetField,
@ -37,8 +36,8 @@ import {
FormulaMap,
FormulaReturn,
HiddenMap,
ListsMap,
ListViewSettings,
ListsMap,
ReadOnlyMap,
RequiredMap,
TreeViewSettings,
@ -47,6 +46,7 @@ import {
import { validateOptions, validateRequired } from './validationFunction';
export class Doc extends Observable<DocValue | Doc[]> {
/* eslint-disable @typescript-eslint/no-floating-promises */
name?: string;
schema: Readonly<Schema>;
fyo: Fyo;
@ -62,15 +62,15 @@ export class Doc extends Observable<DocValue | Doc[]> {
parentSchemaName?: string;
links?: Record<string, Doc>;
_dirty: boolean = true;
_notInserted: boolean = true;
_dirty = true;
_notInserted = true;
_syncing = false;
constructor(
schema: Schema,
data: DocValueMap,
fyo: Fyo,
convertToDocValue: boolean = true
convertToDocValue = true
) {
super();
this.fyo = markRaw(fyo);
@ -289,10 +289,10 @@ export class Doc extends Observable<DocValue | Doc[]> {
async set(
fieldname: string | DocValueMap,
value?: DocValue | Doc[] | DocValueMap[],
retriggerChildDocApplyChange: boolean = false
retriggerChildDocApplyChange = false
): Promise<boolean> {
if (typeof fieldname === 'object') {
return await this.setMultiple(fieldname as DocValueMap);
return await this.setMultiple(fieldname);
}
if (!this._canSet(fieldname, value)) {
@ -391,7 +391,7 @@ export class Doc extends Observable<DocValue | Doc[]> {
}
if (field.fieldtype === FieldTypeEnum.Currency && !isPesa(defaultValue)) {
defaultValue = this.fyo.pesa!(defaultValue as string | number);
defaultValue = this.fyo.pesa(defaultValue as string | number);
}
this[field.fieldname] = defaultValue;
@ -418,7 +418,7 @@ export class Doc extends Observable<DocValue | Doc[]> {
push(
fieldname: string,
docValueMap: Doc | DocValueMap | RawValueMap = {},
convertToDocValue: boolean = false
convertToDocValue = false
) {
const childDocs = [
(this[fieldname] ?? []) as Doc[],
@ -449,7 +449,7 @@ export class Doc extends Observable<DocValue | Doc[]> {
_getChildDoc(
docValueMap: Doc | DocValueMap | RawValueMap,
fieldname: string,
convertToDocValue: boolean = false
convertToDocValue = false
): Doc {
if (!this.name && this.schema.naming !== 'manual') {
this.name = this.fyo.doc.getTemporaryName(this.schema);
@ -528,7 +528,7 @@ export class Doc extends Observable<DocValue | Doc[]> {
field.fieldtype === FieldTypeEnum.Select ||
field.fieldtype === FieldTypeEnum.AutoComplete
) {
validateOptions(field as OptionField, value as string, this);
validateOptions(field, value as string, this);
}
validateRequired(field, value, this);
@ -544,10 +544,7 @@ export class Doc extends Observable<DocValue | Doc[]> {
await validator(value);
}
getValidDict(
filterMeta: boolean = false,
filterComputed: boolean = false
): DocValueMap {
getValidDict(filterMeta = false, filterComputed = false): DocValueMap {
let fields = this.schema.fields;
if (filterMeta) {
fields = this.schema.fields.filter((f) => !f.meta);
@ -639,11 +636,11 @@ export class Doc extends Observable<DocValue | Doc[]> {
async _loadLink(field: Field) {
if (field.fieldtype === FieldTypeEnum.Link) {
return await this._loadLinkField(field as TargetField);
return await this._loadLinkField(field);
}
if (field.fieldtype === FieldTypeEnum.DynamicLink) {
return await this._loadDynamicLinkField(field as DynamicLinkField);
return await this._loadDynamicLinkField(field);
}
}
@ -767,14 +764,13 @@ export class Doc extends Observable<DocValue | Doc[]> {
changedFieldname?: string,
retriggerChildDocApplyChange?: boolean
): Promise<boolean> {
const doc = this;
let changed = await this._callAllTableFieldsApplyFormula(changedFieldname);
changed =
(await this._applyFormulaForFields(doc, changedFieldname)) || changed;
(await this._applyFormulaForFields(this, changedFieldname)) || changed;
if (changed && retriggerChildDocApplyChange) {
await this._callAllTableFieldsApplyFormula(changedFieldname);
await this._applyFormulaForFields(doc, changedFieldname);
await this._applyFormulaForFields(this, changedFieldname);
}
return changed;
@ -803,7 +799,7 @@ export class Doc extends Observable<DocValue | Doc[]> {
childDocs: Doc[],
fieldname?: string
): Promise<boolean> {
let changed: boolean = false;
let changed = false;
for (const childDoc of childDocs) {
if (!childDoc._applyFormula) {
continue;
@ -978,7 +974,7 @@ export class Doc extends Observable<DocValue | Doc[]> {
async trigger(event: string, params?: unknown) {
if (this[event]) {
await (this[event] as Function)(params);
await (this[event] as (args: unknown) => Promise<void>)(params);
}
await super.trigger(event, params);
@ -993,9 +989,9 @@ export class Doc extends Observable<DocValue | Doc[]> {
try {
return this.fyo.pesa(value as string | number);
} catch (err) {
(
err as Error
).message += ` value: '${value}' of type: ${typeof value}, fieldname: '${tablefield}', childfield: '${childfield}'`;
(err as Error).message += ` value: '${String(
value
)}' of type: ${typeof value}, fieldname: '${tablefield}', childfield: '${childfield}'`;
throw err;
}
}
@ -1032,7 +1028,7 @@ export class Doc extends Observable<DocValue | Doc[]> {
if (this.numberSeries) {
delete updateMap.name;
} else {
updateMap.name = updateMap.name + ' CPY';
updateMap.name = String(updateMap.name) + ' CPY';
}
const rawUpdateMap = this.fyo.db.converter.toRawValueMap(
@ -1054,6 +1050,8 @@ export class Doc extends Observable<DocValue | Doc[]> {
*
* This may cause the lifecycle function to execute incorrectly.
*/
/* eslint-disable @typescript-eslint/no-empty-function, @typescript-eslint/no-unused-vars */
async change(ch: ChangeArg) {}
async validate() {}
async beforeSync() {}
@ -1084,7 +1082,9 @@ export class Doc extends Observable<DocValue | Doc[]> {
return {};
}
static getTreeSettings(fyo: Fyo): TreeViewSettings | void {}
static getTreeSettings(fyo: Fyo): TreeViewSettings | void {
return;
}
static getActions(fyo: Fyo): Action[] {
return [];

View File

@ -99,23 +99,18 @@ async function getNotFoundDetailsIfDoesNotExists(
): Promise<NotFoundDetails | null> {
const value = doc.get(field.fieldname);
if (field.fieldtype === FieldTypeEnum.Link && value) {
return getNotFoundLinkDetails(field as TargetField, value as string, fyo);
return getNotFoundLinkDetails(field, value as string, fyo);
}
if (field.fieldtype === FieldTypeEnum.DynamicLink && value) {
return getNotFoundDynamicLinkDetails(
field as DynamicLinkField,
value as string,
fyo,
doc
);
return getNotFoundDynamicLinkDetails(field, value as string, fyo, doc);
}
if (
field.fieldtype === FieldTypeEnum.Table &&
(value as Doc[] | undefined)?.length
) {
return getNotFoundTableDetails(value as Doc[], fyo);
return await getNotFoundTableDetails(value as Doc[], fyo);
}
return null;
@ -127,7 +122,7 @@ async function getNotFoundLinkDetails(
fyo: Fyo
): Promise<NotFoundDetails | null> {
const { target } = field;
const exists = await fyo.db.exists(target as string, value);
const exists = await fyo.db.exists(target, value);
if (!exists) {
return { label: field.label, value };
}
@ -160,7 +155,7 @@ async function getNotFoundTableDetails(
fyo: Fyo
): Promise<NotFoundDetails | null> {
for (const childDoc of value) {
const details = getNotFoundDetails(childDoc, fyo);
const details = await getNotFoundDetails(childDoc, fyo);
if (details) {
return details;
}

View File

@ -34,7 +34,7 @@ export function getPreDefaultValues(
case FieldTypeEnum.Table:
return [] as Doc[];
case FieldTypeEnum.Currency:
return fyo.pesa!(0.0);
return fyo.pesa(0.0);
case FieldTypeEnum.Int:
case FieldTypeEnum.Float:
return 0;
@ -145,9 +145,9 @@ export function isDocValueTruthy(docValue: DocValue | Doc[]) {
}
export function setChildDocIdx(childDocs: Doc[]) {
for (const idx in childDocs) {
childDocs[idx].idx = +idx;
}
childDocs.forEach((cd, idx) => {
cd.idx = idx;
});
}
export function getFormulaSequence(formulas: FormulaMap) {

View File

@ -7,14 +7,26 @@ import { getIsNullOrUndef } from 'utils';
import { Doc } from './doc';
export function validateEmail(value: DocValue) {
const isValid = /(.+)@(.+){2,}\.(.+){2,}/.test(value as string);
if (typeof value !== 'string') {
throw new TypeError(
`Invalid email ${String(value)} of type ${typeof value}`
);
}
const isValid = /(.+)@(.+){2,}\.(.+){2,}/.test(value);
if (!isValid) {
throw new ValidationError(`Invalid email: ${value}`);
}
}
export function validatePhoneNumber(value: DocValue) {
const isValid = /[+]{0,1}[\d ]+/.test(value as string);
if (typeof value !== 'string') {
throw new TypeError(
`Invalid phone ${String(value)} of type ${typeof value}`
);
}
const isValid = /[+]{0,1}[\d ]+/.test(value);
if (!isValid) {
throw new ValidationError(`Invalid phone: ${value}`);
}

View File

@ -18,7 +18,7 @@ export default class SystemSettings extends Doc {
instanceId?: string;
validations: ValidationMap = {
async displayPrecision(value: DocValue) {
displayPrecision(value: DocValue) {
if ((value as number) >= 0 && (value as number) <= 9) {
return;
}
@ -38,7 +38,7 @@ export default class SystemSettings extends Doc {
(c) =>
({
value: countryInfo[c]?.locale,
label: `${c} (${countryInfo[c]?.locale})`,
label: `${c} (${countryInfo[c]?.locale ?? t`Not Found`})`,
} as SelectOption)
);
},

View File

@ -1,5 +1,4 @@
import { Fyo } from 'fyo';
import { ConfigKeys } from 'fyo/core/types';
import { Noun, Telemetry, Verb } from './types';
/**
@ -28,8 +27,8 @@ import { Noun, Telemetry, Verb } from './types';
*/
export class TelemetryManager {
#url: string = '';
#token: string = '';
#url = '';
#token = '';
#started = false;
fyo: Fyo;
@ -67,6 +66,7 @@ export class TelemetryManager {
log(verb: Verb, noun: Noun, more?: Record<string, unknown>) {
if (!this.#started && this.fyo.db.isConnected) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.start().then(() => this.#sendBeacon(verb, noun, more));
return;
}
@ -108,16 +108,12 @@ export class TelemetryManager {
noun: Noun,
more?: Record<string, unknown>
): Telemetry {
const countryCode = this.fyo.singles.SystemSettings?.countryCode as
| string
| undefined;
const countryCode = this.fyo.singles.SystemSettings?.countryCode;
return {
country: countryCode ?? '',
language: this.fyo.store.language,
deviceId:
this.fyo.store.deviceId ||
(this.fyo.config.get(ConfigKeys.DeviceId) as string),
this.fyo.store.deviceId || (this.fyo.config.get('deviceId') ?? '-'),
instanceId: this.fyo.store.instanceId,
version: this.fyo.store.appVersion,
openCount: this.fyo.store.openCount,

View File

@ -2,6 +2,7 @@ import { AuthDemuxBase } from 'utils/auth/types';
import { Creds } from 'utils/types';
export class DummyAuthDemux extends AuthDemuxBase {
// eslint-disable-next-line @typescript-eslint/require-await
async getCreds(): Promise<Creds> {
return { errorLogUrl: '', tokenString: '', telemetryUrl: '' };
}

View File

@ -4,11 +4,7 @@ export class BaseError extends Error {
statusCode: number;
shouldStore: boolean;
constructor(
statusCode: number,
message: string,
shouldStore: boolean = true
) {
constructor(statusCode: number, message: string, shouldStore = true) {
super(message);
this.name = 'BaseError';
this.statusCode = statusCode;
@ -18,63 +14,63 @@ export class BaseError extends Error {
}
export class ValidationError extends BaseError {
constructor(message: string, shouldStore: boolean = false) {
constructor(message: string, shouldStore = false) {
super(417, message, shouldStore);
this.name = 'ValidationError';
}
}
export class NotFoundError extends BaseError {
constructor(message: string, shouldStore: boolean = true) {
constructor(message: string, shouldStore = true) {
super(404, message, shouldStore);
this.name = 'NotFoundError';
}
}
export class ForbiddenError extends BaseError {
constructor(message: string, shouldStore: boolean = true) {
constructor(message: string, shouldStore = true) {
super(403, message, shouldStore);
this.name = 'ForbiddenError';
}
}
export class DuplicateEntryError extends ValidationError {
constructor(message: string, shouldStore: boolean = false) {
constructor(message: string, shouldStore = false) {
super(message, shouldStore);
this.name = 'DuplicateEntryError';
}
}
export class LinkValidationError extends ValidationError {
constructor(message: string, shouldStore: boolean = false) {
constructor(message: string, shouldStore = false) {
super(message, shouldStore);
this.name = 'LinkValidationError';
}
}
export class MandatoryError extends ValidationError {
constructor(message: string, shouldStore: boolean = false) {
constructor(message: string, shouldStore = false) {
super(message, shouldStore);
this.name = 'MandatoryError';
}
}
export class DatabaseError extends BaseError {
constructor(message: string, shouldStore: boolean = true) {
constructor(message: string, shouldStore = true) {
super(500, message, shouldStore);
this.name = 'DatabaseError';
}
}
export class CannotCommitError extends DatabaseError {
constructor(message: string, shouldStore: boolean = true) {
constructor(message: string, shouldStore = true) {
super(message, shouldStore);
this.name = 'CannotCommitError';
}
}
export class NotImplemented extends BaseError {
constructor(message: string = '', shouldStore: boolean = false) {
constructor(message = '', shouldStore = false) {
super(501, message, shouldStore);
this.name = 'NotImplemented';
}

View File

@ -60,7 +60,7 @@ function toDatetime(value: unknown): DateTime | null {
} else if (value instanceof Date) {
return DateTime.fromJSDate(value);
} else if (typeof value === 'number') {
return DateTime.fromSeconds(value as number);
return DateTime.fromSeconds(value);
}
return null;
@ -120,7 +120,9 @@ function formatCurrency(
try {
valueString = formatNumber(value, fyo);
} catch (err) {
(err as Error).message += ` value: '${value}', type: ${typeof value}`;
(err as Error).message += ` value: '${String(
value
)}', type: ${typeof value}`;
throw err;
}
@ -148,7 +150,9 @@ function formatNumber(value: unknown, fyo: Fyo): string {
if (formattedNumber === 'NaN') {
throw Error(
`invalid value passed to formatNumber: '${value}' of type ${typeof value}`
`invalid value passed to formatNumber: '${String(
value
)}' of type ${typeof value}`
);
}

View File

@ -8,7 +8,7 @@ import { getIsNullOrUndef, safeParseInt } from 'utils';
export function slug(str: string) {
return str
.replace(/(?:^\w|[A-Z]|\b\w)/g, function (letter, index) {
.replace(/(?:^\w|[A-Z]|\b\w)/g, (letter, index) => {
return index == 0 ? letter.toLowerCase() : letter.toUpperCase();
})
.replace(/\s+/g, '');
@ -24,7 +24,7 @@ export function unique<T>(list: T[], key = (it: T) => String(it)) {
export function getDuplicates(array: unknown[]) {
const duplicates: unknown[] = [];
for (const i in array) {
for (let i = 0; i < array.length; i++) {
const previous = array[safeParseInt(i) - 1];
const current = array[i];
@ -118,7 +118,7 @@ function getRawOptionList(field: Field, doc: Doc | undefined | null) {
return [];
}
const Model = doc!.fyo.models[doc!.schemaName];
const Model = doc.fyo.models[doc.schemaName];
if (Model === undefined) {
return [];
}
@ -128,7 +128,7 @@ function getRawOptionList(field: Field, doc: Doc | undefined | null) {
return [];
}
return getList(doc!);
return getList(doc);
}
export function getEmptyValuesByFieldTypes(

View File

@ -3,13 +3,16 @@ enum EventType {
OnceListeners = '_onceListeners',
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type Listener = (...args: any[]) => unknown | Promise<unknown>;
export default class Observable<T> {
[key: string]: unknown | T;
_isHot: Map<string, boolean>;
_eventQueue: Map<string, unknown[]>;
_map: Map<string, unknown>;
_listeners: Map<string, Function[]>;
_onceListeners: Map<string, Function[]>;
_listeners: Map<string, Listener[]>;
_onceListeners: Map<string, Listener[]>;
constructor() {
this._map = new Map();
@ -37,6 +40,7 @@ export default class Observable<T> {
*/
set(key: string, value: T) {
this[key] = value;
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.trigger('change', {
doc: this,
changed: key,
@ -50,7 +54,7 @@ export default class Observable<T> {
* @param event : name of the event for which the listener is checked
* @param listener : specific listener that is checked for
*/
hasListener(event: string, listener?: Function) {
hasListener(event: string, listener?: Listener) {
const listeners = this[EventType.Listeners].get(event) ?? [];
const onceListeners = this[EventType.OnceListeners].get(event) ?? [];
@ -69,7 +73,7 @@ export default class Observable<T> {
* @param event : name of the event for which the listener is set
* @param listener : listener that is executed when the event is triggered
*/
on(event: string, listener: Function) {
on(event: string, listener: Listener) {
this._addListener(EventType.Listeners, event, listener);
}
@ -80,7 +84,7 @@ export default class Observable<T> {
* @param event : name of the event for which the listener is set
* @param listener : listener that is executed when the event is triggered
*/
once(event: string, listener: Function) {
once(event: string, listener: Listener) {
this._addListener(EventType.OnceListeners, event, listener);
}
@ -90,7 +94,7 @@ export default class Observable<T> {
* @param event : name of the event from which to remove the listener
* @param listener : listener that was set for the event
*/
off(event: string, listener: Function) {
off(event: string, listener: Listener) {
this._removeListener(EventType.Listeners, event, listener);
this._removeListener(EventType.OnceListeners, event, listener);
}
@ -111,7 +115,7 @@ export default class Observable<T> {
* @param throttle : wait time before triggering the event.
*/
async trigger(event: string, params?: unknown, throttle: number = 0) {
async trigger(event: string, params?: unknown, throttle = 0) {
let isHot = false;
if (throttle > 0) {
isHot = this._throttled(event, params, throttle);
@ -125,7 +129,7 @@ export default class Observable<T> {
await this._executeTriggers(event, params);
}
_removeListener(type: EventType, event: string, listener: Function) {
_removeListener(type: EventType, event: string, listener: Listener) {
const listeners = (this[type].get(event) ?? []).filter(
(l) => l !== listener
);
@ -160,6 +164,7 @@ export default class Observable<T> {
const params = this._eventQueue.get(event);
if (params !== undefined) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this._executeTriggers(event, params);
this._eventQueue.delete(event);
}
@ -168,7 +173,7 @@ export default class Observable<T> {
return false;
}
_addListener(type: EventType, event: string, listener: Function) {
_addListener(type: EventType, event: string, listener: Listener) {
this._initLiseners(type, event);
const list = this[type].get(event)!;
if (list.includes(listener)) {

View File

@ -31,7 +31,7 @@ class TranslationString {
}
#formatArg(arg: string | number | boolean) {
return arg ?? '';
return String(arg ?? '');
}
#translate() {
@ -48,22 +48,23 @@ class TranslationString {
}
#stitch() {
if (!((this.args[0] as any) instanceof Array)) {
if (!((this.args[0] as unknown) instanceof Array)) {
throw new ValueError(
`invalid args passed to TranslationString ${
`invalid args passed to TranslationString ${String(
this.args
} of type ${typeof this.args[0]}`
)} of type ${typeof this.args[0]}`
);
}
this.strList = this.args[0] as any as string[];
this.strList = this.args[0] as unknown as string[];
this.argList = this.args.slice(1) as TranslationArgs[];
if (this.languageMap) {
this.#translate();
}
return this.strList!.map((s, i) => s + this.#formatArg(this.argList![i]))
return this.strList
.map((s, i) => s + this.#formatArg(this.argList![i]))
.join('')
.replace(/\s+/g, ' ')
.trim();

91
main.ts
View File

@ -1,27 +1,33 @@
'use strict';
// eslint-disable-next-line
require('source-map-support').install({
handleUncaughtException: false,
environment: 'node',
});
import {
app,
BrowserWindow,
BrowserWindowConstructorOptions,
protocol,
ProtocolRequest,
ProtocolResponse,
} from 'electron';
import Store from 'electron-store';
import { autoUpdater } from 'electron-updater';
import fs from 'fs';
import path from 'path';
import { createProtocol } from 'vue-cli-plugin-electron-builder/lib';
import registerAppLifecycleListeners from './main/registerAppLifecycleListeners';
import registerAutoUpdaterListeners from './main/registerAutoUpdaterListeners';
import registerIpcMainActionListeners from './main/registerIpcMainActionListeners';
import registerIpcMainMessageListeners from './main/registerIpcMainMessageListeners';
import registerProcessListeners from './main/registerProcessListeners';
import { emitMainProcessError } from 'backend/helpers';
export class Main {
title: string = 'Frappe Books';
title = 'Frappe Books';
icon: string;
winURL: string = '';
isWebpackUrl: boolean = false;
winURL = '';
checkedForUpdate = false;
mainWindow: BrowserWindow | null = null;
@ -57,7 +63,7 @@ export class Main {
}
get isDevelopment() {
return process.env.NODE_ENV !== 'production';
return process.env.NODE_ENV === 'development';
}
get isTest() {
@ -116,35 +122,42 @@ export class Main {
return options;
}
createWindow() {
async createWindow() {
const options = this.getOptions();
this.mainWindow = new BrowserWindow(options);
this.isWebpackUrl = !!process.env.WEBPACK_DEV_SERVER_URL;
if (this.isWebpackUrl) {
this.loadWebpackDevServerURL();
if (this.isDevelopment) {
this.setViteServerURL();
} else {
this.loadAppUrl();
this.registerAppProtocol();
}
await this.mainWindow.loadURL(this.winURL);
if (this.isDevelopment && !this.isTest) {
this.mainWindow.webContents.openDevTools();
}
this.setMainWindowListeners();
}
loadWebpackDevServerURL() {
// Load the url of the dev server if in development mode
this.winURL = process.env.WEBPACK_DEV_SERVER_URL as string;
this.mainWindow!.loadURL(this.winURL);
setViteServerURL() {
let port = 6969;
let host = '0.0.0.0';
if (this.isDevelopment && !this.isTest) {
this.mainWindow!.webContents.openDevTools();
if (process.env.VITE_PORT && process.env.VITE_HOST) {
port = Number(process.env.VITE_PORT);
host = process.env.VITE_HOST;
}
// Load the url of the dev server if in development mode
this.winURL = `http://${host}:${port}/`;
}
loadAppUrl() {
createProtocol('app');
// Load the index.html when not in development
registerAppProtocol() {
protocol.registerBufferProtocol('app', bufferProtocolCallback);
// Use the registered protocol url to load the files.
this.winURL = 'app://./index.html';
this.mainWindow!.loadURL(this.winURL);
}
setMainWindowListeners() {
@ -157,9 +170,43 @@ export class Main {
});
this.mainWindow.webContents.on('did-fail-load', () => {
this.mainWindow!.loadURL(this.winURL);
this.mainWindow!.loadURL(this.winURL).catch((err) =>
emitMainProcessError(err)
);
});
}
}
/**
* Callback used to register the custom app protocol,
* during prod, files are read and served by using this
* protocol.
*/
function bufferProtocolCallback(
request: ProtocolRequest,
callback: (response: ProtocolResponse) => void
) {
const { pathname, host } = new URL(request.url);
const filePath = path.join(
__dirname,
'src',
decodeURI(host),
decodeURI(pathname)
);
fs.readFile(filePath, (_, data) => {
const extension = path.extname(filePath).toLowerCase();
const mimeType =
{
'.js': 'text/javascript',
'.css': 'text/css',
'.html': 'text/html',
'.svg': 'image/svg+xml',
'.json': 'application/json',
}[extension] ?? '';
callback({ mimeType, data });
});
}
export default new Main();

View File

@ -4,6 +4,7 @@ import fetch from 'node-fetch';
import path from 'path';
import { Creds } from 'utils/types';
import { rendererLog } from './helpers';
import type { Main } from 'main';
export function getUrlAndTokenString(): Creds {
const inProduction = app.isPackaged;
@ -13,10 +14,11 @@ export function getUrlAndTokenString(): Creds {
'../creds/log_creds.txt'
);
if (!fs.existsSync(errLogCredsPath)) {
errLogCredsPath = path.join(__dirname, '../log_creds.txt');
errLogCredsPath = path.join(__dirname, '..', '..', 'log_creds.txt');
}
if (!fs.existsSync(errLogCredsPath)) {
// eslint-disable-next-line no-console
!inProduction && console.log(`${errLogCredsPath} doesn't exist, can't log`);
return empty;
}
@ -29,7 +31,9 @@ export function getUrlAndTokenString(): Creds {
.filter((f) => f.length);
} catch (err) {
if (!inProduction) {
// eslint-disable-next-line no-console
console.log(`logging error using creds at: ${errLogCredsPath} failed`);
// eslint-disable-next-line no-console
console.log(err);
}
return empty;
@ -42,7 +46,7 @@ export function getUrlAndTokenString(): Creds {
};
}
export async function sendError(body: string) {
export async function sendError(body: string, main: Main) {
const { errorLogUrl, tokenString } = getUrlAndTokenString();
const headers = {
Authorization: tokenString,
@ -51,6 +55,6 @@ export async function sendError(body: string) {
};
await fetch(errorLogUrl, { method: 'POST', headers, body }).catch((err) => {
rendererLog(err);
rendererLog(main, err);
});
}

View File

@ -16,8 +16,7 @@ import fs from 'fs/promises';
import path from 'path';
import { parseCSV } from 'utils/csvParser';
import { LanguageMap } from 'utils/types';
const fetch = require('node-fetch').default;
import fetch from 'node-fetch';
const VALENTINES_DAY = 1644796800000;
@ -100,7 +99,7 @@ async function fetchContentsFromApi(code: string) {
return null;
}
const resJson = await res.json();
const resJson = (await res.json()) as { content: string };
return Buffer.from(resJson.content, 'base64').toString();
}
@ -138,7 +137,9 @@ async function getLastUpdated(code: string): Promise<Date> {
return new Date(VALENTINES_DAY);
}
const resJson = await res.json();
const resJson = (await res.json()) as {
commit: { author: { date: string } };
}[];
try {
return new Date(resJson[0].commit.author.date);
} catch {
@ -187,7 +188,7 @@ async function storeFile(code: string, contents: string) {
async function errorHandledFetch(url: string) {
try {
return (await fetch(url)) as Response;
return await fetch(url);
} catch {
return null;
}

View File

@ -29,7 +29,7 @@ async function getPrintTemplatePaths(): Promise<{
const files = await fs.readdir(root);
return { files, root };
} catch {
root = path.join(__dirname, `../templates`);
root = path.join(__dirname, '..', '..', `templates`);
}
try {

View File

@ -1,6 +1,6 @@
import { constants } from 'fs';
import fs from 'fs/promises';
import { ConfigFile, ConfigKeys } from 'fyo/core/types';
import { ConfigFile } from 'fyo/core/types';
import { Main } from 'main';
import config from 'utils/config';
import { BackendResponse } from 'utils/ipc/types';
@ -8,7 +8,7 @@ import { IPC_CHANNELS } from 'utils/messages';
import type { ConfigFilesWithModified } from 'utils/types';
export async function setAndGetCleanedConfigFiles() {
const files = config.get(ConfigKeys.Files, []) as ConfigFile[];
const files = config.get('files', []);
const cleanedFileMap: Map<string, ConfigFile> = new Map();
for (const file of files) {
@ -30,7 +30,7 @@ export async function setAndGetCleanedConfigFiles() {
}
const cleanedFiles = Array.from(cleanedFileMap.values());
config.set(ConfigKeys.Files, cleanedFiles);
config.set('files', cleanedFiles);
return cleanedFiles;
}
@ -50,7 +50,9 @@ export async function getConfigFilesWithModified(files: ConfigFile[]) {
return filesWithModified;
}
export async function getErrorHandledReponse(func: () => Promise<unknown>) {
export async function getErrorHandledReponse(
func: () => Promise<unknown> | unknown
) {
const response: BackendResponse = {};
try {

View File

@ -2,6 +2,7 @@ import { app } from 'electron';
import installExtension, { VUEJS3_DEVTOOLS } from 'electron-devtools-installer';
import { Main } from '../main';
import { rendererLog } from './helpers';
import { emitMainProcessError } from 'backend/helpers';
export default function registerAppLifecycleListeners(main: Main) {
app.on('window-all-closed', () => {
@ -12,16 +13,16 @@ export default function registerAppLifecycleListeners(main: Main) {
app.on('activate', () => {
if (main.mainWindow === null) {
main.createWindow();
main.createWindow().catch((err) => emitMainProcessError(err));
}
});
app.on('ready', async () => {
app.on('ready', () => {
if (main.isDevelopment && !main.isTest) {
await installDevTools(main);
installDevTools(main).catch((err) => emitMainProcessError(err));
}
main.createWindow();
main.createWindow().catch((err) => emitMainProcessError(err));
});
}

View File

@ -21,6 +21,7 @@ export default function registerAutoUpdaterListeners(main: Main) {
emitMainProcessError(error);
});
// eslint-disable-next-line @typescript-eslint/no-misused-promises
autoUpdater.on('update-available', async (info: UpdateInfo) => {
const currentVersion = app.getVersion();
const nextVersion = info.version;
@ -46,6 +47,7 @@ export default function registerAutoUpdaterListeners(main: Main) {
await autoUpdater.downloadUpdate();
});
// eslint-disable-next-line @typescript-eslint/no-misused-promises
autoUpdater.on('update-downloaded', async () => {
const option = await dialog.showMessageBox({
type: 'info',

View File

@ -1,4 +1,11 @@
import { app, dialog, ipcMain } from 'electron';
import {
MessageBoxOptions,
OpenDialogOptions,
SaveDialogOptions,
app,
dialog,
ipcMain,
} from 'electron';
import { autoUpdater } from 'electron-updater';
import fs from 'fs/promises';
import path from 'path';
@ -20,39 +27,60 @@ import {
import { saveHtmlAsPdf } from './saveHtmlAsPdf';
export default function registerIpcMainActionListeners(main: Main) {
ipcMain.handle(IPC_ACTIONS.GET_OPEN_FILEPATH, async (event, options) => {
return await dialog.showOpenDialog(main.mainWindow!, options);
});
ipcMain.handle(IPC_ACTIONS.GET_SAVE_FILEPATH, async (event, options) => {
return await dialog.showSaveDialog(main.mainWindow!, options);
});
ipcMain.handle(IPC_ACTIONS.GET_DIALOG_RESPONSE, async (event, options) => {
if (main.isDevelopment || main.isLinux) {
Object.assign(options, { icon: main.icon });
ipcMain.handle(
IPC_ACTIONS.GET_OPEN_FILEPATH,
async (_, options: OpenDialogOptions) => {
return await dialog.showOpenDialog(main.mainWindow!, options);
}
);
return await dialog.showMessageBox(main.mainWindow!, options);
});
ipcMain.handle(
IPC_ACTIONS.GET_SAVE_FILEPATH,
async (_, options: SaveDialogOptions) => {
return await dialog.showSaveDialog(main.mainWindow!, options);
}
);
ipcMain.handle(IPC_ACTIONS.SHOW_ERROR, async (event, { title, content }) => {
return await dialog.showErrorBox(title, content);
});
ipcMain.handle(
IPC_ACTIONS.GET_DIALOG_RESPONSE,
async (_, options: MessageBoxOptions) => {
if (main.isDevelopment || main.isLinux) {
Object.assign(options, { icon: main.icon });
}
return await dialog.showMessageBox(main.mainWindow!, options);
}
);
ipcMain.handle(
IPC_ACTIONS.SHOW_ERROR,
(_, { title, content }: { title: string; content: string }) => {
return dialog.showErrorBox(title, content);
}
);
ipcMain.handle(
IPC_ACTIONS.SAVE_HTML_AS_PDF,
async (event, html, savePath, width: number, height: number) => {
async (
_,
html: string,
savePath: string,
width: number,
height: number
) => {
return await saveHtmlAsPdf(html, savePath, app, width, height);
}
);
ipcMain.handle(IPC_ACTIONS.SAVE_DATA, async (event, data, savePath) => {
return await fs.writeFile(savePath, data, { encoding: 'utf-8' });
});
ipcMain.handle(
IPC_ACTIONS.SAVE_DATA,
async (_, data: string, savePath: string) => {
return await fs.writeFile(savePath, data, { encoding: 'utf-8' });
}
);
ipcMain.handle(IPC_ACTIONS.SEND_ERROR, (event, bodyJson) => {
sendError(bodyJson);
ipcMain.handle(IPC_ACTIONS.SEND_ERROR, async (_, bodyJson: string) => {
await sendError(bodyJson, main);
});
ipcMain.handle(IPC_ACTIONS.CHECK_FOR_UPDATES, async () => {
@ -72,7 +100,7 @@ export default function registerIpcMainActionListeners(main: Main) {
main.checkedForUpdate = true;
});
ipcMain.handle(IPC_ACTIONS.GET_LANGUAGE_MAP, async (event, code) => {
ipcMain.handle(IPC_ACTIONS.GET_LANGUAGE_MAP, async (_, code: string) => {
const obj = { languageMap: {}, success: true, message: '' };
try {
obj.languageMap = await getLanguageMap(code);
@ -117,20 +145,20 @@ export default function registerIpcMainActionListeners(main: Main) {
}
);
ipcMain.handle(IPC_ACTIONS.GET_CREDS, async (event) => {
ipcMain.handle(IPC_ACTIONS.GET_CREDS, () => {
return getUrlAndTokenString();
});
ipcMain.handle(IPC_ACTIONS.DELETE_FILE, async (_, filePath) => {
ipcMain.handle(IPC_ACTIONS.DELETE_FILE, async (_, filePath: string) => {
return getErrorHandledReponse(async () => await fs.unlink(filePath));
});
ipcMain.handle(IPC_ACTIONS.GET_DB_LIST, async (_) => {
ipcMain.handle(IPC_ACTIONS.GET_DB_LIST, async () => {
const files = await setAndGetCleanedConfigFiles();
return await getConfigFilesWithModified(files);
});
ipcMain.handle(IPC_ACTIONS.GET_ENV, async (_) => {
ipcMain.handle(IPC_ACTIONS.GET_ENV, () => {
return {
isDevelopment: main.isDevelopment,
platform: process.platform,
@ -149,7 +177,7 @@ export default function registerIpcMainActionListeners(main: Main) {
ipcMain.handle(
IPC_ACTIONS.DB_CREATE,
async (_, dbPath: string, countryCode: string) => {
return await getErrorHandledReponse(async function dbFunc() {
return await getErrorHandledReponse(async () => {
return await databaseManager.createNewDatabase(dbPath, countryCode);
});
}
@ -158,7 +186,7 @@ export default function registerIpcMainActionListeners(main: Main) {
ipcMain.handle(
IPC_ACTIONS.DB_CONNECT,
async (_, dbPath: string, countryCode?: string) => {
return await getErrorHandledReponse(async function dbFunc() {
return await getErrorHandledReponse(async () => {
return await databaseManager.connectToDatabase(dbPath, countryCode);
});
}
@ -167,7 +195,7 @@ export default function registerIpcMainActionListeners(main: Main) {
ipcMain.handle(
IPC_ACTIONS.DB_CALL,
async (_, method: DatabaseMethod, ...args: unknown[]) => {
return await getErrorHandledReponse(async function dbFunc() {
return await getErrorHandledReponse(async () => {
return await databaseManager.call(method, ...args);
});
}
@ -176,15 +204,15 @@ export default function registerIpcMainActionListeners(main: Main) {
ipcMain.handle(
IPC_ACTIONS.DB_BESPOKE,
async (_, method: string, ...args: unknown[]) => {
return await getErrorHandledReponse(async function dbFunc() {
return await getErrorHandledReponse(async () => {
return await databaseManager.callBespoke(method, ...args);
});
}
);
ipcMain.handle(IPC_ACTIONS.DB_SCHEMA, async (_) => {
return await getErrorHandledReponse(async function dbFunc() {
return await databaseManager.getSchemaMap();
ipcMain.handle(IPC_ACTIONS.DB_SCHEMA, async () => {
return await getErrorHandledReponse(() => {
return databaseManager.getSchemaMap();
});
});
}

View File

@ -1,6 +1,7 @@
import { ipcMain, Menu, shell } from 'electron';
import { Main } from '../main';
import { IPC_MESSAGES } from '../utils/messages';
import { emitMainProcessError } from 'backend/helpers';
export default function registerIpcMainMessageListeners(main: Main) {
ipcMain.on(IPC_MESSAGES.OPEN_MENU, (event) => {
@ -20,11 +21,11 @@ export default function registerIpcMainMessageListeners(main: Main) {
main.mainWindow!.reload();
});
ipcMain.on(IPC_MESSAGES.OPEN_EXTERNAL, (_, link) => {
shell.openExternal(link);
ipcMain.on(IPC_MESSAGES.OPEN_EXTERNAL, (_, link: string) => {
shell.openExternal(link).catch((err) => emitMainProcessError(err));
});
ipcMain.on(IPC_MESSAGES.SHOW_ITEM_IN_FOLDER, (_, filePath) => {
ipcMain.on(IPC_MESSAGES.SHOW_ITEM_IN_FOLDER, (_, filePath: string) => {
return shell.showItemInFolder(filePath);
});
}

View File

@ -1,3 +1,4 @@
import { emitMainProcessError } from 'backend/helpers';
import { App, BrowserWindow } from 'electron';
import fs from 'fs/promises';
import path from 'path';
@ -18,7 +19,7 @@ export async function saveHtmlAsPdf(
const htmlPath = path.join(tempRoot, `${filename}.html`);
await fs.writeFile(htmlPath, html, { encoding: 'utf-8' });
const printWindow = getInitializedPrintWindow(htmlPath, width, height);
const printWindow = await getInitializedPrintWindow(htmlPath, width, height);
const printOptions = {
marginsType: 1, // no margin
pageSize: {
@ -36,19 +37,26 @@ export async function saveHtmlAsPdf(
*/
return await new Promise((resolve) => {
printWindow.webContents.once('did-finish-load', () => {
printWindow.webContents.printToPDF(printOptions).then((data) => {
fs.writeFile(savePath, data).then(() => {
printWindow.close();
fs.unlink(htmlPath).then(() => {
resolve(true);
});
});
});
printWindow.webContents
.printToPDF(printOptions)
.then((data) => {
fs.writeFile(savePath, data)
.then(() => {
printWindow.close();
fs.unlink(htmlPath)
.then(() => {
resolve(true);
})
.catch((err) => emitMainProcessError(err));
})
.catch((err) => emitMainProcessError(err));
})
.catch((err) => emitMainProcessError(err));
});
});
}
function getInitializedPrintWindow(
async function getInitializedPrintWindow(
printFilePath: string,
width: number,
height: number
@ -59,6 +67,6 @@ function getInitializedPrintWindow(
show: false,
});
printWindow.loadFile(printFilePath);
await printWindow.loadFile(printFilePath);
return printWindow;
}

View File

@ -71,9 +71,9 @@ export class LedgerPosting {
const roundOffAccount = await this._getRoundOffAccount();
if (difference.gt(0)) {
this.credit(roundOffAccount, absoluteValue);
await this.credit(roundOffAccount, absoluteValue);
} else {
this.debit(roundOffAccount, absoluteValue);
await this.debit(roundOffAccount, absoluteValue);
}
}

View File

@ -59,10 +59,7 @@ export class Account extends Doc {
return;
}
const account = await this.fyo.db.get(
'Account',
this.parentAccount as string
);
const account = await this.fyo.db.get('Account', this.parentAccount);
this.accountType = account.accountType as AccountType;
}

View File

@ -40,14 +40,7 @@ export class AccountingLedgerEntry extends Doc {
static getListViewSettings(): ListViewSettings {
return {
columns: [
'date',
'account',
'party',
'debit',
'credit',
'referenceName',
],
columns: ['date', 'account', 'party', 'debit', 'credit', 'referenceName'],
};
}
}

View File

@ -1,10 +1,10 @@
import { Fyo, t } from 'fyo';
import { t } from 'fyo';
import { Doc } from 'fyo/model/doc';
import {
EmptyMessageMap,
FormulaMap,
ListsMap,
ListViewSettings,
ListsMap,
} from 'fyo/model/types';
import { codeStateMap } from 'regional/in';
import { getCountryInfo } from 'utils/misc';
@ -12,7 +12,7 @@ import { getCountryInfo } from 'utils/misc';
export class Address extends Doc {
formulas: FormulaMap = {
addressDisplay: {
formula: async () => {
formula: () => {
return [
this.addressLine1,
this.addressLine2,

View File

@ -143,7 +143,7 @@ export abstract class Invoice extends Transactional {
outstandingAmount: this.baseGrandTotal!,
});
const party = (await this.fyo.doc.getDoc('Party', this.party!)) as Party;
const party = (await this.fyo.doc.getDoc('Party', this.party)) as Party;
await party.updateOutstandingAmount();
if (this.makeAutoPayment && this.autoPaymentAccount) {
@ -181,7 +181,7 @@ export abstract class Invoice extends Transactional {
async _updatePartyOutStanding() {
const partyDoc = (await this.fyo.doc.getDoc(
ModelNameEnum.Party,
this.party!
this.party
)) as Party;
await partyDoc.updateOutstandingAmount();
@ -223,7 +223,7 @@ export abstract class Invoice extends Transactional {
return 1.0;
}
const exchangeRate = await getExchangeRate({
fromCurrency: this.currency!,
fromCurrency: this.currency,
toCurrency: currency as string,
});
@ -247,7 +247,7 @@ export abstract class Invoice extends Transactional {
continue;
}
const tax = await this.getTax(item.tax!);
const tax = await this.getTax(item.tax);
for (const { account, rate } of (tax.details ?? []) as TaxDetail[]) {
taxes[account] ??= {
account,
@ -256,7 +256,11 @@ export abstract class Invoice extends Transactional {
};
let amount = item.amount!;
if (this.enableDiscounting && !this.discountAfterTax && !item.itemDiscountedTotal?.isZero()) {
if (
this.enableDiscounting &&
!this.discountAfterTax &&
!item.itemDiscountedTotal?.isZero()
) {
amount = item.itemDiscountedTotal!;
}
@ -285,7 +289,7 @@ export abstract class Invoice extends Transactional {
}
async getTax(tax: string) {
if (!this._taxes![tax]) {
if (!this._taxes[tax]) {
this._taxes[tax] = await this.fyo.doc.getDoc('Tax', tax);
}
@ -302,7 +306,7 @@ export abstract class Invoice extends Transactional {
return itemDiscountAmount.add(invoiceDiscountAmount);
}
async getGrandTotal() {
getGrandTotal() {
const totalDiscount = this.getTotalDiscount();
return ((this.taxes ?? []) as Doc[])
.map((doc) => doc.amount as Money)
@ -407,16 +411,15 @@ export abstract class Invoice extends Transactional {
},
dependsOn: ['party', 'currency'],
},
netTotal: { formula: async () => this.getSum('items', 'amount', false) },
netTotal: { formula: () => this.getSum('items', 'amount', false) },
taxes: { formula: async () => await this.getTaxSummary() },
grandTotal: { formula: async () => await this.getGrandTotal() },
grandTotal: { formula: () => this.getGrandTotal() },
baseGrandTotal: {
formula: async () =>
(this.grandTotal as Money).mul(this.exchangeRate! ?? 1),
formula: () => (this.grandTotal as Money).mul(this.exchangeRate! ?? 1),
dependsOn: ['grandTotal', 'exchangeRate'],
},
outstandingAmount: {
formula: async () => {
formula: () => {
if (this.submitted) {
return;
}
@ -425,7 +428,7 @@ export abstract class Invoice extends Transactional {
},
},
stockNotTransferred: {
formula: async () => {
formula: () => {
if (this.submitted) {
return;
}
@ -526,7 +529,7 @@ export abstract class Invoice extends Transactional {
!!doc.autoStockTransferLocation,
numberSeries: (doc) => getNumberSeries(doc.schemaName, doc.fyo),
terms: (doc) => {
const defaults = doc.fyo.singles.Defaults as Defaults | undefined;
const defaults = doc.fyo.singles.Defaults;
if (doc.schemaName === ModelNameEnum.SalesInvoice) {
return defaults?.salesInvoiceTerms ?? '';
}
@ -616,9 +619,7 @@ export abstract class Invoice extends Transactional {
return this.fyo.doc.getNewDoc(ModelNameEnum.Payment, data) as Payment;
}
async getStockTransfer(
isAuto: boolean = false
): Promise<StockTransfer | null> {
async getStockTransfer(isAuto = false): Promise<StockTransfer | null> {
if (!this.isSubmitted) {
return null;
}

View File

@ -188,7 +188,7 @@ export abstract class InvoiceItem extends Doc {
dependsOn: ['item', 'unit'],
},
transferQuantity: {
formula: async (fieldname) => {
formula: (fieldname) => {
if (fieldname === 'quantity' || this.unit === this.transferUnit) {
return this.quantity;
}
@ -205,7 +205,7 @@ export abstract class InvoiceItem extends Doc {
const itemDoc = await this.fyo.doc.getDoc(
ModelNameEnum.Item,
this.item as string
this.item
);
const unitDoc = itemDoc.getLink('uom');
@ -321,7 +321,7 @@ export abstract class InvoiceItem extends Doc {
],
},
itemTaxedTotal: {
formula: async (fieldname) => {
formula: async () => {
const totalTaxRate = await this.getTotalTaxRate();
const rate = this.rate ?? this.fyo.pesa(0);
const quantity = this.quantity ?? 1;
@ -389,7 +389,7 @@ export abstract class InvoiceItem extends Doc {
};
validations: ValidationMap = {
rate: async (value: DocValue) => {
rate: (value: DocValue) => {
if ((value as Money).gte(0)) {
return;
}
@ -401,7 +401,7 @@ export abstract class InvoiceItem extends Doc {
)}) cannot be less zero.`
);
},
itemDiscountAmount: async (value: DocValue) => {
itemDiscountAmount: (value: DocValue) => {
if ((value as Money).lte(this.amount!)) {
return;
}
@ -416,7 +416,7 @@ export abstract class InvoiceItem extends Doc {
)}).`
);
},
itemDiscountPercent: async (value: DocValue) => {
itemDiscountPercent: (value: DocValue) => {
if ((value as number) < 100) {
return;
}
@ -434,13 +434,14 @@ export abstract class InvoiceItem extends Doc {
const item = await this.fyo.db.getAll(ModelNameEnum.UOMConversionItem, {
fields: ['parent'],
filters: { uom: value as string, parent: this.item! },
filters: { uom: value as string, parent: this.item },
});
if (item.length < 1)
throw new ValidationError(
t`Transfer Unit ${value as string} is not applicable for Item ${this
.item!}`
t`Transfer Unit ${value as string} is not applicable for Item ${
this.item
}`
);
},
};

View File

@ -71,7 +71,7 @@ export class Item extends Doc {
};
validations: ValidationMap = {
rate: async (value: DocValue) => {
rate: (value: DocValue) => {
if ((value as Money).isNegative()) {
throw new ValidationError(this.fyo.t`Rate can't be negative.`);
}
@ -85,13 +85,13 @@ export class Item extends Doc {
label: fyo.t`Sales Invoice`,
condition: (doc) => !doc.notInserted && doc.for !== 'Purchases',
action: async (doc, router) => {
const invoice = await fyo.doc.getNewDoc('SalesInvoice');
const invoice = fyo.doc.getNewDoc('SalesInvoice');
await invoice.append('items', {
item: doc.name as string,
rate: doc.rate as Money,
tax: doc.tax as string,
});
router.push(`/edit/SalesInvoice/${invoice.name}`);
await router.push(`/edit/SalesInvoice/${invoice.name!}`);
},
},
{
@ -99,13 +99,13 @@ export class Item extends Doc {
label: fyo.t`Purchase Invoice`,
condition: (doc) => !doc.notInserted && doc.for !== 'Sales',
action: async (doc, router) => {
const invoice = await fyo.doc.getNewDoc('PurchaseInvoice');
const invoice = fyo.doc.getNewDoc('PurchaseInvoice');
await invoice.append('items', {
item: doc.name as string,
rate: doc.rate as Money,
tax: doc.tax as string,
});
router.push(`/edit/PurchaseInvoice/${invoice.name}`);
await router.push(`/edit/PurchaseInvoice/${invoice.name!}`);
},
},
];
@ -126,7 +126,9 @@ export class Item extends Doc {
hasBatch: () =>
!(this.fyo.singles.InventorySettings?.enableBatches && this.trackItem),
hasSerialNumber: () =>
!(this.fyo.singles.InventorySettings?.enableSerialNumber && this.trackItem),
!(
this.fyo.singles.InventorySettings?.enableSerialNumber && this.trackItem
),
uomConversions: () =>
!this.fyo.singles.InventorySettings?.enableUomConversions,
};

View File

@ -29,10 +29,10 @@ export class JournalEntryAccount extends Doc {
formulas: FormulaMap = {
debit: {
formula: async () => this.getAutoDebitCredit('debit'),
formula: () => this.getAutoDebitCredit('debit'),
},
credit: {
formula: async () => this.getAutoDebitCredit('credit'),
formula: () => this.getAutoDebitCredit('credit'),
},
};

View File

@ -89,7 +89,7 @@ export class Party extends Doc {
dependsOn: ['role'],
},
currency: {
formula: async () => {
formula: () => {
if (!this.currency) {
return this.fyo.singles.SystemSettings!.currency as string;
}
@ -132,12 +132,13 @@ export class Party extends Doc {
condition: (doc: Doc) =>
!doc.notInserted && (doc.role as PartyRole) !== 'Customer',
action: async (partyDoc, router) => {
const doc = await fyo.doc.getNewDoc('PurchaseInvoice', {
const doc = fyo.doc.getNewDoc('PurchaseInvoice', {
party: partyDoc.name,
account: partyDoc.defaultAccount as string,
});
router.push({
path: `/edit/PurchaseInvoice/${doc.name}`,
await router.push({
path: `/edit/PurchaseInvoice/${doc.name!}`,
query: {
schemaName: 'PurchaseInvoice',
values: {
@ -170,7 +171,7 @@ export class Party extends Doc {
});
await router.push({
path: `/edit/SalesInvoice/${doc.name}`,
path: `/edit/SalesInvoice/${doc.name!}`,
query: {
schemaName: 'SalesInvoice',
values: {
@ -186,7 +187,7 @@ export class Party extends Doc {
condition: (doc: Doc) =>
!doc.notInserted && (doc.role as PartyRole) !== 'Supplier',
action: async (partyDoc, router) => {
router.push({
await router.push({
path: '/list/SalesInvoice',
query: { filters: JSON.stringify({ party: partyDoc.name }) },
});

View File

@ -84,9 +84,7 @@ export class Payment extends Transactional {
updateAmountOnReferenceUpdate() {
this.amount = this.fyo.pesa(0);
for (const paymentReference of (this.for ?? []) as Doc[]) {
this.amount = (this.amount as Money).add(
paymentReference.amount as Money
);
this.amount = this.amount.add(paymentReference.amount as Money);
}
}
@ -123,8 +121,9 @@ export class Payment extends Transactional {
if (referenceName && referenceType && !refDoc) {
throw new ValidationError(
t`${referenceType} of type ${this.fyo.schemaMap?.[referenceType]
?.label!} does not exist`
t`${referenceType} of type ${
this.fyo.schemaMap?.[referenceType]?.label ?? referenceType
} does not exist`
);
}
@ -241,8 +240,8 @@ export class Payment extends Transactional {
const account = this.account as string;
const amount = this.amount as Money;
await posting.debit(paymentAccount as string, amount);
await posting.credit(account as string, amount);
await posting.debit(paymentAccount, amount);
await posting.credit(account, amount);
await this.applyWriteOffPosting(posting);
return posting;
@ -269,7 +268,7 @@ export class Payment extends Transactional {
}
async validateReferences() {
const forReferences = (this.for ?? []) as PaymentFor[];
const forReferences = this.for ?? [];
if (forReferences.length === 0) {
return;
}
@ -334,10 +333,10 @@ export class Payment extends Transactional {
}
async updateReferenceDocOutstanding() {
for (const row of (this.for ?? []) as PaymentFor[]) {
for (const row of this.for ?? []) {
const referenceDoc = await this.fyo.doc.getDoc(
row.referenceType!,
row.referenceName!
row.referenceName
);
const previousOutstandingAmount = referenceDoc.outstandingAmount as Money;
@ -348,7 +347,7 @@ export class Payment extends Transactional {
async afterCancel() {
await super.afterCancel();
this.revertOutstandingAmount();
await this.revertOutstandingAmount();
}
async revertOutstandingAmount() {
@ -357,10 +356,10 @@ export class Payment extends Transactional {
}
async _revertReferenceOutstanding() {
for (const ref of (this.for ?? []) as PaymentFor[]) {
for (const ref of this.for ?? []) {
const refDoc = await this.fyo.doc.getDoc(
ref.referenceType!,
ref.referenceName!
ref.referenceName
);
const outstandingAmount = (refDoc.outstandingAmount as Money).add(
@ -374,7 +373,7 @@ export class Payment extends Transactional {
async updatePartyOutstanding() {
const partyDoc = (await this.fyo.doc.getDoc(
ModelNameEnum.Party,
this.party!
this.party
)) as Party;
await partyDoc.updateOutstandingAmount();
}
@ -439,7 +438,7 @@ export class Payment extends Transactional {
'referenceName'
)) as Invoice | null;
return (refDoc?.account ?? null) as string | null;
return refDoc?.account ?? null;
}
formulas: FormulaMap = {
@ -514,17 +513,17 @@ export class Payment extends Transactional {
},
},
amount: {
formula: async () => this.getSum('for', 'amount', false),
formula: () => this.getSum('for', 'amount', false),
dependsOn: ['for'],
},
amountPaid: {
formula: async () => this.amount!.sub(this.writeoff!),
formula: () => this.amount!.sub(this.writeoff!),
dependsOn: ['amount', 'writeoff', 'for'],
},
};
validations: ValidationMap = {
amount: async (value: DocValue) => {
amount: (value: DocValue) => {
if ((value as Money).isNegative()) {
throw new ValidationError(
this.fyo.t`Payment amount cannot be less than zero.`
@ -615,7 +614,7 @@ export class Payment extends Transactional {
return [getLedgerLinkAction(fyo)];
}
static getListViewSettings(fyo: Fyo): ListViewSettings {
static getListViewSettings(): ListViewSettings {
return {
columns: ['name', getDocStatusListColumn(), 'party', 'date', 'amount'],
};

View File

@ -60,7 +60,7 @@ export class PaymentFor extends Doc {
const outstandingAmount = (await this.fyo.getValue(
this.referenceType as string,
this.referenceName as string,
this.referenceName,
'outstandingAmount'
)) as Money;
@ -105,10 +105,11 @@ export class PaymentFor extends Doc {
return;
}
const referenceType = this.referenceType ?? ModelNameEnum.SalesInvoice;
const label = this.fyo.schemaMap[referenceType]?.label ?? referenceType;
throw new NotFoundError(
t`${this.fyo.schemaMap[this.referenceType!]?.label!} ${
value as string
} does not exist`,
t`${label} ${value as string} does not exist`,
false
);
},

View File

@ -1,7 +1,10 @@
import { Doc } from 'fyo/model/doc';
import { ListViewSettings } from 'fyo/model/types';
import { PriceListItem } from './PriceListItem';
import { getPriceListEnabledColumn, getPriceListStatusColumn } from 'models/helpers';
import {
getPriceListEnabledColumn,
getPriceListStatusColumn,
} from 'models/helpers';
export class PriceList extends Doc {
isEnabled?: boolean;
@ -11,7 +14,11 @@ export class PriceList extends Doc {
static getListViewSettings(): ListViewSettings {
return {
columns: ['name', getPriceListEnabledColumn(), getPriceListStatusColumn()],
columns: [
'name',
getPriceListEnabledColumn(),
getPriceListStatusColumn(),
],
};
}
}

View File

@ -46,7 +46,7 @@ export class PrintTemplate extends Doc {
static lists: ListsMap = {
type(doc?: Doc) {
let enableInventory: boolean = false;
let enableInventory = false;
let schemaMap: SchemaMap = {};
if (doc) {
enableInventory = !!doc.fyo.singles.AccountingSettings?.enableInventory;

View File

@ -24,7 +24,7 @@ export class PurchaseInvoice extends Invoice {
}
}
const discountAmount = await this.getTotalDiscount();
const discountAmount = this.getTotalDiscount();
const discountAccount = this.fyo.singles.AccountingSettings
?.discountAccount as string | undefined;
if (discountAccount && discountAmount.isPositive()) {

View File

@ -19,12 +19,12 @@ export class SalesInvoice extends Invoice {
}
if (this.taxes) {
for (const tax of this.taxes!) {
for (const tax of this.taxes) {
await posting.credit(tax.account!, tax.amount!.mul(exchangeRate));
}
}
const discountAmount = await this.getTotalDiscount();
const discountAmount = this.getTotalDiscount();
const discountAccount = this.fyo.singles.AccountingSettings
?.discountAccount as string | undefined;
if (discountAccount && discountAmount.isPositive()) {

View File

@ -61,7 +61,7 @@ export class SetupWizard extends Doc {
formulas: FormulaMap = {
fiscalYearStart: {
formula: async (fieldname?: string) => {
formula: (fieldname?: string) => {
if (
fieldname === 'fiscalYearEnd' &&
this.fiscalYearEnd &&
@ -85,7 +85,7 @@ export class SetupWizard extends Doc {
dependsOn: ['country', 'fiscalYearEnd'],
},
fiscalYearEnd: {
formula: async (fieldname?: string) => {
formula: (fieldname?: string) => {
if (
fieldname === 'fiscalYearStart' &&
this.fiscalYearStart &&
@ -109,7 +109,7 @@ export class SetupWizard extends Doc {
dependsOn: ['country', 'fiscalYearStart'],
},
currency: {
formula: async () => {
formula: () => {
const country = this.get('country');
if (typeof country !== 'string') {
return;
@ -135,7 +135,7 @@ export class SetupWizard extends Doc {
dependsOn: ['country'],
},
chartOfAccounts: {
formula: async () => {
formula: () => {
const country = this.get('country') as string | undefined;
if (country === undefined) {
return;

View File

@ -9,10 +9,7 @@ import {
AccountRootType,
AccountRootTypeEnum,
} from './baseModels/Account/types';
import {
Defaults,
numberSeriesDefaultsMap,
} from './baseModels/Defaults/Defaults';
import { numberSeriesDefaultsMap } from './baseModels/Defaults/Defaults';
import { Invoice } from './baseModels/Invoice/Invoice';
import { StockMovement } from './inventory/StockMovement';
import { StockTransfer } from './inventory/StockTransfer';
@ -55,7 +52,7 @@ export function getMakeStockTransferAction(
condition: (doc: Doc) => doc.isSubmitted && !!doc.stockNotTransferred,
action: async (doc: Doc) => {
const transfer = await (doc as Invoice).getStockTransfer();
if (!transfer) {
if (!transfer || !transfer.name) {
return;
}
@ -81,7 +78,7 @@ export function getMakeInvoiceAction(
condition: (doc: Doc) => doc.isSubmitted && !doc.backReference,
action: async (doc: Doc) => {
const invoice = await (doc as StockTransfer).getInvoice();
if (!invoice) {
if (!invoice || !invoice.name) {
return;
}
@ -128,10 +125,7 @@ export function getMakePaymentAction(fyo: Fyo): Action {
};
}
export function getLedgerLinkAction(
fyo: Fyo,
isStock: boolean = false
): Action {
export function getLedgerLinkAction(fyo: Fyo, isStock = false): Action {
let label = fyo.t`Accounting Entries`;
let reportClassName: 'GeneralLedger' | 'StockLedger' = 'GeneralLedger';
@ -146,7 +140,7 @@ export function getLedgerLinkAction(
condition: (doc: Doc) => doc.isSubmitted,
action: async (doc: Doc, router: Router) => {
const route = getLedgerLink(doc, reportClassName);
router.push(route);
await router.push(route);
},
};
}
@ -174,7 +168,7 @@ export function getTransactionStatusColumn(): ColumnConfig {
fieldtype: 'Select',
render(doc) {
const status = getDocStatus(doc) as InvoiceStatus;
const color = statusColor[status];
const color = statusColor[status] ?? 'gray';
const label = getStatusText(status);
return {
@ -389,9 +383,7 @@ export async function getExchangeRate({
let exchangeRate = 0;
if (localStorage) {
exchangeRate = safeParseFloat(
localStorage.getItem(cacheKey as string) as string
);
exchangeRate = safeParseFloat(localStorage.getItem(cacheKey) as string);
}
if (exchangeRate && exchangeRate !== 1) {
@ -402,9 +394,14 @@ export async function getExchangeRate({
const res = await fetch(
`https://api.vatcomply.com/rates?date=${date}&base=${fromCurrency}&symbols=${toCurrency}`
);
const data = await res.json();
const data = (await res.json()) as {
base: string;
data: string;
rates: Record<string, number>;
};
exchangeRate = data.rates[toCurrency];
} catch (error) {
// eslint-disable-next-line no-console
console.error(error);
exchangeRate ??= 1;
}
@ -439,7 +436,7 @@ export function getNumberSeries(schemaName: string, fyo: Fyo) {
return undefined;
}
const defaults = fyo.singles.Defaults as Defaults | undefined;
const defaults = fyo.singles.Defaults;
const field = fyo.getField(schemaName, 'numberSeries');
const value = defaults?.[numberSeriesKey] as string | undefined;
return value ?? (field?.default as string | undefined);

View File

@ -1,13 +1,10 @@
import { Doc } from 'fyo/model/doc';
import {
ListViewSettings,
} from 'fyo/model/types';
import { ListViewSettings } from 'fyo/model/types';
export class Batch extends Doc {
static getListViewSettings(): ListViewSettings {
return {
columns: ["name", "expiryDate", "manufactureDate"],
columns: ['name', 'expiryDate', 'manufactureDate'],
};
}
}

View File

@ -40,7 +40,7 @@ export class StockManager {
}
for (const details of detailsList) {
await this.#createTransfer(details);
this.#createTransfer(details);
}
await this.#sync();
@ -73,7 +73,7 @@ export class StockManager {
}
}
async #createTransfer(details: SMIDetails) {
#createTransfer(details: SMIDetails) {
const item = new StockManagerItem(details, this.fyo);
item.transferStock();
this.items.push(item);

View File

@ -23,7 +23,7 @@ import {
getSerialNumberFromDoc,
updateSerialNumbers,
validateBatch,
validateSerialNumber
validateSerialNumber,
} from './helpers';
import { MovementType, MovementTypeEnum } from './types';
@ -39,6 +39,7 @@ export class StockMovement extends Transfer {
return false;
}
// eslint-disable-next-line @typescript-eslint/require-await
override async getPosting(): Promise<LedgerPosting | null> {
return null;
}

View File

@ -1,6 +1,5 @@
import { t } from 'fyo';
import { DocValue } from 'fyo/core/types';
import { Doc } from 'fyo/model/doc';
import {
FiltersMap,
FormulaMap,
@ -14,8 +13,8 @@ import { ModelNameEnum } from 'models/types';
import { Money } from 'pesa';
import { safeParseFloat } from 'utils/index';
import { StockMovement } from './StockMovement';
import { MovementTypeEnum } from './types';
import { TransferItem } from './TransferItem';
import { MovementTypeEnum } from './types';
export class StockMovementItem extends TransferItem {
name?: string;
@ -78,8 +77,8 @@ export class StockMovementItem extends TransferItem {
return null;
}
const defaultLocation = this.fyo.singles.InventorySettings
?.defaultLocation as string | undefined;
const defaultLocation =
this.fyo.singles.InventorySettings?.defaultLocation;
if (defaultLocation && !this.fromLocation && this.isIssue) {
return defaultLocation;
}
@ -94,8 +93,8 @@ export class StockMovementItem extends TransferItem {
return null;
}
const defaultLocation = this.fyo.singles.InventorySettings
?.defaultLocation as string | undefined;
const defaultLocation =
this.fyo.singles.InventorySettings?.defaultLocation;
if (defaultLocation && !this.toLocation && this.isReceipt) {
return defaultLocation;
}
@ -128,7 +127,7 @@ export class StockMovementItem extends TransferItem {
dependsOn: ['item', 'unit'],
},
transferQuantity: {
formula: async (fieldname) => {
formula: (fieldname) => {
if (fieldname === 'quantity' || this.unit === this.transferUnit) {
return this.quantity;
}
@ -145,7 +144,7 @@ export class StockMovementItem extends TransferItem {
const itemDoc = await this.fyo.doc.getDoc(
ModelNameEnum.Item,
this.item as string
this.item
);
const unitDoc = itemDoc.getLink('uom');
@ -217,13 +216,14 @@ export class StockMovementItem extends TransferItem {
const item = await this.fyo.db.getAll(ModelNameEnum.UOMConversionItem, {
fields: ['parent'],
filters: { uom: value as string, parent: this.item! },
filters: { uom: value as string, parent: this.item },
});
if (item.length < 1)
throw new ValidationError(
t`Transfer Unit ${value as string} is not applicable for Item ${this
.item!}`
t`Transfer Unit ${value as string} is not applicable for Item ${
this.item
}`
);
},
};

View File

@ -26,7 +26,6 @@ import {
validateBatch,
validateSerialNumber,
} from './helpers';
import { Item } from 'models/baseModels/Item/Item';
export abstract class StockTransfer extends Transfer {
name?: string;
@ -67,7 +66,7 @@ export abstract class StockTransfer extends Transfer {
static defaults: DefaultMap = {
numberSeries: (doc) => getNumberSeries(doc.schemaName, doc.fyo),
terms: (doc) => {
const defaults = doc.fyo.singles.Defaults as Defaults | undefined;
const defaults = doc.fyo.singles.Defaults;
if (doc.schemaName === ModelNameEnum.Shipment) {
return defaults?.shipmentTerms ?? '';
}

View File

@ -1,4 +1,3 @@
import { t } from 'fyo';
import { DocValue } from 'fyo/core/types';
import { Doc } from 'fyo/model/doc';
import {
@ -73,7 +72,7 @@ export class StockTransferItem extends TransferItem {
dependsOn: ['item', 'unit'],
},
transferQuantity: {
formula: async (fieldname) => {
formula: (fieldname) => {
if (fieldname === 'quantity' || this.unit === this.transferUnit) {
return this.quantity;
}
@ -90,7 +89,7 @@ export class StockTransferItem extends TransferItem {
const itemDoc = await this.fyo.doc.getDoc(
ModelNameEnum.Item,
this.item as string
this.item
);
const unitDoc = itemDoc.getLink('uom');
@ -146,7 +145,7 @@ export class StockTransferItem extends TransferItem {
dependsOn: ['rate', 'quantity'],
},
rate: {
formula: async (fieldname) => {
formula: async () => {
const rate = (await this.fyo.getValue(
'Item',
this.item as string,
@ -177,8 +176,8 @@ export class StockTransferItem extends TransferItem {
return;
}
const defaultLocation = this.fyo.singles.InventorySettings
?.defaultLocation as string | undefined;
const defaultLocation =
this.fyo.singles.InventorySettings?.defaultLocation;
if (defaultLocation && !this.location) {
return defaultLocation;
@ -195,13 +194,14 @@ export class StockTransferItem extends TransferItem {
const item = await this.fyo.db.getAll(ModelNameEnum.UOMConversionItem, {
fields: ['parent'],
filters: { uom: value as string, parent: this.item! },
filters: { uom: value as string, parent: this.item },
});
if (item.length < 1)
throw new ValidationError(
t`Transfer Unit ${value as string} is not applicable for Item ${this
.item!}`
this.fyo.t`Transfer Unit ${
value as string
} is not applicable for Item ${this.item}`
);
},
};

View File

@ -55,7 +55,7 @@ export class StockQueue {
return null;
}
let incomingRate: number = 0;
let incomingRate = 0;
this.quantity -= quantity;
let remaining = quantity;

View File

@ -37,17 +37,22 @@ interface TransferTwo extends Omit<Transfer, 'from' | 'to'> {
location: string;
}
export function getItem(name: string, rate: number, hasBatch: boolean = false, hasSerialNumber: boolean = false) {
export function getItem(
name: string,
rate: number,
hasBatch = false,
hasSerialNumber = false
) {
return { name, rate, trackItem: true, hasBatch, hasSerialNumber };
}
export async function getBatch(
export function getBatch(
schemaName: ModelNameEnum.Batch,
batch: string,
expiryDate: Date,
manufactureDate: Date,
fyo: Fyo
): Promise<Batch> {
): Batch {
const doc = fyo.doc.getNewDoc(schemaName, {
batch,
expiryDate,

View File

@ -5,7 +5,7 @@ import { codeStateMap } from 'regional/in';
export class Address extends BaseAddress {
formulas: FormulaMap = {
addressDisplay: {
formula: async () => {
formula: () => {
return [
this.addressLine1,
this.addressLine2,
@ -28,7 +28,7 @@ export class Address extends BaseAddress {
},
pos: {
formula: async () => {
formula: () => {
const stateList = Object.values(codeStateMap).sort();
const state = this.state as string;
if (stateList.includes(state)) {

View File

@ -6,6 +6,7 @@ export class Party extends BaseParty {
gstin?: string;
gstType?: GSTType;
// eslint-disable-next-line @typescript-eslint/require-await
async beforeSync() {
const gstin = this.get('gstin') as string | undefined;
const gstType = this.get('gstType') as GSTType;

View File

@ -2,23 +2,20 @@
"name": "frappe-books",
"version": "0.16.0",
"description": "Simple book-keeping app for everyone",
"main": "background.js",
"author": {
"name": "Frappe Technologies Pvt. Ltd.",
"email": "hello@frappe.io"
},
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint",
"dev": "node build/scripts/dev.mjs",
"build": "node build/scripts/build.mjs",
"release": "scripts/publish-mac-arm.sh",
"postinstall": "electron-rebuild",
"postuninstall": "electron-rebuild",
"electron:build": "vue-cli-service electron:build",
"electron:serve": "vue-cli-service electron:serve",
"script:translate": "scripts/runner.sh scripts/generateTranslations.ts",
"script:profile": "scripts/profile.sh",
"test": "scripts/test.sh"
"test": "scripts/test.sh",
"lint": "eslint . --ext ts,vue"
},
"dependencies": {
"@codemirror/autocomplete": "^6.4.2",
@ -34,12 +31,15 @@
"luxon": "^2.5.2",
"node-fetch": "2",
"pesa": "^1.1.12",
"source-map-support": "^0.5.21",
"vue": "^3.2.40",
"vue-router": "^4.0.12"
},
"devDependencies": {
"@babel/core": "^7.16.0",
"@babel/eslint-parser": "^7.16.0",
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"@lezer/common": "^1.0.0",
"@types/assert": "^1.5.6",
"@types/electron-devtools-installer": "^2.2.0",
"@types/lodash": "^4.14.179",
@ -48,30 +48,25 @@
"@types/node": "^17.0.23",
"@types/node-fetch": "^2.6.1",
"@types/tape": "^4.13.2",
"@typescript-eslint/eslint-plugin": "^4.15.1",
"@typescript-eslint/parser": "^4.15.1",
"@vue/cli-plugin-babel": "^4.5.0",
"@vue/cli-plugin-eslint": "^5.0.0-beta.7",
"@vue/cli-plugin-router": "^4.5.0",
"@vue/cli-plugin-typescript": "~4.5.0",
"@vue/cli-service": "^4.5.0",
"@vue/eslint-config-typescript": "^7.0.0",
"@typescript-eslint/eslint-plugin": "5.60.0",
"@typescript-eslint/parser": "5.60.0",
"@vitejs/plugin-vue": "^4.2.3",
"autoprefixer": "^9",
"babel-loader": "^8.2.3",
"chokidar": "^3.5.3",
"dotenv": "^16.0.0",
"electron": "18.3.7",
"electron-builder": "24.0.0-alpha.12",
"electron-builder": "^24.4.0",
"electron-devtools-installer": "^3.2.0",
"electron-rebuild": "^3.2.9",
"electron-updater": "^5.2.1",
"eslint": "^7.32.0",
"eslint": "^8.43.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-vue": "^7.0.0",
"lint-staged": "^11.2.6",
"eslint-plugin-vue": "^9.15.0",
"execa": "^7.1.1",
"fs-extra": "^11.1.1",
"postcss": "^8",
"prettier": "^2.4.1",
"raw-loader": "^4.0.2",
"tailwindcss": "npm:@tailwindcss/postcss7-compat",
"tailwindcss-rtl": "^0.9.0",
"tap-spec": "^5.0.0",
@ -80,29 +75,18 @@
"tsconfig-paths": "^3.14.1",
"tslib": "^2.3.1",
"typescript": "^4.6.2",
"vue-cli-plugin-electron-builder": "https://github.com/nklayman/vue-cli-plugin-electron-builder#ebb9183f4913f927d4e4f4eb1fbab61a960f7a09",
"webpack": "^5.76.0"
},
"resolutions": {
"electron-builder": "24.0.0-alpha.12"
"vite": "^4.3.9",
"vue-tsc": "^1.6.5",
"yargs": "^17.7.2"
},
"prettier": {
"semi": true,
"singleQuote": true,
"trailingComma": "es5"
},
"engineStrict": true,
"engines": {
"node": ">=16.13.1 <17"
},
"gitHooks": {
"pre-commit": "lint-staged"
},
"homepage": "https://frappebooks.com",
"lint-staged": {
"*.{js,vue}": "vue-cli-service lint"
},
"repository": {
"url": "https://github.com/frappe/books"
}
},
"license": "AGPL-3.0-only"
}

View File

@ -1,16 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title>Frappe Books</title>
</head>
<body>
<noscript>
<strong>We're sorry but Frappe Books doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<!-- built files will replace body.innerHTML -->
</body>
</html>

View File

@ -34,11 +34,11 @@ export const ACC_BAL_WIDTH = 1.25;
export abstract class AccountReport extends LedgerReport {
toDate?: string;
count: number = 3;
count = 3;
fromYear?: number;
toYear?: number;
consolidateColumns: boolean = false;
hideGroupAmounts: boolean = false;
consolidateColumns = false;
hideGroupAmounts = false;
periodicity: Periodicity = 'Monthly';
basedOn: BasedOn = 'Until Date';
@ -88,10 +88,7 @@ export abstract class AccountReport extends LedgerReport {
};
}
async getTotalNode(
rootNode: AccountTreeNode,
name: string
): Promise<AccountListNode> {
getTotalNode(rootNode: AccountTreeNode, name: string): AccountListNode {
const accountTree = { [rootNode.name]: rootNode };
const leafNodes = getListOfLeafNodes(accountTree) as AccountTreeNode[];
@ -153,7 +150,7 @@ export abstract class AccountReport extends LedgerReport {
): Promise<AccountNameValueMapMap> {
const accountValueMap: AccountNameValueMapMap = new Map();
if (!this.accountMap) {
await this._getAccountMap();
await this._setAndReturnAccountMap();
}
for (const account of map.keys()) {
@ -169,17 +166,17 @@ export abstract class AccountReport extends LedgerReport {
}
if (!this.accountMap?.[entry.account]) {
this._getAccountMap(true);
await this._setAndReturnAccountMap(true);
}
const totalBalance = valueMap.get(key!)?.balance ?? 0;
const totalBalance = valueMap.get(key)?.balance ?? 0;
const balance = (entry.debit ?? 0) - (entry.credit ?? 0);
const rootType = this.accountMap![entry.account]?.rootType;
if (isCredit(rootType)) {
valueMap.set(key!, { balance: totalBalance - balance });
valueMap.set(key, { balance: totalBalance - balance });
} else {
valueMap.set(key!, { balance: totalBalance + balance });
valueMap.set(key, { balance: totalBalance + balance });
}
}
accountValueMap.set(account, valueMap);
@ -189,7 +186,9 @@ export abstract class AccountReport extends LedgerReport {
}
async _getAccountTree(rangeGroupedMap: AccountNameValueMapMap) {
const accountTree = cloneDeep(await this._getAccountMap()) as AccountTree;
const accountTree = cloneDeep(
await this._setAndReturnAccountMap()
) as AccountTree;
setPruneFlagOnAccountTreeNodes(accountTree);
setValueMapOnAccountTreeNodes(accountTree, rangeGroupedMap);
@ -200,7 +199,7 @@ export abstract class AccountReport extends LedgerReport {
return accountTree;
}
async _getAccountMap(force: boolean = false) {
async _setAndReturnAccountMap(force = false) {
if (this.accountMap && !force) {
return this.accountMap;
}
@ -510,14 +509,14 @@ function updateParentAccountWithChildValues(
parentAccount.valueMap ??= new Map();
for (const key of valueMap.keys()) {
const value = parentAccount.valueMap!.get(key);
const value = parentAccount.valueMap.get(key);
const childValue = valueMap.get(key);
const map: Record<string, number> = {};
for (const key of Object.keys(childValue!)) {
map[key] = (value?.[key] ?? 0) + (childValue?.[key] ?? 0);
}
parentAccount.valueMap!.set(key, map);
parentAccount.valueMap.set(key, map);
}
return parentAccount.parentAccount!;
@ -533,7 +532,7 @@ function setChildrenOnAccountTreeNodes(accountTree: AccountTree) {
}
accountTree[ac.parentAccount].children ??= [];
accountTree[ac.parentAccount].children!.push(ac!);
accountTree[ac.parentAccount].children!.push(ac);
parentNodes.add(ac.parentAccount);
}

View File

@ -13,7 +13,7 @@ import { getMapFromList } from 'utils';
export class BalanceSheet extends AccountReport {
static title = t`Balance Sheet`;
static reportName = 'balance-sheet';
loading: boolean = false;
loading = false;
get rootTypes(): AccountRootType[] {
return [
@ -54,15 +54,15 @@ export class BalanceSheet extends AccountReport {
})
.filter((row) => !!row.rootNode);
this.reportData = await this.getReportDataFromRows(
this.reportData = this.getReportDataFromRows(
getMapFromList(rootTypeRows, 'rootType')
);
this.loading = false;
}
async getReportDataFromRows(
getReportDataFromRows(
rootTypeRows: Record<AccountRootType, RootTypeRow | undefined>
): Promise<ReportData> {
): ReportData {
const typeNameList = [
{
rootType: AccountRootTypeEnum.Asset,
@ -89,7 +89,7 @@ export class BalanceSheet extends AccountReport {
reportData.push(...row.rows);
if (row.rootNode) {
const totalNode = await this.getTotalNode(row.rootNode, totalName);
const totalNode = this.getTotalNode(row.rootNode, totalName);
const totalRow = this.getRowFromAccountListNode(totalNode);
reportData.push(totalRow);
}

View File

@ -24,11 +24,11 @@ type ReferenceType =
export class GeneralLedger extends LedgerReport {
static title = t`General Ledger`;
static reportName = 'general-ledger';
usePagination: boolean = true;
loading: boolean = false;
usePagination = true;
loading = false;
ascending: boolean = false;
reverted: boolean = false;
ascending = false;
reverted = false;
referenceType: ReferenceType = 'All';
groupBy: 'none' | 'party' | 'account' | 'referenceName' = 'none';
_rawData: LedgerEntry[] = [];
@ -37,7 +37,7 @@ export class GeneralLedger extends LedgerReport {
super(fyo);
}
async setDefaultFilters() {
setDefaultFilters() {
if (!this.toDate) {
this.toDate = DateTime.now().plus({ days: 1 }).toISODate();
this.fromDate = DateTime.now().minus({ years: 1 }).toISODate();
@ -239,7 +239,7 @@ export class GeneralLedger extends LedgerReport {
return { totalDebit, totalCredit };
}
async _getQueryFilters(): Promise<QueryFilter> {
_getQueryFilters(): QueryFilter {
const filters: QueryFilter = {};
const stringFilters = ['account', 'party', 'referenceName'];

View File

@ -17,9 +17,9 @@ export abstract class BaseGSTR extends Report {
toDate?: string;
fromDate?: string;
transferType?: TransferType;
usePagination: boolean = true;
usePagination = true;
gstrRows?: GSTRRow[];
loading: boolean = false;
loading = false;
abstract gstrType: GSTRType;
@ -111,7 +111,7 @@ export abstract class BaseGSTR extends Report {
return (row) => row.rate === 0; // this takes care of both nil rated, exempted goods
}
return (_) => true;
return () => true;
}
async getEntries() {
@ -133,7 +133,7 @@ export abstract class BaseGSTR extends Report {
const entries = await this.getEntries();
const gstrRows: GSTRRow[] = [];
for (const entry of entries) {
const gstrRow = await this.getGstrRow(entry.name as string);
const gstrRow = await this.getGstrRow(entry.name);
gstrRows.push(gstrRow);
}
return gstrRows;
@ -149,7 +149,7 @@ export abstract class BaseGSTR extends Report {
'gstin'
)) as string | null;
const party = (await this.fyo.doc.getDoc('Party', entry.party!)) as Party;
const party = (await this.fyo.doc.getDoc('Party', entry.party)) as Party;
let place = '';
if (party.address) {
@ -216,7 +216,7 @@ export abstract class BaseGSTR extends Report {
}
}
async setDefaultFilters() {
setDefaultFilters() {
if (!this.toDate) {
this.toDate = DateTime.local().toISODate();
}

View File

@ -175,7 +175,7 @@ async function getCanExport(report: BaseGSTR) {
return true;
}
showDialog({
await showDialog({
title: report.fyo.t`Cannot Export`,
detail: report.fyo.t`Please set GSTIN in General Settings.`,
type: 'error',
@ -204,7 +204,7 @@ export async function getGstrJsonData(report: BaseGSTR): Promise<string> {
} else if (transferType === TransferTypeEnum.B2CL) {
gstData.b2cl = await generateB2clData(report);
} else if (transferType === TransferTypeEnum.B2CS) {
gstData.b2cs = await generateB2csData(report);
gstData.b2cs = generateB2csData(report);
}
return JSON.stringify(gstData);

View File

@ -14,7 +14,7 @@ export abstract class LedgerReport extends Report {
static reportName = 'general-ledger';
_rawData: LedgerEntry[] = [];
shouldRefresh: boolean = false;
shouldRefresh = false;
constructor(fyo: Fyo) {
super(fyo);
@ -117,7 +117,7 @@ export abstract class LedgerReport extends Report {
});
}
abstract _getQueryFilters(): Promise<QueryFilter>;
abstract _getQueryFilters(): QueryFilter | Promise<QueryFilter>;
getActions(): Action[] {
return getCommonExportActions(this);

View File

@ -17,7 +17,7 @@ import {
export class ProfitAndLoss extends AccountReport {
static title = t`Profit And Loss`;
static reportName = 'profit-and-loss';
loading: boolean = false;
loading = false;
get rootTypes(): AccountRootType[] {
return [AccountRootTypeEnum.Income, AccountRootTypeEnum.Expense];
@ -62,7 +62,7 @@ export class ProfitAndLoss extends AccountReport {
const expenseList = convertAccountRootNodeToAccountList(expenseRoot);
const expenseRows = this.getReportRowsFromAccountList(expenseList);
this.reportData = await this.getReportDataFromRows(
this.reportData = this.getReportDataFromRows(
incomeRows,
expenseRows,
incomeRoot,
@ -71,14 +71,14 @@ export class ProfitAndLoss extends AccountReport {
this.loading = false;
}
async getReportDataFromRows(
getReportDataFromRows(
incomeRows: ReportData,
expenseRows: ReportData,
incomeRoot: AccountTreeNode | undefined,
expenseRoot: AccountTreeNode | undefined
): Promise<ReportData> {
): ReportData {
if (incomeRoot && !expenseRoot) {
return await this.getIncomeOrExpenseRows(
return this.getIncomeOrExpenseRows(
incomeRoot,
incomeRows,
t`Total Income (Credit)`
@ -86,7 +86,7 @@ export class ProfitAndLoss extends AccountReport {
}
if (expenseRoot && !incomeRoot) {
return await this.getIncomeOrExpenseRows(
return this.getIncomeOrExpenseRows(
expenseRoot,
expenseRows,
t`Total Income (Credit)`
@ -97,7 +97,7 @@ export class ProfitAndLoss extends AccountReport {
return [];
}
return await this.getIncomeAndExpenseRows(
return this.getIncomeAndExpenseRows(
incomeRows,
expenseRows,
incomeRoot,
@ -105,30 +105,27 @@ export class ProfitAndLoss extends AccountReport {
);
}
async getIncomeOrExpenseRows(
getIncomeOrExpenseRows(
root: AccountTreeNode,
rows: ReportData,
totalRowName: string
): Promise<ReportData> {
const total = await this.getTotalNode(root, totalRowName);
): ReportData {
const total = this.getTotalNode(root, totalRowName);
const totalRow = this.getRowFromAccountListNode(total);
return [rows, totalRow].flat();
}
async getIncomeAndExpenseRows(
getIncomeAndExpenseRows(
incomeRows: ReportData,
expenseRows: ReportData,
incomeRoot: AccountTreeNode,
expenseRoot: AccountTreeNode
) {
const totalIncome = await this.getTotalNode(
incomeRoot,
t`Total Income (Credit)`
);
const totalIncome = this.getTotalNode(incomeRoot, t`Total Income (Credit)`);
const totalIncomeRow = this.getRowFromAccountListNode(totalIncome);
const totalExpense = await this.getTotalNode(
const totalExpense = this.getTotalNode(
expenseRoot,
t`Total Expense (Debit)`
);

View File

@ -10,14 +10,14 @@ import { ColumnField, ReportData } from './types';
export abstract class Report extends Observable<RawValue> {
static title: string;
static reportName: string;
static isInventory: boolean = false;
static isInventory = false;
fyo: Fyo;
columns: ColumnField[] = [];
filters: Field[] = [];
reportData: ReportData;
usePagination: boolean = false;
shouldRefresh: boolean = false;
usePagination = false;
shouldRefresh = false;
abstract loading: boolean;
constructor(fyo: Fyo) {
@ -26,14 +26,12 @@ export abstract class Report extends Observable<RawValue> {
this.reportData = [];
}
get title() {
// @ts-ignore
return this.constructor.title;
get title(): string {
return (this.constructor as typeof Report).title;
}
get reportName() {
// @ts-ignore
return this.constructor.reportName;
get reportName(): string {
return (this.constructor as typeof Report).reportName;
}
async initialize() {
@ -61,7 +59,7 @@ export abstract class Report extends Observable<RawValue> {
return filterMap;
}
async set(key: string, value: DocValue, callPostSet: boolean = true) {
async set(key: string, value: DocValue, callPostSet = true) {
const field = this.filters.find((f) => f.fieldname === key);
if (field === undefined) {
return;
@ -95,7 +93,7 @@ export abstract class Report extends Observable<RawValue> {
* Should first check if filter value is set
* and update only if it is not set.
*/
async setDefaultFilters() {}
abstract setDefaultFilters(): void | Promise<void>;
abstract getActions(): Action[];
abstract getFilters(): Field[] | Promise<Field[]>;
abstract getColumns(): ColumnField[] | Promise<ColumnField[]>;

View File

@ -35,8 +35,8 @@ export class TrialBalance extends AccountReport {
fromDate?: string;
toDate?: string;
hideGroupAmounts: boolean = false;
loading: boolean = false;
hideGroupAmounts = false;
loading = false;
_rawData: LedgerEntry[] = [];
_dateRanges?: DateRange[];
@ -79,6 +79,7 @@ export class TrialBalance extends AccountReport {
this.loading = false;
}
// eslint-disable-next-line @typescript-eslint/require-await
async getReportDataFromRows(
rootTypeRows: RootTypeRow[]
): Promise<ReportData> {
@ -93,6 +94,7 @@ export class TrialBalance extends AccountReport {
return reportData;
}
// eslint-disable-next-line @typescript-eslint/require-await
async _getGroupedByDateRanges(
map: GroupedMap
): Promise<AccountNameValueMapMap> {
@ -108,11 +110,11 @@ export class TrialBalance extends AccountReport {
const key = this._getRangeMapKey(entry);
if (key === null) {
throw new ValueError(
`invalid entry in trial balance ${entry.date?.toISOString()}`
`invalid entry in trial balance ${entry.date?.toISOString() ?? ''}`
);
}
const map = valueMap.get(key!);
const map = valueMap.get(key);
const totalCredit = map?.credit ?? 0;
const totalDebit = map?.debit ?? 0;
@ -188,6 +190,7 @@ export class TrialBalance extends AccountReport {
} as ReportRow;
}
// eslint-disable-next-line @typescript-eslint/require-await
async _getQueryFilters(): Promise<QueryFilter> {
const filters: QueryFilter = {};
filters.reverted = false;

View File

@ -1,4 +1,4 @@
import { Fyo, t } from 'fyo';
import { t } from 'fyo';
import { Action } from 'fyo/model/types';
import { Verb } from 'fyo/telemetry/types';
import { getSavePath, saveData, showExportInFolder } from 'src/utils/ipcCalls';
@ -88,7 +88,7 @@ function getJsonData(report: Report): string {
}
const rowObj: Record<string, unknown> = {};
for (const c in row.cells) {
for (let c = 0; c < row.cells.length; c++) {
const { label } = columns[c];
const cell = getValueFromCell(row.cells[c], displayPrecision);
rowObj[label] = cell;
@ -129,7 +129,7 @@ function convertReportToCSVMatrix(report: Report): unknown[][] {
const displayPrecision =
(report.fyo.singles.SystemSettings?.displayPrecision as number) ?? 2;
const reportData = report.reportData;
const columns = report.columns!;
const columns = report.columns;
const csvdata: unknown[][] = [];
csvdata.push(columns.map((c) => c.label));
@ -140,7 +140,7 @@ function convertReportToCSVMatrix(report: Report): unknown[][] {
}
const csvrow: unknown[] = [];
for (const c in row.cells) {
for (let c = 0; c < row.cells.length; c++) {
const cell = getValueFromCell(row.cells[c], displayPrecision);
csvrow.push(cell);
}

View File

@ -13,9 +13,9 @@ export class StockBalance extends StockLedger {
static reportName = 'stock-balance';
static isInventory = true;
override ascending: boolean = true;
override ascending = true;
override referenceType: ReferenceType = 'All';
override referenceName: string = '';
override referenceName = '';
override async _getReportData(force?: boolean): Promise<ReportData> {
if (this.shouldRefresh || force || !this._rawData?.length) {

View File

@ -19,11 +19,11 @@ export class StockLedger extends Report {
static reportName = 'stock-ledger';
static isInventory = true;
usePagination: boolean = true;
usePagination = true;
_rawData?: ComputedStockLedgerEntry[];
loading: boolean = false;
shouldRefresh: boolean = false;
loading = false;
shouldRefresh = false;
item?: string;
location?: string;
@ -52,7 +52,7 @@ export class StockLedger extends Report {
this._setObservers();
}
async setDefaultFilters() {
setDefaultFilters() {
if (!this.toDate) {
this.toDate = DateTime.now().plus({ days: 1 }).toISODate();
this.fromDate = DateTime.now().minus({ years: 1 }).toISODate();
@ -91,9 +91,8 @@ export class StockLedger extends Report {
async _setRawData() {
const valuationMethod =
(this.fyo.singles.InventorySettings?.valuationMethod as
| ValuationMethod
| undefined) ?? ValuationMethod.FIFO;
this.fyo.singles.InventorySettings?.valuationMethod ??
ValuationMethod.FIFO;
const rawSLEs = await getRawStockLedgerEntries(this.fyo);
this._rawData = getStockLedgerEntries(rawSLEs, valuationMethod);
@ -113,7 +112,7 @@ export class StockLedger extends Report {
}
let i = 0;
for (const idx in rawData) {
for (let idx = 0; idx < rawData.length; idx++) {
const row = rawData[idx];
if (this.item && row.item !== this.item) {
continue;

View File

@ -12,7 +12,7 @@ const NAME_FIELD = {
readOnly: true,
};
export function getSchemas(countryCode: string = '-'): Readonly<SchemaMap> {
export function getSchemas(countryCode = '-'): Readonly<SchemaMap> {
const builtCoreSchemas = getCoreSchemas();
const builtAppSchemas = getAppSchemas(countryCode);
@ -209,14 +209,14 @@ export function getAbstractCombinedSchemas(schemas: SchemaStubMap): SchemaMap {
for (const name of extendingSchemaNames) {
const extendingSchema = schemas[name] as Schema;
const abstractSchema = schemas[extendingSchema.extends!] as SchemaStub;
const abstractSchema = schemas[extendingSchema.extends!];
schemaMap[name] = getCombined(extendingSchema, abstractSchema) as Schema;
}
for (const name in abstractSchemaNames) {
abstractSchemaNames.forEach((name) => {
delete schemaMap[name];
}
});
return schemaMap;
}

View File

@ -8,6 +8,8 @@ import {
schemaTranslateables,
} from '../utils/translationHelpers';
/* eslint-disable no-console, @typescript-eslint/no-floating-promises */
const translationsFolder = path.resolve(__dirname, '..', 'translations');
const PATTERN = /(?<!\w)t`([^`]+)`/g;
@ -21,7 +23,7 @@ function shouldIgnore(p: string, ignoreList: string[]): boolean {
async function getFileList(
root: string,
ignoreList: string[],
extPattern: RegExp = /\.(js|ts|vue)$/
extPattern = /\.(js|ts|vue)$/
): Promise<string[]> {
const contents: string[] = await fs.readdir(root);
const files: string[] = [];
@ -86,7 +88,7 @@ function getTStrings(content: string): Promise<string[]> {
}
function tStringFinder(content: string): string[] {
return [...content.matchAll(PATTERN)].map(([_, t]) => {
return [...content.matchAll(PATTERN)].map(([, t]) => {
t = getIndexFormat(t);
return getWhitespaceSanitized(t);
});

View File

@ -19,4 +19,5 @@ async function run() {
await unlink(dbPath);
}
// eslint-disable-next-line @typescript-eslint/no-floating-promises
run();

View File

@ -3,14 +3,10 @@
set -e
# Check node and yarn versions
NODE_VERSION=$(node --version)
YARN_VERSION=$(yarn --version)
if [ "$YARN_VERSION" != "1.22.18" ]; then
echo "Incorrect yarn version: $YARN_VERSION"
exit 1
elif [ "$NODE_VERSION" != "v16.13.1" ]; then
echo "Incorrect node version: $NODE_VERSION"
exit 1
fi
# Source secrets
@ -42,6 +38,6 @@ export GH_TOKEN=$GH_TOKEN &&
export APPLE_ID=$APPLE_ID &&
export APPLE_TEAM_ID=$APPLE_TEAM_ID &&
export APPLE_APP_SPECIFIC_PASSWORD=$APPLE_APP_SPECIFIC_PASSWORD &&
yarn electron:build --mac --publish=always
yarn build --mac --publish=always
cd ../books

View File

@ -12,13 +12,13 @@
/>
<!-- Main Contents -->
<Desk
class="flex-1"
v-if="activeScreen === 'Desk'"
class="flex-1"
@change-db-file="showDbSelector"
/>
<DatabaseSelector
ref="databaseSelector"
v-if="activeScreen === 'DatabaseSelector'"
ref="databaseSelector"
@file-selected="fileSelected"
/>
<SetupWizard
@ -36,7 +36,6 @@
</div>
</template>
<script lang="ts">
import { ConfigKeys } from 'fyo/core/types';
import { RTL_LANGUAGES } from 'fyo/utils/consts';
import { ModelNameEnum } from 'models/types';
import { systemLanguageRef } from 'src/utils/refs';
@ -69,6 +68,12 @@ enum Screen {
export default defineComponent({
name: 'App',
components: {
Desk,
SetupWizard,
DatabaseSelector,
WindowsTitleBar,
},
setup() {
const keys = useKeys();
const searcher: Ref<null | Search> = ref(null);
@ -105,31 +110,22 @@ export default defineComponent({
companyName: string;
};
},
components: {
Desk,
SetupWizard,
DatabaseSelector,
WindowsTitleBar,
},
async mounted() {
this.setInitialScreen();
},
watch: {
language(value) {
this.languageDirection = getLanguageDirection(value);
},
},
computed: {
language(): string {
return systemLanguageRef.value;
},
},
watch: {
language(value: string) {
this.languageDirection = getLanguageDirection(value);
},
},
async mounted() {
await this.setInitialScreen();
},
methods: {
async setInitialScreen(): Promise<void> {
const lastSelectedFilePath = fyo.config.get(
ConfigKeys.LastSelectedFilePath,
null
);
const lastSelectedFilePath = fyo.config.get('lastSelectedFilePath', null);
if (
typeof lastSelectedFilePath !== 'string' ||
@ -159,7 +155,7 @@ export default defineComponent({
updateConfigFiles(fyo);
},
async fileSelected(filePath: string, isNew?: boolean): Promise<void> {
fyo.config.set(ConfigKeys.LastSelectedFilePath, filePath);
fyo.config.set('lastSelectedFilePath', filePath);
if (isNew) {
this.activeScreen = Screen.SetupWizard;
return;
@ -173,7 +169,7 @@ export default defineComponent({
}
},
async setupComplete(setupWizardOptions: SetupWizardOptions): Promise<void> {
const filePath = fyo.config.get(ConfigKeys.LastSelectedFilePath);
const filePath = fyo.config.get('lastSelectedFilePath');
if (typeof filePath !== 'string') {
return;
}
@ -213,7 +209,8 @@ export default defineComponent({
}
if (actionSymbol === dbErrorActionSymbols.SelectFile) {
return await this.databaseSelector?.existingDatabase();
await this.databaseSelector?.existingDatabase();
return;
}
throw error;
@ -223,9 +220,9 @@ export default defineComponent({
const { hideGetStarted } = await fyo.doc.getDoc('SystemSettings');
if (hideGetStarted || onboardingComplete) {
routeTo('/');
await routeTo('/');
} else {
routeTo('/get-started');
await routeTo('/get-started');
}
},
async showDbSelector(): Promise<void> {

View File

@ -1,5 +1,8 @@
<template>
<div class="inline-block rounded-md px-2 py-1 truncate select-none" :class="colorClass">
<div
class="inline-block rounded-md px-2 py-1 truncate select-none"
:class="colorClass"
>
<slot></slot>
</div>
</template>

View File

@ -37,9 +37,9 @@
<!-- x Labels -->
<template v-if="xLabels.length > 0">
<text
:style="fontStyle"
v-for="(i, j) in count"
:key="j + '-xlabels'"
:style="fontStyle"
:y="
viewBoxHeight -
axisPadding +
@ -57,9 +57,9 @@
<!-- y Labels -->
<template v-if="yLabelDivisions > 0">
<text
:style="fontStyle"
v-for="(i, j) in yLabelDivisions + 1"
:key="j + '-ylabels'"
:style="fontStyle"
:y="yScalerLocation(i - 1)"
:x="axisPadding - xLabelOffset + left"
text-anchor="end"
@ -92,10 +92,10 @@
:width="width"
:height="rec.height"
:fill="rec.color"
clip-path="url(#positive-rect-clip)"
@mouseenter="() => create(rec.xi, rec.yi)"
@mousemove="update"
@mouseleave="destroy"
clip-path="url(#positive-rect-clip)"
/>
<rect
@ -108,10 +108,10 @@
:width="width"
:height="rec.height"
:fill="rec.color"
clip-path="url(#negative-rect-clip)"
@mouseenter="() => create(rec.xi, rec.yi)"
@mousemove="update"
@mouseleave="destroy"
clip-path="url(#negative-rect-clip)"
/>
</svg>
<Tooltip
@ -137,6 +137,7 @@ import { prefixFormat } from 'src/utils/chart';
import Tooltip from '../Tooltip.vue';
export default {
components: { Tooltip },
props: {
skipXLabel: { type: Number, default: 2 },
colors: { type: Array, default: () => [] },
@ -171,6 +172,9 @@ export default {
tooltipDispDistThreshold: { type: Number, default: 20 },
drawZeroLine: { type: Boolean, default: true },
},
data() {
return { xi: -1, yi: -1, activeColor: 'rgba(0, 0, 0, 0.2)' };
},
computed: {
fontStyle() {
return { fontSize: this.fontSize, fill: this.fontColor };
@ -263,9 +267,6 @@ export default {
return hMax;
},
},
data() {
return { xi: -1, yi: -1, activeColor: 'rgba(0, 0, 0, 0.2)' };
},
methods: {
gradY(i) {
return Math.min(...this.ys[i]).toFixed();
@ -351,7 +352,6 @@ export default {
this.$refs.tooltip.destroy();
},
},
components: { Tooltip },
};
</script>

View File

@ -22,12 +22,6 @@
:cx="cx"
:cy="cy"
:r="radius"
@mouseover="
$emit(
'change',
thetasAndStarts.length === 1 ? thetasAndStarts[0][0] : null
)
"
:stroke-width="
thickness +
(hasNonZeroValues && active === thetasAndStarts[0][0] ? 4 : 0)
@ -38,12 +32,18 @@
:class="hasNonZeroValues ? 'sector' : ''"
:style="{ transformOrigin: `${cx}px ${cy}px` }"
fill="transparent"
@mouseover="
$emit(
'change',
thetasAndStarts.length === 1 ? thetasAndStarts[0][0] : null
)
"
/>
<template v-if="thetasAndStarts.length > 1">
<path
clip-path="url(#donut-hole)"
v-for="[i, theta, start_] in thetasAndStarts"
:key="i"
clip-path="url(#donut-hole)"
:d="getArcPath(cx, cy, radius, start_, theta)"
:stroke="sectors[i].color"
:stroke-width="thickness + (active === i ? 4 : 0)"

View File

@ -28,9 +28,9 @@
<!-- x Labels -->
<template v-if="drawLabels && xLabels.length > 0">
<text
:style="fontStyle"
v-for="(i, j) in count"
:key="j + '-xlabels'"
:style="fontStyle"
:y="
viewBoxHeight -
axisPadding +
@ -48,9 +48,9 @@
<!-- y Labels -->
<template v-if="drawLabels && yLabelDivisions > 0">
<text
:style="fontStyle"
v-for="(i, j) in yLabelDivisions + 1"
:key="j + '-ylabels'"
:style="fontStyle"
:y="yScalerLocation(i - 1)"
:x="axisPadding - xLabelOffset + left"
text-anchor="end"
@ -67,7 +67,7 @@
<stop offset="70%" stop-color="rgba(255, 255, 255, 0)" />
</linearGradient>
<mask v-for="(i, j) in num" :key="j + '-mask'" :id="'rect-mask-' + i">
<mask v-for="(i, j) in num" :id="'rect-mask-' + i" :key="j + '-mask'">
<rect
x="0"
:y="gradY(j)"
@ -136,6 +136,7 @@ import { euclideanDistance, prefixFormat } from 'src/utils/chart';
import Tooltip from '../Tooltip.vue';
export default {
components: { Tooltip },
props: {
colors: { type: Array, default: () => [] },
xLabels: { type: Array, default: () => [] },
@ -168,6 +169,9 @@ export default {
tooltipDispDistThreshold: { type: Number, default: 40 },
showTooltip: { type: Boolean, default: true },
},
data() {
return { cx: -1, cy: -1, xi: -1, yi: -1 };
},
computed: {
fontStyle() {
return { fontSize: this.fontSize, fill: this.fontColor };
@ -249,9 +253,6 @@ export default {
return hMax;
},
},
data() {
return { cx: -1, cy: -1, xi: -1, yi: -1 };
},
methods: {
gradY(i) {
return Math.min(...this.ys[i]).toFixed();
@ -339,6 +340,5 @@ export default {
return { xi, yi, cx: px, cy: pys[yi], d: minDist };
},
},
components: { Tooltip },
};
</script>

View File

@ -2,15 +2,15 @@
<div
class="relative bg-white border flex-center overflow-hidden group"
:class="{
'rounded': size === 'form',
rounded: size === 'form',
'w-20 h-20 rounded-full': size !== 'small' && size !== 'form',
'w-12 h-12 rounded-full': size === 'small',
}"
:title="df?.label"
:style="imageSizeStyle"
>
<img :src="value" v-if="value" />
<div :class="[!isReadOnly ? 'group-hover:opacity-90' : '']" v-else>
<img v-if="value" :src="value" />
<div v-else :class="[!isReadOnly ? 'group-hover:opacity-90' : '']">
<div
v-if="letterPlaceholder"
class="
@ -65,12 +65,24 @@ import Base from './Base.vue';
export default defineComponent({
name: 'AttachImage',
components: { FeatherIcon },
extends: Base,
props: {
letterPlaceholder: { type: String, default: '' },
value: { type: String, default: '' },
df: { type: Object as PropType<Field> },
},
computed: {
imageSizeStyle() {
if (this.size === 'form') {
return { width: '135px', height: '135px' };
}
return {};
},
shouldClear() {
return !!this.value;
},
},
methods: {
async handleClick() {
if (this.value) {
@ -106,17 +118,5 @@ export default defineComponent({
this.triggerChange(dataURL);
},
},
computed: {
imageSizeStyle() {
if (this.size === 'form') {
return { width: '135px', height: '135px' };
}
return {};
},
shouldClear() {
return !!this.value;
},
},
components: { FeatherIcon },
});
</script>

Some files were not shown because too many files have changed in this diff Show More