2
0
mirror of https://github.com/frappe/books.git synced 2025-02-02 12:08:27 +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 = { module.exports = {
root: true, root: true,
env: { env: {
node: true, node: true,
browser: true,
es2018: true,
}, },
rules: { rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', 'no-console': 'warn',
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', 'no-debugger': 'warn',
'arrow-body-style': 'off', 'arrow-body-style': 'off',
'prefer-arrow-callback': 'off', 'prettier/prettier': 'warn',
'prefer-arrow-callback': 'warn',
'vue/no-mutating-props': 'off', 'vue/no-mutating-props': 'off',
'vue/multi-word-component-names': 'off', 'vue/multi-word-component-names': 'off',
'vue/no-useless-template-attributes': 'off', 'vue/no-useless-template-attributes': 'off',
'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: { parserOptions: {
parser: '@typescript-eslint/parser', parser: '@typescript-eslint/parser',
project: true,
tsconfigRootDir: __dirname,
sourceType: 'module',
extraFileExtensions: ['.vue'],
}, },
plugins: ['@typescript-eslint', 'prettier'],
extends: ['plugin:vue/vue3-essential', '@vue/typescript'], 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 - name: Setup node
uses: actions/setup-node@v2 uses: actions/setup-node@v2
with: with:
node-version: '16.13.1' node-version: '16.14.0'
- name: Set yarn version - name: Set yarn version
run: yarn set version 1.22.18 run: yarn set version 1.22.18
@ -28,4 +28,4 @@ jobs:
env: env:
CSC_IDENTITY_AUTO_DISCOVERY: false CSC_IDENTITY_AUTO_DISCOVERY: false
APPLE_NOTARIZE: 0 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 - name: Setup node
uses: actions/setup-node@v2 uses: actions/setup-node@v2
with: with:
node-version: '16.13.1' node-version: '16.14.0'
- name: Checkout Books - name: Checkout Books
uses: actions/checkout@v2 uses: actions/checkout@v2
@ -42,7 +42,7 @@ jobs:
GH_TOKEN: ${{ secrets.GH_TOKEN }} GH_TOKEN: ${{ secrets.GH_TOKEN }}
run: | run: |
yarn set version 1.22.18 yarn set version 1.22.18
yarn electron:build --mac --publish always yarn build --mac --publish always
- name: Tar files - name: Tar files
run: tar -cvf dist-macOS.tar dist_electron run: tar -cvf dist-macOS.tar dist_electron
@ -59,7 +59,7 @@ jobs:
- name: Setup node - name: Setup node
uses: actions/setup-node@v2 uses: actions/setup-node@v2
with: with:
node-version: '16.13.1' node-version: '16.14.0'
- name: Checkout Books - name: Checkout Books
uses: actions/checkout@v2 uses: actions/checkout@v2
@ -86,7 +86,7 @@ jobs:
GH_TOKEN: ${{ secrets.GH_TOKEN }} GH_TOKEN: ${{ secrets.GH_TOKEN }}
run: | run: |
yarn set version 1.22.18 yarn set version 1.22.18
yarn electron:build --linux --publish always yarn build --linux --publish always
- name: Tar files - name: Tar files
run: tar -cvf dist-linux.tar dist_electron run: tar -cvf dist-linux.tar dist_electron
@ -107,7 +107,7 @@ jobs:
- name: Setup node - name: Setup node
uses: actions/setup-node@v2 uses: actions/setup-node@v2
with: with:
node-version: '16.13.1' node-version: '16.14.0'
- name: Checkout Books - name: Checkout Books
uses: actions/checkout@v2 uses: actions/checkout@v2
@ -132,7 +132,7 @@ jobs:
WIN_CSC_LINK: ${{ secrets.WIN_CSC_LINK }} WIN_CSC_LINK: ${{ secrets.WIN_CSC_LINK }}
WIN_CSC_KEY_PASSWORD: ${{ secrets.WIN_CSC_KEY_PASSWORD }} WIN_CSC_KEY_PASSWORD: ${{ secrets.WIN_CSC_KEY_PASSWORD }}
GH_TOKEN: ${{ secrets.GH_TOKEN }} GH_TOKEN: ${{ secrets.GH_TOKEN }}
run: yarn electron:build --win --publish always run: yarn build --win --publish always
- name: Tar files - name: Tar files
run: tar -cvf dist-windows.tar dist_electron run: tar -cvf dist-windows.tar dist_electron

View File

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

View File

@ -60,7 +60,7 @@ a local SQLite file as the database.
### Pre-requisites ### Pre-requisites
To get the dev environment up and running you need to first set up Node.js version 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). [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). 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 ```bash
# start the electron app # 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 #### Build
To build Frappe Books and create an installer: To build Frappe Books and create an installer:
```bash ```bash
# start the electron app # 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 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 architecture. To build for other environments (example: for linux from a windows
computer) check the _Building_ section at computer) check the _Building_ section at
[electron.build/cli](https://www.electron.build/cli). [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 ## 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 { ModelNameEnum } from '../../models/types';
import DatabaseCore from './core'; import DatabaseCore from './core';
import { BespokeFunction } from './types'; import { BespokeFunction } from './types';
@ -43,7 +50,7 @@ export class BespokeQueries {
.groupBy('account') .groupBy('account')
.orderBy('total', 'desc') .orderBy('total', 'desc')
.limit(5); .limit(5);
return topExpenses; return topExpenses as TopExpenses;
} }
static async getTotalOutstanding( static async getTotalOutstanding(
@ -52,13 +59,13 @@ export class BespokeQueries {
fromDate: string, fromDate: string,
toDate: string toDate: string
) { ) {
return await db.knex!(schemaName) return (await db.knex!(schemaName)
.sum({ total: 'baseGrandTotal' }) .sum({ total: 'baseGrandTotal' })
.sum({ outstanding: 'outstandingAmount' }) .sum({ outstanding: 'outstandingAmount' })
.where('submitted', true) .where('submitted', true)
.where('cancelled', false) .where('cancelled', false)
.whereBetween('date', [fromDate, toDate]) .whereBetween('date', [fromDate, toDate])
.first(); .first()) as TotalOutstanding;
} }
static async getCashflow(db: DatabaseCore, fromDate: string, toDate: string) { static async getCashflow(db: DatabaseCore, fromDate: string, toDate: string) {
@ -67,7 +74,7 @@ export class BespokeQueries {
.where('accountType', 'in', ['Cash', 'Bank']) .where('accountType', 'in', ['Cash', 'Bank'])
.andWhere('isGroup', false); .andWhere('isGroup', false);
const dateAsMonthYear = db.knex!.raw(`strftime('%Y-%m', ??)`, 'date'); const dateAsMonthYear = db.knex!.raw(`strftime('%Y-%m', ??)`, 'date');
return await db.knex!('AccountingLedgerEntry') return (await db.knex!('AccountingLedgerEntry')
.where('reverted', false) .where('reverted', false)
.sum({ .sum({
inflow: 'debit', inflow: 'debit',
@ -78,7 +85,7 @@ export class BespokeQueries {
}) })
.where('account', 'in', cashAndBankAccounts) .where('account', 'in', cashAndBankAccounts)
.whereBetween('date', [fromDate, toDate]) .whereBetween('date', [fromDate, toDate])
.groupBy(dateAsMonthYear); .groupBy(dateAsMonthYear)) as Cashflow;
} }
static async getIncomeAndExpenses( static async getIncomeAndExpenses(
@ -86,7 +93,7 @@ export class BespokeQueries {
fromDate: string, fromDate: string,
toDate: 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 select sum(cast(credit as real) - cast(debit as real)) as balance, strftime('%Y-%m', date) as yearmonth
from AccountingLedgerEntry from AccountingLedgerEntry
@ -100,9 +107,9 @@ export class BespokeQueries {
) )
group by yearmonth`, group by yearmonth`,
[fromDate, toDate] [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 select sum(cast(debit as real) - cast(credit as real)) as balance, strftime('%Y-%m', date) as yearmonth
from AccountingLedgerEntry from AccountingLedgerEntry
@ -116,20 +123,20 @@ export class BespokeQueries {
) )
group by yearmonth`, group by yearmonth`,
[fromDate, toDate] [fromDate, toDate]
); )) as IncomeExpense['expense'];
return { income, expense }; return { income, expense };
} }
static async getTotalCreditAndDebit(db: DatabaseCore) { static async getTotalCreditAndDebit(db: DatabaseCore) {
return await db.knex!.raw(` return (await db.knex!.raw(`
select select
account, account,
sum(cast(credit as real)) as totalCredit, sum(cast(credit as real)) as totalCredit,
sum(cast(debit as real)) as totalDebit sum(cast(debit as real)) as totalDebit
from AccountingLedgerEntry from AccountingLedgerEntry
group by account group by account
`); `)) as unknown as TotalCreditAndDebit;
} }
static async getStockQuantity( static async getStockQuantity(
@ -141,6 +148,7 @@ export class BespokeQueries {
batch?: string, batch?: string,
serialNumbers?: string[] serialNumbers?: string[]
): Promise<number | null> { ): Promise<number | null> {
/* eslint-disable @typescript-eslint/no-floating-promises */
const query = db.knex!(ModelNameEnum.StockLedgerEntry) const query = db.knex!(ModelNameEnum.StockLedgerEntry)
.sum('quantity') .sum('quantity')
.where('item', item); .where('item', item);

View File

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

View File

@ -204,7 +204,7 @@ export async function assertDoesNotThrow(
throw new assert.AssertionError({ throw new assert.AssertionError({
message: `Got unwanted exception${ message: `Got unwanted exception${
message ? `: ${message}` : '' 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, error: unknown,
more?: Record<string, 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) { export async function checkFileAccess(filePath: string, mode?: number) {

View File

@ -1,5 +1,6 @@
import { DatabaseManager } from '../database/manager'; import { DatabaseManager } from '../database/manager';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async function execute(dm: DatabaseManager) { async function execute(dm: DatabaseManager) {
/** /**
* Execute function will receive the DatabaseManager which is to be used * 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 sourceKnex = dm.db!.knex!;
const version = ( const version = (
await sourceKnex('SingleValue') (await sourceKnex('SingleValue')
.select('value') .select('value')
.where({ fieldname: 'version' }) .where({ fieldname: 'version' })) as { value: string }[]
)?.[0]?.value; )?.[0]?.value;
/** /**
@ -58,7 +58,7 @@ async function execute(dm: DatabaseManager) {
await copyData(sourceKnex, destDm); await copyData(sourceKnex, destDm);
} catch (err) { } catch (err) {
const destPath = destDm.db!.dbPath; const destPath = destDm.db!.dbPath;
destDm.db!.close(); await destDm.db!.close();
await fs.unlink(destPath); await fs.unlink(destPath);
throw err; throw err;
} }
@ -94,7 +94,7 @@ async function replaceDatabaseCore(
async function copyData(sourceKnex: Knex, destDm: DatabaseManager) { async function copyData(sourceKnex: Knex, destDm: DatabaseManager) {
const destKnex = destDm.db!.knex!; const destKnex = destDm.db!.knex!;
const schemaMap = destDm.getSchemaMap(); const schemaMap = destDm.getSchemaMap();
await destKnex!.raw('PRAGMA foreign_keys=OFF'); await destKnex.raw('PRAGMA foreign_keys=OFF');
await copySingleValues(sourceKnex, destKnex, schemaMap); await copySingleValues(sourceKnex, destKnex, schemaMap);
await copyParty(sourceKnex, destKnex, schemaMap[ModelNameEnum.Party]!); await copyParty(sourceKnex, destKnex, schemaMap[ModelNameEnum.Party]!);
await copyItem(sourceKnex, destKnex, schemaMap[ModelNameEnum.Item]!); await copyItem(sourceKnex, destKnex, schemaMap[ModelNameEnum.Item]!);
@ -111,7 +111,7 @@ async function copyData(sourceKnex: Knex, destDm: DatabaseManager) {
destKnex, destKnex,
schemaMap[ModelNameEnum.NumberSeries]! schemaMap[ModelNameEnum.NumberSeries]!
); );
await destKnex!.raw('PRAGMA foreign_keys=ON'); await destKnex.raw('PRAGMA foreign_keys=ON');
} }
async function copyNumberSeries( async function copyNumberSeries(
@ -137,14 +137,14 @@ async function copyNumberSeries(
continue; continue;
} }
const indices = await sourceKnex.raw( const indices = (await sourceKnex.raw(
` `
select cast(substr(name, ??) as int) as idx select cast(substr(name, ??) as int) as idx
from ?? from ??
order by idx desc order by idx desc
limit 1`, limit 1`,
[name.length + 1, referenceType] [name.length + 1, referenceType]
); )) as { idx: number }[];
value.start = 1001; value.start = 1001;
value.current = indices[0]?.idx ?? value.current ?? value.start; 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 * Need to account for schema changes, in 0.4.3-beta.0
*/ */
const country = ( const country = (
await knex('SingleValue').select('value').where({ fieldname: 'country' }) (await knex('SingleValue')
.select('value')
.where({ fieldname: 'country' })) as { value: string }[]
)?.[0]?.value; )?.[0]?.value;
if (!country) { 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( export async function setupDummyInstance(
dbPath: string, dbPath: string,
fyo: Fyo, fyo: Fyo,
years: number = 1, years = 1,
baseCount: number = 1000, baseCount = 1000,
notifier?: Notifier notifier?: Notifier
) { ) {
await fyo.purgeCache(); await fyo.purgeCache();
@ -251,7 +251,7 @@ async function getSalesInvoices(
* For each date create a Sales Invoice. * For each date create a Sales Invoice.
*/ */
for (const d in dates) { for (let d = 0; d < dates.length; d++) {
const date = dates[d]; const date = dates[d];
notifier?.( notifier?.(
@ -424,7 +424,7 @@ async function getSalesPurchaseInvoices(
for (const item of supplierGrouped[supplier]) { for (const item of supplierGrouped[supplier]) {
await doc.append('items', {}); await doc.append('items', {});
const quantity = purchaseQty[item]; const quantity = purchaseQty[item];
doc.items!.at(-1)!.set({ item, quantity }); await doc.items!.at(-1)!.set({ item, quantity });
} }
invoices.push(doc); invoices.push(doc);
@ -527,7 +527,7 @@ async function syncAndSubmit(docs: Doc[], notifier?: Notifier) {
}; };
const total = docs.length; const total = docs.length;
for (const i in docs) { for (let i = 0; i < docs.length; i++) {
const doc = docs[i]; const doc = docs[i];
notifier?.( notifier?.(
`Syncing ${nameMap[doc.schemaName]}, ${i} out of ${total}`, `Syncing ${nameMap[doc.schemaName]}, ${i} out of ${total}`,

View File

@ -58,59 +58,8 @@ export class AuthHandler {
return { ...this.#config }; return { ...this.#config };
} }
init() {} init() {
async login(email: string, password: string) { return null;
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 || '';
} }
async getCreds(): Promise<Creds> { async getCreds(): Promise<Creds> {

View File

@ -144,7 +144,7 @@ export class Converter {
return this.#toRawValueMap(parentSchemaName, value.getValidDict()); return this.#toRawValueMap(parentSchemaName, value.getValidDict());
} }
return this.#toRawValueMap(parentSchemaName, value as DocValueMap); return this.#toRawValueMap(parentSchemaName, value);
}); });
} else { } else {
rawValueMap[fieldname] = Converter.toRawValue( rawValueMap[fieldname] = Converter.toRawValue(
@ -176,7 +176,7 @@ function toDocString(value: RawValue, field: Field) {
} }
function toDocDate(value: RawValue, field: Field) { function toDocDate(value: RawValue, field: Field) {
if ((value as any) instanceof Date) { if ((value as unknown) instanceof Date) {
return value; return value;
} }
@ -286,7 +286,7 @@ function toDocAttachment(value: RawValue, field: Field): null | Attachment {
} }
try { try {
return JSON.parse(value) || null; return (JSON.parse(value) as Attachment) || null;
} catch { } catch {
throwError(value, field, 'doc'); throwError(value, field, 'doc');
} }
@ -322,7 +322,7 @@ function toRawInt(value: DocValue, field: Field): number {
} }
if (typeof value === 'number') { if (typeof value === 'number') {
return Math.floor(value as number); return Math.floor(value);
} }
throwError(value, field, 'raw'); throwError(value, field, 'raw');
@ -363,7 +363,7 @@ function toRawDateTime(value: DocValue, field: Field): string | null {
} }
if (value instanceof Date) { if (value instanceof Date) {
return (value as Date).toISOString(); return value.toISOString();
} }
if (value instanceof DateTime) { 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 { function throwError<T>(value: T, field: Field, type: 'raw' | 'doc'): never {
throw new ValueError( throw new ValueError(
`invalid ${type} conversion '${value}' of type ${typeof value} found, field: ${JSON.stringify( `invalid ${type} conversion '${String(
field 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 { Field, RawValue, SchemaMap } from 'schemas/types';
import { getMapFromList } from 'utils'; import { getMapFromList } from 'utils';
import { import {
Cashflow,
DatabaseBase, DatabaseBase,
DatabaseDemuxBase, DatabaseDemuxBase,
GetAllOptions, GetAllOptions,
IncomeExpense,
QueryFilter, QueryFilter,
TopExpenses,
TotalCreditAndDebit,
TotalOutstanding,
} from 'utils/db/types'; } from 'utils/db/types';
import { schemaTranslateables } from 'utils/translationHelpers'; import { schemaTranslateables } from 'utils/translationHelpers';
import { LanguageMap } from 'utils/types'; import { LanguageMap } from 'utils/types';
@ -22,20 +27,10 @@ import {
RawValueMap, RawValueMap,
} from './types'; } 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>>; type FieldMap = Record<string, Record<string, Field>>;
export class DatabaseHandler extends DatabaseBase { export class DatabaseHandler extends DatabaseBase {
/* eslint-disable @typescript-eslint/no-floating-promises */
#fyo: Fyo; #fyo: Fyo;
converter: Converter; converter: Converter;
#demux: DatabaseDemuxBase; #demux: DatabaseDemuxBase;
@ -83,7 +78,7 @@ export class DatabaseHandler extends DatabaseBase {
} }
async init() { async init() {
this.#schemaMap = (await this.#demux.getSchemaMap()) as SchemaMap; this.#schemaMap = await this.#demux.getSchemaMap();
this.#setFieldMap(); this.#setFieldMap();
this.observer = new Observable(); this.observer = new Observable();
} }
@ -92,7 +87,7 @@ export class DatabaseHandler extends DatabaseBase {
if (languageMap) { if (languageMap) {
translateSchema(this.#schemaMap, languageMap, schemaTranslateables); translateSchema(this.#schemaMap, languageMap, schemaTranslateables);
} else { } else {
this.#schemaMap = (await this.#demux.getSchemaMap()) as SchemaMap; this.#schemaMap = await this.#demux.getSchemaMap();
this.#setFieldMap(); this.#setFieldMap();
} }
} }
@ -142,6 +137,7 @@ export class DatabaseHandler extends DatabaseBase {
options: GetAllOptions = {} options: GetAllOptions = {}
): Promise<DocValueMap[]> { ): Promise<DocValueMap[]> {
const rawValueMap = await this.#getAll(schemaName, options); const rawValueMap = await this.#getAll(schemaName, options);
this.observer.trigger(`getAll:${schemaName}`, options); this.observer.trigger(`getAll:${schemaName}`, options);
return this.converter.toDocValueMap( return this.converter.toDocValueMap(
schemaName, schemaName,
@ -154,6 +150,7 @@ export class DatabaseHandler extends DatabaseBase {
options: GetAllOptions = {} options: GetAllOptions = {}
): Promise<RawValueMap[]> { ): Promise<RawValueMap[]> {
const all = await this.#getAll(schemaName, options); const all = await this.#getAll(schemaName, options);
this.observer.trigger(`getAllRaw:${schemaName}`, options); this.observer.trigger(`getAllRaw:${schemaName}`, options);
return all; return all;
} }
@ -188,6 +185,7 @@ export class DatabaseHandler extends DatabaseBase {
): Promise<number> { ): Promise<number> {
const rawValueMap = await this.#getAll(schemaName, options); const rawValueMap = await this.#getAll(schemaName, options);
const count = rawValueMap.length; const count = rawValueMap.length;
this.observer.trigger(`count:${schemaName}`, options); this.observer.trigger(`count:${schemaName}`, options);
return count; return count;
} }
@ -199,18 +197,21 @@ export class DatabaseHandler extends DatabaseBase {
newName: string newName: string
): Promise<void> { ): Promise<void> {
await this.#demux.call('rename', schemaName, oldName, newName); await this.#demux.call('rename', schemaName, oldName, newName);
this.observer.trigger(`rename:${schemaName}`, { oldName, newName }); this.observer.trigger(`rename:${schemaName}`, { oldName, newName });
} }
async update(schemaName: string, docValueMap: DocValueMap): Promise<void> { async update(schemaName: string, docValueMap: DocValueMap): Promise<void> {
const rawValueMap = this.converter.toRawValueMap(schemaName, docValueMap); const rawValueMap = this.converter.toRawValueMap(schemaName, docValueMap);
await this.#demux.call('update', schemaName, rawValueMap); await this.#demux.call('update', schemaName, rawValueMap);
this.observer.trigger(`update:${schemaName}`, docValueMap); this.observer.trigger(`update:${schemaName}`, docValueMap);
} }
// Delete // Delete
async delete(schemaName: string, name: string): Promise<void> { async delete(schemaName: string, name: string): Promise<void> {
await this.#demux.call('delete', schemaName, name); await this.#demux.call('delete', schemaName, name);
this.observer.trigger(`delete:${schemaName}`, name); this.observer.trigger(`delete:${schemaName}`, name);
} }
@ -220,6 +221,7 @@ export class DatabaseHandler extends DatabaseBase {
schemaName, schemaName,
filters filters
)) as number; )) as number;
this.observer.trigger(`deleteAll:${schemaName}`, filters); this.observer.trigger(`deleteAll:${schemaName}`, filters);
return count; return count;
} }
@ -231,6 +233,7 @@ export class DatabaseHandler extends DatabaseBase {
schemaName, schemaName,
name name
)) as boolean; )) as boolean;
this.observer.trigger(`exists:${schemaName}`, name); this.observer.trigger(`exists:${schemaName}`, name);
return doesExist; return doesExist;
} }
@ -302,7 +305,7 @@ export class DatabaseHandler extends DatabaseBase {
)) as IncomeExpense; )) as IncomeExpense;
} }
async getTotalCreditAndDebit(): Promise<unknown> { async getTotalCreditAndDebit(): Promise<TotalCreditAndDebit[]> {
return (await this.#demux.callBespoke( return (await this.#demux.callBespoke(
'getTotalCreditAndDebit' 'getTotalCreditAndDebit'
)) as TotalCreditAndDebit[]; )) as TotalCreditAndDebit[];

View File

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

View File

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

View File

@ -1,10 +1,10 @@
import { ipcRenderer } from 'electron';
import { AuthDemuxBase } from 'utils/auth/types'; import { AuthDemuxBase } from 'utils/auth/types';
import { IPC_ACTIONS } from 'utils/messages'; import { IPC_ACTIONS } from 'utils/messages';
import { Creds } from 'utils/types'; import { Creds } from 'utils/types';
const { ipcRenderer } = require('electron');
export class AuthDemux extends AuthDemuxBase { export class AuthDemux extends AuthDemuxBase {
#isElectron: boolean = false; #isElectron = false;
constructor(isElectron: boolean) { constructor(isElectron: boolean) {
super(); super();
this.#isElectron = isElectron; 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 { export class Config {
#useElectronConfig: boolean; config: Map<string, unknown> | Store;
fallback: Map<string, unknown> = new Map();
constructor(isElectron: boolean) { 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> { get store() {
if (this.#useElectronConfig) { if (this.config instanceof Map) {
return config.store;
} else {
const store: Record<string, unknown> = {}; const store: Record<string, unknown> = {};
for (const key of this.config.keys()) {
for (const key of this.fallback.keys()) { store[key] = this.config.get(key);
store[key] = this.fallback.get(key);
} }
return store; return store;
} else {
return this.config;
} }
} }
get(key: string, defaultValue?: unknown): unknown { get<K extends keyof ConfigMap>(
if (this.#useElectronConfig) { key: K,
return config.get(key, defaultValue); defaultValue?: ConfigMap[K]
} else { ): ConfigMap[K] | undefined {
return this.fallback.get(key) ?? defaultValue; const value = this.config.get(key) as ConfigMap[K] | undefined;
} return value ?? defaultValue;
} }
set(key: string, value: unknown) { set<K extends keyof ConfigMap>(key: K, value: ConfigMap[K]) {
if (this.#useElectronConfig) { this.config.set(key, value);
config.set(key, value);
} else {
this.fallback.set(key, value);
}
} }
delete(key: string) { delete(key: keyof ConfigMap) {
if (this.#useElectronConfig) { this.config.delete(key);
config.delete(key);
} else {
this.fallback.delete(key);
}
} }
clear() { clear() {
if (this.#useElectronConfig) { this.config.clear();
config.clear();
} else {
this.fallback.clear();
}
} }
} }

View File

@ -1,4 +1,4 @@
import { ipcRenderer } from 'electron'; const { ipcRenderer } = require('electron');
import { DatabaseError, NotImplemented } from 'fyo/utils/errors'; import { DatabaseError, NotImplemented } from 'fyo/utils/errors';
import { SchemaMap } from 'schemas/types'; import { SchemaMap } from 'schemas/types';
import { DatabaseDemuxBase, DatabaseMethod } from 'utils/db/types'; import { DatabaseDemuxBase, DatabaseMethod } from 'utils/db/types';
@ -6,7 +6,7 @@ import { BackendResponse } from 'utils/ipc/types';
import { IPC_ACTIONS } from 'utils/messages'; import { IPC_ACTIONS } from 'utils/messages';
export class DatabaseDemux extends DatabaseDemuxBase { export class DatabaseDemux extends DatabaseDemuxBase {
#isElectron: boolean = false; #isElectron = false;
constructor(isElectron: boolean) { constructor(isElectron: boolean) {
super(); super();
this.#isElectron = isElectron; this.#isElectron = isElectron;
@ -27,70 +27,76 @@ export class DatabaseDemux extends DatabaseDemuxBase {
} }
async getSchemaMap(): Promise<SchemaMap> { async getSchemaMap(): Promise<SchemaMap> {
if (this.#isElectron) { if (!this.#isElectron) {
return (await this.#handleDBCall(async function dbFunc() { throw new NotImplemented();
return await ipcRenderer.invoke(IPC_ACTIONS.DB_SCHEMA);
})) as SchemaMap;
} }
throw new NotImplemented(); return (await this.#handleDBCall(async () => {
return (await ipcRenderer.invoke(
IPC_ACTIONS.DB_SCHEMA
)) as BackendResponse;
})) as SchemaMap;
} }
async createNewDatabase( async createNewDatabase(
dbPath: string, dbPath: string,
countryCode?: string countryCode?: string
): Promise<string> { ): Promise<string> {
if (this.#isElectron) { if (!this.#isElectron) {
return (await this.#handleDBCall(async function dbFunc() { throw new NotImplemented();
return await ipcRenderer.invoke(
IPC_ACTIONS.DB_CREATE,
dbPath,
countryCode
);
})) as string;
} }
throw new NotImplemented(); return (await this.#handleDBCall(async () => {
return (await ipcRenderer.invoke(
IPC_ACTIONS.DB_CREATE,
dbPath,
countryCode
)) as BackendResponse;
})) as string;
} }
async connectToDatabase( async connectToDatabase(
dbPath: string, dbPath: string,
countryCode?: string countryCode?: string
): Promise<string> { ): Promise<string> {
if (this.#isElectron) { if (!this.#isElectron) {
return (await this.#handleDBCall(async function dbFunc() { throw new NotImplemented();
return await ipcRenderer.invoke(
IPC_ACTIONS.DB_CONNECT,
dbPath,
countryCode
);
})) as string;
} }
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> { async call(method: DatabaseMethod, ...args: unknown[]): Promise<unknown> {
if (this.#isElectron) { if (!this.#isElectron) {
return (await this.#handleDBCall(async function dbFunc() { throw new NotImplemented();
return await ipcRenderer.invoke(IPC_ACTIONS.DB_CALL, method, ...args);
})) as unknown;
} }
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> { async callBespoke(method: string, ...args: unknown[]): Promise<unknown> {
if (this.#isElectron) { if (!this.#isElectron) {
return (await this.#handleDBCall(async function dbFunc() { throw new NotImplemented();
return await ipcRenderer.invoke(
IPC_ACTIONS.DB_BESPOKE,
method,
...args
);
})) as unknown;
} }
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; doc: DocHandler;
db: DatabaseHandler; db: DatabaseHandler;
_initialized: boolean = false; _initialized = false;
errorLog: ErrorLog[] = []; errorLog: ErrorLog[] = [];
temp?: Record<string, unknown>; temp?: Record<string, unknown>;
@ -94,10 +94,9 @@ export class Fyo {
return format(value, field, doc ?? null, this); return format(value, field, doc ?? null, this);
} }
async setIsElectron() { setIsElectron() {
try { try {
const { ipcRenderer } = await import('electron'); this.isElectron = Boolean(require('electron'));
this.isElectron = Boolean(ipcRenderer);
} catch { } catch {
this.isElectron = false; this.isElectron = false;
} }
@ -106,7 +105,7 @@ export class Fyo {
async initializeAndRegister( async initializeAndRegister(
models: ModelMap = {}, models: ModelMap = {},
regionalModels: ModelMap = {}, regionalModels: ModelMap = {},
force: boolean = false force = false
) { ) {
if (this._initialized && !force) return; if (this._initialized && !force) return;
@ -122,8 +121,8 @@ export class Fyo {
// temp params while calling routes // temp params while calling routes
this.temp = {}; this.temp = {};
await this.doc.init(); this.doc.init();
await this.auth.init(); this.auth.init();
await this.db.init(); await this.db.init();
} }
@ -165,7 +164,6 @@ export class Fyo {
async close() { async close() {
await this.db.close(); await this.db.close();
await this.auth.logout();
} }
getField(schemaName: string, fieldname: string) { getField(schemaName: string, fieldname: string) {
@ -190,14 +188,14 @@ export class Fyo {
let value: DocValue | Doc[]; let value: DocValue | Doc[];
try { try {
doc = await this.doc.getDoc(schemaName, name); doc = await this.doc.getDoc(schemaName, name);
value = doc.get(fieldname!); value = doc.get(fieldname);
} catch (err) { } catch (err) {
value = undefined; value = undefined;
} }
if (value === undefined && schemaName === name) { if (value === undefined && schemaName === name) {
const sv = await this.db.getSingleValues({ const sv = await this.db.getSingleValues({
fieldname: fieldname!, fieldname: fieldname,
parent: schemaName, parent: schemaName,
}); });
@ -222,8 +220,7 @@ export class Fyo {
this.errorLog = []; this.errorLog = [];
this.temp = {}; this.temp = {};
await this.db.purgeCache(); await this.db.purgeCache();
await this.auth.purgeCache(); this.doc.purgeCache();
await this.doc.purgeCache();
} }
store = { store = {

View File

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

View File

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

View File

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

View File

@ -7,14 +7,26 @@ import { getIsNullOrUndef } from 'utils';
import { Doc } from './doc'; import { Doc } from './doc';
export function validateEmail(value: DocValue) { 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) { if (!isValid) {
throw new ValidationError(`Invalid email: ${value}`); throw new ValidationError(`Invalid email: ${value}`);
} }
} }
export function validatePhoneNumber(value: DocValue) { 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) { if (!isValid) {
throw new ValidationError(`Invalid phone: ${value}`); throw new ValidationError(`Invalid phone: ${value}`);
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -60,7 +60,7 @@ function toDatetime(value: unknown): DateTime | null {
} else if (value instanceof Date) { } else if (value instanceof Date) {
return DateTime.fromJSDate(value); return DateTime.fromJSDate(value);
} else if (typeof value === 'number') { } else if (typeof value === 'number') {
return DateTime.fromSeconds(value as number); return DateTime.fromSeconds(value);
} }
return null; return null;
@ -120,7 +120,9 @@ function formatCurrency(
try { try {
valueString = formatNumber(value, fyo); valueString = formatNumber(value, fyo);
} catch (err) { } catch (err) {
(err as Error).message += ` value: '${value}', type: ${typeof value}`; (err as Error).message += ` value: '${String(
value
)}', type: ${typeof value}`;
throw err; throw err;
} }
@ -148,7 +150,9 @@ function formatNumber(value: unknown, fyo: Fyo): string {
if (formattedNumber === 'NaN') { if (formattedNumber === 'NaN') {
throw Error( 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) { export function slug(str: string) {
return str 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(); return index == 0 ? letter.toLowerCase() : letter.toUpperCase();
}) })
.replace(/\s+/g, ''); .replace(/\s+/g, '');
@ -24,7 +24,7 @@ export function unique<T>(list: T[], key = (it: T) => String(it)) {
export function getDuplicates(array: unknown[]) { export function getDuplicates(array: unknown[]) {
const duplicates: unknown[] = []; const duplicates: unknown[] = [];
for (const i in array) { for (let i = 0; i < array.length; i++) {
const previous = array[safeParseInt(i) - 1]; const previous = array[safeParseInt(i) - 1];
const current = array[i]; const current = array[i];
@ -118,7 +118,7 @@ function getRawOptionList(field: Field, doc: Doc | undefined | null) {
return []; return [];
} }
const Model = doc!.fyo.models[doc!.schemaName]; const Model = doc.fyo.models[doc.schemaName];
if (Model === undefined) { if (Model === undefined) {
return []; return [];
} }
@ -128,7 +128,7 @@ function getRawOptionList(field: Field, doc: Doc | undefined | null) {
return []; return [];
} }
return getList(doc!); return getList(doc);
} }
export function getEmptyValuesByFieldTypes( export function getEmptyValuesByFieldTypes(

View File

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

View File

@ -31,7 +31,7 @@ class TranslationString {
} }
#formatArg(arg: string | number | boolean) { #formatArg(arg: string | number | boolean) {
return arg ?? ''; return String(arg ?? '');
} }
#translate() { #translate() {
@ -48,22 +48,23 @@ class TranslationString {
} }
#stitch() { #stitch() {
if (!((this.args[0] as any) instanceof Array)) { if (!((this.args[0] as unknown) instanceof Array)) {
throw new ValueError( throw new ValueError(
`invalid args passed to TranslationString ${ `invalid args passed to TranslationString ${String(
this.args 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[]; this.argList = this.args.slice(1) as TranslationArgs[];
if (this.languageMap) { if (this.languageMap) {
this.#translate(); 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('') .join('')
.replace(/\s+/g, ' ') .replace(/\s+/g, ' ')
.trim(); .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 { import {
app, app,
BrowserWindow, BrowserWindow,
BrowserWindowConstructorOptions, BrowserWindowConstructorOptions,
protocol, protocol,
ProtocolRequest,
ProtocolResponse,
} from 'electron'; } from 'electron';
import Store from 'electron-store'; import Store from 'electron-store';
import { autoUpdater } from 'electron-updater'; import { autoUpdater } from 'electron-updater';
import fs from 'fs';
import path from 'path'; import path from 'path';
import { createProtocol } from 'vue-cli-plugin-electron-builder/lib';
import registerAppLifecycleListeners from './main/registerAppLifecycleListeners'; import registerAppLifecycleListeners from './main/registerAppLifecycleListeners';
import registerAutoUpdaterListeners from './main/registerAutoUpdaterListeners'; import registerAutoUpdaterListeners from './main/registerAutoUpdaterListeners';
import registerIpcMainActionListeners from './main/registerIpcMainActionListeners'; import registerIpcMainActionListeners from './main/registerIpcMainActionListeners';
import registerIpcMainMessageListeners from './main/registerIpcMainMessageListeners'; import registerIpcMainMessageListeners from './main/registerIpcMainMessageListeners';
import registerProcessListeners from './main/registerProcessListeners'; import registerProcessListeners from './main/registerProcessListeners';
import { emitMainProcessError } from 'backend/helpers';
export class Main { export class Main {
title: string = 'Frappe Books'; title = 'Frappe Books';
icon: string; icon: string;
winURL: string = ''; winURL = '';
isWebpackUrl: boolean = false;
checkedForUpdate = false; checkedForUpdate = false;
mainWindow: BrowserWindow | null = null; mainWindow: BrowserWindow | null = null;
@ -57,7 +63,7 @@ export class Main {
} }
get isDevelopment() { get isDevelopment() {
return process.env.NODE_ENV !== 'production'; return process.env.NODE_ENV === 'development';
} }
get isTest() { get isTest() {
@ -116,35 +122,42 @@ export class Main {
return options; return options;
} }
createWindow() { async createWindow() {
const options = this.getOptions(); const options = this.getOptions();
this.mainWindow = new BrowserWindow(options); this.mainWindow = new BrowserWindow(options);
this.isWebpackUrl = !!process.env.WEBPACK_DEV_SERVER_URL; if (this.isDevelopment) {
if (this.isWebpackUrl) { this.setViteServerURL();
this.loadWebpackDevServerURL();
} else { } else {
this.loadAppUrl(); this.registerAppProtocol();
}
await this.mainWindow.loadURL(this.winURL);
if (this.isDevelopment && !this.isTest) {
this.mainWindow.webContents.openDevTools();
} }
this.setMainWindowListeners(); this.setMainWindowListeners();
} }
loadWebpackDevServerURL() { setViteServerURL() {
// Load the url of the dev server if in development mode let port = 6969;
this.winURL = process.env.WEBPACK_DEV_SERVER_URL as string; let host = '0.0.0.0';
this.mainWindow!.loadURL(this.winURL);
if (this.isDevelopment && !this.isTest) { if (process.env.VITE_PORT && process.env.VITE_HOST) {
this.mainWindow!.webContents.openDevTools(); 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() { registerAppProtocol() {
createProtocol('app'); protocol.registerBufferProtocol('app', bufferProtocolCallback);
// Load the index.html when not in development
// Use the registered protocol url to load the files.
this.winURL = 'app://./index.html'; this.winURL = 'app://./index.html';
this.mainWindow!.loadURL(this.winURL);
} }
setMainWindowListeners() { setMainWindowListeners() {
@ -157,9 +170,43 @@ export class Main {
}); });
this.mainWindow.webContents.on('did-fail-load', () => { 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(); export default new Main();

View File

@ -4,6 +4,7 @@ import fetch from 'node-fetch';
import path from 'path'; import path from 'path';
import { Creds } from 'utils/types'; import { Creds } from 'utils/types';
import { rendererLog } from './helpers'; import { rendererLog } from './helpers';
import type { Main } from 'main';
export function getUrlAndTokenString(): Creds { export function getUrlAndTokenString(): Creds {
const inProduction = app.isPackaged; const inProduction = app.isPackaged;
@ -13,10 +14,11 @@ export function getUrlAndTokenString(): Creds {
'../creds/log_creds.txt' '../creds/log_creds.txt'
); );
if (!fs.existsSync(errLogCredsPath)) { if (!fs.existsSync(errLogCredsPath)) {
errLogCredsPath = path.join(__dirname, '../log_creds.txt'); errLogCredsPath = path.join(__dirname, '..', '..', 'log_creds.txt');
} }
if (!fs.existsSync(errLogCredsPath)) { if (!fs.existsSync(errLogCredsPath)) {
// eslint-disable-next-line no-console
!inProduction && console.log(`${errLogCredsPath} doesn't exist, can't log`); !inProduction && console.log(`${errLogCredsPath} doesn't exist, can't log`);
return empty; return empty;
} }
@ -29,7 +31,9 @@ export function getUrlAndTokenString(): Creds {
.filter((f) => f.length); .filter((f) => f.length);
} catch (err) { } catch (err) {
if (!inProduction) { if (!inProduction) {
// eslint-disable-next-line no-console
console.log(`logging error using creds at: ${errLogCredsPath} failed`); console.log(`logging error using creds at: ${errLogCredsPath} failed`);
// eslint-disable-next-line no-console
console.log(err); console.log(err);
} }
return empty; 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 { errorLogUrl, tokenString } = getUrlAndTokenString();
const headers = { const headers = {
Authorization: tokenString, Authorization: tokenString,
@ -51,6 +55,6 @@ export async function sendError(body: string) {
}; };
await fetch(errorLogUrl, { method: 'POST', headers, body }).catch((err) => { 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 path from 'path';
import { parseCSV } from 'utils/csvParser'; import { parseCSV } from 'utils/csvParser';
import { LanguageMap } from 'utils/types'; import { LanguageMap } from 'utils/types';
import fetch from 'node-fetch';
const fetch = require('node-fetch').default;
const VALENTINES_DAY = 1644796800000; const VALENTINES_DAY = 1644796800000;
@ -100,7 +99,7 @@ async function fetchContentsFromApi(code: string) {
return null; return null;
} }
const resJson = await res.json(); const resJson = (await res.json()) as { content: string };
return Buffer.from(resJson.content, 'base64').toString(); return Buffer.from(resJson.content, 'base64').toString();
} }
@ -138,7 +137,9 @@ async function getLastUpdated(code: string): Promise<Date> {
return new Date(VALENTINES_DAY); return new Date(VALENTINES_DAY);
} }
const resJson = await res.json(); const resJson = (await res.json()) as {
commit: { author: { date: string } };
}[];
try { try {
return new Date(resJson[0].commit.author.date); return new Date(resJson[0].commit.author.date);
} catch { } catch {
@ -187,7 +188,7 @@ async function storeFile(code: string, contents: string) {
async function errorHandledFetch(url: string) { async function errorHandledFetch(url: string) {
try { try {
return (await fetch(url)) as Response; return await fetch(url);
} catch { } catch {
return null; return null;
} }

View File

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

View File

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

View File

@ -2,6 +2,7 @@ import { app } from 'electron';
import installExtension, { VUEJS3_DEVTOOLS } from 'electron-devtools-installer'; import installExtension, { VUEJS3_DEVTOOLS } from 'electron-devtools-installer';
import { Main } from '../main'; import { Main } from '../main';
import { rendererLog } from './helpers'; import { rendererLog } from './helpers';
import { emitMainProcessError } from 'backend/helpers';
export default function registerAppLifecycleListeners(main: Main) { export default function registerAppLifecycleListeners(main: Main) {
app.on('window-all-closed', () => { app.on('window-all-closed', () => {
@ -12,16 +13,16 @@ export default function registerAppLifecycleListeners(main: Main) {
app.on('activate', () => { app.on('activate', () => {
if (main.mainWindow === null) { if (main.mainWindow === null) {
main.createWindow(); main.createWindow().catch((err) => emitMainProcessError(err));
} }
}); });
app.on('ready', async () => { app.on('ready', () => {
if (main.isDevelopment && !main.isTest) { 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); emitMainProcessError(error);
}); });
// eslint-disable-next-line @typescript-eslint/no-misused-promises
autoUpdater.on('update-available', async (info: UpdateInfo) => { autoUpdater.on('update-available', async (info: UpdateInfo) => {
const currentVersion = app.getVersion(); const currentVersion = app.getVersion();
const nextVersion = info.version; const nextVersion = info.version;
@ -46,6 +47,7 @@ export default function registerAutoUpdaterListeners(main: Main) {
await autoUpdater.downloadUpdate(); await autoUpdater.downloadUpdate();
}); });
// eslint-disable-next-line @typescript-eslint/no-misused-promises
autoUpdater.on('update-downloaded', async () => { autoUpdater.on('update-downloaded', async () => {
const option = await dialog.showMessageBox({ const option = await dialog.showMessageBox({
type: 'info', 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 { autoUpdater } from 'electron-updater';
import fs from 'fs/promises'; import fs from 'fs/promises';
import path from 'path'; import path from 'path';
@ -20,39 +27,60 @@ import {
import { saveHtmlAsPdf } from './saveHtmlAsPdf'; import { saveHtmlAsPdf } from './saveHtmlAsPdf';
export default function registerIpcMainActionListeners(main: Main) { export default function registerIpcMainActionListeners(main: Main) {
ipcMain.handle(IPC_ACTIONS.GET_OPEN_FILEPATH, async (event, options) => { ipcMain.handle(
return await dialog.showOpenDialog(main.mainWindow!, options); IPC_ACTIONS.GET_OPEN_FILEPATH,
}); async (_, options: OpenDialogOptions) => {
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 });
} }
);
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 }) => { ipcMain.handle(
return await dialog.showErrorBox(title, content); 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( ipcMain.handle(
IPC_ACTIONS.SAVE_HTML_AS_PDF, 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); return await saveHtmlAsPdf(html, savePath, app, width, height);
} }
); );
ipcMain.handle(IPC_ACTIONS.SAVE_DATA, async (event, data, savePath) => { ipcMain.handle(
return await fs.writeFile(savePath, data, { encoding: 'utf-8' }); 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) => { ipcMain.handle(IPC_ACTIONS.SEND_ERROR, async (_, bodyJson: string) => {
sendError(bodyJson); await sendError(bodyJson, main);
}); });
ipcMain.handle(IPC_ACTIONS.CHECK_FOR_UPDATES, async () => { ipcMain.handle(IPC_ACTIONS.CHECK_FOR_UPDATES, async () => {
@ -72,7 +100,7 @@ export default function registerIpcMainActionListeners(main: Main) {
main.checkedForUpdate = true; 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: '' }; const obj = { languageMap: {}, success: true, message: '' };
try { try {
obj.languageMap = await getLanguageMap(code); 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(); 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)); 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(); const files = await setAndGetCleanedConfigFiles();
return await getConfigFilesWithModified(files); return await getConfigFilesWithModified(files);
}); });
ipcMain.handle(IPC_ACTIONS.GET_ENV, async (_) => { ipcMain.handle(IPC_ACTIONS.GET_ENV, () => {
return { return {
isDevelopment: main.isDevelopment, isDevelopment: main.isDevelopment,
platform: process.platform, platform: process.platform,
@ -149,7 +177,7 @@ export default function registerIpcMainActionListeners(main: Main) {
ipcMain.handle( ipcMain.handle(
IPC_ACTIONS.DB_CREATE, IPC_ACTIONS.DB_CREATE,
async (_, dbPath: string, countryCode: string) => { async (_, dbPath: string, countryCode: string) => {
return await getErrorHandledReponse(async function dbFunc() { return await getErrorHandledReponse(async () => {
return await databaseManager.createNewDatabase(dbPath, countryCode); return await databaseManager.createNewDatabase(dbPath, countryCode);
}); });
} }
@ -158,7 +186,7 @@ export default function registerIpcMainActionListeners(main: Main) {
ipcMain.handle( ipcMain.handle(
IPC_ACTIONS.DB_CONNECT, IPC_ACTIONS.DB_CONNECT,
async (_, dbPath: string, countryCode?: string) => { async (_, dbPath: string, countryCode?: string) => {
return await getErrorHandledReponse(async function dbFunc() { return await getErrorHandledReponse(async () => {
return await databaseManager.connectToDatabase(dbPath, countryCode); return await databaseManager.connectToDatabase(dbPath, countryCode);
}); });
} }
@ -167,7 +195,7 @@ export default function registerIpcMainActionListeners(main: Main) {
ipcMain.handle( ipcMain.handle(
IPC_ACTIONS.DB_CALL, IPC_ACTIONS.DB_CALL,
async (_, method: DatabaseMethod, ...args: unknown[]) => { async (_, method: DatabaseMethod, ...args: unknown[]) => {
return await getErrorHandledReponse(async function dbFunc() { return await getErrorHandledReponse(async () => {
return await databaseManager.call(method, ...args); return await databaseManager.call(method, ...args);
}); });
} }
@ -176,15 +204,15 @@ export default function registerIpcMainActionListeners(main: Main) {
ipcMain.handle( ipcMain.handle(
IPC_ACTIONS.DB_BESPOKE, IPC_ACTIONS.DB_BESPOKE,
async (_, method: string, ...args: unknown[]) => { async (_, method: string, ...args: unknown[]) => {
return await getErrorHandledReponse(async function dbFunc() { return await getErrorHandledReponse(async () => {
return await databaseManager.callBespoke(method, ...args); return await databaseManager.callBespoke(method, ...args);
}); });
} }
); );
ipcMain.handle(IPC_ACTIONS.DB_SCHEMA, async (_) => { ipcMain.handle(IPC_ACTIONS.DB_SCHEMA, async () => {
return await getErrorHandledReponse(async function dbFunc() { return await getErrorHandledReponse(() => {
return await databaseManager.getSchemaMap(); return databaseManager.getSchemaMap();
}); });
}); });
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -40,14 +40,7 @@ export class AccountingLedgerEntry extends Doc {
static getListViewSettings(): ListViewSettings { static getListViewSettings(): ListViewSettings {
return { return {
columns: [ columns: ['date', 'account', 'party', 'debit', 'credit', 'referenceName'],
'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 { Doc } from 'fyo/model/doc';
import { import {
EmptyMessageMap, EmptyMessageMap,
FormulaMap, FormulaMap,
ListsMap,
ListViewSettings, ListViewSettings,
ListsMap,
} from 'fyo/model/types'; } from 'fyo/model/types';
import { codeStateMap } from 'regional/in'; import { codeStateMap } from 'regional/in';
import { getCountryInfo } from 'utils/misc'; import { getCountryInfo } from 'utils/misc';
@ -12,7 +12,7 @@ import { getCountryInfo } from 'utils/misc';
export class Address extends Doc { export class Address extends Doc {
formulas: FormulaMap = { formulas: FormulaMap = {
addressDisplay: { addressDisplay: {
formula: async () => { formula: () => {
return [ return [
this.addressLine1, this.addressLine1,
this.addressLine2, this.addressLine2,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -46,7 +46,7 @@ export class PrintTemplate extends Doc {
static lists: ListsMap = { static lists: ListsMap = {
type(doc?: Doc) { type(doc?: Doc) {
let enableInventory: boolean = false; let enableInventory = false;
let schemaMap: SchemaMap = {}; let schemaMap: SchemaMap = {};
if (doc) { if (doc) {
enableInventory = !!doc.fyo.singles.AccountingSettings?.enableInventory; 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 const discountAccount = this.fyo.singles.AccountingSettings
?.discountAccount as string | undefined; ?.discountAccount as string | undefined;
if (discountAccount && discountAmount.isPositive()) { if (discountAccount && discountAmount.isPositive()) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,23 +2,20 @@
"name": "frappe-books", "name": "frappe-books",
"version": "0.16.0", "version": "0.16.0",
"description": "Simple book-keeping app for everyone", "description": "Simple book-keeping app for everyone",
"main": "background.js",
"author": { "author": {
"name": "Frappe Technologies Pvt. Ltd.", "name": "Frappe Technologies Pvt. Ltd.",
"email": "hello@frappe.io" "email": "hello@frappe.io"
}, },
"scripts": { "scripts": {
"serve": "vue-cli-service serve", "dev": "node build/scripts/dev.mjs",
"build": "vue-cli-service build", "build": "node build/scripts/build.mjs",
"lint": "vue-cli-service lint",
"release": "scripts/publish-mac-arm.sh", "release": "scripts/publish-mac-arm.sh",
"postinstall": "electron-rebuild", "postinstall": "electron-rebuild",
"postuninstall": "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:translate": "scripts/runner.sh scripts/generateTranslations.ts",
"script:profile": "scripts/profile.sh", "script:profile": "scripts/profile.sh",
"test": "scripts/test.sh" "test": "scripts/test.sh",
"lint": "eslint . --ext ts,vue"
}, },
"dependencies": { "dependencies": {
"@codemirror/autocomplete": "^6.4.2", "@codemirror/autocomplete": "^6.4.2",
@ -34,12 +31,15 @@
"luxon": "^2.5.2", "luxon": "^2.5.2",
"node-fetch": "2", "node-fetch": "2",
"pesa": "^1.1.12", "pesa": "^1.1.12",
"source-map-support": "^0.5.21",
"vue": "^3.2.40", "vue": "^3.2.40",
"vue-router": "^4.0.12" "vue-router": "^4.0.12"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.16.0", "@codemirror/language": "^6.0.0",
"@babel/eslint-parser": "^7.16.0", "@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
"@lezer/common": "^1.0.0",
"@types/assert": "^1.5.6", "@types/assert": "^1.5.6",
"@types/electron-devtools-installer": "^2.2.0", "@types/electron-devtools-installer": "^2.2.0",
"@types/lodash": "^4.14.179", "@types/lodash": "^4.14.179",
@ -48,30 +48,25 @@
"@types/node": "^17.0.23", "@types/node": "^17.0.23",
"@types/node-fetch": "^2.6.1", "@types/node-fetch": "^2.6.1",
"@types/tape": "^4.13.2", "@types/tape": "^4.13.2",
"@typescript-eslint/eslint-plugin": "^4.15.1", "@typescript-eslint/eslint-plugin": "5.60.0",
"@typescript-eslint/parser": "^4.15.1", "@typescript-eslint/parser": "5.60.0",
"@vue/cli-plugin-babel": "^4.5.0", "@vitejs/plugin-vue": "^4.2.3",
"@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",
"autoprefixer": "^9", "autoprefixer": "^9",
"babel-loader": "^8.2.3", "chokidar": "^3.5.3",
"dotenv": "^16.0.0", "dotenv": "^16.0.0",
"electron": "18.3.7", "electron": "18.3.7",
"electron-builder": "24.0.0-alpha.12", "electron-builder": "^24.4.0",
"electron-devtools-installer": "^3.2.0", "electron-devtools-installer": "^3.2.0",
"electron-rebuild": "^3.2.9", "electron-rebuild": "^3.2.9",
"electron-updater": "^5.2.1", "electron-updater": "^5.2.1",
"eslint": "^7.32.0", "eslint": "^8.43.0",
"eslint-config-prettier": "^8.3.0", "eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0", "eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-vue": "^7.0.0", "eslint-plugin-vue": "^9.15.0",
"lint-staged": "^11.2.6", "execa": "^7.1.1",
"fs-extra": "^11.1.1",
"postcss": "^8", "postcss": "^8",
"prettier": "^2.4.1", "prettier": "^2.4.1",
"raw-loader": "^4.0.2",
"tailwindcss": "npm:@tailwindcss/postcss7-compat", "tailwindcss": "npm:@tailwindcss/postcss7-compat",
"tailwindcss-rtl": "^0.9.0", "tailwindcss-rtl": "^0.9.0",
"tap-spec": "^5.0.0", "tap-spec": "^5.0.0",
@ -80,29 +75,18 @@
"tsconfig-paths": "^3.14.1", "tsconfig-paths": "^3.14.1",
"tslib": "^2.3.1", "tslib": "^2.3.1",
"typescript": "^4.6.2", "typescript": "^4.6.2",
"vue-cli-plugin-electron-builder": "https://github.com/nklayman/vue-cli-plugin-electron-builder#ebb9183f4913f927d4e4f4eb1fbab61a960f7a09", "vite": "^4.3.9",
"webpack": "^5.76.0" "vue-tsc": "^1.6.5",
}, "yargs": "^17.7.2"
"resolutions": {
"electron-builder": "24.0.0-alpha.12"
}, },
"prettier": { "prettier": {
"semi": true, "semi": true,
"singleQuote": true, "singleQuote": true,
"trailingComma": "es5" "trailingComma": "es5"
}, },
"engineStrict": true,
"engines": {
"node": ">=16.13.1 <17"
},
"gitHooks": {
"pre-commit": "lint-staged"
},
"homepage": "https://frappebooks.com", "homepage": "https://frappebooks.com",
"lint-staged": {
"*.{js,vue}": "vue-cli-service lint"
},
"repository": { "repository": {
"url": "https://github.com/frappe/books" "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 { export abstract class AccountReport extends LedgerReport {
toDate?: string; toDate?: string;
count: number = 3; count = 3;
fromYear?: number; fromYear?: number;
toYear?: number; toYear?: number;
consolidateColumns: boolean = false; consolidateColumns = false;
hideGroupAmounts: boolean = false; hideGroupAmounts = false;
periodicity: Periodicity = 'Monthly'; periodicity: Periodicity = 'Monthly';
basedOn: BasedOn = 'Until Date'; basedOn: BasedOn = 'Until Date';
@ -88,10 +88,7 @@ export abstract class AccountReport extends LedgerReport {
}; };
} }
async getTotalNode( getTotalNode(rootNode: AccountTreeNode, name: string): AccountListNode {
rootNode: AccountTreeNode,
name: string
): Promise<AccountListNode> {
const accountTree = { [rootNode.name]: rootNode }; const accountTree = { [rootNode.name]: rootNode };
const leafNodes = getListOfLeafNodes(accountTree) as AccountTreeNode[]; const leafNodes = getListOfLeafNodes(accountTree) as AccountTreeNode[];
@ -153,7 +150,7 @@ export abstract class AccountReport extends LedgerReport {
): Promise<AccountNameValueMapMap> { ): Promise<AccountNameValueMapMap> {
const accountValueMap: AccountNameValueMapMap = new Map(); const accountValueMap: AccountNameValueMapMap = new Map();
if (!this.accountMap) { if (!this.accountMap) {
await this._getAccountMap(); await this._setAndReturnAccountMap();
} }
for (const account of map.keys()) { for (const account of map.keys()) {
@ -169,17 +166,17 @@ export abstract class AccountReport extends LedgerReport {
} }
if (!this.accountMap?.[entry.account]) { 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 balance = (entry.debit ?? 0) - (entry.credit ?? 0);
const rootType = this.accountMap![entry.account]?.rootType; const rootType = this.accountMap![entry.account]?.rootType;
if (isCredit(rootType)) { if (isCredit(rootType)) {
valueMap.set(key!, { balance: totalBalance - balance }); valueMap.set(key, { balance: totalBalance - balance });
} else { } else {
valueMap.set(key!, { balance: totalBalance + balance }); valueMap.set(key, { balance: totalBalance + balance });
} }
} }
accountValueMap.set(account, valueMap); accountValueMap.set(account, valueMap);
@ -189,7 +186,9 @@ export abstract class AccountReport extends LedgerReport {
} }
async _getAccountTree(rangeGroupedMap: AccountNameValueMapMap) { async _getAccountTree(rangeGroupedMap: AccountNameValueMapMap) {
const accountTree = cloneDeep(await this._getAccountMap()) as AccountTree; const accountTree = cloneDeep(
await this._setAndReturnAccountMap()
) as AccountTree;
setPruneFlagOnAccountTreeNodes(accountTree); setPruneFlagOnAccountTreeNodes(accountTree);
setValueMapOnAccountTreeNodes(accountTree, rangeGroupedMap); setValueMapOnAccountTreeNodes(accountTree, rangeGroupedMap);
@ -200,7 +199,7 @@ export abstract class AccountReport extends LedgerReport {
return accountTree; return accountTree;
} }
async _getAccountMap(force: boolean = false) { async _setAndReturnAccountMap(force = false) {
if (this.accountMap && !force) { if (this.accountMap && !force) {
return this.accountMap; return this.accountMap;
} }
@ -510,14 +509,14 @@ function updateParentAccountWithChildValues(
parentAccount.valueMap ??= new Map(); parentAccount.valueMap ??= new Map();
for (const key of valueMap.keys()) { for (const key of valueMap.keys()) {
const value = parentAccount.valueMap!.get(key); const value = parentAccount.valueMap.get(key);
const childValue = valueMap.get(key); const childValue = valueMap.get(key);
const map: Record<string, number> = {}; const map: Record<string, number> = {};
for (const key of Object.keys(childValue!)) { for (const key of Object.keys(childValue!)) {
map[key] = (value?.[key] ?? 0) + (childValue?.[key] ?? 0); map[key] = (value?.[key] ?? 0) + (childValue?.[key] ?? 0);
} }
parentAccount.valueMap!.set(key, map); parentAccount.valueMap.set(key, map);
} }
return parentAccount.parentAccount!; return parentAccount.parentAccount!;
@ -533,7 +532,7 @@ function setChildrenOnAccountTreeNodes(accountTree: AccountTree) {
} }
accountTree[ac.parentAccount].children ??= []; accountTree[ac.parentAccount].children ??= [];
accountTree[ac.parentAccount].children!.push(ac!); accountTree[ac.parentAccount].children!.push(ac);
parentNodes.add(ac.parentAccount); parentNodes.add(ac.parentAccount);
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -35,8 +35,8 @@ export class TrialBalance extends AccountReport {
fromDate?: string; fromDate?: string;
toDate?: string; toDate?: string;
hideGroupAmounts: boolean = false; hideGroupAmounts = false;
loading: boolean = false; loading = false;
_rawData: LedgerEntry[] = []; _rawData: LedgerEntry[] = [];
_dateRanges?: DateRange[]; _dateRanges?: DateRange[];
@ -79,6 +79,7 @@ export class TrialBalance extends AccountReport {
this.loading = false; this.loading = false;
} }
// eslint-disable-next-line @typescript-eslint/require-await
async getReportDataFromRows( async getReportDataFromRows(
rootTypeRows: RootTypeRow[] rootTypeRows: RootTypeRow[]
): Promise<ReportData> { ): Promise<ReportData> {
@ -93,6 +94,7 @@ export class TrialBalance extends AccountReport {
return reportData; return reportData;
} }
// eslint-disable-next-line @typescript-eslint/require-await
async _getGroupedByDateRanges( async _getGroupedByDateRanges(
map: GroupedMap map: GroupedMap
): Promise<AccountNameValueMapMap> { ): Promise<AccountNameValueMapMap> {
@ -108,11 +110,11 @@ export class TrialBalance extends AccountReport {
const key = this._getRangeMapKey(entry); const key = this._getRangeMapKey(entry);
if (key === null) { if (key === null) {
throw new ValueError( 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 totalCredit = map?.credit ?? 0;
const totalDebit = map?.debit ?? 0; const totalDebit = map?.debit ?? 0;
@ -188,6 +190,7 @@ export class TrialBalance extends AccountReport {
} as ReportRow; } as ReportRow;
} }
// eslint-disable-next-line @typescript-eslint/require-await
async _getQueryFilters(): Promise<QueryFilter> { async _getQueryFilters(): Promise<QueryFilter> {
const filters: QueryFilter = {}; const filters: QueryFilter = {};
filters.reverted = false; 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 { Action } from 'fyo/model/types';
import { Verb } from 'fyo/telemetry/types'; import { Verb } from 'fyo/telemetry/types';
import { getSavePath, saveData, showExportInFolder } from 'src/utils/ipcCalls'; import { getSavePath, saveData, showExportInFolder } from 'src/utils/ipcCalls';
@ -88,7 +88,7 @@ function getJsonData(report: Report): string {
} }
const rowObj: Record<string, unknown> = {}; 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 { label } = columns[c];
const cell = getValueFromCell(row.cells[c], displayPrecision); const cell = getValueFromCell(row.cells[c], displayPrecision);
rowObj[label] = cell; rowObj[label] = cell;
@ -129,7 +129,7 @@ function convertReportToCSVMatrix(report: Report): unknown[][] {
const displayPrecision = const displayPrecision =
(report.fyo.singles.SystemSettings?.displayPrecision as number) ?? 2; (report.fyo.singles.SystemSettings?.displayPrecision as number) ?? 2;
const reportData = report.reportData; const reportData = report.reportData;
const columns = report.columns!; const columns = report.columns;
const csvdata: unknown[][] = []; const csvdata: unknown[][] = [];
csvdata.push(columns.map((c) => c.label)); csvdata.push(columns.map((c) => c.label));
@ -140,7 +140,7 @@ function convertReportToCSVMatrix(report: Report): unknown[][] {
} }
const csvrow: 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); const cell = getValueFromCell(row.cells[c], displayPrecision);
csvrow.push(cell); csvrow.push(cell);
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,8 @@
<template> <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> <slot></slot>
</div> </div>
</template> </template>

View File

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

View File

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

View File

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

View File

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

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