mirror of
https://github.com/frappe/books.git
synced 2024-11-09 15:20:56 +00:00
commit
5f2cef7534
55
.eslintrc.js
55
.eslintrc.js
@ -1,23 +1,62 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
|
||||
env: {
|
||||
node: true,
|
||||
browser: true,
|
||||
es2018: true,
|
||||
},
|
||||
|
||||
rules: {
|
||||
'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
|
||||
'no-console': 'warn',
|
||||
'no-debugger': 'warn',
|
||||
'arrow-body-style': 'off',
|
||||
'prefer-arrow-callback': 'off',
|
||||
'prettier/prettier': 'warn',
|
||||
'prefer-arrow-callback': 'warn',
|
||||
'vue/no-mutating-props': 'off',
|
||||
'vue/multi-word-component-names': 'off',
|
||||
'vue/no-useless-template-attributes': 'off',
|
||||
'vue/one-component-per-file': 'off',
|
||||
'vue/no-reserved-component-names': 'off',
|
||||
'@typescript-eslint/ban-ts-comment': 'off',
|
||||
'@typescript-eslint/no-var-requires': 'off',
|
||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||
'@typescript-eslint/no-floating-promises': 'warn',
|
||||
'@typescript-eslint/no-misused-promises': 'warn',
|
||||
},
|
||||
|
||||
parser: 'vue-eslint-parser',
|
||||
parserOptions: {
|
||||
parser: '@typescript-eslint/parser',
|
||||
project: true,
|
||||
tsconfigRootDir: __dirname,
|
||||
sourceType: 'module',
|
||||
extraFileExtensions: ['.vue'],
|
||||
},
|
||||
|
||||
extends: ['plugin:vue/vue3-essential', '@vue/typescript'],
|
||||
plugins: ['@typescript-eslint', 'prettier'],
|
||||
extends: [
|
||||
'plugin:vue/vue3-recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:@typescript-eslint/recommended-requiring-type-checking',
|
||||
'plugin:prettier/recommended',
|
||||
],
|
||||
overrides: [
|
||||
{
|
||||
files: ['*.vue'],
|
||||
rules: {
|
||||
'@typescript-eslint/no-misused-promises': 'off',
|
||||
'@typescript-eslint/no-unsafe-assignment': 'off',
|
||||
'@typescript-eslint/no-unsafe-member-access': 'off',
|
||||
'@typescript-eslint/no-unsafe-call': 'off',
|
||||
},
|
||||
},
|
||||
],
|
||||
ignorePatterns: [
|
||||
'*.mjs',
|
||||
'.eslintrc.js',
|
||||
'tailwind.config.js',
|
||||
'node_modules',
|
||||
'dist_electron',
|
||||
'*.spec.ts',
|
||||
'vite.config.ts',
|
||||
'postcss.config.js',
|
||||
'src/components/**/*.vue', // Incrementally fix these
|
||||
],
|
||||
};
|
||||
|
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@ -13,7 +13,7 @@ jobs:
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16.13.1'
|
||||
node-version: '16.14.0'
|
||||
|
||||
- name: Set yarn version
|
||||
run: yarn set version 1.22.18
|
||||
@ -28,4 +28,4 @@ jobs:
|
||||
env:
|
||||
CSC_IDENTITY_AUTO_DISCOVERY: false
|
||||
APPLE_NOTARIZE: 0
|
||||
run: yarn electron:build -mw --publish never
|
||||
run: yarn build -mw --publish never
|
||||
|
32
.github/workflows/lint.yml
vendored
Normal file
32
.github/workflows/lint.yml
vendored
Normal 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
|
12
.github/workflows/publish.yml
vendored
12
.github/workflows/publish.yml
vendored
@ -9,7 +9,7 @@ jobs:
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16.13.1'
|
||||
node-version: '16.14.0'
|
||||
|
||||
- name: Checkout Books
|
||||
uses: actions/checkout@v2
|
||||
@ -42,7 +42,7 @@ jobs:
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
run: |
|
||||
yarn set version 1.22.18
|
||||
yarn electron:build --mac --publish always
|
||||
yarn build --mac --publish always
|
||||
|
||||
- name: Tar files
|
||||
run: tar -cvf dist-macOS.tar dist_electron
|
||||
@ -59,7 +59,7 @@ jobs:
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16.13.1'
|
||||
node-version: '16.14.0'
|
||||
|
||||
- name: Checkout Books
|
||||
uses: actions/checkout@v2
|
||||
@ -86,7 +86,7 @@ jobs:
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
run: |
|
||||
yarn set version 1.22.18
|
||||
yarn electron:build --linux --publish always
|
||||
yarn build --linux --publish always
|
||||
|
||||
- name: Tar files
|
||||
run: tar -cvf dist-linux.tar dist_electron
|
||||
@ -107,7 +107,7 @@ jobs:
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16.13.1'
|
||||
node-version: '16.14.0'
|
||||
|
||||
- name: Checkout Books
|
||||
uses: actions/checkout@v2
|
||||
@ -132,7 +132,7 @@ jobs:
|
||||
WIN_CSC_LINK: ${{ secrets.WIN_CSC_LINK }}
|
||||
WIN_CSC_KEY_PASSWORD: ${{ secrets.WIN_CSC_KEY_PASSWORD }}
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
run: yarn electron:build --win --publish always
|
||||
run: yarn build --win --publish always
|
||||
|
||||
- name: Tar files
|
||||
run: tar -cvf dist-windows.tar dist_electron
|
||||
|
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@ -17,7 +17,7 @@ jobs:
|
||||
- name: Setup node
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16.13.1'
|
||||
node-version: '16.14.0'
|
||||
|
||||
- name: Set yarn version
|
||||
run: yarn set version 1.22.18
|
||||
|
26
README.md
26
README.md
@ -60,7 +60,7 @@ a local SQLite file as the database.
|
||||
### Pre-requisites
|
||||
|
||||
To get the dev environment up and running you need to first set up Node.js version
|
||||
16.13.1 and npm. For this, we suggest using
|
||||
16.14.0 and npm. For this, we suggest using
|
||||
[nvm](https://github.com/nvm-sh/nvm#installing-and-updating).
|
||||
|
||||
Next, you will need to install [yarn](https://classic.yarnpkg.com/lang/en/docs/install/#mac-stable).
|
||||
@ -87,25 +87,41 @@ To run Frappe Books in development mode (with hot reload, etc):
|
||||
|
||||
```bash
|
||||
# start the electron app
|
||||
yarn electron:serve
|
||||
yarn dev
|
||||
```
|
||||
|
||||
**Note: First Boot**
|
||||
|
||||
When you run `yarn dev` electron will run immediately but the UI will take a
|
||||
couple of seconds to render this because of how dev mode works. Each file is
|
||||
individually served by the dev server. And there are many files that have to be
|
||||
sent.
|
||||
|
||||
|
||||
**Note: Debug Electron Main Process**
|
||||
|
||||
When in dev mode electron runs with the `--inspect` flag which allows an
|
||||
external debugger to connect to port 5858. You can use chrome for this by
|
||||
visiting `chrome://inspect` while Frappe Books is running in dev mode.
|
||||
|
||||
See more [here](https://www.electronjs.org/docs/latest/tutorial/debugging-main-process#external-debuggers).
|
||||
|
||||
#### Build
|
||||
|
||||
To build Frappe Books and create an installer:
|
||||
|
||||
```bash
|
||||
# start the electron app
|
||||
yarn electron:build
|
||||
yarn build
|
||||
```
|
||||
|
||||
**Note**
|
||||
**Note: Build Target**
|
||||
By default the above command will build for your computer's operating system and
|
||||
architecture. To build for other environments (example: for linux from a windows
|
||||
computer) check the _Building_ section at
|
||||
[electron.build/cli](https://www.electron.build/cli).
|
||||
|
||||
So to build for linux you could use the `--linux` flag like so: `yarn electron:build --linux`.
|
||||
So to build for linux you could use the `--linux` flag like so: `yarn build --linux`.
|
||||
|
||||
## Contributions and Community
|
||||
|
||||
|
@ -1,3 +0,0 @@
|
||||
module.exports = {
|
||||
presets: ['@vue/cli-plugin-babel/preset']
|
||||
};
|
@ -1,3 +1,10 @@
|
||||
import {
|
||||
Cashflow,
|
||||
IncomeExpense,
|
||||
TopExpenses,
|
||||
TotalCreditAndDebit,
|
||||
TotalOutstanding,
|
||||
} from 'utils/db/types';
|
||||
import { ModelNameEnum } from '../../models/types';
|
||||
import DatabaseCore from './core';
|
||||
import { BespokeFunction } from './types';
|
||||
@ -43,7 +50,7 @@ export class BespokeQueries {
|
||||
.groupBy('account')
|
||||
.orderBy('total', 'desc')
|
||||
.limit(5);
|
||||
return topExpenses;
|
||||
return topExpenses as TopExpenses;
|
||||
}
|
||||
|
||||
static async getTotalOutstanding(
|
||||
@ -52,13 +59,13 @@ export class BespokeQueries {
|
||||
fromDate: string,
|
||||
toDate: string
|
||||
) {
|
||||
return await db.knex!(schemaName)
|
||||
return (await db.knex!(schemaName)
|
||||
.sum({ total: 'baseGrandTotal' })
|
||||
.sum({ outstanding: 'outstandingAmount' })
|
||||
.where('submitted', true)
|
||||
.where('cancelled', false)
|
||||
.whereBetween('date', [fromDate, toDate])
|
||||
.first();
|
||||
.first()) as TotalOutstanding;
|
||||
}
|
||||
|
||||
static async getCashflow(db: DatabaseCore, fromDate: string, toDate: string) {
|
||||
@ -67,7 +74,7 @@ export class BespokeQueries {
|
||||
.where('accountType', 'in', ['Cash', 'Bank'])
|
||||
.andWhere('isGroup', false);
|
||||
const dateAsMonthYear = db.knex!.raw(`strftime('%Y-%m', ??)`, 'date');
|
||||
return await db.knex!('AccountingLedgerEntry')
|
||||
return (await db.knex!('AccountingLedgerEntry')
|
||||
.where('reverted', false)
|
||||
.sum({
|
||||
inflow: 'debit',
|
||||
@ -78,7 +85,7 @@ export class BespokeQueries {
|
||||
})
|
||||
.where('account', 'in', cashAndBankAccounts)
|
||||
.whereBetween('date', [fromDate, toDate])
|
||||
.groupBy(dateAsMonthYear);
|
||||
.groupBy(dateAsMonthYear)) as Cashflow;
|
||||
}
|
||||
|
||||
static async getIncomeAndExpenses(
|
||||
@ -86,7 +93,7 @@ export class BespokeQueries {
|
||||
fromDate: string,
|
||||
toDate: string
|
||||
) {
|
||||
const income = await db.knex!.raw(
|
||||
const income = (await db.knex!.raw(
|
||||
`
|
||||
select sum(cast(credit as real) - cast(debit as real)) as balance, strftime('%Y-%m', date) as yearmonth
|
||||
from AccountingLedgerEntry
|
||||
@ -100,9 +107,9 @@ export class BespokeQueries {
|
||||
)
|
||||
group by yearmonth`,
|
||||
[fromDate, toDate]
|
||||
);
|
||||
)) as IncomeExpense['income'];
|
||||
|
||||
const expense = await db.knex!.raw(
|
||||
const expense = (await db.knex!.raw(
|
||||
`
|
||||
select sum(cast(debit as real) - cast(credit as real)) as balance, strftime('%Y-%m', date) as yearmonth
|
||||
from AccountingLedgerEntry
|
||||
@ -116,20 +123,20 @@ export class BespokeQueries {
|
||||
)
|
||||
group by yearmonth`,
|
||||
[fromDate, toDate]
|
||||
);
|
||||
)) as IncomeExpense['expense'];
|
||||
|
||||
return { income, expense };
|
||||
}
|
||||
|
||||
static async getTotalCreditAndDebit(db: DatabaseCore) {
|
||||
return await db.knex!.raw(`
|
||||
return (await db.knex!.raw(`
|
||||
select
|
||||
account,
|
||||
sum(cast(credit as real)) as totalCredit,
|
||||
sum(cast(debit as real)) as totalDebit
|
||||
from AccountingLedgerEntry
|
||||
group by account
|
||||
`);
|
||||
`)) as unknown as TotalCreditAndDebit;
|
||||
}
|
||||
|
||||
static async getStockQuantity(
|
||||
@ -141,6 +148,7 @@ export class BespokeQueries {
|
||||
batch?: string,
|
||||
serialNumbers?: string[]
|
||||
): Promise<number | null> {
|
||||
/* eslint-disable @typescript-eslint/no-floating-promises */
|
||||
const query = db.knex!(ModelNameEnum.StockLedgerEntry)
|
||||
.sum('quantity')
|
||||
.where('item', item);
|
||||
|
@ -1,9 +1,4 @@
|
||||
import {
|
||||
CannotCommitError,
|
||||
getDbError,
|
||||
NotFoundError,
|
||||
ValueError,
|
||||
} from 'fyo/utils/errors';
|
||||
import { getDbError, NotFoundError, ValueError } from 'fyo/utils/errors';
|
||||
import { knex, Knex } from 'knex';
|
||||
import {
|
||||
Field,
|
||||
@ -73,16 +68,16 @@ export default class DatabaseCore extends DatabaseBase {
|
||||
|
||||
let query: { value: string }[] = [];
|
||||
try {
|
||||
query = await db.knex!('SingleValue').where({
|
||||
query = (await db.knex!('SingleValue').where({
|
||||
fieldname: 'countryCode',
|
||||
parent: 'SystemSettings',
|
||||
});
|
||||
})) as { value: string }[];
|
||||
} catch {
|
||||
// Database not inialized and no countryCode passed
|
||||
}
|
||||
|
||||
if (query.length > 0) {
|
||||
countryCode = query[0].value as string;
|
||||
countryCode = query[0].value;
|
||||
}
|
||||
|
||||
await db.close();
|
||||
@ -95,9 +90,6 @@ export default class DatabaseCore extends DatabaseBase {
|
||||
|
||||
async connect() {
|
||||
this.knex = knex(this.connectionParams);
|
||||
this.knex.on('query-error', (error) => {
|
||||
error.type = getDbError(error);
|
||||
});
|
||||
await this.knex.raw('PRAGMA foreign_keys=ON');
|
||||
}
|
||||
|
||||
@ -105,22 +97,6 @@ export default class DatabaseCore extends DatabaseBase {
|
||||
await this.knex!.destroy();
|
||||
}
|
||||
|
||||
async commit() {
|
||||
/**
|
||||
* this auto commits, commit is not required
|
||||
* will later wrap the outermost functions in
|
||||
* transactions.
|
||||
*/
|
||||
try {
|
||||
// await this.knex!.raw('commit');
|
||||
} catch (err) {
|
||||
const type = getDbError(err as Error);
|
||||
if (type !== CannotCommitError) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async migrate() {
|
||||
for (const schemaName in this.schemaMap) {
|
||||
const schema = this.schemaMap[schemaName] as Schema;
|
||||
@ -135,7 +111,6 @@ export default class DatabaseCore extends DatabaseBase {
|
||||
}
|
||||
}
|
||||
|
||||
await this.commit();
|
||||
await this.#initializeSingles();
|
||||
}
|
||||
|
||||
@ -149,6 +124,7 @@ export default class DatabaseCore extends DatabaseBase {
|
||||
try {
|
||||
const qb = this.knex!(schemaName);
|
||||
if (name !== undefined) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
qb.where({ name });
|
||||
}
|
||||
row = await qb.limit(1);
|
||||
@ -178,7 +154,7 @@ export default class DatabaseCore extends DatabaseBase {
|
||||
|
||||
async get(
|
||||
schemaName: string,
|
||||
name: string = '',
|
||||
name = '',
|
||||
fields?: string | string[]
|
||||
): Promise<FieldValueMap> {
|
||||
const schema = this.schemaMap[schemaName] as Schema;
|
||||
@ -316,7 +292,6 @@ export default class DatabaseCore extends DatabaseBase {
|
||||
await this.knex!(schemaName)
|
||||
.update({ name: newName })
|
||||
.where('name', oldName);
|
||||
await this.commit();
|
||||
}
|
||||
|
||||
async update(schemaName: string, fieldValueMap: FieldValueMap) {
|
||||
@ -422,6 +397,7 @@ export default class DatabaseCore extends DatabaseBase {
|
||||
filters: QueryFilter,
|
||||
options: GetQueryBuilderOptions
|
||||
): Knex.QueryBuilder {
|
||||
/* eslint-disable @typescript-eslint/no-floating-promises */
|
||||
const builder = this.knex!.select(fields).from(schemaName);
|
||||
|
||||
this.#applyFiltersToBuilder(builder, filters);
|
||||
@ -464,12 +440,12 @@ export default class DatabaseCore extends DatabaseBase {
|
||||
// => `date >= 2017-09-09 and date <= 2017-11-01`
|
||||
|
||||
const filtersArray = this.#getFiltersArray(filters);
|
||||
for (const i in filtersArray) {
|
||||
for (let i = 0; i < filtersArray.length; i++) {
|
||||
const filter = filtersArray[i];
|
||||
const field = filter[0] as string;
|
||||
const operator = filter[1];
|
||||
const comparisonValue = filter[2];
|
||||
const type = i === '0' ? 'where' : 'andWhere';
|
||||
const type = i === 0 ? 'where' : 'andWhere';
|
||||
|
||||
if (operator === '=') {
|
||||
builder[type](field, comparisonValue);
|
||||
@ -505,7 +481,8 @@ export default class DatabaseCore extends DatabaseBase {
|
||||
|
||||
if (
|
||||
operator === 'like' &&
|
||||
!(comparisonValue as (string | number)[]).includes('%')
|
||||
typeof comparisonValue === 'string' &&
|
||||
!comparisonValue.includes('%')
|
||||
) {
|
||||
comparisonValue = `%${comparisonValue}%`;
|
||||
}
|
||||
@ -595,11 +572,8 @@ export default class DatabaseCore extends DatabaseBase {
|
||||
}
|
||||
|
||||
// link
|
||||
if (
|
||||
field.fieldtype === FieldTypeEnum.Link &&
|
||||
(field as TargetField).target
|
||||
) {
|
||||
const targetSchemaName = (field as TargetField).target as string;
|
||||
if (field.fieldtype === FieldTypeEnum.Link && field.target) {
|
||||
const targetSchemaName = field.target;
|
||||
const schema = this.schemaMap[targetSchemaName] as Schema;
|
||||
table
|
||||
.foreign(field.fieldname)
|
||||
@ -630,7 +604,7 @@ export default class DatabaseCore extends DatabaseBase {
|
||||
}
|
||||
|
||||
if (newForeignKeys.length) {
|
||||
await this.#addForeignKeys(schemaName, newForeignKeys);
|
||||
await this.#addForeignKeys(schemaName);
|
||||
}
|
||||
}
|
||||
|
||||
@ -652,15 +626,15 @@ export default class DatabaseCore extends DatabaseBase {
|
||||
|
||||
async #getNonExtantSingleValues(singleSchemaName: string) {
|
||||
const existingFields = (
|
||||
await this.knex!('SingleValue')
|
||||
(await this.knex!('SingleValue')
|
||||
.where({ parent: singleSchemaName })
|
||||
.select('fieldname')
|
||||
.select('fieldname')) as { fieldname: string }[]
|
||||
).map(({ fieldname }) => fieldname);
|
||||
|
||||
return this.schemaMap[singleSchemaName]!.fields.map(
|
||||
({ fieldname, default: value }) => ({
|
||||
fieldname,
|
||||
value: value as RawValue | undefined,
|
||||
value: value,
|
||||
})
|
||||
).filter(
|
||||
({ fieldname, value }) =>
|
||||
@ -710,7 +684,7 @@ export default class DatabaseCore extends DatabaseBase {
|
||||
child.idx ??= idx;
|
||||
}
|
||||
|
||||
async #addForeignKeys(schemaName: string, newForeignKeys: Field[]) {
|
||||
async #addForeignKeys(schemaName: string) {
|
||||
const tableRows = await this.knex!.select().from(schemaName);
|
||||
await this.prestigeTheTable(schemaName, tableRows);
|
||||
}
|
||||
@ -731,10 +705,10 @@ export default class DatabaseCore extends DatabaseBase {
|
||||
}
|
||||
|
||||
async #getOne(schemaName: string, name: string, fields: string[]) {
|
||||
const fieldValueMap: FieldValueMap = await this.knex!.select(fields)
|
||||
const fieldValueMap = (await this.knex!.select(fields)
|
||||
.from(schemaName)
|
||||
.where('name', name)
|
||||
.first();
|
||||
.first()) as FieldValueMap;
|
||||
return fieldValueMap;
|
||||
}
|
||||
|
||||
@ -794,9 +768,9 @@ export default class DatabaseCore extends DatabaseBase {
|
||||
fieldname,
|
||||
};
|
||||
|
||||
const names: { name: string }[] = await this.knex!('SingleValue')
|
||||
const names = (await this.knex!('SingleValue')
|
||||
.select('name')
|
||||
.where(updateKey);
|
||||
.where(updateKey)) as { name: string }[];
|
||||
|
||||
if (!names?.length) {
|
||||
this.#insertSingleValue(singleSchemaName, fieldname, value);
|
||||
@ -899,7 +873,7 @@ export default class DatabaseCore extends DatabaseBase {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const child of tableFieldValue!) {
|
||||
for (const child of tableFieldValue) {
|
||||
this.#prepareChild(schemaName, parentName, child, field, added.length);
|
||||
|
||||
if (
|
||||
|
@ -204,7 +204,7 @@ export async function assertDoesNotThrow(
|
||||
throw new assert.AssertionError({
|
||||
message: `Got unwanted exception${
|
||||
message ? `: ${message}` : ''
|
||||
}\nError: ${(err as Error).message}\n${(err as Error).stack}`,
|
||||
}\nError: ${(err as Error).message}\n${(err as Error).stack ?? ''}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -55,7 +55,13 @@ export function emitMainProcessError(
|
||||
error: unknown,
|
||||
more?: Record<string, unknown>
|
||||
) {
|
||||
(process.emit as Function)(CUSTOM_EVENTS.MAIN_PROCESS_ERROR, error, more);
|
||||
(
|
||||
process.emit as (
|
||||
event: string,
|
||||
error: unknown,
|
||||
more?: Record<string, unknown>
|
||||
) => void
|
||||
)(CUSTOM_EVENTS.MAIN_PROCESS_ERROR, error, more);
|
||||
}
|
||||
|
||||
export async function checkFileAccess(filePath: string, mode?: number) {
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { DatabaseManager } from '../database/manager';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async function execute(dm: DatabaseManager) {
|
||||
/**
|
||||
* Execute function will receive the DatabaseManager which is to be used
|
||||
|
@ -30,9 +30,9 @@ async function execute(dm: DatabaseManager) {
|
||||
|
||||
const sourceKnex = dm.db!.knex!;
|
||||
const version = (
|
||||
await sourceKnex('SingleValue')
|
||||
(await sourceKnex('SingleValue')
|
||||
.select('value')
|
||||
.where({ fieldname: 'version' })
|
||||
.where({ fieldname: 'version' })) as { value: string }[]
|
||||
)?.[0]?.value;
|
||||
|
||||
/**
|
||||
@ -58,7 +58,7 @@ async function execute(dm: DatabaseManager) {
|
||||
await copyData(sourceKnex, destDm);
|
||||
} catch (err) {
|
||||
const destPath = destDm.db!.dbPath;
|
||||
destDm.db!.close();
|
||||
await destDm.db!.close();
|
||||
await fs.unlink(destPath);
|
||||
throw err;
|
||||
}
|
||||
@ -94,7 +94,7 @@ async function replaceDatabaseCore(
|
||||
async function copyData(sourceKnex: Knex, destDm: DatabaseManager) {
|
||||
const destKnex = destDm.db!.knex!;
|
||||
const schemaMap = destDm.getSchemaMap();
|
||||
await destKnex!.raw('PRAGMA foreign_keys=OFF');
|
||||
await destKnex.raw('PRAGMA foreign_keys=OFF');
|
||||
await copySingleValues(sourceKnex, destKnex, schemaMap);
|
||||
await copyParty(sourceKnex, destKnex, schemaMap[ModelNameEnum.Party]!);
|
||||
await copyItem(sourceKnex, destKnex, schemaMap[ModelNameEnum.Item]!);
|
||||
@ -111,7 +111,7 @@ async function copyData(sourceKnex: Knex, destDm: DatabaseManager) {
|
||||
destKnex,
|
||||
schemaMap[ModelNameEnum.NumberSeries]!
|
||||
);
|
||||
await destKnex!.raw('PRAGMA foreign_keys=ON');
|
||||
await destKnex.raw('PRAGMA foreign_keys=ON');
|
||||
}
|
||||
|
||||
async function copyNumberSeries(
|
||||
@ -137,14 +137,14 @@ async function copyNumberSeries(
|
||||
continue;
|
||||
}
|
||||
|
||||
const indices = await sourceKnex.raw(
|
||||
const indices = (await sourceKnex.raw(
|
||||
`
|
||||
select cast(substr(name, ??) as int) as idx
|
||||
from ??
|
||||
order by idx desc
|
||||
limit 1`,
|
||||
[name.length + 1, referenceType]
|
||||
);
|
||||
)) as { idx: number }[];
|
||||
|
||||
value.start = 1001;
|
||||
value.current = indices[0]?.idx ?? value.current ?? value.start;
|
||||
@ -358,7 +358,9 @@ async function getCountryCode(knex: Knex) {
|
||||
* Need to account for schema changes, in 0.4.3-beta.0
|
||||
*/
|
||||
const country = (
|
||||
await knex('SingleValue').select('value').where({ fieldname: 'country' })
|
||||
(await knex('SingleValue')
|
||||
.select('value')
|
||||
.where({ fieldname: 'country' })) as { value: string }[]
|
||||
)?.[0]?.value;
|
||||
|
||||
if (!country) {
|
||||
|
166
build/scripts/build.mjs
Normal file
166
build/scripts/build.mjs
Normal 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
142
build/scripts/dev.mjs
Normal 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
51
build/scripts/helpers.mjs
Normal 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
115
colors.json
Normal 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"
|
||||
}
|
||||
}
|
@ -25,8 +25,8 @@ type Notifier = (stage: string, percent: number) => void;
|
||||
export async function setupDummyInstance(
|
||||
dbPath: string,
|
||||
fyo: Fyo,
|
||||
years: number = 1,
|
||||
baseCount: number = 1000,
|
||||
years = 1,
|
||||
baseCount = 1000,
|
||||
notifier?: Notifier
|
||||
) {
|
||||
await fyo.purgeCache();
|
||||
@ -251,7 +251,7 @@ async function getSalesInvoices(
|
||||
* For each date create a Sales Invoice.
|
||||
*/
|
||||
|
||||
for (const d in dates) {
|
||||
for (let d = 0; d < dates.length; d++) {
|
||||
const date = dates[d];
|
||||
|
||||
notifier?.(
|
||||
@ -424,7 +424,7 @@ async function getSalesPurchaseInvoices(
|
||||
for (const item of supplierGrouped[supplier]) {
|
||||
await doc.append('items', {});
|
||||
const quantity = purchaseQty[item];
|
||||
doc.items!.at(-1)!.set({ item, quantity });
|
||||
await doc.items!.at(-1)!.set({ item, quantity });
|
||||
}
|
||||
|
||||
invoices.push(doc);
|
||||
@ -527,7 +527,7 @@ async function syncAndSubmit(docs: Doc[], notifier?: Notifier) {
|
||||
};
|
||||
|
||||
const total = docs.length;
|
||||
for (const i in docs) {
|
||||
for (let i = 0; i < docs.length; i++) {
|
||||
const doc = docs[i];
|
||||
notifier?.(
|
||||
`Syncing ${nameMap[doc.schemaName]}, ${i} out of ${total}`,
|
||||
|
@ -58,59 +58,8 @@ export class AuthHandler {
|
||||
return { ...this.#config };
|
||||
}
|
||||
|
||||
init() {}
|
||||
async login(email: string, password: string) {
|
||||
if (email === 'Administrator') {
|
||||
this.#session.user = 'Administrator';
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch(this.#getServerURL() + '/api/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ email, password }),
|
||||
});
|
||||
|
||||
if (response.status === 200) {
|
||||
const res = await response.json();
|
||||
|
||||
this.#session.user = email;
|
||||
this.#session.token = res.token;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async signup(email: string, fullName: string, password: string) {
|
||||
const response = await fetch(this.#getServerURL() + '/api/signup', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ email, fullName, password }),
|
||||
});
|
||||
|
||||
if (response.status === 200) {
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async logout() {
|
||||
// TODO: Implement this with auth flow
|
||||
}
|
||||
|
||||
async purgeCache() {}
|
||||
|
||||
#getServerURL() {
|
||||
return this.#config.serverURL || '';
|
||||
init() {
|
||||
return null;
|
||||
}
|
||||
|
||||
async getCreds(): Promise<Creds> {
|
||||
|
@ -144,7 +144,7 @@ export class Converter {
|
||||
return this.#toRawValueMap(parentSchemaName, value.getValidDict());
|
||||
}
|
||||
|
||||
return this.#toRawValueMap(parentSchemaName, value as DocValueMap);
|
||||
return this.#toRawValueMap(parentSchemaName, value);
|
||||
});
|
||||
} else {
|
||||
rawValueMap[fieldname] = Converter.toRawValue(
|
||||
@ -176,7 +176,7 @@ function toDocString(value: RawValue, field: Field) {
|
||||
}
|
||||
|
||||
function toDocDate(value: RawValue, field: Field) {
|
||||
if ((value as any) instanceof Date) {
|
||||
if ((value as unknown) instanceof Date) {
|
||||
return value;
|
||||
}
|
||||
|
||||
@ -286,7 +286,7 @@ function toDocAttachment(value: RawValue, field: Field): null | Attachment {
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(value) || null;
|
||||
return (JSON.parse(value) as Attachment) || null;
|
||||
} catch {
|
||||
throwError(value, field, 'doc');
|
||||
}
|
||||
@ -322,7 +322,7 @@ function toRawInt(value: DocValue, field: Field): number {
|
||||
}
|
||||
|
||||
if (typeof value === 'number') {
|
||||
return Math.floor(value as number);
|
||||
return Math.floor(value);
|
||||
}
|
||||
|
||||
throwError(value, field, 'raw');
|
||||
@ -363,7 +363,7 @@ function toRawDateTime(value: DocValue, field: Field): string | null {
|
||||
}
|
||||
|
||||
if (value instanceof Date) {
|
||||
return (value as Date).toISOString();
|
||||
return value.toISOString();
|
||||
}
|
||||
|
||||
if (value instanceof DateTime) {
|
||||
@ -431,8 +431,8 @@ function toRawAttachment(value: DocValue, field: Field): null | string {
|
||||
|
||||
function throwError<T>(value: T, field: Field, type: 'raw' | 'doc'): never {
|
||||
throw new ValueError(
|
||||
`invalid ${type} conversion '${value}' of type ${typeof value} found, field: ${JSON.stringify(
|
||||
field
|
||||
)}`
|
||||
`invalid ${type} conversion '${String(
|
||||
value
|
||||
)}' of type ${typeof value} found, field: ${JSON.stringify(field)}`
|
||||
);
|
||||
}
|
||||
|
@ -7,10 +7,15 @@ import { translateSchema } from 'fyo/utils/translation';
|
||||
import { Field, RawValue, SchemaMap } from 'schemas/types';
|
||||
import { getMapFromList } from 'utils';
|
||||
import {
|
||||
Cashflow,
|
||||
DatabaseBase,
|
||||
DatabaseDemuxBase,
|
||||
GetAllOptions,
|
||||
IncomeExpense,
|
||||
QueryFilter,
|
||||
TopExpenses,
|
||||
TotalCreditAndDebit,
|
||||
TotalOutstanding,
|
||||
} from 'utils/db/types';
|
||||
import { schemaTranslateables } from 'utils/translationHelpers';
|
||||
import { LanguageMap } from 'utils/types';
|
||||
@ -22,20 +27,10 @@ import {
|
||||
RawValueMap,
|
||||
} from './types';
|
||||
|
||||
// Return types of Bespoke Queries
|
||||
type TopExpenses = { account: string; total: number }[];
|
||||
type TotalOutstanding = { total: number; outstanding: number };
|
||||
type Cashflow = { inflow: number; outflow: number; yearmonth: string }[];
|
||||
type Balance = { balance: number; yearmonth: string }[];
|
||||
type IncomeExpense = { income: Balance; expense: Balance };
|
||||
type TotalCreditAndDebit = {
|
||||
account: string;
|
||||
totalCredit: number;
|
||||
totalDebit: number;
|
||||
};
|
||||
type FieldMap = Record<string, Record<string, Field>>;
|
||||
|
||||
export class DatabaseHandler extends DatabaseBase {
|
||||
/* eslint-disable @typescript-eslint/no-floating-promises */
|
||||
#fyo: Fyo;
|
||||
converter: Converter;
|
||||
#demux: DatabaseDemuxBase;
|
||||
@ -83,7 +78,7 @@ export class DatabaseHandler extends DatabaseBase {
|
||||
}
|
||||
|
||||
async init() {
|
||||
this.#schemaMap = (await this.#demux.getSchemaMap()) as SchemaMap;
|
||||
this.#schemaMap = await this.#demux.getSchemaMap();
|
||||
this.#setFieldMap();
|
||||
this.observer = new Observable();
|
||||
}
|
||||
@ -92,7 +87,7 @@ export class DatabaseHandler extends DatabaseBase {
|
||||
if (languageMap) {
|
||||
translateSchema(this.#schemaMap, languageMap, schemaTranslateables);
|
||||
} else {
|
||||
this.#schemaMap = (await this.#demux.getSchemaMap()) as SchemaMap;
|
||||
this.#schemaMap = await this.#demux.getSchemaMap();
|
||||
this.#setFieldMap();
|
||||
}
|
||||
}
|
||||
@ -142,6 +137,7 @@ export class DatabaseHandler extends DatabaseBase {
|
||||
options: GetAllOptions = {}
|
||||
): Promise<DocValueMap[]> {
|
||||
const rawValueMap = await this.#getAll(schemaName, options);
|
||||
|
||||
this.observer.trigger(`getAll:${schemaName}`, options);
|
||||
return this.converter.toDocValueMap(
|
||||
schemaName,
|
||||
@ -154,6 +150,7 @@ export class DatabaseHandler extends DatabaseBase {
|
||||
options: GetAllOptions = {}
|
||||
): Promise<RawValueMap[]> {
|
||||
const all = await this.#getAll(schemaName, options);
|
||||
|
||||
this.observer.trigger(`getAllRaw:${schemaName}`, options);
|
||||
return all;
|
||||
}
|
||||
@ -188,6 +185,7 @@ export class DatabaseHandler extends DatabaseBase {
|
||||
): Promise<number> {
|
||||
const rawValueMap = await this.#getAll(schemaName, options);
|
||||
const count = rawValueMap.length;
|
||||
|
||||
this.observer.trigger(`count:${schemaName}`, options);
|
||||
return count;
|
||||
}
|
||||
@ -199,18 +197,21 @@ export class DatabaseHandler extends DatabaseBase {
|
||||
newName: string
|
||||
): Promise<void> {
|
||||
await this.#demux.call('rename', schemaName, oldName, newName);
|
||||
|
||||
this.observer.trigger(`rename:${schemaName}`, { oldName, newName });
|
||||
}
|
||||
|
||||
async update(schemaName: string, docValueMap: DocValueMap): Promise<void> {
|
||||
const rawValueMap = this.converter.toRawValueMap(schemaName, docValueMap);
|
||||
await this.#demux.call('update', schemaName, rawValueMap);
|
||||
|
||||
this.observer.trigger(`update:${schemaName}`, docValueMap);
|
||||
}
|
||||
|
||||
// Delete
|
||||
async delete(schemaName: string, name: string): Promise<void> {
|
||||
await this.#demux.call('delete', schemaName, name);
|
||||
|
||||
this.observer.trigger(`delete:${schemaName}`, name);
|
||||
}
|
||||
|
||||
@ -220,6 +221,7 @@ export class DatabaseHandler extends DatabaseBase {
|
||||
schemaName,
|
||||
filters
|
||||
)) as number;
|
||||
|
||||
this.observer.trigger(`deleteAll:${schemaName}`, filters);
|
||||
return count;
|
||||
}
|
||||
@ -231,6 +233,7 @@ export class DatabaseHandler extends DatabaseBase {
|
||||
schemaName,
|
||||
name
|
||||
)) as boolean;
|
||||
|
||||
this.observer.trigger(`exists:${schemaName}`, name);
|
||||
return doesExist;
|
||||
}
|
||||
@ -302,7 +305,7 @@ export class DatabaseHandler extends DatabaseBase {
|
||||
)) as IncomeExpense;
|
||||
}
|
||||
|
||||
async getTotalCreditAndDebit(): Promise<unknown> {
|
||||
async getTotalCreditAndDebit(): Promise<TotalCreditAndDebit[]> {
|
||||
return (await this.#demux.callBespoke(
|
||||
'getTotalCreditAndDebit'
|
||||
)) as TotalCreditAndDebit[];
|
||||
|
@ -28,7 +28,7 @@ export class DocHandler {
|
||||
this.observer = new Observable();
|
||||
}
|
||||
|
||||
async purgeCache() {
|
||||
purgeCache() {
|
||||
this.init();
|
||||
}
|
||||
|
||||
@ -82,10 +82,10 @@ export class DocHandler {
|
||||
getNewDoc(
|
||||
schemaName: string,
|
||||
data: DocValueMap | RawValueMap = {},
|
||||
cacheDoc: boolean = true,
|
||||
cacheDoc = true,
|
||||
schema?: Schema,
|
||||
Model?: typeof Doc,
|
||||
isRawValueMap: boolean = true
|
||||
isRawValueMap = true
|
||||
): Doc {
|
||||
if (!this.models[schemaName] && Model) {
|
||||
this.models[schemaName] = Model;
|
||||
@ -153,7 +153,8 @@ export class DocHandler {
|
||||
|
||||
// propagate change to `docs`
|
||||
doc.on('change', (params: unknown) => {
|
||||
this.docs!.trigger('change', params);
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.docs.trigger('change', params);
|
||||
});
|
||||
|
||||
doc.on('afterSync', () => {
|
||||
@ -167,22 +168,24 @@ export class DocHandler {
|
||||
}
|
||||
|
||||
#setCacheUpdationListeners(schemaName: string) {
|
||||
this.fyo.db.observer.on(`delete:${schemaName}`, (name: string) => {
|
||||
this.fyo.db.observer.on(`delete:${schemaName}`, (name) => {
|
||||
if (typeof name !== 'string') {
|
||||
return;
|
||||
}
|
||||
|
||||
this.removeFromCache(schemaName, name);
|
||||
});
|
||||
|
||||
this.fyo.db.observer.on(
|
||||
`rename:${schemaName}`,
|
||||
(names: { oldName: string; newName: string }) => {
|
||||
const doc = this.#getFromCache(schemaName, names.oldName);
|
||||
if (doc === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.removeFromCache(schemaName, names.oldName);
|
||||
this.#addToCache(doc);
|
||||
this.fyo.db.observer.on(`rename:${schemaName}`, (names) => {
|
||||
const { oldName } = names as { oldName: string };
|
||||
const doc = this.#getFromCache(schemaName, oldName);
|
||||
if (doc === undefined) {
|
||||
return;
|
||||
}
|
||||
);
|
||||
|
||||
this.removeFromCache(schemaName, oldName);
|
||||
this.#addToCache(doc);
|
||||
});
|
||||
}
|
||||
|
||||
removeFromCache(schemaName: string, name: string) {
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { Doc } from 'fyo/model/doc';
|
||||
import { Money } from 'pesa';
|
||||
import { RawValue } from 'schemas/types';
|
||||
import { AuthDemuxBase } from 'utils/auth/types';
|
||||
import { DatabaseDemuxBase } from 'utils/db/types';
|
||||
import type { Doc } from 'fyo/model/doc';
|
||||
import type { Money } from 'pesa';
|
||||
import type { RawValue } from 'schemas/types';
|
||||
import type { AuthDemuxBase } from 'utils/auth/types';
|
||||
import type { DatabaseDemuxBase } from 'utils/db/types';
|
||||
|
||||
export type Attachment = { name: string; type: string; data: string };
|
||||
export type DocValue =
|
||||
@ -31,12 +31,12 @@ export type DatabaseDemuxConstructor = new (
|
||||
|
||||
export type AuthDemuxConstructor = new (isElectron?: boolean) => AuthDemuxBase;
|
||||
|
||||
export enum ConfigKeys {
|
||||
Files = 'files',
|
||||
LastSelectedFilePath = 'lastSelectedFilePath',
|
||||
Language = 'language',
|
||||
DeviceId = 'deviceId',
|
||||
}
|
||||
export type ConfigMap = {
|
||||
files: ConfigFile[];
|
||||
lastSelectedFilePath: null | string;
|
||||
language: string
|
||||
deviceId: string
|
||||
};
|
||||
|
||||
export interface ConfigFile {
|
||||
id: string;
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { ipcRenderer } from 'electron';
|
||||
import { AuthDemuxBase } from 'utils/auth/types';
|
||||
import { IPC_ACTIONS } from 'utils/messages';
|
||||
import { Creds } from 'utils/types';
|
||||
const { ipcRenderer } = require('electron');
|
||||
|
||||
export class AuthDemux extends AuthDemuxBase {
|
||||
#isElectron: boolean = false;
|
||||
#isElectron = false;
|
||||
constructor(isElectron: boolean) {
|
||||
super();
|
||||
this.#isElectron = isElectron;
|
||||
|
@ -1,55 +1,46 @@
|
||||
import config from 'utils/config';
|
||||
import type Store from 'electron-store';
|
||||
import { ConfigMap } from 'fyo/core/types';
|
||||
|
||||
export class Config {
|
||||
#useElectronConfig: boolean;
|
||||
fallback: Map<string, unknown> = new Map();
|
||||
config: Map<string, unknown> | Store;
|
||||
constructor(isElectron: boolean) {
|
||||
this.#useElectronConfig = isElectron;
|
||||
this.config = new Map();
|
||||
if (isElectron) {
|
||||
const Config = require('electron-store') as typeof Store;
|
||||
this.config = new Config();
|
||||
}
|
||||
}
|
||||
|
||||
get store(): Record<string, unknown> {
|
||||
if (this.#useElectronConfig) {
|
||||
return config.store;
|
||||
} else {
|
||||
get store() {
|
||||
if (this.config instanceof Map) {
|
||||
const store: Record<string, unknown> = {};
|
||||
|
||||
for (const key of this.fallback.keys()) {
|
||||
store[key] = this.fallback.get(key);
|
||||
for (const key of this.config.keys()) {
|
||||
store[key] = this.config.get(key);
|
||||
}
|
||||
|
||||
return store;
|
||||
} else {
|
||||
return this.config;
|
||||
}
|
||||
}
|
||||
|
||||
get(key: string, defaultValue?: unknown): unknown {
|
||||
if (this.#useElectronConfig) {
|
||||
return config.get(key, defaultValue);
|
||||
} else {
|
||||
return this.fallback.get(key) ?? defaultValue;
|
||||
}
|
||||
get<K extends keyof ConfigMap>(
|
||||
key: K,
|
||||
defaultValue?: ConfigMap[K]
|
||||
): ConfigMap[K] | undefined {
|
||||
const value = this.config.get(key) as ConfigMap[K] | undefined;
|
||||
return value ?? defaultValue;
|
||||
}
|
||||
|
||||
set(key: string, value: unknown) {
|
||||
if (this.#useElectronConfig) {
|
||||
config.set(key, value);
|
||||
} else {
|
||||
this.fallback.set(key, value);
|
||||
}
|
||||
set<K extends keyof ConfigMap>(key: K, value: ConfigMap[K]) {
|
||||
this.config.set(key, value);
|
||||
}
|
||||
|
||||
delete(key: string) {
|
||||
if (this.#useElectronConfig) {
|
||||
config.delete(key);
|
||||
} else {
|
||||
this.fallback.delete(key);
|
||||
}
|
||||
delete(key: keyof ConfigMap) {
|
||||
this.config.delete(key);
|
||||
}
|
||||
|
||||
clear() {
|
||||
if (this.#useElectronConfig) {
|
||||
config.clear();
|
||||
} else {
|
||||
this.fallback.clear();
|
||||
}
|
||||
this.config.clear();
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { ipcRenderer } from 'electron';
|
||||
const { ipcRenderer } = require('electron');
|
||||
import { DatabaseError, NotImplemented } from 'fyo/utils/errors';
|
||||
import { SchemaMap } from 'schemas/types';
|
||||
import { DatabaseDemuxBase, DatabaseMethod } from 'utils/db/types';
|
||||
@ -6,7 +6,7 @@ import { BackendResponse } from 'utils/ipc/types';
|
||||
import { IPC_ACTIONS } from 'utils/messages';
|
||||
|
||||
export class DatabaseDemux extends DatabaseDemuxBase {
|
||||
#isElectron: boolean = false;
|
||||
#isElectron = false;
|
||||
constructor(isElectron: boolean) {
|
||||
super();
|
||||
this.#isElectron = isElectron;
|
||||
@ -27,70 +27,76 @@ export class DatabaseDemux extends DatabaseDemuxBase {
|
||||
}
|
||||
|
||||
async getSchemaMap(): Promise<SchemaMap> {
|
||||
if (this.#isElectron) {
|
||||
return (await this.#handleDBCall(async function dbFunc() {
|
||||
return await ipcRenderer.invoke(IPC_ACTIONS.DB_SCHEMA);
|
||||
})) as SchemaMap;
|
||||
if (!this.#isElectron) {
|
||||
throw new NotImplemented();
|
||||
}
|
||||
|
||||
throw new NotImplemented();
|
||||
return (await this.#handleDBCall(async () => {
|
||||
return (await ipcRenderer.invoke(
|
||||
IPC_ACTIONS.DB_SCHEMA
|
||||
)) as BackendResponse;
|
||||
})) as SchemaMap;
|
||||
}
|
||||
|
||||
async createNewDatabase(
|
||||
dbPath: string,
|
||||
countryCode?: string
|
||||
): Promise<string> {
|
||||
if (this.#isElectron) {
|
||||
return (await this.#handleDBCall(async function dbFunc() {
|
||||
return await ipcRenderer.invoke(
|
||||
IPC_ACTIONS.DB_CREATE,
|
||||
dbPath,
|
||||
countryCode
|
||||
);
|
||||
})) as string;
|
||||
if (!this.#isElectron) {
|
||||
throw new NotImplemented();
|
||||
}
|
||||
|
||||
throw new NotImplemented();
|
||||
return (await this.#handleDBCall(async () => {
|
||||
return (await ipcRenderer.invoke(
|
||||
IPC_ACTIONS.DB_CREATE,
|
||||
dbPath,
|
||||
countryCode
|
||||
)) as BackendResponse;
|
||||
})) as string;
|
||||
}
|
||||
|
||||
async connectToDatabase(
|
||||
dbPath: string,
|
||||
countryCode?: string
|
||||
): Promise<string> {
|
||||
if (this.#isElectron) {
|
||||
return (await this.#handleDBCall(async function dbFunc() {
|
||||
return await ipcRenderer.invoke(
|
||||
IPC_ACTIONS.DB_CONNECT,
|
||||
dbPath,
|
||||
countryCode
|
||||
);
|
||||
})) as string;
|
||||
if (!this.#isElectron) {
|
||||
throw new NotImplemented();
|
||||
}
|
||||
|
||||
throw new NotImplemented();
|
||||
return (await this.#handleDBCall(async () => {
|
||||
return (await ipcRenderer.invoke(
|
||||
IPC_ACTIONS.DB_CONNECT,
|
||||
dbPath,
|
||||
countryCode
|
||||
)) as BackendResponse;
|
||||
})) as string;
|
||||
}
|
||||
|
||||
async call(method: DatabaseMethod, ...args: unknown[]): Promise<unknown> {
|
||||
if (this.#isElectron) {
|
||||
return (await this.#handleDBCall(async function dbFunc() {
|
||||
return await ipcRenderer.invoke(IPC_ACTIONS.DB_CALL, method, ...args);
|
||||
})) as unknown;
|
||||
if (!this.#isElectron) {
|
||||
throw new NotImplemented();
|
||||
}
|
||||
|
||||
throw new NotImplemented();
|
||||
return await this.#handleDBCall(async () => {
|
||||
return (await ipcRenderer.invoke(
|
||||
IPC_ACTIONS.DB_CALL,
|
||||
method,
|
||||
...args
|
||||
)) as BackendResponse;
|
||||
});
|
||||
}
|
||||
|
||||
async callBespoke(method: string, ...args: unknown[]): Promise<unknown> {
|
||||
if (this.#isElectron) {
|
||||
return (await this.#handleDBCall(async function dbFunc() {
|
||||
return await ipcRenderer.invoke(
|
||||
IPC_ACTIONS.DB_BESPOKE,
|
||||
method,
|
||||
...args
|
||||
);
|
||||
})) as unknown;
|
||||
if (!this.#isElectron) {
|
||||
throw new NotImplemented();
|
||||
}
|
||||
|
||||
throw new NotImplemented();
|
||||
return await this.#handleDBCall(async () => {
|
||||
return (await ipcRenderer.invoke(
|
||||
IPC_ACTIONS.DB_BESPOKE,
|
||||
method,
|
||||
...args
|
||||
)) as BackendResponse;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
21
fyo/index.ts
21
fyo/index.ts
@ -35,7 +35,7 @@ export class Fyo {
|
||||
doc: DocHandler;
|
||||
db: DatabaseHandler;
|
||||
|
||||
_initialized: boolean = false;
|
||||
_initialized = false;
|
||||
|
||||
errorLog: ErrorLog[] = [];
|
||||
temp?: Record<string, unknown>;
|
||||
@ -94,10 +94,9 @@ export class Fyo {
|
||||
return format(value, field, doc ?? null, this);
|
||||
}
|
||||
|
||||
async setIsElectron() {
|
||||
setIsElectron() {
|
||||
try {
|
||||
const { ipcRenderer } = await import('electron');
|
||||
this.isElectron = Boolean(ipcRenderer);
|
||||
this.isElectron = Boolean(require('electron'));
|
||||
} catch {
|
||||
this.isElectron = false;
|
||||
}
|
||||
@ -106,7 +105,7 @@ export class Fyo {
|
||||
async initializeAndRegister(
|
||||
models: ModelMap = {},
|
||||
regionalModels: ModelMap = {},
|
||||
force: boolean = false
|
||||
force = false
|
||||
) {
|
||||
if (this._initialized && !force) return;
|
||||
|
||||
@ -122,8 +121,8 @@ export class Fyo {
|
||||
// temp params while calling routes
|
||||
this.temp = {};
|
||||
|
||||
await this.doc.init();
|
||||
await this.auth.init();
|
||||
this.doc.init();
|
||||
this.auth.init();
|
||||
await this.db.init();
|
||||
}
|
||||
|
||||
@ -165,7 +164,6 @@ export class Fyo {
|
||||
|
||||
async close() {
|
||||
await this.db.close();
|
||||
await this.auth.logout();
|
||||
}
|
||||
|
||||
getField(schemaName: string, fieldname: string) {
|
||||
@ -190,14 +188,14 @@ export class Fyo {
|
||||
let value: DocValue | Doc[];
|
||||
try {
|
||||
doc = await this.doc.getDoc(schemaName, name);
|
||||
value = doc.get(fieldname!);
|
||||
value = doc.get(fieldname);
|
||||
} catch (err) {
|
||||
value = undefined;
|
||||
}
|
||||
|
||||
if (value === undefined && schemaName === name) {
|
||||
const sv = await this.db.getSingleValues({
|
||||
fieldname: fieldname!,
|
||||
fieldname: fieldname,
|
||||
parent: schemaName,
|
||||
});
|
||||
|
||||
@ -222,8 +220,7 @@ export class Fyo {
|
||||
this.errorLog = [];
|
||||
this.temp = {};
|
||||
await this.db.purgeCache();
|
||||
await this.auth.purgeCache();
|
||||
await this.doc.purgeCache();
|
||||
this.doc.purgeCache();
|
||||
}
|
||||
|
||||
store = {
|
||||
|
@ -9,7 +9,6 @@ import {
|
||||
DynamicLinkField,
|
||||
Field,
|
||||
FieldTypeEnum,
|
||||
OptionField,
|
||||
RawValue,
|
||||
Schema,
|
||||
TargetField,
|
||||
@ -37,8 +36,8 @@ import {
|
||||
FormulaMap,
|
||||
FormulaReturn,
|
||||
HiddenMap,
|
||||
ListsMap,
|
||||
ListViewSettings,
|
||||
ListsMap,
|
||||
ReadOnlyMap,
|
||||
RequiredMap,
|
||||
TreeViewSettings,
|
||||
@ -47,6 +46,7 @@ import {
|
||||
import { validateOptions, validateRequired } from './validationFunction';
|
||||
|
||||
export class Doc extends Observable<DocValue | Doc[]> {
|
||||
/* eslint-disable @typescript-eslint/no-floating-promises */
|
||||
name?: string;
|
||||
schema: Readonly<Schema>;
|
||||
fyo: Fyo;
|
||||
@ -62,15 +62,15 @@ export class Doc extends Observable<DocValue | Doc[]> {
|
||||
parentSchemaName?: string;
|
||||
|
||||
links?: Record<string, Doc>;
|
||||
_dirty: boolean = true;
|
||||
_notInserted: boolean = true;
|
||||
_dirty = true;
|
||||
_notInserted = true;
|
||||
|
||||
_syncing = false;
|
||||
constructor(
|
||||
schema: Schema,
|
||||
data: DocValueMap,
|
||||
fyo: Fyo,
|
||||
convertToDocValue: boolean = true
|
||||
convertToDocValue = true
|
||||
) {
|
||||
super();
|
||||
this.fyo = markRaw(fyo);
|
||||
@ -289,10 +289,10 @@ export class Doc extends Observable<DocValue | Doc[]> {
|
||||
async set(
|
||||
fieldname: string | DocValueMap,
|
||||
value?: DocValue | Doc[] | DocValueMap[],
|
||||
retriggerChildDocApplyChange: boolean = false
|
||||
retriggerChildDocApplyChange = false
|
||||
): Promise<boolean> {
|
||||
if (typeof fieldname === 'object') {
|
||||
return await this.setMultiple(fieldname as DocValueMap);
|
||||
return await this.setMultiple(fieldname);
|
||||
}
|
||||
|
||||
if (!this._canSet(fieldname, value)) {
|
||||
@ -391,7 +391,7 @@ export class Doc extends Observable<DocValue | Doc[]> {
|
||||
}
|
||||
|
||||
if (field.fieldtype === FieldTypeEnum.Currency && !isPesa(defaultValue)) {
|
||||
defaultValue = this.fyo.pesa!(defaultValue as string | number);
|
||||
defaultValue = this.fyo.pesa(defaultValue as string | number);
|
||||
}
|
||||
|
||||
this[field.fieldname] = defaultValue;
|
||||
@ -418,7 +418,7 @@ export class Doc extends Observable<DocValue | Doc[]> {
|
||||
push(
|
||||
fieldname: string,
|
||||
docValueMap: Doc | DocValueMap | RawValueMap = {},
|
||||
convertToDocValue: boolean = false
|
||||
convertToDocValue = false
|
||||
) {
|
||||
const childDocs = [
|
||||
(this[fieldname] ?? []) as Doc[],
|
||||
@ -449,7 +449,7 @@ export class Doc extends Observable<DocValue | Doc[]> {
|
||||
_getChildDoc(
|
||||
docValueMap: Doc | DocValueMap | RawValueMap,
|
||||
fieldname: string,
|
||||
convertToDocValue: boolean = false
|
||||
convertToDocValue = false
|
||||
): Doc {
|
||||
if (!this.name && this.schema.naming !== 'manual') {
|
||||
this.name = this.fyo.doc.getTemporaryName(this.schema);
|
||||
@ -528,7 +528,7 @@ export class Doc extends Observable<DocValue | Doc[]> {
|
||||
field.fieldtype === FieldTypeEnum.Select ||
|
||||
field.fieldtype === FieldTypeEnum.AutoComplete
|
||||
) {
|
||||
validateOptions(field as OptionField, value as string, this);
|
||||
validateOptions(field, value as string, this);
|
||||
}
|
||||
|
||||
validateRequired(field, value, this);
|
||||
@ -544,10 +544,7 @@ export class Doc extends Observable<DocValue | Doc[]> {
|
||||
await validator(value);
|
||||
}
|
||||
|
||||
getValidDict(
|
||||
filterMeta: boolean = false,
|
||||
filterComputed: boolean = false
|
||||
): DocValueMap {
|
||||
getValidDict(filterMeta = false, filterComputed = false): DocValueMap {
|
||||
let fields = this.schema.fields;
|
||||
if (filterMeta) {
|
||||
fields = this.schema.fields.filter((f) => !f.meta);
|
||||
@ -639,11 +636,11 @@ export class Doc extends Observable<DocValue | Doc[]> {
|
||||
|
||||
async _loadLink(field: Field) {
|
||||
if (field.fieldtype === FieldTypeEnum.Link) {
|
||||
return await this._loadLinkField(field as TargetField);
|
||||
return await this._loadLinkField(field);
|
||||
}
|
||||
|
||||
if (field.fieldtype === FieldTypeEnum.DynamicLink) {
|
||||
return await this._loadDynamicLinkField(field as DynamicLinkField);
|
||||
return await this._loadDynamicLinkField(field);
|
||||
}
|
||||
}
|
||||
|
||||
@ -767,14 +764,13 @@ export class Doc extends Observable<DocValue | Doc[]> {
|
||||
changedFieldname?: string,
|
||||
retriggerChildDocApplyChange?: boolean
|
||||
): Promise<boolean> {
|
||||
const doc = this;
|
||||
let changed = await this._callAllTableFieldsApplyFormula(changedFieldname);
|
||||
changed =
|
||||
(await this._applyFormulaForFields(doc, changedFieldname)) || changed;
|
||||
(await this._applyFormulaForFields(this, changedFieldname)) || changed;
|
||||
|
||||
if (changed && retriggerChildDocApplyChange) {
|
||||
await this._callAllTableFieldsApplyFormula(changedFieldname);
|
||||
await this._applyFormulaForFields(doc, changedFieldname);
|
||||
await this._applyFormulaForFields(this, changedFieldname);
|
||||
}
|
||||
|
||||
return changed;
|
||||
@ -803,7 +799,7 @@ export class Doc extends Observable<DocValue | Doc[]> {
|
||||
childDocs: Doc[],
|
||||
fieldname?: string
|
||||
): Promise<boolean> {
|
||||
let changed: boolean = false;
|
||||
let changed = false;
|
||||
for (const childDoc of childDocs) {
|
||||
if (!childDoc._applyFormula) {
|
||||
continue;
|
||||
@ -978,7 +974,7 @@ export class Doc extends Observable<DocValue | Doc[]> {
|
||||
|
||||
async trigger(event: string, params?: unknown) {
|
||||
if (this[event]) {
|
||||
await (this[event] as Function)(params);
|
||||
await (this[event] as (args: unknown) => Promise<void>)(params);
|
||||
}
|
||||
|
||||
await super.trigger(event, params);
|
||||
@ -993,9 +989,9 @@ export class Doc extends Observable<DocValue | Doc[]> {
|
||||
try {
|
||||
return this.fyo.pesa(value as string | number);
|
||||
} catch (err) {
|
||||
(
|
||||
err as Error
|
||||
).message += ` value: '${value}' of type: ${typeof value}, fieldname: '${tablefield}', childfield: '${childfield}'`;
|
||||
(err as Error).message += ` value: '${String(
|
||||
value
|
||||
)}' of type: ${typeof value}, fieldname: '${tablefield}', childfield: '${childfield}'`;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
@ -1032,7 +1028,7 @@ export class Doc extends Observable<DocValue | Doc[]> {
|
||||
if (this.numberSeries) {
|
||||
delete updateMap.name;
|
||||
} else {
|
||||
updateMap.name = updateMap.name + ' CPY';
|
||||
updateMap.name = String(updateMap.name) + ' CPY';
|
||||
}
|
||||
|
||||
const rawUpdateMap = this.fyo.db.converter.toRawValueMap(
|
||||
@ -1054,6 +1050,8 @@ export class Doc extends Observable<DocValue | Doc[]> {
|
||||
*
|
||||
* This may cause the lifecycle function to execute incorrectly.
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-empty-function, @typescript-eslint/no-unused-vars */
|
||||
async change(ch: ChangeArg) {}
|
||||
async validate() {}
|
||||
async beforeSync() {}
|
||||
@ -1084,7 +1082,9 @@ export class Doc extends Observable<DocValue | Doc[]> {
|
||||
return {};
|
||||
}
|
||||
|
||||
static getTreeSettings(fyo: Fyo): TreeViewSettings | void {}
|
||||
static getTreeSettings(fyo: Fyo): TreeViewSettings | void {
|
||||
return;
|
||||
}
|
||||
|
||||
static getActions(fyo: Fyo): Action[] {
|
||||
return [];
|
||||
|
@ -99,23 +99,18 @@ async function getNotFoundDetailsIfDoesNotExists(
|
||||
): Promise<NotFoundDetails | null> {
|
||||
const value = doc.get(field.fieldname);
|
||||
if (field.fieldtype === FieldTypeEnum.Link && value) {
|
||||
return getNotFoundLinkDetails(field as TargetField, value as string, fyo);
|
||||
return getNotFoundLinkDetails(field, value as string, fyo);
|
||||
}
|
||||
|
||||
if (field.fieldtype === FieldTypeEnum.DynamicLink && value) {
|
||||
return getNotFoundDynamicLinkDetails(
|
||||
field as DynamicLinkField,
|
||||
value as string,
|
||||
fyo,
|
||||
doc
|
||||
);
|
||||
return getNotFoundDynamicLinkDetails(field, value as string, fyo, doc);
|
||||
}
|
||||
|
||||
if (
|
||||
field.fieldtype === FieldTypeEnum.Table &&
|
||||
(value as Doc[] | undefined)?.length
|
||||
) {
|
||||
return getNotFoundTableDetails(value as Doc[], fyo);
|
||||
return await getNotFoundTableDetails(value as Doc[], fyo);
|
||||
}
|
||||
|
||||
return null;
|
||||
@ -127,7 +122,7 @@ async function getNotFoundLinkDetails(
|
||||
fyo: Fyo
|
||||
): Promise<NotFoundDetails | null> {
|
||||
const { target } = field;
|
||||
const exists = await fyo.db.exists(target as string, value);
|
||||
const exists = await fyo.db.exists(target, value);
|
||||
if (!exists) {
|
||||
return { label: field.label, value };
|
||||
}
|
||||
@ -160,7 +155,7 @@ async function getNotFoundTableDetails(
|
||||
fyo: Fyo
|
||||
): Promise<NotFoundDetails | null> {
|
||||
for (const childDoc of value) {
|
||||
const details = getNotFoundDetails(childDoc, fyo);
|
||||
const details = await getNotFoundDetails(childDoc, fyo);
|
||||
if (details) {
|
||||
return details;
|
||||
}
|
||||
|
@ -34,7 +34,7 @@ export function getPreDefaultValues(
|
||||
case FieldTypeEnum.Table:
|
||||
return [] as Doc[];
|
||||
case FieldTypeEnum.Currency:
|
||||
return fyo.pesa!(0.0);
|
||||
return fyo.pesa(0.0);
|
||||
case FieldTypeEnum.Int:
|
||||
case FieldTypeEnum.Float:
|
||||
return 0;
|
||||
@ -145,9 +145,9 @@ export function isDocValueTruthy(docValue: DocValue | Doc[]) {
|
||||
}
|
||||
|
||||
export function setChildDocIdx(childDocs: Doc[]) {
|
||||
for (const idx in childDocs) {
|
||||
childDocs[idx].idx = +idx;
|
||||
}
|
||||
childDocs.forEach((cd, idx) => {
|
||||
cd.idx = idx;
|
||||
});
|
||||
}
|
||||
|
||||
export function getFormulaSequence(formulas: FormulaMap) {
|
||||
|
@ -7,14 +7,26 @@ import { getIsNullOrUndef } from 'utils';
|
||||
import { Doc } from './doc';
|
||||
|
||||
export function validateEmail(value: DocValue) {
|
||||
const isValid = /(.+)@(.+){2,}\.(.+){2,}/.test(value as string);
|
||||
if (typeof value !== 'string') {
|
||||
throw new TypeError(
|
||||
`Invalid email ${String(value)} of type ${typeof value}`
|
||||
);
|
||||
}
|
||||
|
||||
const isValid = /(.+)@(.+){2,}\.(.+){2,}/.test(value);
|
||||
if (!isValid) {
|
||||
throw new ValidationError(`Invalid email: ${value}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function validatePhoneNumber(value: DocValue) {
|
||||
const isValid = /[+]{0,1}[\d ]+/.test(value as string);
|
||||
if (typeof value !== 'string') {
|
||||
throw new TypeError(
|
||||
`Invalid phone ${String(value)} of type ${typeof value}`
|
||||
);
|
||||
}
|
||||
|
||||
const isValid = /[+]{0,1}[\d ]+/.test(value);
|
||||
if (!isValid) {
|
||||
throw new ValidationError(`Invalid phone: ${value}`);
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ export default class SystemSettings extends Doc {
|
||||
instanceId?: string;
|
||||
|
||||
validations: ValidationMap = {
|
||||
async displayPrecision(value: DocValue) {
|
||||
displayPrecision(value: DocValue) {
|
||||
if ((value as number) >= 0 && (value as number) <= 9) {
|
||||
return;
|
||||
}
|
||||
@ -38,7 +38,7 @@ export default class SystemSettings extends Doc {
|
||||
(c) =>
|
||||
({
|
||||
value: countryInfo[c]?.locale,
|
||||
label: `${c} (${countryInfo[c]?.locale})`,
|
||||
label: `${c} (${countryInfo[c]?.locale ?? t`Not Found`})`,
|
||||
} as SelectOption)
|
||||
);
|
||||
},
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { Fyo } from 'fyo';
|
||||
import { ConfigKeys } from 'fyo/core/types';
|
||||
import { Noun, Telemetry, Verb } from './types';
|
||||
|
||||
/**
|
||||
@ -28,8 +27,8 @@ import { Noun, Telemetry, Verb } from './types';
|
||||
*/
|
||||
|
||||
export class TelemetryManager {
|
||||
#url: string = '';
|
||||
#token: string = '';
|
||||
#url = '';
|
||||
#token = '';
|
||||
#started = false;
|
||||
fyo: Fyo;
|
||||
|
||||
@ -67,6 +66,7 @@ export class TelemetryManager {
|
||||
|
||||
log(verb: Verb, noun: Noun, more?: Record<string, unknown>) {
|
||||
if (!this.#started && this.fyo.db.isConnected) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.start().then(() => this.#sendBeacon(verb, noun, more));
|
||||
return;
|
||||
}
|
||||
@ -108,16 +108,12 @@ export class TelemetryManager {
|
||||
noun: Noun,
|
||||
more?: Record<string, unknown>
|
||||
): Telemetry {
|
||||
const countryCode = this.fyo.singles.SystemSettings?.countryCode as
|
||||
| string
|
||||
| undefined;
|
||||
|
||||
const countryCode = this.fyo.singles.SystemSettings?.countryCode;
|
||||
return {
|
||||
country: countryCode ?? '',
|
||||
language: this.fyo.store.language,
|
||||
deviceId:
|
||||
this.fyo.store.deviceId ||
|
||||
(this.fyo.config.get(ConfigKeys.DeviceId) as string),
|
||||
this.fyo.store.deviceId || (this.fyo.config.get('deviceId') ?? '-'),
|
||||
instanceId: this.fyo.store.instanceId,
|
||||
version: this.fyo.store.appVersion,
|
||||
openCount: this.fyo.store.openCount,
|
||||
|
@ -2,6 +2,7 @@ import { AuthDemuxBase } from 'utils/auth/types';
|
||||
import { Creds } from 'utils/types';
|
||||
|
||||
export class DummyAuthDemux extends AuthDemuxBase {
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
async getCreds(): Promise<Creds> {
|
||||
return { errorLogUrl: '', tokenString: '', telemetryUrl: '' };
|
||||
}
|
||||
|
@ -4,11 +4,7 @@ export class BaseError extends Error {
|
||||
statusCode: number;
|
||||
shouldStore: boolean;
|
||||
|
||||
constructor(
|
||||
statusCode: number,
|
||||
message: string,
|
||||
shouldStore: boolean = true
|
||||
) {
|
||||
constructor(statusCode: number, message: string, shouldStore = true) {
|
||||
super(message);
|
||||
this.name = 'BaseError';
|
||||
this.statusCode = statusCode;
|
||||
@ -18,63 +14,63 @@ export class BaseError extends Error {
|
||||
}
|
||||
|
||||
export class ValidationError extends BaseError {
|
||||
constructor(message: string, shouldStore: boolean = false) {
|
||||
constructor(message: string, shouldStore = false) {
|
||||
super(417, message, shouldStore);
|
||||
this.name = 'ValidationError';
|
||||
}
|
||||
}
|
||||
|
||||
export class NotFoundError extends BaseError {
|
||||
constructor(message: string, shouldStore: boolean = true) {
|
||||
constructor(message: string, shouldStore = true) {
|
||||
super(404, message, shouldStore);
|
||||
this.name = 'NotFoundError';
|
||||
}
|
||||
}
|
||||
|
||||
export class ForbiddenError extends BaseError {
|
||||
constructor(message: string, shouldStore: boolean = true) {
|
||||
constructor(message: string, shouldStore = true) {
|
||||
super(403, message, shouldStore);
|
||||
this.name = 'ForbiddenError';
|
||||
}
|
||||
}
|
||||
|
||||
export class DuplicateEntryError extends ValidationError {
|
||||
constructor(message: string, shouldStore: boolean = false) {
|
||||
constructor(message: string, shouldStore = false) {
|
||||
super(message, shouldStore);
|
||||
this.name = 'DuplicateEntryError';
|
||||
}
|
||||
}
|
||||
|
||||
export class LinkValidationError extends ValidationError {
|
||||
constructor(message: string, shouldStore: boolean = false) {
|
||||
constructor(message: string, shouldStore = false) {
|
||||
super(message, shouldStore);
|
||||
this.name = 'LinkValidationError';
|
||||
}
|
||||
}
|
||||
|
||||
export class MandatoryError extends ValidationError {
|
||||
constructor(message: string, shouldStore: boolean = false) {
|
||||
constructor(message: string, shouldStore = false) {
|
||||
super(message, shouldStore);
|
||||
this.name = 'MandatoryError';
|
||||
}
|
||||
}
|
||||
|
||||
export class DatabaseError extends BaseError {
|
||||
constructor(message: string, shouldStore: boolean = true) {
|
||||
constructor(message: string, shouldStore = true) {
|
||||
super(500, message, shouldStore);
|
||||
this.name = 'DatabaseError';
|
||||
}
|
||||
}
|
||||
|
||||
export class CannotCommitError extends DatabaseError {
|
||||
constructor(message: string, shouldStore: boolean = true) {
|
||||
constructor(message: string, shouldStore = true) {
|
||||
super(message, shouldStore);
|
||||
this.name = 'CannotCommitError';
|
||||
}
|
||||
}
|
||||
|
||||
export class NotImplemented extends BaseError {
|
||||
constructor(message: string = '', shouldStore: boolean = false) {
|
||||
constructor(message = '', shouldStore = false) {
|
||||
super(501, message, shouldStore);
|
||||
this.name = 'NotImplemented';
|
||||
}
|
||||
|
@ -60,7 +60,7 @@ function toDatetime(value: unknown): DateTime | null {
|
||||
} else if (value instanceof Date) {
|
||||
return DateTime.fromJSDate(value);
|
||||
} else if (typeof value === 'number') {
|
||||
return DateTime.fromSeconds(value as number);
|
||||
return DateTime.fromSeconds(value);
|
||||
}
|
||||
|
||||
return null;
|
||||
@ -120,7 +120,9 @@ function formatCurrency(
|
||||
try {
|
||||
valueString = formatNumber(value, fyo);
|
||||
} catch (err) {
|
||||
(err as Error).message += ` value: '${value}', type: ${typeof value}`;
|
||||
(err as Error).message += ` value: '${String(
|
||||
value
|
||||
)}', type: ${typeof value}`;
|
||||
throw err;
|
||||
}
|
||||
|
||||
@ -148,7 +150,9 @@ function formatNumber(value: unknown, fyo: Fyo): string {
|
||||
|
||||
if (formattedNumber === 'NaN') {
|
||||
throw Error(
|
||||
`invalid value passed to formatNumber: '${value}' of type ${typeof value}`
|
||||
`invalid value passed to formatNumber: '${String(
|
||||
value
|
||||
)}' of type ${typeof value}`
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -8,7 +8,7 @@ import { getIsNullOrUndef, safeParseInt } from 'utils';
|
||||
|
||||
export function slug(str: string) {
|
||||
return str
|
||||
.replace(/(?:^\w|[A-Z]|\b\w)/g, function (letter, index) {
|
||||
.replace(/(?:^\w|[A-Z]|\b\w)/g, (letter, index) => {
|
||||
return index == 0 ? letter.toLowerCase() : letter.toUpperCase();
|
||||
})
|
||||
.replace(/\s+/g, '');
|
||||
@ -24,7 +24,7 @@ export function unique<T>(list: T[], key = (it: T) => String(it)) {
|
||||
|
||||
export function getDuplicates(array: unknown[]) {
|
||||
const duplicates: unknown[] = [];
|
||||
for (const i in array) {
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
const previous = array[safeParseInt(i) - 1];
|
||||
const current = array[i];
|
||||
|
||||
@ -118,7 +118,7 @@ function getRawOptionList(field: Field, doc: Doc | undefined | null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const Model = doc!.fyo.models[doc!.schemaName];
|
||||
const Model = doc.fyo.models[doc.schemaName];
|
||||
if (Model === undefined) {
|
||||
return [];
|
||||
}
|
||||
@ -128,7 +128,7 @@ function getRawOptionList(field: Field, doc: Doc | undefined | null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return getList(doc!);
|
||||
return getList(doc);
|
||||
}
|
||||
|
||||
export function getEmptyValuesByFieldTypes(
|
||||
|
@ -3,13 +3,16 @@ enum EventType {
|
||||
OnceListeners = '_onceListeners',
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type Listener = (...args: any[]) => unknown | Promise<unknown>;
|
||||
|
||||
export default class Observable<T> {
|
||||
[key: string]: unknown | T;
|
||||
_isHot: Map<string, boolean>;
|
||||
_eventQueue: Map<string, unknown[]>;
|
||||
_map: Map<string, unknown>;
|
||||
_listeners: Map<string, Function[]>;
|
||||
_onceListeners: Map<string, Function[]>;
|
||||
_listeners: Map<string, Listener[]>;
|
||||
_onceListeners: Map<string, Listener[]>;
|
||||
|
||||
constructor() {
|
||||
this._map = new Map();
|
||||
@ -37,6 +40,7 @@ export default class Observable<T> {
|
||||
*/
|
||||
set(key: string, value: T) {
|
||||
this[key] = value;
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.trigger('change', {
|
||||
doc: this,
|
||||
changed: key,
|
||||
@ -50,7 +54,7 @@ export default class Observable<T> {
|
||||
* @param event : name of the event for which the listener is checked
|
||||
* @param listener : specific listener that is checked for
|
||||
*/
|
||||
hasListener(event: string, listener?: Function) {
|
||||
hasListener(event: string, listener?: Listener) {
|
||||
const listeners = this[EventType.Listeners].get(event) ?? [];
|
||||
const onceListeners = this[EventType.OnceListeners].get(event) ?? [];
|
||||
|
||||
@ -69,7 +73,7 @@ export default class Observable<T> {
|
||||
* @param event : name of the event for which the listener is set
|
||||
* @param listener : listener that is executed when the event is triggered
|
||||
*/
|
||||
on(event: string, listener: Function) {
|
||||
on(event: string, listener: Listener) {
|
||||
this._addListener(EventType.Listeners, event, listener);
|
||||
}
|
||||
|
||||
@ -80,7 +84,7 @@ export default class Observable<T> {
|
||||
* @param event : name of the event for which the listener is set
|
||||
* @param listener : listener that is executed when the event is triggered
|
||||
*/
|
||||
once(event: string, listener: Function) {
|
||||
once(event: string, listener: Listener) {
|
||||
this._addListener(EventType.OnceListeners, event, listener);
|
||||
}
|
||||
|
||||
@ -90,7 +94,7 @@ export default class Observable<T> {
|
||||
* @param event : name of the event from which to remove the listener
|
||||
* @param listener : listener that was set for the event
|
||||
*/
|
||||
off(event: string, listener: Function) {
|
||||
off(event: string, listener: Listener) {
|
||||
this._removeListener(EventType.Listeners, event, listener);
|
||||
this._removeListener(EventType.OnceListeners, event, listener);
|
||||
}
|
||||
@ -111,7 +115,7 @@ export default class Observable<T> {
|
||||
* @param throttle : wait time before triggering the event.
|
||||
*/
|
||||
|
||||
async trigger(event: string, params?: unknown, throttle: number = 0) {
|
||||
async trigger(event: string, params?: unknown, throttle = 0) {
|
||||
let isHot = false;
|
||||
if (throttle > 0) {
|
||||
isHot = this._throttled(event, params, throttle);
|
||||
@ -125,7 +129,7 @@ export default class Observable<T> {
|
||||
await this._executeTriggers(event, params);
|
||||
}
|
||||
|
||||
_removeListener(type: EventType, event: string, listener: Function) {
|
||||
_removeListener(type: EventType, event: string, listener: Listener) {
|
||||
const listeners = (this[type].get(event) ?? []).filter(
|
||||
(l) => l !== listener
|
||||
);
|
||||
@ -160,6 +164,7 @@ export default class Observable<T> {
|
||||
|
||||
const params = this._eventQueue.get(event);
|
||||
if (params !== undefined) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this._executeTriggers(event, params);
|
||||
this._eventQueue.delete(event);
|
||||
}
|
||||
@ -168,7 +173,7 @@ export default class Observable<T> {
|
||||
return false;
|
||||
}
|
||||
|
||||
_addListener(type: EventType, event: string, listener: Function) {
|
||||
_addListener(type: EventType, event: string, listener: Listener) {
|
||||
this._initLiseners(type, event);
|
||||
const list = this[type].get(event)!;
|
||||
if (list.includes(listener)) {
|
||||
|
@ -31,7 +31,7 @@ class TranslationString {
|
||||
}
|
||||
|
||||
#formatArg(arg: string | number | boolean) {
|
||||
return arg ?? '';
|
||||
return String(arg ?? '');
|
||||
}
|
||||
|
||||
#translate() {
|
||||
@ -48,22 +48,23 @@ class TranslationString {
|
||||
}
|
||||
|
||||
#stitch() {
|
||||
if (!((this.args[0] as any) instanceof Array)) {
|
||||
if (!((this.args[0] as unknown) instanceof Array)) {
|
||||
throw new ValueError(
|
||||
`invalid args passed to TranslationString ${
|
||||
`invalid args passed to TranslationString ${String(
|
||||
this.args
|
||||
} of type ${typeof this.args[0]}`
|
||||
)} of type ${typeof this.args[0]}`
|
||||
);
|
||||
}
|
||||
|
||||
this.strList = this.args[0] as any as string[];
|
||||
this.strList = this.args[0] as unknown as string[];
|
||||
this.argList = this.args.slice(1) as TranslationArgs[];
|
||||
|
||||
if (this.languageMap) {
|
||||
this.#translate();
|
||||
}
|
||||
|
||||
return this.strList!.map((s, i) => s + this.#formatArg(this.argList![i]))
|
||||
return this.strList
|
||||
.map((s, i) => s + this.#formatArg(this.argList![i]))
|
||||
.join('')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
|
91
main.ts
91
main.ts
@ -1,27 +1,33 @@
|
||||
'use strict';
|
||||
// eslint-disable-next-line
|
||||
require('source-map-support').install({
|
||||
handleUncaughtException: false,
|
||||
environment: 'node',
|
||||
});
|
||||
|
||||
import {
|
||||
app,
|
||||
BrowserWindow,
|
||||
BrowserWindowConstructorOptions,
|
||||
protocol,
|
||||
ProtocolRequest,
|
||||
ProtocolResponse,
|
||||
} from 'electron';
|
||||
import Store from 'electron-store';
|
||||
import { autoUpdater } from 'electron-updater';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { createProtocol } from 'vue-cli-plugin-electron-builder/lib';
|
||||
import registerAppLifecycleListeners from './main/registerAppLifecycleListeners';
|
||||
import registerAutoUpdaterListeners from './main/registerAutoUpdaterListeners';
|
||||
import registerIpcMainActionListeners from './main/registerIpcMainActionListeners';
|
||||
import registerIpcMainMessageListeners from './main/registerIpcMainMessageListeners';
|
||||
import registerProcessListeners from './main/registerProcessListeners';
|
||||
import { emitMainProcessError } from 'backend/helpers';
|
||||
|
||||
export class Main {
|
||||
title: string = 'Frappe Books';
|
||||
title = 'Frappe Books';
|
||||
icon: string;
|
||||
|
||||
winURL: string = '';
|
||||
isWebpackUrl: boolean = false;
|
||||
winURL = '';
|
||||
checkedForUpdate = false;
|
||||
mainWindow: BrowserWindow | null = null;
|
||||
|
||||
@ -57,7 +63,7 @@ export class Main {
|
||||
}
|
||||
|
||||
get isDevelopment() {
|
||||
return process.env.NODE_ENV !== 'production';
|
||||
return process.env.NODE_ENV === 'development';
|
||||
}
|
||||
|
||||
get isTest() {
|
||||
@ -116,35 +122,42 @@ export class Main {
|
||||
return options;
|
||||
}
|
||||
|
||||
createWindow() {
|
||||
async createWindow() {
|
||||
const options = this.getOptions();
|
||||
this.mainWindow = new BrowserWindow(options);
|
||||
|
||||
this.isWebpackUrl = !!process.env.WEBPACK_DEV_SERVER_URL;
|
||||
if (this.isWebpackUrl) {
|
||||
this.loadWebpackDevServerURL();
|
||||
if (this.isDevelopment) {
|
||||
this.setViteServerURL();
|
||||
} else {
|
||||
this.loadAppUrl();
|
||||
this.registerAppProtocol();
|
||||
}
|
||||
|
||||
await this.mainWindow.loadURL(this.winURL);
|
||||
if (this.isDevelopment && !this.isTest) {
|
||||
this.mainWindow.webContents.openDevTools();
|
||||
}
|
||||
|
||||
this.setMainWindowListeners();
|
||||
}
|
||||
|
||||
loadWebpackDevServerURL() {
|
||||
// Load the url of the dev server if in development mode
|
||||
this.winURL = process.env.WEBPACK_DEV_SERVER_URL as string;
|
||||
this.mainWindow!.loadURL(this.winURL);
|
||||
setViteServerURL() {
|
||||
let port = 6969;
|
||||
let host = '0.0.0.0';
|
||||
|
||||
if (this.isDevelopment && !this.isTest) {
|
||||
this.mainWindow!.webContents.openDevTools();
|
||||
if (process.env.VITE_PORT && process.env.VITE_HOST) {
|
||||
port = Number(process.env.VITE_PORT);
|
||||
host = process.env.VITE_HOST;
|
||||
}
|
||||
|
||||
// Load the url of the dev server if in development mode
|
||||
this.winURL = `http://${host}:${port}/`;
|
||||
}
|
||||
|
||||
loadAppUrl() {
|
||||
createProtocol('app');
|
||||
// Load the index.html when not in development
|
||||
registerAppProtocol() {
|
||||
protocol.registerBufferProtocol('app', bufferProtocolCallback);
|
||||
|
||||
// Use the registered protocol url to load the files.
|
||||
this.winURL = 'app://./index.html';
|
||||
this.mainWindow!.loadURL(this.winURL);
|
||||
}
|
||||
|
||||
setMainWindowListeners() {
|
||||
@ -157,9 +170,43 @@ export class Main {
|
||||
});
|
||||
|
||||
this.mainWindow.webContents.on('did-fail-load', () => {
|
||||
this.mainWindow!.loadURL(this.winURL);
|
||||
this.mainWindow!.loadURL(this.winURL).catch((err) =>
|
||||
emitMainProcessError(err)
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback used to register the custom app protocol,
|
||||
* during prod, files are read and served by using this
|
||||
* protocol.
|
||||
*/
|
||||
function bufferProtocolCallback(
|
||||
request: ProtocolRequest,
|
||||
callback: (response: ProtocolResponse) => void
|
||||
) {
|
||||
const { pathname, host } = new URL(request.url);
|
||||
const filePath = path.join(
|
||||
__dirname,
|
||||
'src',
|
||||
decodeURI(host),
|
||||
decodeURI(pathname)
|
||||
);
|
||||
|
||||
fs.readFile(filePath, (_, data) => {
|
||||
const extension = path.extname(filePath).toLowerCase();
|
||||
const mimeType =
|
||||
{
|
||||
'.js': 'text/javascript',
|
||||
'.css': 'text/css',
|
||||
'.html': 'text/html',
|
||||
'.svg': 'image/svg+xml',
|
||||
'.json': 'application/json',
|
||||
}[extension] ?? '';
|
||||
|
||||
callback({ mimeType, data });
|
||||
});
|
||||
}
|
||||
|
||||
export default new Main();
|
||||
|
@ -4,6 +4,7 @@ import fetch from 'node-fetch';
|
||||
import path from 'path';
|
||||
import { Creds } from 'utils/types';
|
||||
import { rendererLog } from './helpers';
|
||||
import type { Main } from 'main';
|
||||
|
||||
export function getUrlAndTokenString(): Creds {
|
||||
const inProduction = app.isPackaged;
|
||||
@ -13,10 +14,11 @@ export function getUrlAndTokenString(): Creds {
|
||||
'../creds/log_creds.txt'
|
||||
);
|
||||
if (!fs.existsSync(errLogCredsPath)) {
|
||||
errLogCredsPath = path.join(__dirname, '../log_creds.txt');
|
||||
errLogCredsPath = path.join(__dirname, '..', '..', 'log_creds.txt');
|
||||
}
|
||||
|
||||
if (!fs.existsSync(errLogCredsPath)) {
|
||||
// eslint-disable-next-line no-console
|
||||
!inProduction && console.log(`${errLogCredsPath} doesn't exist, can't log`);
|
||||
return empty;
|
||||
}
|
||||
@ -29,7 +31,9 @@ export function getUrlAndTokenString(): Creds {
|
||||
.filter((f) => f.length);
|
||||
} catch (err) {
|
||||
if (!inProduction) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`logging error using creds at: ${errLogCredsPath} failed`);
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(err);
|
||||
}
|
||||
return empty;
|
||||
@ -42,7 +46,7 @@ export function getUrlAndTokenString(): Creds {
|
||||
};
|
||||
}
|
||||
|
||||
export async function sendError(body: string) {
|
||||
export async function sendError(body: string, main: Main) {
|
||||
const { errorLogUrl, tokenString } = getUrlAndTokenString();
|
||||
const headers = {
|
||||
Authorization: tokenString,
|
||||
@ -51,6 +55,6 @@ export async function sendError(body: string) {
|
||||
};
|
||||
|
||||
await fetch(errorLogUrl, { method: 'POST', headers, body }).catch((err) => {
|
||||
rendererLog(err);
|
||||
rendererLog(main, err);
|
||||
});
|
||||
}
|
||||
|
@ -16,8 +16,7 @@ import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import { parseCSV } from 'utils/csvParser';
|
||||
import { LanguageMap } from 'utils/types';
|
||||
|
||||
const fetch = require('node-fetch').default;
|
||||
import fetch from 'node-fetch';
|
||||
|
||||
const VALENTINES_DAY = 1644796800000;
|
||||
|
||||
@ -100,7 +99,7 @@ async function fetchContentsFromApi(code: string) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const resJson = await res.json();
|
||||
const resJson = (await res.json()) as { content: string };
|
||||
return Buffer.from(resJson.content, 'base64').toString();
|
||||
}
|
||||
|
||||
@ -138,7 +137,9 @@ async function getLastUpdated(code: string): Promise<Date> {
|
||||
return new Date(VALENTINES_DAY);
|
||||
}
|
||||
|
||||
const resJson = await res.json();
|
||||
const resJson = (await res.json()) as {
|
||||
commit: { author: { date: string } };
|
||||
}[];
|
||||
try {
|
||||
return new Date(resJson[0].commit.author.date);
|
||||
} catch {
|
||||
@ -187,7 +188,7 @@ async function storeFile(code: string, contents: string) {
|
||||
|
||||
async function errorHandledFetch(url: string) {
|
||||
try {
|
||||
return (await fetch(url)) as Response;
|
||||
return await fetch(url);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ async function getPrintTemplatePaths(): Promise<{
|
||||
const files = await fs.readdir(root);
|
||||
return { files, root };
|
||||
} catch {
|
||||
root = path.join(__dirname, `../templates`);
|
||||
root = path.join(__dirname, '..', '..', `templates`);
|
||||
}
|
||||
|
||||
try {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { constants } from 'fs';
|
||||
import fs from 'fs/promises';
|
||||
import { ConfigFile, ConfigKeys } from 'fyo/core/types';
|
||||
import { ConfigFile } from 'fyo/core/types';
|
||||
import { Main } from 'main';
|
||||
import config from 'utils/config';
|
||||
import { BackendResponse } from 'utils/ipc/types';
|
||||
@ -8,7 +8,7 @@ import { IPC_CHANNELS } from 'utils/messages';
|
||||
import type { ConfigFilesWithModified } from 'utils/types';
|
||||
|
||||
export async function setAndGetCleanedConfigFiles() {
|
||||
const files = config.get(ConfigKeys.Files, []) as ConfigFile[];
|
||||
const files = config.get('files', []);
|
||||
|
||||
const cleanedFileMap: Map<string, ConfigFile> = new Map();
|
||||
for (const file of files) {
|
||||
@ -30,7 +30,7 @@ export async function setAndGetCleanedConfigFiles() {
|
||||
}
|
||||
|
||||
const cleanedFiles = Array.from(cleanedFileMap.values());
|
||||
config.set(ConfigKeys.Files, cleanedFiles);
|
||||
config.set('files', cleanedFiles);
|
||||
return cleanedFiles;
|
||||
}
|
||||
|
||||
@ -50,7 +50,9 @@ export async function getConfigFilesWithModified(files: ConfigFile[]) {
|
||||
return filesWithModified;
|
||||
}
|
||||
|
||||
export async function getErrorHandledReponse(func: () => Promise<unknown>) {
|
||||
export async function getErrorHandledReponse(
|
||||
func: () => Promise<unknown> | unknown
|
||||
) {
|
||||
const response: BackendResponse = {};
|
||||
|
||||
try {
|
||||
|
@ -2,6 +2,7 @@ import { app } from 'electron';
|
||||
import installExtension, { VUEJS3_DEVTOOLS } from 'electron-devtools-installer';
|
||||
import { Main } from '../main';
|
||||
import { rendererLog } from './helpers';
|
||||
import { emitMainProcessError } from 'backend/helpers';
|
||||
|
||||
export default function registerAppLifecycleListeners(main: Main) {
|
||||
app.on('window-all-closed', () => {
|
||||
@ -12,16 +13,16 @@ export default function registerAppLifecycleListeners(main: Main) {
|
||||
|
||||
app.on('activate', () => {
|
||||
if (main.mainWindow === null) {
|
||||
main.createWindow();
|
||||
main.createWindow().catch((err) => emitMainProcessError(err));
|
||||
}
|
||||
});
|
||||
|
||||
app.on('ready', async () => {
|
||||
app.on('ready', () => {
|
||||
if (main.isDevelopment && !main.isTest) {
|
||||
await installDevTools(main);
|
||||
installDevTools(main).catch((err) => emitMainProcessError(err));
|
||||
}
|
||||
|
||||
main.createWindow();
|
||||
main.createWindow().catch((err) => emitMainProcessError(err));
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -21,6 +21,7 @@ export default function registerAutoUpdaterListeners(main: Main) {
|
||||
emitMainProcessError(error);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
autoUpdater.on('update-available', async (info: UpdateInfo) => {
|
||||
const currentVersion = app.getVersion();
|
||||
const nextVersion = info.version;
|
||||
@ -46,6 +47,7 @@ export default function registerAutoUpdaterListeners(main: Main) {
|
||||
await autoUpdater.downloadUpdate();
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
autoUpdater.on('update-downloaded', async () => {
|
||||
const option = await dialog.showMessageBox({
|
||||
type: 'info',
|
||||
|
@ -1,4 +1,11 @@
|
||||
import { app, dialog, ipcMain } from 'electron';
|
||||
import {
|
||||
MessageBoxOptions,
|
||||
OpenDialogOptions,
|
||||
SaveDialogOptions,
|
||||
app,
|
||||
dialog,
|
||||
ipcMain,
|
||||
} from 'electron';
|
||||
import { autoUpdater } from 'electron-updater';
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
@ -20,39 +27,60 @@ import {
|
||||
import { saveHtmlAsPdf } from './saveHtmlAsPdf';
|
||||
|
||||
export default function registerIpcMainActionListeners(main: Main) {
|
||||
ipcMain.handle(IPC_ACTIONS.GET_OPEN_FILEPATH, async (event, options) => {
|
||||
return await dialog.showOpenDialog(main.mainWindow!, options);
|
||||
});
|
||||
|
||||
ipcMain.handle(IPC_ACTIONS.GET_SAVE_FILEPATH, async (event, options) => {
|
||||
return await dialog.showSaveDialog(main.mainWindow!, options);
|
||||
});
|
||||
|
||||
ipcMain.handle(IPC_ACTIONS.GET_DIALOG_RESPONSE, async (event, options) => {
|
||||
if (main.isDevelopment || main.isLinux) {
|
||||
Object.assign(options, { icon: main.icon });
|
||||
ipcMain.handle(
|
||||
IPC_ACTIONS.GET_OPEN_FILEPATH,
|
||||
async (_, options: OpenDialogOptions) => {
|
||||
return await dialog.showOpenDialog(main.mainWindow!, options);
|
||||
}
|
||||
);
|
||||
|
||||
return await dialog.showMessageBox(main.mainWindow!, options);
|
||||
});
|
||||
ipcMain.handle(
|
||||
IPC_ACTIONS.GET_SAVE_FILEPATH,
|
||||
async (_, options: SaveDialogOptions) => {
|
||||
return await dialog.showSaveDialog(main.mainWindow!, options);
|
||||
}
|
||||
);
|
||||
|
||||
ipcMain.handle(IPC_ACTIONS.SHOW_ERROR, async (event, { title, content }) => {
|
||||
return await dialog.showErrorBox(title, content);
|
||||
});
|
||||
ipcMain.handle(
|
||||
IPC_ACTIONS.GET_DIALOG_RESPONSE,
|
||||
async (_, options: MessageBoxOptions) => {
|
||||
if (main.isDevelopment || main.isLinux) {
|
||||
Object.assign(options, { icon: main.icon });
|
||||
}
|
||||
|
||||
return await dialog.showMessageBox(main.mainWindow!, options);
|
||||
}
|
||||
);
|
||||
|
||||
ipcMain.handle(
|
||||
IPC_ACTIONS.SHOW_ERROR,
|
||||
(_, { title, content }: { title: string; content: string }) => {
|
||||
return dialog.showErrorBox(title, content);
|
||||
}
|
||||
);
|
||||
|
||||
ipcMain.handle(
|
||||
IPC_ACTIONS.SAVE_HTML_AS_PDF,
|
||||
async (event, html, savePath, width: number, height: number) => {
|
||||
async (
|
||||
_,
|
||||
html: string,
|
||||
savePath: string,
|
||||
width: number,
|
||||
height: number
|
||||
) => {
|
||||
return await saveHtmlAsPdf(html, savePath, app, width, height);
|
||||
}
|
||||
);
|
||||
|
||||
ipcMain.handle(IPC_ACTIONS.SAVE_DATA, async (event, data, savePath) => {
|
||||
return await fs.writeFile(savePath, data, { encoding: 'utf-8' });
|
||||
});
|
||||
ipcMain.handle(
|
||||
IPC_ACTIONS.SAVE_DATA,
|
||||
async (_, data: string, savePath: string) => {
|
||||
return await fs.writeFile(savePath, data, { encoding: 'utf-8' });
|
||||
}
|
||||
);
|
||||
|
||||
ipcMain.handle(IPC_ACTIONS.SEND_ERROR, (event, bodyJson) => {
|
||||
sendError(bodyJson);
|
||||
ipcMain.handle(IPC_ACTIONS.SEND_ERROR, async (_, bodyJson: string) => {
|
||||
await sendError(bodyJson, main);
|
||||
});
|
||||
|
||||
ipcMain.handle(IPC_ACTIONS.CHECK_FOR_UPDATES, async () => {
|
||||
@ -72,7 +100,7 @@ export default function registerIpcMainActionListeners(main: Main) {
|
||||
main.checkedForUpdate = true;
|
||||
});
|
||||
|
||||
ipcMain.handle(IPC_ACTIONS.GET_LANGUAGE_MAP, async (event, code) => {
|
||||
ipcMain.handle(IPC_ACTIONS.GET_LANGUAGE_MAP, async (_, code: string) => {
|
||||
const obj = { languageMap: {}, success: true, message: '' };
|
||||
try {
|
||||
obj.languageMap = await getLanguageMap(code);
|
||||
@ -117,20 +145,20 @@ export default function registerIpcMainActionListeners(main: Main) {
|
||||
}
|
||||
);
|
||||
|
||||
ipcMain.handle(IPC_ACTIONS.GET_CREDS, async (event) => {
|
||||
ipcMain.handle(IPC_ACTIONS.GET_CREDS, () => {
|
||||
return getUrlAndTokenString();
|
||||
});
|
||||
|
||||
ipcMain.handle(IPC_ACTIONS.DELETE_FILE, async (_, filePath) => {
|
||||
ipcMain.handle(IPC_ACTIONS.DELETE_FILE, async (_, filePath: string) => {
|
||||
return getErrorHandledReponse(async () => await fs.unlink(filePath));
|
||||
});
|
||||
|
||||
ipcMain.handle(IPC_ACTIONS.GET_DB_LIST, async (_) => {
|
||||
ipcMain.handle(IPC_ACTIONS.GET_DB_LIST, async () => {
|
||||
const files = await setAndGetCleanedConfigFiles();
|
||||
return await getConfigFilesWithModified(files);
|
||||
});
|
||||
|
||||
ipcMain.handle(IPC_ACTIONS.GET_ENV, async (_) => {
|
||||
ipcMain.handle(IPC_ACTIONS.GET_ENV, () => {
|
||||
return {
|
||||
isDevelopment: main.isDevelopment,
|
||||
platform: process.platform,
|
||||
@ -149,7 +177,7 @@ export default function registerIpcMainActionListeners(main: Main) {
|
||||
ipcMain.handle(
|
||||
IPC_ACTIONS.DB_CREATE,
|
||||
async (_, dbPath: string, countryCode: string) => {
|
||||
return await getErrorHandledReponse(async function dbFunc() {
|
||||
return await getErrorHandledReponse(async () => {
|
||||
return await databaseManager.createNewDatabase(dbPath, countryCode);
|
||||
});
|
||||
}
|
||||
@ -158,7 +186,7 @@ export default function registerIpcMainActionListeners(main: Main) {
|
||||
ipcMain.handle(
|
||||
IPC_ACTIONS.DB_CONNECT,
|
||||
async (_, dbPath: string, countryCode?: string) => {
|
||||
return await getErrorHandledReponse(async function dbFunc() {
|
||||
return await getErrorHandledReponse(async () => {
|
||||
return await databaseManager.connectToDatabase(dbPath, countryCode);
|
||||
});
|
||||
}
|
||||
@ -167,7 +195,7 @@ export default function registerIpcMainActionListeners(main: Main) {
|
||||
ipcMain.handle(
|
||||
IPC_ACTIONS.DB_CALL,
|
||||
async (_, method: DatabaseMethod, ...args: unknown[]) => {
|
||||
return await getErrorHandledReponse(async function dbFunc() {
|
||||
return await getErrorHandledReponse(async () => {
|
||||
return await databaseManager.call(method, ...args);
|
||||
});
|
||||
}
|
||||
@ -176,15 +204,15 @@ export default function registerIpcMainActionListeners(main: Main) {
|
||||
ipcMain.handle(
|
||||
IPC_ACTIONS.DB_BESPOKE,
|
||||
async (_, method: string, ...args: unknown[]) => {
|
||||
return await getErrorHandledReponse(async function dbFunc() {
|
||||
return await getErrorHandledReponse(async () => {
|
||||
return await databaseManager.callBespoke(method, ...args);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
ipcMain.handle(IPC_ACTIONS.DB_SCHEMA, async (_) => {
|
||||
return await getErrorHandledReponse(async function dbFunc() {
|
||||
return await databaseManager.getSchemaMap();
|
||||
ipcMain.handle(IPC_ACTIONS.DB_SCHEMA, async () => {
|
||||
return await getErrorHandledReponse(() => {
|
||||
return databaseManager.getSchemaMap();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { ipcMain, Menu, shell } from 'electron';
|
||||
import { Main } from '../main';
|
||||
import { IPC_MESSAGES } from '../utils/messages';
|
||||
import { emitMainProcessError } from 'backend/helpers';
|
||||
|
||||
export default function registerIpcMainMessageListeners(main: Main) {
|
||||
ipcMain.on(IPC_MESSAGES.OPEN_MENU, (event) => {
|
||||
@ -20,11 +21,11 @@ export default function registerIpcMainMessageListeners(main: Main) {
|
||||
main.mainWindow!.reload();
|
||||
});
|
||||
|
||||
ipcMain.on(IPC_MESSAGES.OPEN_EXTERNAL, (_, link) => {
|
||||
shell.openExternal(link);
|
||||
ipcMain.on(IPC_MESSAGES.OPEN_EXTERNAL, (_, link: string) => {
|
||||
shell.openExternal(link).catch((err) => emitMainProcessError(err));
|
||||
});
|
||||
|
||||
ipcMain.on(IPC_MESSAGES.SHOW_ITEM_IN_FOLDER, (_, filePath) => {
|
||||
ipcMain.on(IPC_MESSAGES.SHOW_ITEM_IN_FOLDER, (_, filePath: string) => {
|
||||
return shell.showItemInFolder(filePath);
|
||||
});
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { emitMainProcessError } from 'backend/helpers';
|
||||
import { App, BrowserWindow } from 'electron';
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
@ -18,7 +19,7 @@ export async function saveHtmlAsPdf(
|
||||
const htmlPath = path.join(tempRoot, `${filename}.html`);
|
||||
await fs.writeFile(htmlPath, html, { encoding: 'utf-8' });
|
||||
|
||||
const printWindow = getInitializedPrintWindow(htmlPath, width, height);
|
||||
const printWindow = await getInitializedPrintWindow(htmlPath, width, height);
|
||||
const printOptions = {
|
||||
marginsType: 1, // no margin
|
||||
pageSize: {
|
||||
@ -36,19 +37,26 @@ export async function saveHtmlAsPdf(
|
||||
*/
|
||||
return await new Promise((resolve) => {
|
||||
printWindow.webContents.once('did-finish-load', () => {
|
||||
printWindow.webContents.printToPDF(printOptions).then((data) => {
|
||||
fs.writeFile(savePath, data).then(() => {
|
||||
printWindow.close();
|
||||
fs.unlink(htmlPath).then(() => {
|
||||
resolve(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
printWindow.webContents
|
||||
.printToPDF(printOptions)
|
||||
.then((data) => {
|
||||
fs.writeFile(savePath, data)
|
||||
.then(() => {
|
||||
printWindow.close();
|
||||
fs.unlink(htmlPath)
|
||||
.then(() => {
|
||||
resolve(true);
|
||||
})
|
||||
.catch((err) => emitMainProcessError(err));
|
||||
})
|
||||
.catch((err) => emitMainProcessError(err));
|
||||
})
|
||||
.catch((err) => emitMainProcessError(err));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getInitializedPrintWindow(
|
||||
async function getInitializedPrintWindow(
|
||||
printFilePath: string,
|
||||
width: number,
|
||||
height: number
|
||||
@ -59,6 +67,6 @@ function getInitializedPrintWindow(
|
||||
show: false,
|
||||
});
|
||||
|
||||
printWindow.loadFile(printFilePath);
|
||||
await printWindow.loadFile(printFilePath);
|
||||
return printWindow;
|
||||
}
|
||||
|
@ -71,9 +71,9 @@ export class LedgerPosting {
|
||||
|
||||
const roundOffAccount = await this._getRoundOffAccount();
|
||||
if (difference.gt(0)) {
|
||||
this.credit(roundOffAccount, absoluteValue);
|
||||
await this.credit(roundOffAccount, absoluteValue);
|
||||
} else {
|
||||
this.debit(roundOffAccount, absoluteValue);
|
||||
await this.debit(roundOffAccount, absoluteValue);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -59,10 +59,7 @@ export class Account extends Doc {
|
||||
return;
|
||||
}
|
||||
|
||||
const account = await this.fyo.db.get(
|
||||
'Account',
|
||||
this.parentAccount as string
|
||||
);
|
||||
const account = await this.fyo.db.get('Account', this.parentAccount);
|
||||
this.accountType = account.accountType as AccountType;
|
||||
}
|
||||
|
||||
|
@ -40,14 +40,7 @@ export class AccountingLedgerEntry extends Doc {
|
||||
|
||||
static getListViewSettings(): ListViewSettings {
|
||||
return {
|
||||
columns: [
|
||||
'date',
|
||||
'account',
|
||||
'party',
|
||||
'debit',
|
||||
'credit',
|
||||
'referenceName',
|
||||
],
|
||||
columns: ['date', 'account', 'party', 'debit', 'credit', 'referenceName'],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { Fyo, t } from 'fyo';
|
||||
import { t } from 'fyo';
|
||||
import { Doc } from 'fyo/model/doc';
|
||||
import {
|
||||
EmptyMessageMap,
|
||||
FormulaMap,
|
||||
ListsMap,
|
||||
ListViewSettings,
|
||||
ListsMap,
|
||||
} from 'fyo/model/types';
|
||||
import { codeStateMap } from 'regional/in';
|
||||
import { getCountryInfo } from 'utils/misc';
|
||||
@ -12,7 +12,7 @@ import { getCountryInfo } from 'utils/misc';
|
||||
export class Address extends Doc {
|
||||
formulas: FormulaMap = {
|
||||
addressDisplay: {
|
||||
formula: async () => {
|
||||
formula: () => {
|
||||
return [
|
||||
this.addressLine1,
|
||||
this.addressLine2,
|
||||
|
@ -143,7 +143,7 @@ export abstract class Invoice extends Transactional {
|
||||
outstandingAmount: this.baseGrandTotal!,
|
||||
});
|
||||
|
||||
const party = (await this.fyo.doc.getDoc('Party', this.party!)) as Party;
|
||||
const party = (await this.fyo.doc.getDoc('Party', this.party)) as Party;
|
||||
await party.updateOutstandingAmount();
|
||||
|
||||
if (this.makeAutoPayment && this.autoPaymentAccount) {
|
||||
@ -181,7 +181,7 @@ export abstract class Invoice extends Transactional {
|
||||
async _updatePartyOutStanding() {
|
||||
const partyDoc = (await this.fyo.doc.getDoc(
|
||||
ModelNameEnum.Party,
|
||||
this.party!
|
||||
this.party
|
||||
)) as Party;
|
||||
|
||||
await partyDoc.updateOutstandingAmount();
|
||||
@ -223,7 +223,7 @@ export abstract class Invoice extends Transactional {
|
||||
return 1.0;
|
||||
}
|
||||
const exchangeRate = await getExchangeRate({
|
||||
fromCurrency: this.currency!,
|
||||
fromCurrency: this.currency,
|
||||
toCurrency: currency as string,
|
||||
});
|
||||
|
||||
@ -247,7 +247,7 @@ export abstract class Invoice extends Transactional {
|
||||
continue;
|
||||
}
|
||||
|
||||
const tax = await this.getTax(item.tax!);
|
||||
const tax = await this.getTax(item.tax);
|
||||
for (const { account, rate } of (tax.details ?? []) as TaxDetail[]) {
|
||||
taxes[account] ??= {
|
||||
account,
|
||||
@ -256,7 +256,11 @@ export abstract class Invoice extends Transactional {
|
||||
};
|
||||
|
||||
let amount = item.amount!;
|
||||
if (this.enableDiscounting && !this.discountAfterTax && !item.itemDiscountedTotal?.isZero()) {
|
||||
if (
|
||||
this.enableDiscounting &&
|
||||
!this.discountAfterTax &&
|
||||
!item.itemDiscountedTotal?.isZero()
|
||||
) {
|
||||
amount = item.itemDiscountedTotal!;
|
||||
}
|
||||
|
||||
@ -285,7 +289,7 @@ export abstract class Invoice extends Transactional {
|
||||
}
|
||||
|
||||
async getTax(tax: string) {
|
||||
if (!this._taxes![tax]) {
|
||||
if (!this._taxes[tax]) {
|
||||
this._taxes[tax] = await this.fyo.doc.getDoc('Tax', tax);
|
||||
}
|
||||
|
||||
@ -302,7 +306,7 @@ export abstract class Invoice extends Transactional {
|
||||
return itemDiscountAmount.add(invoiceDiscountAmount);
|
||||
}
|
||||
|
||||
async getGrandTotal() {
|
||||
getGrandTotal() {
|
||||
const totalDiscount = this.getTotalDiscount();
|
||||
return ((this.taxes ?? []) as Doc[])
|
||||
.map((doc) => doc.amount as Money)
|
||||
@ -407,16 +411,15 @@ export abstract class Invoice extends Transactional {
|
||||
},
|
||||
dependsOn: ['party', 'currency'],
|
||||
},
|
||||
netTotal: { formula: async () => this.getSum('items', 'amount', false) },
|
||||
netTotal: { formula: () => this.getSum('items', 'amount', false) },
|
||||
taxes: { formula: async () => await this.getTaxSummary() },
|
||||
grandTotal: { formula: async () => await this.getGrandTotal() },
|
||||
grandTotal: { formula: () => this.getGrandTotal() },
|
||||
baseGrandTotal: {
|
||||
formula: async () =>
|
||||
(this.grandTotal as Money).mul(this.exchangeRate! ?? 1),
|
||||
formula: () => (this.grandTotal as Money).mul(this.exchangeRate! ?? 1),
|
||||
dependsOn: ['grandTotal', 'exchangeRate'],
|
||||
},
|
||||
outstandingAmount: {
|
||||
formula: async () => {
|
||||
formula: () => {
|
||||
if (this.submitted) {
|
||||
return;
|
||||
}
|
||||
@ -425,7 +428,7 @@ export abstract class Invoice extends Transactional {
|
||||
},
|
||||
},
|
||||
stockNotTransferred: {
|
||||
formula: async () => {
|
||||
formula: () => {
|
||||
if (this.submitted) {
|
||||
return;
|
||||
}
|
||||
@ -526,7 +529,7 @@ export abstract class Invoice extends Transactional {
|
||||
!!doc.autoStockTransferLocation,
|
||||
numberSeries: (doc) => getNumberSeries(doc.schemaName, doc.fyo),
|
||||
terms: (doc) => {
|
||||
const defaults = doc.fyo.singles.Defaults as Defaults | undefined;
|
||||
const defaults = doc.fyo.singles.Defaults;
|
||||
if (doc.schemaName === ModelNameEnum.SalesInvoice) {
|
||||
return defaults?.salesInvoiceTerms ?? '';
|
||||
}
|
||||
@ -616,9 +619,7 @@ export abstract class Invoice extends Transactional {
|
||||
return this.fyo.doc.getNewDoc(ModelNameEnum.Payment, data) as Payment;
|
||||
}
|
||||
|
||||
async getStockTransfer(
|
||||
isAuto: boolean = false
|
||||
): Promise<StockTransfer | null> {
|
||||
async getStockTransfer(isAuto = false): Promise<StockTransfer | null> {
|
||||
if (!this.isSubmitted) {
|
||||
return null;
|
||||
}
|
||||
|
@ -188,7 +188,7 @@ export abstract class InvoiceItem extends Doc {
|
||||
dependsOn: ['item', 'unit'],
|
||||
},
|
||||
transferQuantity: {
|
||||
formula: async (fieldname) => {
|
||||
formula: (fieldname) => {
|
||||
if (fieldname === 'quantity' || this.unit === this.transferUnit) {
|
||||
return this.quantity;
|
||||
}
|
||||
@ -205,7 +205,7 @@ export abstract class InvoiceItem extends Doc {
|
||||
|
||||
const itemDoc = await this.fyo.doc.getDoc(
|
||||
ModelNameEnum.Item,
|
||||
this.item as string
|
||||
this.item
|
||||
);
|
||||
const unitDoc = itemDoc.getLink('uom');
|
||||
|
||||
@ -321,7 +321,7 @@ export abstract class InvoiceItem extends Doc {
|
||||
],
|
||||
},
|
||||
itemTaxedTotal: {
|
||||
formula: async (fieldname) => {
|
||||
formula: async () => {
|
||||
const totalTaxRate = await this.getTotalTaxRate();
|
||||
const rate = this.rate ?? this.fyo.pesa(0);
|
||||
const quantity = this.quantity ?? 1;
|
||||
@ -389,7 +389,7 @@ export abstract class InvoiceItem extends Doc {
|
||||
};
|
||||
|
||||
validations: ValidationMap = {
|
||||
rate: async (value: DocValue) => {
|
||||
rate: (value: DocValue) => {
|
||||
if ((value as Money).gte(0)) {
|
||||
return;
|
||||
}
|
||||
@ -401,7 +401,7 @@ export abstract class InvoiceItem extends Doc {
|
||||
)}) cannot be less zero.`
|
||||
);
|
||||
},
|
||||
itemDiscountAmount: async (value: DocValue) => {
|
||||
itemDiscountAmount: (value: DocValue) => {
|
||||
if ((value as Money).lte(this.amount!)) {
|
||||
return;
|
||||
}
|
||||
@ -416,7 +416,7 @@ export abstract class InvoiceItem extends Doc {
|
||||
)}).`
|
||||
);
|
||||
},
|
||||
itemDiscountPercent: async (value: DocValue) => {
|
||||
itemDiscountPercent: (value: DocValue) => {
|
||||
if ((value as number) < 100) {
|
||||
return;
|
||||
}
|
||||
@ -434,13 +434,14 @@ export abstract class InvoiceItem extends Doc {
|
||||
|
||||
const item = await this.fyo.db.getAll(ModelNameEnum.UOMConversionItem, {
|
||||
fields: ['parent'],
|
||||
filters: { uom: value as string, parent: this.item! },
|
||||
filters: { uom: value as string, parent: this.item },
|
||||
});
|
||||
|
||||
if (item.length < 1)
|
||||
throw new ValidationError(
|
||||
t`Transfer Unit ${value as string} is not applicable for Item ${this
|
||||
.item!}`
|
||||
t`Transfer Unit ${value as string} is not applicable for Item ${
|
||||
this.item
|
||||
}`
|
||||
);
|
||||
},
|
||||
};
|
||||
|
@ -71,7 +71,7 @@ export class Item extends Doc {
|
||||
};
|
||||
|
||||
validations: ValidationMap = {
|
||||
rate: async (value: DocValue) => {
|
||||
rate: (value: DocValue) => {
|
||||
if ((value as Money).isNegative()) {
|
||||
throw new ValidationError(this.fyo.t`Rate can't be negative.`);
|
||||
}
|
||||
@ -85,13 +85,13 @@ export class Item extends Doc {
|
||||
label: fyo.t`Sales Invoice`,
|
||||
condition: (doc) => !doc.notInserted && doc.for !== 'Purchases',
|
||||
action: async (doc, router) => {
|
||||
const invoice = await fyo.doc.getNewDoc('SalesInvoice');
|
||||
const invoice = fyo.doc.getNewDoc('SalesInvoice');
|
||||
await invoice.append('items', {
|
||||
item: doc.name as string,
|
||||
rate: doc.rate as Money,
|
||||
tax: doc.tax as string,
|
||||
});
|
||||
router.push(`/edit/SalesInvoice/${invoice.name}`);
|
||||
await router.push(`/edit/SalesInvoice/${invoice.name!}`);
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -99,13 +99,13 @@ export class Item extends Doc {
|
||||
label: fyo.t`Purchase Invoice`,
|
||||
condition: (doc) => !doc.notInserted && doc.for !== 'Sales',
|
||||
action: async (doc, router) => {
|
||||
const invoice = await fyo.doc.getNewDoc('PurchaseInvoice');
|
||||
const invoice = fyo.doc.getNewDoc('PurchaseInvoice');
|
||||
await invoice.append('items', {
|
||||
item: doc.name as string,
|
||||
rate: doc.rate as Money,
|
||||
tax: doc.tax as string,
|
||||
});
|
||||
router.push(`/edit/PurchaseInvoice/${invoice.name}`);
|
||||
await router.push(`/edit/PurchaseInvoice/${invoice.name!}`);
|
||||
},
|
||||
},
|
||||
];
|
||||
@ -126,7 +126,9 @@ export class Item extends Doc {
|
||||
hasBatch: () =>
|
||||
!(this.fyo.singles.InventorySettings?.enableBatches && this.trackItem),
|
||||
hasSerialNumber: () =>
|
||||
!(this.fyo.singles.InventorySettings?.enableSerialNumber && this.trackItem),
|
||||
!(
|
||||
this.fyo.singles.InventorySettings?.enableSerialNumber && this.trackItem
|
||||
),
|
||||
uomConversions: () =>
|
||||
!this.fyo.singles.InventorySettings?.enableUomConversions,
|
||||
};
|
||||
|
@ -29,10 +29,10 @@ export class JournalEntryAccount extends Doc {
|
||||
|
||||
formulas: FormulaMap = {
|
||||
debit: {
|
||||
formula: async () => this.getAutoDebitCredit('debit'),
|
||||
formula: () => this.getAutoDebitCredit('debit'),
|
||||
},
|
||||
credit: {
|
||||
formula: async () => this.getAutoDebitCredit('credit'),
|
||||
formula: () => this.getAutoDebitCredit('credit'),
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -89,7 +89,7 @@ export class Party extends Doc {
|
||||
dependsOn: ['role'],
|
||||
},
|
||||
currency: {
|
||||
formula: async () => {
|
||||
formula: () => {
|
||||
if (!this.currency) {
|
||||
return this.fyo.singles.SystemSettings!.currency as string;
|
||||
}
|
||||
@ -132,12 +132,13 @@ export class Party extends Doc {
|
||||
condition: (doc: Doc) =>
|
||||
!doc.notInserted && (doc.role as PartyRole) !== 'Customer',
|
||||
action: async (partyDoc, router) => {
|
||||
const doc = await fyo.doc.getNewDoc('PurchaseInvoice', {
|
||||
const doc = fyo.doc.getNewDoc('PurchaseInvoice', {
|
||||
party: partyDoc.name,
|
||||
account: partyDoc.defaultAccount as string,
|
||||
});
|
||||
router.push({
|
||||
path: `/edit/PurchaseInvoice/${doc.name}`,
|
||||
|
||||
await router.push({
|
||||
path: `/edit/PurchaseInvoice/${doc.name!}`,
|
||||
query: {
|
||||
schemaName: 'PurchaseInvoice',
|
||||
values: {
|
||||
@ -170,7 +171,7 @@ export class Party extends Doc {
|
||||
});
|
||||
|
||||
await router.push({
|
||||
path: `/edit/SalesInvoice/${doc.name}`,
|
||||
path: `/edit/SalesInvoice/${doc.name!}`,
|
||||
query: {
|
||||
schemaName: 'SalesInvoice',
|
||||
values: {
|
||||
@ -186,7 +187,7 @@ export class Party extends Doc {
|
||||
condition: (doc: Doc) =>
|
||||
!doc.notInserted && (doc.role as PartyRole) !== 'Supplier',
|
||||
action: async (partyDoc, router) => {
|
||||
router.push({
|
||||
await router.push({
|
||||
path: '/list/SalesInvoice',
|
||||
query: { filters: JSON.stringify({ party: partyDoc.name }) },
|
||||
});
|
||||
|
@ -84,9 +84,7 @@ export class Payment extends Transactional {
|
||||
updateAmountOnReferenceUpdate() {
|
||||
this.amount = this.fyo.pesa(0);
|
||||
for (const paymentReference of (this.for ?? []) as Doc[]) {
|
||||
this.amount = (this.amount as Money).add(
|
||||
paymentReference.amount as Money
|
||||
);
|
||||
this.amount = this.amount.add(paymentReference.amount as Money);
|
||||
}
|
||||
}
|
||||
|
||||
@ -123,8 +121,9 @@ export class Payment extends Transactional {
|
||||
|
||||
if (referenceName && referenceType && !refDoc) {
|
||||
throw new ValidationError(
|
||||
t`${referenceType} of type ${this.fyo.schemaMap?.[referenceType]
|
||||
?.label!} does not exist`
|
||||
t`${referenceType} of type ${
|
||||
this.fyo.schemaMap?.[referenceType]?.label ?? referenceType
|
||||
} does not exist`
|
||||
);
|
||||
}
|
||||
|
||||
@ -241,8 +240,8 @@ export class Payment extends Transactional {
|
||||
const account = this.account as string;
|
||||
const amount = this.amount as Money;
|
||||
|
||||
await posting.debit(paymentAccount as string, amount);
|
||||
await posting.credit(account as string, amount);
|
||||
await posting.debit(paymentAccount, amount);
|
||||
await posting.credit(account, amount);
|
||||
|
||||
await this.applyWriteOffPosting(posting);
|
||||
return posting;
|
||||
@ -269,7 +268,7 @@ export class Payment extends Transactional {
|
||||
}
|
||||
|
||||
async validateReferences() {
|
||||
const forReferences = (this.for ?? []) as PaymentFor[];
|
||||
const forReferences = this.for ?? [];
|
||||
if (forReferences.length === 0) {
|
||||
return;
|
||||
}
|
||||
@ -334,10 +333,10 @@ export class Payment extends Transactional {
|
||||
}
|
||||
|
||||
async updateReferenceDocOutstanding() {
|
||||
for (const row of (this.for ?? []) as PaymentFor[]) {
|
||||
for (const row of this.for ?? []) {
|
||||
const referenceDoc = await this.fyo.doc.getDoc(
|
||||
row.referenceType!,
|
||||
row.referenceName!
|
||||
row.referenceName
|
||||
);
|
||||
|
||||
const previousOutstandingAmount = referenceDoc.outstandingAmount as Money;
|
||||
@ -348,7 +347,7 @@ export class Payment extends Transactional {
|
||||
|
||||
async afterCancel() {
|
||||
await super.afterCancel();
|
||||
this.revertOutstandingAmount();
|
||||
await this.revertOutstandingAmount();
|
||||
}
|
||||
|
||||
async revertOutstandingAmount() {
|
||||
@ -357,10 +356,10 @@ export class Payment extends Transactional {
|
||||
}
|
||||
|
||||
async _revertReferenceOutstanding() {
|
||||
for (const ref of (this.for ?? []) as PaymentFor[]) {
|
||||
for (const ref of this.for ?? []) {
|
||||
const refDoc = await this.fyo.doc.getDoc(
|
||||
ref.referenceType!,
|
||||
ref.referenceName!
|
||||
ref.referenceName
|
||||
);
|
||||
|
||||
const outstandingAmount = (refDoc.outstandingAmount as Money).add(
|
||||
@ -374,7 +373,7 @@ export class Payment extends Transactional {
|
||||
async updatePartyOutstanding() {
|
||||
const partyDoc = (await this.fyo.doc.getDoc(
|
||||
ModelNameEnum.Party,
|
||||
this.party!
|
||||
this.party
|
||||
)) as Party;
|
||||
await partyDoc.updateOutstandingAmount();
|
||||
}
|
||||
@ -439,7 +438,7 @@ export class Payment extends Transactional {
|
||||
'referenceName'
|
||||
)) as Invoice | null;
|
||||
|
||||
return (refDoc?.account ?? null) as string | null;
|
||||
return refDoc?.account ?? null;
|
||||
}
|
||||
|
||||
formulas: FormulaMap = {
|
||||
@ -514,17 +513,17 @@ export class Payment extends Transactional {
|
||||
},
|
||||
},
|
||||
amount: {
|
||||
formula: async () => this.getSum('for', 'amount', false),
|
||||
formula: () => this.getSum('for', 'amount', false),
|
||||
dependsOn: ['for'],
|
||||
},
|
||||
amountPaid: {
|
||||
formula: async () => this.amount!.sub(this.writeoff!),
|
||||
formula: () => this.amount!.sub(this.writeoff!),
|
||||
dependsOn: ['amount', 'writeoff', 'for'],
|
||||
},
|
||||
};
|
||||
|
||||
validations: ValidationMap = {
|
||||
amount: async (value: DocValue) => {
|
||||
amount: (value: DocValue) => {
|
||||
if ((value as Money).isNegative()) {
|
||||
throw new ValidationError(
|
||||
this.fyo.t`Payment amount cannot be less than zero.`
|
||||
@ -615,7 +614,7 @@ export class Payment extends Transactional {
|
||||
return [getLedgerLinkAction(fyo)];
|
||||
}
|
||||
|
||||
static getListViewSettings(fyo: Fyo): ListViewSettings {
|
||||
static getListViewSettings(): ListViewSettings {
|
||||
return {
|
||||
columns: ['name', getDocStatusListColumn(), 'party', 'date', 'amount'],
|
||||
};
|
||||
|
@ -60,7 +60,7 @@ export class PaymentFor extends Doc {
|
||||
|
||||
const outstandingAmount = (await this.fyo.getValue(
|
||||
this.referenceType as string,
|
||||
this.referenceName as string,
|
||||
this.referenceName,
|
||||
'outstandingAmount'
|
||||
)) as Money;
|
||||
|
||||
@ -105,10 +105,11 @@ export class PaymentFor extends Doc {
|
||||
return;
|
||||
}
|
||||
|
||||
const referenceType = this.referenceType ?? ModelNameEnum.SalesInvoice;
|
||||
const label = this.fyo.schemaMap[referenceType]?.label ?? referenceType;
|
||||
|
||||
throw new NotFoundError(
|
||||
t`${this.fyo.schemaMap[this.referenceType!]?.label!} ${
|
||||
value as string
|
||||
} does not exist`,
|
||||
t`${label} ${value as string} does not exist`,
|
||||
false
|
||||
);
|
||||
},
|
||||
|
@ -1,7 +1,10 @@
|
||||
import { Doc } from 'fyo/model/doc';
|
||||
import { ListViewSettings } from 'fyo/model/types';
|
||||
import { PriceListItem } from './PriceListItem';
|
||||
import { getPriceListEnabledColumn, getPriceListStatusColumn } from 'models/helpers';
|
||||
import {
|
||||
getPriceListEnabledColumn,
|
||||
getPriceListStatusColumn,
|
||||
} from 'models/helpers';
|
||||
|
||||
export class PriceList extends Doc {
|
||||
isEnabled?: boolean;
|
||||
@ -11,7 +14,11 @@ export class PriceList extends Doc {
|
||||
|
||||
static getListViewSettings(): ListViewSettings {
|
||||
return {
|
||||
columns: ['name', getPriceListEnabledColumn(), getPriceListStatusColumn()],
|
||||
columns: [
|
||||
'name',
|
||||
getPriceListEnabledColumn(),
|
||||
getPriceListStatusColumn(),
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -46,7 +46,7 @@ export class PrintTemplate extends Doc {
|
||||
|
||||
static lists: ListsMap = {
|
||||
type(doc?: Doc) {
|
||||
let enableInventory: boolean = false;
|
||||
let enableInventory = false;
|
||||
let schemaMap: SchemaMap = {};
|
||||
if (doc) {
|
||||
enableInventory = !!doc.fyo.singles.AccountingSettings?.enableInventory;
|
||||
|
@ -24,7 +24,7 @@ export class PurchaseInvoice extends Invoice {
|
||||
}
|
||||
}
|
||||
|
||||
const discountAmount = await this.getTotalDiscount();
|
||||
const discountAmount = this.getTotalDiscount();
|
||||
const discountAccount = this.fyo.singles.AccountingSettings
|
||||
?.discountAccount as string | undefined;
|
||||
if (discountAccount && discountAmount.isPositive()) {
|
||||
|
@ -19,12 +19,12 @@ export class SalesInvoice extends Invoice {
|
||||
}
|
||||
|
||||
if (this.taxes) {
|
||||
for (const tax of this.taxes!) {
|
||||
for (const tax of this.taxes) {
|
||||
await posting.credit(tax.account!, tax.amount!.mul(exchangeRate));
|
||||
}
|
||||
}
|
||||
|
||||
const discountAmount = await this.getTotalDiscount();
|
||||
const discountAmount = this.getTotalDiscount();
|
||||
const discountAccount = this.fyo.singles.AccountingSettings
|
||||
?.discountAccount as string | undefined;
|
||||
if (discountAccount && discountAmount.isPositive()) {
|
||||
|
@ -61,7 +61,7 @@ export class SetupWizard extends Doc {
|
||||
|
||||
formulas: FormulaMap = {
|
||||
fiscalYearStart: {
|
||||
formula: async (fieldname?: string) => {
|
||||
formula: (fieldname?: string) => {
|
||||
if (
|
||||
fieldname === 'fiscalYearEnd' &&
|
||||
this.fiscalYearEnd &&
|
||||
@ -85,7 +85,7 @@ export class SetupWizard extends Doc {
|
||||
dependsOn: ['country', 'fiscalYearEnd'],
|
||||
},
|
||||
fiscalYearEnd: {
|
||||
formula: async (fieldname?: string) => {
|
||||
formula: (fieldname?: string) => {
|
||||
if (
|
||||
fieldname === 'fiscalYearStart' &&
|
||||
this.fiscalYearStart &&
|
||||
@ -109,7 +109,7 @@ export class SetupWizard extends Doc {
|
||||
dependsOn: ['country', 'fiscalYearStart'],
|
||||
},
|
||||
currency: {
|
||||
formula: async () => {
|
||||
formula: () => {
|
||||
const country = this.get('country');
|
||||
if (typeof country !== 'string') {
|
||||
return;
|
||||
@ -135,7 +135,7 @@ export class SetupWizard extends Doc {
|
||||
dependsOn: ['country'],
|
||||
},
|
||||
chartOfAccounts: {
|
||||
formula: async () => {
|
||||
formula: () => {
|
||||
const country = this.get('country') as string | undefined;
|
||||
if (country === undefined) {
|
||||
return;
|
||||
|
@ -9,10 +9,7 @@ import {
|
||||
AccountRootType,
|
||||
AccountRootTypeEnum,
|
||||
} from './baseModels/Account/types';
|
||||
import {
|
||||
Defaults,
|
||||
numberSeriesDefaultsMap,
|
||||
} from './baseModels/Defaults/Defaults';
|
||||
import { numberSeriesDefaultsMap } from './baseModels/Defaults/Defaults';
|
||||
import { Invoice } from './baseModels/Invoice/Invoice';
|
||||
import { StockMovement } from './inventory/StockMovement';
|
||||
import { StockTransfer } from './inventory/StockTransfer';
|
||||
@ -55,7 +52,7 @@ export function getMakeStockTransferAction(
|
||||
condition: (doc: Doc) => doc.isSubmitted && !!doc.stockNotTransferred,
|
||||
action: async (doc: Doc) => {
|
||||
const transfer = await (doc as Invoice).getStockTransfer();
|
||||
if (!transfer) {
|
||||
if (!transfer || !transfer.name) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -81,7 +78,7 @@ export function getMakeInvoiceAction(
|
||||
condition: (doc: Doc) => doc.isSubmitted && !doc.backReference,
|
||||
action: async (doc: Doc) => {
|
||||
const invoice = await (doc as StockTransfer).getInvoice();
|
||||
if (!invoice) {
|
||||
if (!invoice || !invoice.name) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -128,10 +125,7 @@ export function getMakePaymentAction(fyo: Fyo): Action {
|
||||
};
|
||||
}
|
||||
|
||||
export function getLedgerLinkAction(
|
||||
fyo: Fyo,
|
||||
isStock: boolean = false
|
||||
): Action {
|
||||
export function getLedgerLinkAction(fyo: Fyo, isStock = false): Action {
|
||||
let label = fyo.t`Accounting Entries`;
|
||||
let reportClassName: 'GeneralLedger' | 'StockLedger' = 'GeneralLedger';
|
||||
|
||||
@ -146,7 +140,7 @@ export function getLedgerLinkAction(
|
||||
condition: (doc: Doc) => doc.isSubmitted,
|
||||
action: async (doc: Doc, router: Router) => {
|
||||
const route = getLedgerLink(doc, reportClassName);
|
||||
router.push(route);
|
||||
await router.push(route);
|
||||
},
|
||||
};
|
||||
}
|
||||
@ -174,7 +168,7 @@ export function getTransactionStatusColumn(): ColumnConfig {
|
||||
fieldtype: 'Select',
|
||||
render(doc) {
|
||||
const status = getDocStatus(doc) as InvoiceStatus;
|
||||
const color = statusColor[status];
|
||||
const color = statusColor[status] ?? 'gray';
|
||||
const label = getStatusText(status);
|
||||
|
||||
return {
|
||||
@ -389,9 +383,7 @@ export async function getExchangeRate({
|
||||
|
||||
let exchangeRate = 0;
|
||||
if (localStorage) {
|
||||
exchangeRate = safeParseFloat(
|
||||
localStorage.getItem(cacheKey as string) as string
|
||||
);
|
||||
exchangeRate = safeParseFloat(localStorage.getItem(cacheKey) as string);
|
||||
}
|
||||
|
||||
if (exchangeRate && exchangeRate !== 1) {
|
||||
@ -402,9 +394,14 @@ export async function getExchangeRate({
|
||||
const res = await fetch(
|
||||
`https://api.vatcomply.com/rates?date=${date}&base=${fromCurrency}&symbols=${toCurrency}`
|
||||
);
|
||||
const data = await res.json();
|
||||
const data = (await res.json()) as {
|
||||
base: string;
|
||||
data: string;
|
||||
rates: Record<string, number>;
|
||||
};
|
||||
exchangeRate = data.rates[toCurrency];
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(error);
|
||||
exchangeRate ??= 1;
|
||||
}
|
||||
@ -439,7 +436,7 @@ export function getNumberSeries(schemaName: string, fyo: Fyo) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const defaults = fyo.singles.Defaults as Defaults | undefined;
|
||||
const defaults = fyo.singles.Defaults;
|
||||
const field = fyo.getField(schemaName, 'numberSeries');
|
||||
const value = defaults?.[numberSeriesKey] as string | undefined;
|
||||
return value ?? (field?.default as string | undefined);
|
||||
|
@ -1,13 +1,10 @@
|
||||
import { Doc } from 'fyo/model/doc';
|
||||
import {
|
||||
ListViewSettings,
|
||||
} from 'fyo/model/types';
|
||||
import { ListViewSettings } from 'fyo/model/types';
|
||||
|
||||
export class Batch extends Doc {
|
||||
static getListViewSettings(): ListViewSettings {
|
||||
return {
|
||||
columns: ["name", "expiryDate", "manufactureDate"],
|
||||
columns: ['name', 'expiryDate', 'manufactureDate'],
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -40,7 +40,7 @@ export class StockManager {
|
||||
}
|
||||
|
||||
for (const details of detailsList) {
|
||||
await this.#createTransfer(details);
|
||||
this.#createTransfer(details);
|
||||
}
|
||||
|
||||
await this.#sync();
|
||||
@ -73,7 +73,7 @@ export class StockManager {
|
||||
}
|
||||
}
|
||||
|
||||
async #createTransfer(details: SMIDetails) {
|
||||
#createTransfer(details: SMIDetails) {
|
||||
const item = new StockManagerItem(details, this.fyo);
|
||||
item.transferStock();
|
||||
this.items.push(item);
|
||||
|
@ -23,7 +23,7 @@ import {
|
||||
getSerialNumberFromDoc,
|
||||
updateSerialNumbers,
|
||||
validateBatch,
|
||||
validateSerialNumber
|
||||
validateSerialNumber,
|
||||
} from './helpers';
|
||||
import { MovementType, MovementTypeEnum } from './types';
|
||||
|
||||
@ -39,6 +39,7 @@ export class StockMovement extends Transfer {
|
||||
return false;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
override async getPosting(): Promise<LedgerPosting | null> {
|
||||
return null;
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { t } from 'fyo';
|
||||
import { DocValue } from 'fyo/core/types';
|
||||
import { Doc } from 'fyo/model/doc';
|
||||
import {
|
||||
FiltersMap,
|
||||
FormulaMap,
|
||||
@ -14,8 +13,8 @@ import { ModelNameEnum } from 'models/types';
|
||||
import { Money } from 'pesa';
|
||||
import { safeParseFloat } from 'utils/index';
|
||||
import { StockMovement } from './StockMovement';
|
||||
import { MovementTypeEnum } from './types';
|
||||
import { TransferItem } from './TransferItem';
|
||||
import { MovementTypeEnum } from './types';
|
||||
|
||||
export class StockMovementItem extends TransferItem {
|
||||
name?: string;
|
||||
@ -78,8 +77,8 @@ export class StockMovementItem extends TransferItem {
|
||||
return null;
|
||||
}
|
||||
|
||||
const defaultLocation = this.fyo.singles.InventorySettings
|
||||
?.defaultLocation as string | undefined;
|
||||
const defaultLocation =
|
||||
this.fyo.singles.InventorySettings?.defaultLocation;
|
||||
if (defaultLocation && !this.fromLocation && this.isIssue) {
|
||||
return defaultLocation;
|
||||
}
|
||||
@ -94,8 +93,8 @@ export class StockMovementItem extends TransferItem {
|
||||
return null;
|
||||
}
|
||||
|
||||
const defaultLocation = this.fyo.singles.InventorySettings
|
||||
?.defaultLocation as string | undefined;
|
||||
const defaultLocation =
|
||||
this.fyo.singles.InventorySettings?.defaultLocation;
|
||||
if (defaultLocation && !this.toLocation && this.isReceipt) {
|
||||
return defaultLocation;
|
||||
}
|
||||
@ -128,7 +127,7 @@ export class StockMovementItem extends TransferItem {
|
||||
dependsOn: ['item', 'unit'],
|
||||
},
|
||||
transferQuantity: {
|
||||
formula: async (fieldname) => {
|
||||
formula: (fieldname) => {
|
||||
if (fieldname === 'quantity' || this.unit === this.transferUnit) {
|
||||
return this.quantity;
|
||||
}
|
||||
@ -145,7 +144,7 @@ export class StockMovementItem extends TransferItem {
|
||||
|
||||
const itemDoc = await this.fyo.doc.getDoc(
|
||||
ModelNameEnum.Item,
|
||||
this.item as string
|
||||
this.item
|
||||
);
|
||||
const unitDoc = itemDoc.getLink('uom');
|
||||
|
||||
@ -217,13 +216,14 @@ export class StockMovementItem extends TransferItem {
|
||||
|
||||
const item = await this.fyo.db.getAll(ModelNameEnum.UOMConversionItem, {
|
||||
fields: ['parent'],
|
||||
filters: { uom: value as string, parent: this.item! },
|
||||
filters: { uom: value as string, parent: this.item },
|
||||
});
|
||||
|
||||
if (item.length < 1)
|
||||
throw new ValidationError(
|
||||
t`Transfer Unit ${value as string} is not applicable for Item ${this
|
||||
.item!}`
|
||||
t`Transfer Unit ${value as string} is not applicable for Item ${
|
||||
this.item
|
||||
}`
|
||||
);
|
||||
},
|
||||
};
|
||||
|
@ -26,7 +26,6 @@ import {
|
||||
validateBatch,
|
||||
validateSerialNumber,
|
||||
} from './helpers';
|
||||
import { Item } from 'models/baseModels/Item/Item';
|
||||
|
||||
export abstract class StockTransfer extends Transfer {
|
||||
name?: string;
|
||||
@ -67,7 +66,7 @@ export abstract class StockTransfer extends Transfer {
|
||||
static defaults: DefaultMap = {
|
||||
numberSeries: (doc) => getNumberSeries(doc.schemaName, doc.fyo),
|
||||
terms: (doc) => {
|
||||
const defaults = doc.fyo.singles.Defaults as Defaults | undefined;
|
||||
const defaults = doc.fyo.singles.Defaults;
|
||||
if (doc.schemaName === ModelNameEnum.Shipment) {
|
||||
return defaults?.shipmentTerms ?? '';
|
||||
}
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { t } from 'fyo';
|
||||
import { DocValue } from 'fyo/core/types';
|
||||
import { Doc } from 'fyo/model/doc';
|
||||
import {
|
||||
@ -73,7 +72,7 @@ export class StockTransferItem extends TransferItem {
|
||||
dependsOn: ['item', 'unit'],
|
||||
},
|
||||
transferQuantity: {
|
||||
formula: async (fieldname) => {
|
||||
formula: (fieldname) => {
|
||||
if (fieldname === 'quantity' || this.unit === this.transferUnit) {
|
||||
return this.quantity;
|
||||
}
|
||||
@ -90,7 +89,7 @@ export class StockTransferItem extends TransferItem {
|
||||
|
||||
const itemDoc = await this.fyo.doc.getDoc(
|
||||
ModelNameEnum.Item,
|
||||
this.item as string
|
||||
this.item
|
||||
);
|
||||
const unitDoc = itemDoc.getLink('uom');
|
||||
|
||||
@ -146,7 +145,7 @@ export class StockTransferItem extends TransferItem {
|
||||
dependsOn: ['rate', 'quantity'],
|
||||
},
|
||||
rate: {
|
||||
formula: async (fieldname) => {
|
||||
formula: async () => {
|
||||
const rate = (await this.fyo.getValue(
|
||||
'Item',
|
||||
this.item as string,
|
||||
@ -177,8 +176,8 @@ export class StockTransferItem extends TransferItem {
|
||||
return;
|
||||
}
|
||||
|
||||
const defaultLocation = this.fyo.singles.InventorySettings
|
||||
?.defaultLocation as string | undefined;
|
||||
const defaultLocation =
|
||||
this.fyo.singles.InventorySettings?.defaultLocation;
|
||||
|
||||
if (defaultLocation && !this.location) {
|
||||
return defaultLocation;
|
||||
@ -195,13 +194,14 @@ export class StockTransferItem extends TransferItem {
|
||||
|
||||
const item = await this.fyo.db.getAll(ModelNameEnum.UOMConversionItem, {
|
||||
fields: ['parent'],
|
||||
filters: { uom: value as string, parent: this.item! },
|
||||
filters: { uom: value as string, parent: this.item },
|
||||
});
|
||||
|
||||
if (item.length < 1)
|
||||
throw new ValidationError(
|
||||
t`Transfer Unit ${value as string} is not applicable for Item ${this
|
||||
.item!}`
|
||||
this.fyo.t`Transfer Unit ${
|
||||
value as string
|
||||
} is not applicable for Item ${this.item}`
|
||||
);
|
||||
},
|
||||
};
|
||||
|
@ -55,7 +55,7 @@ export class StockQueue {
|
||||
return null;
|
||||
}
|
||||
|
||||
let incomingRate: number = 0;
|
||||
let incomingRate = 0;
|
||||
this.quantity -= quantity;
|
||||
let remaining = quantity;
|
||||
|
||||
|
@ -37,17 +37,22 @@ interface TransferTwo extends Omit<Transfer, 'from' | 'to'> {
|
||||
location: string;
|
||||
}
|
||||
|
||||
export function getItem(name: string, rate: number, hasBatch: boolean = false, hasSerialNumber: boolean = false) {
|
||||
export function getItem(
|
||||
name: string,
|
||||
rate: number,
|
||||
hasBatch = false,
|
||||
hasSerialNumber = false
|
||||
) {
|
||||
return { name, rate, trackItem: true, hasBatch, hasSerialNumber };
|
||||
}
|
||||
|
||||
export async function getBatch(
|
||||
export function getBatch(
|
||||
schemaName: ModelNameEnum.Batch,
|
||||
batch: string,
|
||||
expiryDate: Date,
|
||||
manufactureDate: Date,
|
||||
fyo: Fyo
|
||||
): Promise<Batch> {
|
||||
): Batch {
|
||||
const doc = fyo.doc.getNewDoc(schemaName, {
|
||||
batch,
|
||||
expiryDate,
|
||||
|
@ -5,7 +5,7 @@ import { codeStateMap } from 'regional/in';
|
||||
export class Address extends BaseAddress {
|
||||
formulas: FormulaMap = {
|
||||
addressDisplay: {
|
||||
formula: async () => {
|
||||
formula: () => {
|
||||
return [
|
||||
this.addressLine1,
|
||||
this.addressLine2,
|
||||
@ -28,7 +28,7 @@ export class Address extends BaseAddress {
|
||||
},
|
||||
|
||||
pos: {
|
||||
formula: async () => {
|
||||
formula: () => {
|
||||
const stateList = Object.values(codeStateMap).sort();
|
||||
const state = this.state as string;
|
||||
if (stateList.includes(state)) {
|
||||
|
@ -6,6 +6,7 @@ export class Party extends BaseParty {
|
||||
gstin?: string;
|
||||
gstType?: GSTType;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
async beforeSync() {
|
||||
const gstin = this.get('gstin') as string | undefined;
|
||||
const gstType = this.get('gstType') as GSTType;
|
||||
|
62
package.json
62
package.json
@ -2,23 +2,20 @@
|
||||
"name": "frappe-books",
|
||||
"version": "0.16.0",
|
||||
"description": "Simple book-keeping app for everyone",
|
||||
"main": "background.js",
|
||||
"author": {
|
||||
"name": "Frappe Technologies Pvt. Ltd.",
|
||||
"email": "hello@frappe.io"
|
||||
},
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"lint": "vue-cli-service lint",
|
||||
"dev": "node build/scripts/dev.mjs",
|
||||
"build": "node build/scripts/build.mjs",
|
||||
"release": "scripts/publish-mac-arm.sh",
|
||||
"postinstall": "electron-rebuild",
|
||||
"postuninstall": "electron-rebuild",
|
||||
"electron:build": "vue-cli-service electron:build",
|
||||
"electron:serve": "vue-cli-service electron:serve",
|
||||
"script:translate": "scripts/runner.sh scripts/generateTranslations.ts",
|
||||
"script:profile": "scripts/profile.sh",
|
||||
"test": "scripts/test.sh"
|
||||
"test": "scripts/test.sh",
|
||||
"lint": "eslint . --ext ts,vue"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/autocomplete": "^6.4.2",
|
||||
@ -34,12 +31,15 @@
|
||||
"luxon": "^2.5.2",
|
||||
"node-fetch": "2",
|
||||
"pesa": "^1.1.12",
|
||||
"source-map-support": "^0.5.21",
|
||||
"vue": "^3.2.40",
|
||||
"vue-router": "^4.0.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.16.0",
|
||||
"@babel/eslint-parser": "^7.16.0",
|
||||
"@codemirror/language": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.0.0",
|
||||
"@lezer/common": "^1.0.0",
|
||||
"@types/assert": "^1.5.6",
|
||||
"@types/electron-devtools-installer": "^2.2.0",
|
||||
"@types/lodash": "^4.14.179",
|
||||
@ -48,30 +48,25 @@
|
||||
"@types/node": "^17.0.23",
|
||||
"@types/node-fetch": "^2.6.1",
|
||||
"@types/tape": "^4.13.2",
|
||||
"@typescript-eslint/eslint-plugin": "^4.15.1",
|
||||
"@typescript-eslint/parser": "^4.15.1",
|
||||
"@vue/cli-plugin-babel": "^4.5.0",
|
||||
"@vue/cli-plugin-eslint": "^5.0.0-beta.7",
|
||||
"@vue/cli-plugin-router": "^4.5.0",
|
||||
"@vue/cli-plugin-typescript": "~4.5.0",
|
||||
"@vue/cli-service": "^4.5.0",
|
||||
"@vue/eslint-config-typescript": "^7.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "5.60.0",
|
||||
"@typescript-eslint/parser": "5.60.0",
|
||||
"@vitejs/plugin-vue": "^4.2.3",
|
||||
"autoprefixer": "^9",
|
||||
"babel-loader": "^8.2.3",
|
||||
"chokidar": "^3.5.3",
|
||||
"dotenv": "^16.0.0",
|
||||
"electron": "18.3.7",
|
||||
"electron-builder": "24.0.0-alpha.12",
|
||||
"electron-builder": "^24.4.0",
|
||||
"electron-devtools-installer": "^3.2.0",
|
||||
"electron-rebuild": "^3.2.9",
|
||||
"electron-updater": "^5.2.1",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint": "^8.43.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"eslint-plugin-vue": "^7.0.0",
|
||||
"lint-staged": "^11.2.6",
|
||||
"eslint-plugin-vue": "^9.15.0",
|
||||
"execa": "^7.1.1",
|
||||
"fs-extra": "^11.1.1",
|
||||
"postcss": "^8",
|
||||
"prettier": "^2.4.1",
|
||||
"raw-loader": "^4.0.2",
|
||||
"tailwindcss": "npm:@tailwindcss/postcss7-compat",
|
||||
"tailwindcss-rtl": "^0.9.0",
|
||||
"tap-spec": "^5.0.0",
|
||||
@ -80,29 +75,18 @@
|
||||
"tsconfig-paths": "^3.14.1",
|
||||
"tslib": "^2.3.1",
|
||||
"typescript": "^4.6.2",
|
||||
"vue-cli-plugin-electron-builder": "https://github.com/nklayman/vue-cli-plugin-electron-builder#ebb9183f4913f927d4e4f4eb1fbab61a960f7a09",
|
||||
"webpack": "^5.76.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"electron-builder": "24.0.0-alpha.12"
|
||||
"vite": "^4.3.9",
|
||||
"vue-tsc": "^1.6.5",
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"prettier": {
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "es5"
|
||||
},
|
||||
"engineStrict": true,
|
||||
"engines": {
|
||||
"node": ">=16.13.1 <17"
|
||||
},
|
||||
"gitHooks": {
|
||||
"pre-commit": "lint-staged"
|
||||
},
|
||||
"homepage": "https://frappebooks.com",
|
||||
"lint-staged": {
|
||||
"*.{js,vue}": "vue-cli-service lint"
|
||||
},
|
||||
"repository": {
|
||||
"url": "https://github.com/frappe/books"
|
||||
}
|
||||
},
|
||||
"license": "AGPL-3.0-only"
|
||||
}
|
||||
|
@ -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>
|
@ -34,11 +34,11 @@ export const ACC_BAL_WIDTH = 1.25;
|
||||
|
||||
export abstract class AccountReport extends LedgerReport {
|
||||
toDate?: string;
|
||||
count: number = 3;
|
||||
count = 3;
|
||||
fromYear?: number;
|
||||
toYear?: number;
|
||||
consolidateColumns: boolean = false;
|
||||
hideGroupAmounts: boolean = false;
|
||||
consolidateColumns = false;
|
||||
hideGroupAmounts = false;
|
||||
periodicity: Periodicity = 'Monthly';
|
||||
basedOn: BasedOn = 'Until Date';
|
||||
|
||||
@ -88,10 +88,7 @@ export abstract class AccountReport extends LedgerReport {
|
||||
};
|
||||
}
|
||||
|
||||
async getTotalNode(
|
||||
rootNode: AccountTreeNode,
|
||||
name: string
|
||||
): Promise<AccountListNode> {
|
||||
getTotalNode(rootNode: AccountTreeNode, name: string): AccountListNode {
|
||||
const accountTree = { [rootNode.name]: rootNode };
|
||||
const leafNodes = getListOfLeafNodes(accountTree) as AccountTreeNode[];
|
||||
|
||||
@ -153,7 +150,7 @@ export abstract class AccountReport extends LedgerReport {
|
||||
): Promise<AccountNameValueMapMap> {
|
||||
const accountValueMap: AccountNameValueMapMap = new Map();
|
||||
if (!this.accountMap) {
|
||||
await this._getAccountMap();
|
||||
await this._setAndReturnAccountMap();
|
||||
}
|
||||
|
||||
for (const account of map.keys()) {
|
||||
@ -169,17 +166,17 @@ export abstract class AccountReport extends LedgerReport {
|
||||
}
|
||||
|
||||
if (!this.accountMap?.[entry.account]) {
|
||||
this._getAccountMap(true);
|
||||
await this._setAndReturnAccountMap(true);
|
||||
}
|
||||
|
||||
const totalBalance = valueMap.get(key!)?.balance ?? 0;
|
||||
const totalBalance = valueMap.get(key)?.balance ?? 0;
|
||||
const balance = (entry.debit ?? 0) - (entry.credit ?? 0);
|
||||
const rootType = this.accountMap![entry.account]?.rootType;
|
||||
|
||||
if (isCredit(rootType)) {
|
||||
valueMap.set(key!, { balance: totalBalance - balance });
|
||||
valueMap.set(key, { balance: totalBalance - balance });
|
||||
} else {
|
||||
valueMap.set(key!, { balance: totalBalance + balance });
|
||||
valueMap.set(key, { balance: totalBalance + balance });
|
||||
}
|
||||
}
|
||||
accountValueMap.set(account, valueMap);
|
||||
@ -189,7 +186,9 @@ export abstract class AccountReport extends LedgerReport {
|
||||
}
|
||||
|
||||
async _getAccountTree(rangeGroupedMap: AccountNameValueMapMap) {
|
||||
const accountTree = cloneDeep(await this._getAccountMap()) as AccountTree;
|
||||
const accountTree = cloneDeep(
|
||||
await this._setAndReturnAccountMap()
|
||||
) as AccountTree;
|
||||
|
||||
setPruneFlagOnAccountTreeNodes(accountTree);
|
||||
setValueMapOnAccountTreeNodes(accountTree, rangeGroupedMap);
|
||||
@ -200,7 +199,7 @@ export abstract class AccountReport extends LedgerReport {
|
||||
return accountTree;
|
||||
}
|
||||
|
||||
async _getAccountMap(force: boolean = false) {
|
||||
async _setAndReturnAccountMap(force = false) {
|
||||
if (this.accountMap && !force) {
|
||||
return this.accountMap;
|
||||
}
|
||||
@ -510,14 +509,14 @@ function updateParentAccountWithChildValues(
|
||||
parentAccount.valueMap ??= new Map();
|
||||
|
||||
for (const key of valueMap.keys()) {
|
||||
const value = parentAccount.valueMap!.get(key);
|
||||
const value = parentAccount.valueMap.get(key);
|
||||
const childValue = valueMap.get(key);
|
||||
const map: Record<string, number> = {};
|
||||
|
||||
for (const key of Object.keys(childValue!)) {
|
||||
map[key] = (value?.[key] ?? 0) + (childValue?.[key] ?? 0);
|
||||
}
|
||||
parentAccount.valueMap!.set(key, map);
|
||||
parentAccount.valueMap.set(key, map);
|
||||
}
|
||||
|
||||
return parentAccount.parentAccount!;
|
||||
@ -533,7 +532,7 @@ function setChildrenOnAccountTreeNodes(accountTree: AccountTree) {
|
||||
}
|
||||
|
||||
accountTree[ac.parentAccount].children ??= [];
|
||||
accountTree[ac.parentAccount].children!.push(ac!);
|
||||
accountTree[ac.parentAccount].children!.push(ac);
|
||||
|
||||
parentNodes.add(ac.parentAccount);
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ import { getMapFromList } from 'utils';
|
||||
export class BalanceSheet extends AccountReport {
|
||||
static title = t`Balance Sheet`;
|
||||
static reportName = 'balance-sheet';
|
||||
loading: boolean = false;
|
||||
loading = false;
|
||||
|
||||
get rootTypes(): AccountRootType[] {
|
||||
return [
|
||||
@ -54,15 +54,15 @@ export class BalanceSheet extends AccountReport {
|
||||
})
|
||||
.filter((row) => !!row.rootNode);
|
||||
|
||||
this.reportData = await this.getReportDataFromRows(
|
||||
this.reportData = this.getReportDataFromRows(
|
||||
getMapFromList(rootTypeRows, 'rootType')
|
||||
);
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
async getReportDataFromRows(
|
||||
getReportDataFromRows(
|
||||
rootTypeRows: Record<AccountRootType, RootTypeRow | undefined>
|
||||
): Promise<ReportData> {
|
||||
): ReportData {
|
||||
const typeNameList = [
|
||||
{
|
||||
rootType: AccountRootTypeEnum.Asset,
|
||||
@ -89,7 +89,7 @@ export class BalanceSheet extends AccountReport {
|
||||
reportData.push(...row.rows);
|
||||
|
||||
if (row.rootNode) {
|
||||
const totalNode = await this.getTotalNode(row.rootNode, totalName);
|
||||
const totalNode = this.getTotalNode(row.rootNode, totalName);
|
||||
const totalRow = this.getRowFromAccountListNode(totalNode);
|
||||
reportData.push(totalRow);
|
||||
}
|
||||
|
@ -24,11 +24,11 @@ type ReferenceType =
|
||||
export class GeneralLedger extends LedgerReport {
|
||||
static title = t`General Ledger`;
|
||||
static reportName = 'general-ledger';
|
||||
usePagination: boolean = true;
|
||||
loading: boolean = false;
|
||||
usePagination = true;
|
||||
loading = false;
|
||||
|
||||
ascending: boolean = false;
|
||||
reverted: boolean = false;
|
||||
ascending = false;
|
||||
reverted = false;
|
||||
referenceType: ReferenceType = 'All';
|
||||
groupBy: 'none' | 'party' | 'account' | 'referenceName' = 'none';
|
||||
_rawData: LedgerEntry[] = [];
|
||||
@ -37,7 +37,7 @@ export class GeneralLedger extends LedgerReport {
|
||||
super(fyo);
|
||||
}
|
||||
|
||||
async setDefaultFilters() {
|
||||
setDefaultFilters() {
|
||||
if (!this.toDate) {
|
||||
this.toDate = DateTime.now().plus({ days: 1 }).toISODate();
|
||||
this.fromDate = DateTime.now().minus({ years: 1 }).toISODate();
|
||||
@ -239,7 +239,7 @@ export class GeneralLedger extends LedgerReport {
|
||||
return { totalDebit, totalCredit };
|
||||
}
|
||||
|
||||
async _getQueryFilters(): Promise<QueryFilter> {
|
||||
_getQueryFilters(): QueryFilter {
|
||||
const filters: QueryFilter = {};
|
||||
const stringFilters = ['account', 'party', 'referenceName'];
|
||||
|
||||
|
@ -17,9 +17,9 @@ export abstract class BaseGSTR extends Report {
|
||||
toDate?: string;
|
||||
fromDate?: string;
|
||||
transferType?: TransferType;
|
||||
usePagination: boolean = true;
|
||||
usePagination = true;
|
||||
gstrRows?: GSTRRow[];
|
||||
loading: boolean = false;
|
||||
loading = false;
|
||||
|
||||
abstract gstrType: GSTRType;
|
||||
|
||||
@ -111,7 +111,7 @@ export abstract class BaseGSTR extends Report {
|
||||
return (row) => row.rate === 0; // this takes care of both nil rated, exempted goods
|
||||
}
|
||||
|
||||
return (_) => true;
|
||||
return () => true;
|
||||
}
|
||||
|
||||
async getEntries() {
|
||||
@ -133,7 +133,7 @@ export abstract class BaseGSTR extends Report {
|
||||
const entries = await this.getEntries();
|
||||
const gstrRows: GSTRRow[] = [];
|
||||
for (const entry of entries) {
|
||||
const gstrRow = await this.getGstrRow(entry.name as string);
|
||||
const gstrRow = await this.getGstrRow(entry.name);
|
||||
gstrRows.push(gstrRow);
|
||||
}
|
||||
return gstrRows;
|
||||
@ -149,7 +149,7 @@ export abstract class BaseGSTR extends Report {
|
||||
'gstin'
|
||||
)) as string | null;
|
||||
|
||||
const party = (await this.fyo.doc.getDoc('Party', entry.party!)) as Party;
|
||||
const party = (await this.fyo.doc.getDoc('Party', entry.party)) as Party;
|
||||
|
||||
let place = '';
|
||||
if (party.address) {
|
||||
@ -216,7 +216,7 @@ export abstract class BaseGSTR extends Report {
|
||||
}
|
||||
}
|
||||
|
||||
async setDefaultFilters() {
|
||||
setDefaultFilters() {
|
||||
if (!this.toDate) {
|
||||
this.toDate = DateTime.local().toISODate();
|
||||
}
|
||||
|
@ -175,7 +175,7 @@ async function getCanExport(report: BaseGSTR) {
|
||||
return true;
|
||||
}
|
||||
|
||||
showDialog({
|
||||
await showDialog({
|
||||
title: report.fyo.t`Cannot Export`,
|
||||
detail: report.fyo.t`Please set GSTIN in General Settings.`,
|
||||
type: 'error',
|
||||
@ -204,7 +204,7 @@ export async function getGstrJsonData(report: BaseGSTR): Promise<string> {
|
||||
} else if (transferType === TransferTypeEnum.B2CL) {
|
||||
gstData.b2cl = await generateB2clData(report);
|
||||
} else if (transferType === TransferTypeEnum.B2CS) {
|
||||
gstData.b2cs = await generateB2csData(report);
|
||||
gstData.b2cs = generateB2csData(report);
|
||||
}
|
||||
|
||||
return JSON.stringify(gstData);
|
||||
|
@ -14,7 +14,7 @@ export abstract class LedgerReport extends Report {
|
||||
static reportName = 'general-ledger';
|
||||
|
||||
_rawData: LedgerEntry[] = [];
|
||||
shouldRefresh: boolean = false;
|
||||
shouldRefresh = false;
|
||||
|
||||
constructor(fyo: Fyo) {
|
||||
super(fyo);
|
||||
@ -117,7 +117,7 @@ export abstract class LedgerReport extends Report {
|
||||
});
|
||||
}
|
||||
|
||||
abstract _getQueryFilters(): Promise<QueryFilter>;
|
||||
abstract _getQueryFilters(): QueryFilter | Promise<QueryFilter>;
|
||||
|
||||
getActions(): Action[] {
|
||||
return getCommonExportActions(this);
|
||||
|
@ -17,7 +17,7 @@ import {
|
||||
export class ProfitAndLoss extends AccountReport {
|
||||
static title = t`Profit And Loss`;
|
||||
static reportName = 'profit-and-loss';
|
||||
loading: boolean = false;
|
||||
loading = false;
|
||||
|
||||
get rootTypes(): AccountRootType[] {
|
||||
return [AccountRootTypeEnum.Income, AccountRootTypeEnum.Expense];
|
||||
@ -62,7 +62,7 @@ export class ProfitAndLoss extends AccountReport {
|
||||
const expenseList = convertAccountRootNodeToAccountList(expenseRoot);
|
||||
const expenseRows = this.getReportRowsFromAccountList(expenseList);
|
||||
|
||||
this.reportData = await this.getReportDataFromRows(
|
||||
this.reportData = this.getReportDataFromRows(
|
||||
incomeRows,
|
||||
expenseRows,
|
||||
incomeRoot,
|
||||
@ -71,14 +71,14 @@ export class ProfitAndLoss extends AccountReport {
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
async getReportDataFromRows(
|
||||
getReportDataFromRows(
|
||||
incomeRows: ReportData,
|
||||
expenseRows: ReportData,
|
||||
incomeRoot: AccountTreeNode | undefined,
|
||||
expenseRoot: AccountTreeNode | undefined
|
||||
): Promise<ReportData> {
|
||||
): ReportData {
|
||||
if (incomeRoot && !expenseRoot) {
|
||||
return await this.getIncomeOrExpenseRows(
|
||||
return this.getIncomeOrExpenseRows(
|
||||
incomeRoot,
|
||||
incomeRows,
|
||||
t`Total Income (Credit)`
|
||||
@ -86,7 +86,7 @@ export class ProfitAndLoss extends AccountReport {
|
||||
}
|
||||
|
||||
if (expenseRoot && !incomeRoot) {
|
||||
return await this.getIncomeOrExpenseRows(
|
||||
return this.getIncomeOrExpenseRows(
|
||||
expenseRoot,
|
||||
expenseRows,
|
||||
t`Total Income (Credit)`
|
||||
@ -97,7 +97,7 @@ export class ProfitAndLoss extends AccountReport {
|
||||
return [];
|
||||
}
|
||||
|
||||
return await this.getIncomeAndExpenseRows(
|
||||
return this.getIncomeAndExpenseRows(
|
||||
incomeRows,
|
||||
expenseRows,
|
||||
incomeRoot,
|
||||
@ -105,30 +105,27 @@ export class ProfitAndLoss extends AccountReport {
|
||||
);
|
||||
}
|
||||
|
||||
async getIncomeOrExpenseRows(
|
||||
getIncomeOrExpenseRows(
|
||||
root: AccountTreeNode,
|
||||
rows: ReportData,
|
||||
totalRowName: string
|
||||
): Promise<ReportData> {
|
||||
const total = await this.getTotalNode(root, totalRowName);
|
||||
): ReportData {
|
||||
const total = this.getTotalNode(root, totalRowName);
|
||||
const totalRow = this.getRowFromAccountListNode(total);
|
||||
|
||||
return [rows, totalRow].flat();
|
||||
}
|
||||
|
||||
async getIncomeAndExpenseRows(
|
||||
getIncomeAndExpenseRows(
|
||||
incomeRows: ReportData,
|
||||
expenseRows: ReportData,
|
||||
incomeRoot: AccountTreeNode,
|
||||
expenseRoot: AccountTreeNode
|
||||
) {
|
||||
const totalIncome = await this.getTotalNode(
|
||||
incomeRoot,
|
||||
t`Total Income (Credit)`
|
||||
);
|
||||
const totalIncome = this.getTotalNode(incomeRoot, t`Total Income (Credit)`);
|
||||
const totalIncomeRow = this.getRowFromAccountListNode(totalIncome);
|
||||
|
||||
const totalExpense = await this.getTotalNode(
|
||||
const totalExpense = this.getTotalNode(
|
||||
expenseRoot,
|
||||
t`Total Expense (Debit)`
|
||||
);
|
||||
|
@ -10,14 +10,14 @@ import { ColumnField, ReportData } from './types';
|
||||
export abstract class Report extends Observable<RawValue> {
|
||||
static title: string;
|
||||
static reportName: string;
|
||||
static isInventory: boolean = false;
|
||||
static isInventory = false;
|
||||
|
||||
fyo: Fyo;
|
||||
columns: ColumnField[] = [];
|
||||
filters: Field[] = [];
|
||||
reportData: ReportData;
|
||||
usePagination: boolean = false;
|
||||
shouldRefresh: boolean = false;
|
||||
usePagination = false;
|
||||
shouldRefresh = false;
|
||||
abstract loading: boolean;
|
||||
|
||||
constructor(fyo: Fyo) {
|
||||
@ -26,14 +26,12 @@ export abstract class Report extends Observable<RawValue> {
|
||||
this.reportData = [];
|
||||
}
|
||||
|
||||
get title() {
|
||||
// @ts-ignore
|
||||
return this.constructor.title;
|
||||
get title(): string {
|
||||
return (this.constructor as typeof Report).title;
|
||||
}
|
||||
|
||||
get reportName() {
|
||||
// @ts-ignore
|
||||
return this.constructor.reportName;
|
||||
get reportName(): string {
|
||||
return (this.constructor as typeof Report).reportName;
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
@ -61,7 +59,7 @@ export abstract class Report extends Observable<RawValue> {
|
||||
return filterMap;
|
||||
}
|
||||
|
||||
async set(key: string, value: DocValue, callPostSet: boolean = true) {
|
||||
async set(key: string, value: DocValue, callPostSet = true) {
|
||||
const field = this.filters.find((f) => f.fieldname === key);
|
||||
if (field === undefined) {
|
||||
return;
|
||||
@ -95,7 +93,7 @@ export abstract class Report extends Observable<RawValue> {
|
||||
* Should first check if filter value is set
|
||||
* and update only if it is not set.
|
||||
*/
|
||||
async setDefaultFilters() {}
|
||||
abstract setDefaultFilters(): void | Promise<void>;
|
||||
abstract getActions(): Action[];
|
||||
abstract getFilters(): Field[] | Promise<Field[]>;
|
||||
abstract getColumns(): ColumnField[] | Promise<ColumnField[]>;
|
||||
|
@ -35,8 +35,8 @@ export class TrialBalance extends AccountReport {
|
||||
|
||||
fromDate?: string;
|
||||
toDate?: string;
|
||||
hideGroupAmounts: boolean = false;
|
||||
loading: boolean = false;
|
||||
hideGroupAmounts = false;
|
||||
loading = false;
|
||||
|
||||
_rawData: LedgerEntry[] = [];
|
||||
_dateRanges?: DateRange[];
|
||||
@ -79,6 +79,7 @@ export class TrialBalance extends AccountReport {
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
async getReportDataFromRows(
|
||||
rootTypeRows: RootTypeRow[]
|
||||
): Promise<ReportData> {
|
||||
@ -93,6 +94,7 @@ export class TrialBalance extends AccountReport {
|
||||
return reportData;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
async _getGroupedByDateRanges(
|
||||
map: GroupedMap
|
||||
): Promise<AccountNameValueMapMap> {
|
||||
@ -108,11 +110,11 @@ export class TrialBalance extends AccountReport {
|
||||
const key = this._getRangeMapKey(entry);
|
||||
if (key === null) {
|
||||
throw new ValueError(
|
||||
`invalid entry in trial balance ${entry.date?.toISOString()}`
|
||||
`invalid entry in trial balance ${entry.date?.toISOString() ?? ''}`
|
||||
);
|
||||
}
|
||||
|
||||
const map = valueMap.get(key!);
|
||||
const map = valueMap.get(key);
|
||||
const totalCredit = map?.credit ?? 0;
|
||||
const totalDebit = map?.debit ?? 0;
|
||||
|
||||
@ -188,6 +190,7 @@ export class TrialBalance extends AccountReport {
|
||||
} as ReportRow;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
async _getQueryFilters(): Promise<QueryFilter> {
|
||||
const filters: QueryFilter = {};
|
||||
filters.reverted = false;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Fyo, t } from 'fyo';
|
||||
import { t } from 'fyo';
|
||||
import { Action } from 'fyo/model/types';
|
||||
import { Verb } from 'fyo/telemetry/types';
|
||||
import { getSavePath, saveData, showExportInFolder } from 'src/utils/ipcCalls';
|
||||
@ -88,7 +88,7 @@ function getJsonData(report: Report): string {
|
||||
}
|
||||
|
||||
const rowObj: Record<string, unknown> = {};
|
||||
for (const c in row.cells) {
|
||||
for (let c = 0; c < row.cells.length; c++) {
|
||||
const { label } = columns[c];
|
||||
const cell = getValueFromCell(row.cells[c], displayPrecision);
|
||||
rowObj[label] = cell;
|
||||
@ -129,7 +129,7 @@ function convertReportToCSVMatrix(report: Report): unknown[][] {
|
||||
const displayPrecision =
|
||||
(report.fyo.singles.SystemSettings?.displayPrecision as number) ?? 2;
|
||||
const reportData = report.reportData;
|
||||
const columns = report.columns!;
|
||||
const columns = report.columns;
|
||||
|
||||
const csvdata: unknown[][] = [];
|
||||
csvdata.push(columns.map((c) => c.label));
|
||||
@ -140,7 +140,7 @@ function convertReportToCSVMatrix(report: Report): unknown[][] {
|
||||
}
|
||||
|
||||
const csvrow: unknown[] = [];
|
||||
for (const c in row.cells) {
|
||||
for (let c = 0; c < row.cells.length; c++) {
|
||||
const cell = getValueFromCell(row.cells[c], displayPrecision);
|
||||
csvrow.push(cell);
|
||||
}
|
||||
|
@ -13,9 +13,9 @@ export class StockBalance extends StockLedger {
|
||||
static reportName = 'stock-balance';
|
||||
static isInventory = true;
|
||||
|
||||
override ascending: boolean = true;
|
||||
override ascending = true;
|
||||
override referenceType: ReferenceType = 'All';
|
||||
override referenceName: string = '';
|
||||
override referenceName = '';
|
||||
|
||||
override async _getReportData(force?: boolean): Promise<ReportData> {
|
||||
if (this.shouldRefresh || force || !this._rawData?.length) {
|
||||
|
@ -19,11 +19,11 @@ export class StockLedger extends Report {
|
||||
static reportName = 'stock-ledger';
|
||||
static isInventory = true;
|
||||
|
||||
usePagination: boolean = true;
|
||||
usePagination = true;
|
||||
|
||||
_rawData?: ComputedStockLedgerEntry[];
|
||||
loading: boolean = false;
|
||||
shouldRefresh: boolean = false;
|
||||
loading = false;
|
||||
shouldRefresh = false;
|
||||
|
||||
item?: string;
|
||||
location?: string;
|
||||
@ -52,7 +52,7 @@ export class StockLedger extends Report {
|
||||
this._setObservers();
|
||||
}
|
||||
|
||||
async setDefaultFilters() {
|
||||
setDefaultFilters() {
|
||||
if (!this.toDate) {
|
||||
this.toDate = DateTime.now().plus({ days: 1 }).toISODate();
|
||||
this.fromDate = DateTime.now().minus({ years: 1 }).toISODate();
|
||||
@ -91,9 +91,8 @@ export class StockLedger extends Report {
|
||||
|
||||
async _setRawData() {
|
||||
const valuationMethod =
|
||||
(this.fyo.singles.InventorySettings?.valuationMethod as
|
||||
| ValuationMethod
|
||||
| undefined) ?? ValuationMethod.FIFO;
|
||||
this.fyo.singles.InventorySettings?.valuationMethod ??
|
||||
ValuationMethod.FIFO;
|
||||
|
||||
const rawSLEs = await getRawStockLedgerEntries(this.fyo);
|
||||
this._rawData = getStockLedgerEntries(rawSLEs, valuationMethod);
|
||||
@ -113,7 +112,7 @@ export class StockLedger extends Report {
|
||||
}
|
||||
|
||||
let i = 0;
|
||||
for (const idx in rawData) {
|
||||
for (let idx = 0; idx < rawData.length; idx++) {
|
||||
const row = rawData[idx];
|
||||
if (this.item && row.item !== this.item) {
|
||||
continue;
|
||||
|
@ -12,7 +12,7 @@ const NAME_FIELD = {
|
||||
readOnly: true,
|
||||
};
|
||||
|
||||
export function getSchemas(countryCode: string = '-'): Readonly<SchemaMap> {
|
||||
export function getSchemas(countryCode = '-'): Readonly<SchemaMap> {
|
||||
const builtCoreSchemas = getCoreSchemas();
|
||||
const builtAppSchemas = getAppSchemas(countryCode);
|
||||
|
||||
@ -209,14 +209,14 @@ export function getAbstractCombinedSchemas(schemas: SchemaStubMap): SchemaMap {
|
||||
|
||||
for (const name of extendingSchemaNames) {
|
||||
const extendingSchema = schemas[name] as Schema;
|
||||
const abstractSchema = schemas[extendingSchema.extends!] as SchemaStub;
|
||||
const abstractSchema = schemas[extendingSchema.extends!];
|
||||
|
||||
schemaMap[name] = getCombined(extendingSchema, abstractSchema) as Schema;
|
||||
}
|
||||
|
||||
for (const name in abstractSchemaNames) {
|
||||
abstractSchemaNames.forEach((name) => {
|
||||
delete schemaMap[name];
|
||||
}
|
||||
});
|
||||
|
||||
return schemaMap;
|
||||
}
|
||||
|
@ -8,6 +8,8 @@ import {
|
||||
schemaTranslateables,
|
||||
} from '../utils/translationHelpers';
|
||||
|
||||
/* eslint-disable no-console, @typescript-eslint/no-floating-promises */
|
||||
|
||||
const translationsFolder = path.resolve(__dirname, '..', 'translations');
|
||||
const PATTERN = /(?<!\w)t`([^`]+)`/g;
|
||||
|
||||
@ -21,7 +23,7 @@ function shouldIgnore(p: string, ignoreList: string[]): boolean {
|
||||
async function getFileList(
|
||||
root: string,
|
||||
ignoreList: string[],
|
||||
extPattern: RegExp = /\.(js|ts|vue)$/
|
||||
extPattern = /\.(js|ts|vue)$/
|
||||
): Promise<string[]> {
|
||||
const contents: string[] = await fs.readdir(root);
|
||||
const files: string[] = [];
|
||||
@ -86,7 +88,7 @@ function getTStrings(content: string): Promise<string[]> {
|
||||
}
|
||||
|
||||
function tStringFinder(content: string): string[] {
|
||||
return [...content.matchAll(PATTERN)].map(([_, t]) => {
|
||||
return [...content.matchAll(PATTERN)].map(([, t]) => {
|
||||
t = getIndexFormat(t);
|
||||
return getWhitespaceSanitized(t);
|
||||
});
|
||||
|
@ -19,4 +19,5 @@ async function run() {
|
||||
await unlink(dbPath);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
run();
|
||||
|
@ -3,14 +3,10 @@
|
||||
set -e
|
||||
|
||||
# Check node and yarn versions
|
||||
NODE_VERSION=$(node --version)
|
||||
YARN_VERSION=$(yarn --version)
|
||||
if [ "$YARN_VERSION" != "1.22.18" ]; then
|
||||
echo "Incorrect yarn version: $YARN_VERSION"
|
||||
exit 1
|
||||
elif [ "$NODE_VERSION" != "v16.13.1" ]; then
|
||||
echo "Incorrect node version: $NODE_VERSION"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Source secrets
|
||||
@ -42,6 +38,6 @@ export GH_TOKEN=$GH_TOKEN &&
|
||||
export APPLE_ID=$APPLE_ID &&
|
||||
export APPLE_TEAM_ID=$APPLE_TEAM_ID &&
|
||||
export APPLE_APP_SPECIFIC_PASSWORD=$APPLE_APP_SPECIFIC_PASSWORD &&
|
||||
yarn electron:build --mac --publish=always
|
||||
yarn build --mac --publish=always
|
||||
|
||||
cd ../books
|
||||
|
49
src/App.vue
49
src/App.vue
@ -12,13 +12,13 @@
|
||||
/>
|
||||
<!-- Main Contents -->
|
||||
<Desk
|
||||
class="flex-1"
|
||||
v-if="activeScreen === 'Desk'"
|
||||
class="flex-1"
|
||||
@change-db-file="showDbSelector"
|
||||
/>
|
||||
<DatabaseSelector
|
||||
ref="databaseSelector"
|
||||
v-if="activeScreen === 'DatabaseSelector'"
|
||||
ref="databaseSelector"
|
||||
@file-selected="fileSelected"
|
||||
/>
|
||||
<SetupWizard
|
||||
@ -36,7 +36,6 @@
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { ConfigKeys } from 'fyo/core/types';
|
||||
import { RTL_LANGUAGES } from 'fyo/utils/consts';
|
||||
import { ModelNameEnum } from 'models/types';
|
||||
import { systemLanguageRef } from 'src/utils/refs';
|
||||
@ -69,6 +68,12 @@ enum Screen {
|
||||
|
||||
export default defineComponent({
|
||||
name: 'App',
|
||||
components: {
|
||||
Desk,
|
||||
SetupWizard,
|
||||
DatabaseSelector,
|
||||
WindowsTitleBar,
|
||||
},
|
||||
setup() {
|
||||
const keys = useKeys();
|
||||
const searcher: Ref<null | Search> = ref(null);
|
||||
@ -105,31 +110,22 @@ export default defineComponent({
|
||||
companyName: string;
|
||||
};
|
||||
},
|
||||
components: {
|
||||
Desk,
|
||||
SetupWizard,
|
||||
DatabaseSelector,
|
||||
WindowsTitleBar,
|
||||
},
|
||||
async mounted() {
|
||||
this.setInitialScreen();
|
||||
},
|
||||
watch: {
|
||||
language(value) {
|
||||
this.languageDirection = getLanguageDirection(value);
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
language(): string {
|
||||
return systemLanguageRef.value;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
language(value: string) {
|
||||
this.languageDirection = getLanguageDirection(value);
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
await this.setInitialScreen();
|
||||
},
|
||||
methods: {
|
||||
async setInitialScreen(): Promise<void> {
|
||||
const lastSelectedFilePath = fyo.config.get(
|
||||
ConfigKeys.LastSelectedFilePath,
|
||||
null
|
||||
);
|
||||
const lastSelectedFilePath = fyo.config.get('lastSelectedFilePath', null);
|
||||
|
||||
if (
|
||||
typeof lastSelectedFilePath !== 'string' ||
|
||||
@ -159,7 +155,7 @@ export default defineComponent({
|
||||
updateConfigFiles(fyo);
|
||||
},
|
||||
async fileSelected(filePath: string, isNew?: boolean): Promise<void> {
|
||||
fyo.config.set(ConfigKeys.LastSelectedFilePath, filePath);
|
||||
fyo.config.set('lastSelectedFilePath', filePath);
|
||||
if (isNew) {
|
||||
this.activeScreen = Screen.SetupWizard;
|
||||
return;
|
||||
@ -173,7 +169,7 @@ export default defineComponent({
|
||||
}
|
||||
},
|
||||
async setupComplete(setupWizardOptions: SetupWizardOptions): Promise<void> {
|
||||
const filePath = fyo.config.get(ConfigKeys.LastSelectedFilePath);
|
||||
const filePath = fyo.config.get('lastSelectedFilePath');
|
||||
if (typeof filePath !== 'string') {
|
||||
return;
|
||||
}
|
||||
@ -213,7 +209,8 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
if (actionSymbol === dbErrorActionSymbols.SelectFile) {
|
||||
return await this.databaseSelector?.existingDatabase();
|
||||
await this.databaseSelector?.existingDatabase();
|
||||
return;
|
||||
}
|
||||
|
||||
throw error;
|
||||
@ -223,9 +220,9 @@ export default defineComponent({
|
||||
const { hideGetStarted } = await fyo.doc.getDoc('SystemSettings');
|
||||
|
||||
if (hideGetStarted || onboardingComplete) {
|
||||
routeTo('/');
|
||||
await routeTo('/');
|
||||
} else {
|
||||
routeTo('/get-started');
|
||||
await routeTo('/get-started');
|
||||
}
|
||||
},
|
||||
async showDbSelector(): Promise<void> {
|
||||
|
@ -1,5 +1,8 @@
|
||||
<template>
|
||||
<div class="inline-block rounded-md px-2 py-1 truncate select-none" :class="colorClass">
|
||||
<div
|
||||
class="inline-block rounded-md px-2 py-1 truncate select-none"
|
||||
:class="colorClass"
|
||||
>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -37,9 +37,9 @@
|
||||
<!-- x Labels -->
|
||||
<template v-if="xLabels.length > 0">
|
||||
<text
|
||||
:style="fontStyle"
|
||||
v-for="(i, j) in count"
|
||||
:key="j + '-xlabels'"
|
||||
:style="fontStyle"
|
||||
:y="
|
||||
viewBoxHeight -
|
||||
axisPadding +
|
||||
@ -57,9 +57,9 @@
|
||||
<!-- y Labels -->
|
||||
<template v-if="yLabelDivisions > 0">
|
||||
<text
|
||||
:style="fontStyle"
|
||||
v-for="(i, j) in yLabelDivisions + 1"
|
||||
:key="j + '-ylabels'"
|
||||
:style="fontStyle"
|
||||
:y="yScalerLocation(i - 1)"
|
||||
:x="axisPadding - xLabelOffset + left"
|
||||
text-anchor="end"
|
||||
@ -92,10 +92,10 @@
|
||||
:width="width"
|
||||
:height="rec.height"
|
||||
:fill="rec.color"
|
||||
clip-path="url(#positive-rect-clip)"
|
||||
@mouseenter="() => create(rec.xi, rec.yi)"
|
||||
@mousemove="update"
|
||||
@mouseleave="destroy"
|
||||
clip-path="url(#positive-rect-clip)"
|
||||
/>
|
||||
|
||||
<rect
|
||||
@ -108,10 +108,10 @@
|
||||
:width="width"
|
||||
:height="rec.height"
|
||||
:fill="rec.color"
|
||||
clip-path="url(#negative-rect-clip)"
|
||||
@mouseenter="() => create(rec.xi, rec.yi)"
|
||||
@mousemove="update"
|
||||
@mouseleave="destroy"
|
||||
clip-path="url(#negative-rect-clip)"
|
||||
/>
|
||||
</svg>
|
||||
<Tooltip
|
||||
@ -137,6 +137,7 @@ import { prefixFormat } from 'src/utils/chart';
|
||||
import Tooltip from '../Tooltip.vue';
|
||||
|
||||
export default {
|
||||
components: { Tooltip },
|
||||
props: {
|
||||
skipXLabel: { type: Number, default: 2 },
|
||||
colors: { type: Array, default: () => [] },
|
||||
@ -171,6 +172,9 @@ export default {
|
||||
tooltipDispDistThreshold: { type: Number, default: 20 },
|
||||
drawZeroLine: { type: Boolean, default: true },
|
||||
},
|
||||
data() {
|
||||
return { xi: -1, yi: -1, activeColor: 'rgba(0, 0, 0, 0.2)' };
|
||||
},
|
||||
computed: {
|
||||
fontStyle() {
|
||||
return { fontSize: this.fontSize, fill: this.fontColor };
|
||||
@ -263,9 +267,6 @@ export default {
|
||||
return hMax;
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return { xi: -1, yi: -1, activeColor: 'rgba(0, 0, 0, 0.2)' };
|
||||
},
|
||||
methods: {
|
||||
gradY(i) {
|
||||
return Math.min(...this.ys[i]).toFixed();
|
||||
@ -351,7 +352,6 @@ export default {
|
||||
this.$refs.tooltip.destroy();
|
||||
},
|
||||
},
|
||||
components: { Tooltip },
|
||||
};
|
||||
</script>
|
||||
|
||||
|
@ -22,12 +22,6 @@
|
||||
:cx="cx"
|
||||
:cy="cy"
|
||||
:r="radius"
|
||||
@mouseover="
|
||||
$emit(
|
||||
'change',
|
||||
thetasAndStarts.length === 1 ? thetasAndStarts[0][0] : null
|
||||
)
|
||||
"
|
||||
:stroke-width="
|
||||
thickness +
|
||||
(hasNonZeroValues && active === thetasAndStarts[0][0] ? 4 : 0)
|
||||
@ -38,12 +32,18 @@
|
||||
:class="hasNonZeroValues ? 'sector' : ''"
|
||||
:style="{ transformOrigin: `${cx}px ${cy}px` }"
|
||||
fill="transparent"
|
||||
@mouseover="
|
||||
$emit(
|
||||
'change',
|
||||
thetasAndStarts.length === 1 ? thetasAndStarts[0][0] : null
|
||||
)
|
||||
"
|
||||
/>
|
||||
<template v-if="thetasAndStarts.length > 1">
|
||||
<path
|
||||
clip-path="url(#donut-hole)"
|
||||
v-for="[i, theta, start_] in thetasAndStarts"
|
||||
:key="i"
|
||||
clip-path="url(#donut-hole)"
|
||||
:d="getArcPath(cx, cy, radius, start_, theta)"
|
||||
:stroke="sectors[i].color"
|
||||
:stroke-width="thickness + (active === i ? 4 : 0)"
|
||||
|
@ -28,9 +28,9 @@
|
||||
<!-- x Labels -->
|
||||
<template v-if="drawLabels && xLabels.length > 0">
|
||||
<text
|
||||
:style="fontStyle"
|
||||
v-for="(i, j) in count"
|
||||
:key="j + '-xlabels'"
|
||||
:style="fontStyle"
|
||||
:y="
|
||||
viewBoxHeight -
|
||||
axisPadding +
|
||||
@ -48,9 +48,9 @@
|
||||
<!-- y Labels -->
|
||||
<template v-if="drawLabels && yLabelDivisions > 0">
|
||||
<text
|
||||
:style="fontStyle"
|
||||
v-for="(i, j) in yLabelDivisions + 1"
|
||||
:key="j + '-ylabels'"
|
||||
:style="fontStyle"
|
||||
:y="yScalerLocation(i - 1)"
|
||||
:x="axisPadding - xLabelOffset + left"
|
||||
text-anchor="end"
|
||||
@ -67,7 +67,7 @@
|
||||
<stop offset="70%" stop-color="rgba(255, 255, 255, 0)" />
|
||||
</linearGradient>
|
||||
|
||||
<mask v-for="(i, j) in num" :key="j + '-mask'" :id="'rect-mask-' + i">
|
||||
<mask v-for="(i, j) in num" :id="'rect-mask-' + i" :key="j + '-mask'">
|
||||
<rect
|
||||
x="0"
|
||||
:y="gradY(j)"
|
||||
@ -136,6 +136,7 @@ import { euclideanDistance, prefixFormat } from 'src/utils/chart';
|
||||
import Tooltip from '../Tooltip.vue';
|
||||
|
||||
export default {
|
||||
components: { Tooltip },
|
||||
props: {
|
||||
colors: { type: Array, default: () => [] },
|
||||
xLabels: { type: Array, default: () => [] },
|
||||
@ -168,6 +169,9 @@ export default {
|
||||
tooltipDispDistThreshold: { type: Number, default: 40 },
|
||||
showTooltip: { type: Boolean, default: true },
|
||||
},
|
||||
data() {
|
||||
return { cx: -1, cy: -1, xi: -1, yi: -1 };
|
||||
},
|
||||
computed: {
|
||||
fontStyle() {
|
||||
return { fontSize: this.fontSize, fill: this.fontColor };
|
||||
@ -249,9 +253,6 @@ export default {
|
||||
return hMax;
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return { cx: -1, cy: -1, xi: -1, yi: -1 };
|
||||
},
|
||||
methods: {
|
||||
gradY(i) {
|
||||
return Math.min(...this.ys[i]).toFixed();
|
||||
@ -339,6 +340,5 @@ export default {
|
||||
return { xi, yi, cx: px, cy: pys[yi], d: minDist };
|
||||
},
|
||||
},
|
||||
components: { Tooltip },
|
||||
};
|
||||
</script>
|
||||
|
@ -2,15 +2,15 @@
|
||||
<div
|
||||
class="relative bg-white border flex-center overflow-hidden group"
|
||||
:class="{
|
||||
'rounded': size === 'form',
|
||||
rounded: size === 'form',
|
||||
'w-20 h-20 rounded-full': size !== 'small' && size !== 'form',
|
||||
'w-12 h-12 rounded-full': size === 'small',
|
||||
}"
|
||||
:title="df?.label"
|
||||
:style="imageSizeStyle"
|
||||
>
|
||||
<img :src="value" v-if="value" />
|
||||
<div :class="[!isReadOnly ? 'group-hover:opacity-90' : '']" v-else>
|
||||
<img v-if="value" :src="value" />
|
||||
<div v-else :class="[!isReadOnly ? 'group-hover:opacity-90' : '']">
|
||||
<div
|
||||
v-if="letterPlaceholder"
|
||||
class="
|
||||
@ -65,12 +65,24 @@ import Base from './Base.vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'AttachImage',
|
||||
components: { FeatherIcon },
|
||||
extends: Base,
|
||||
props: {
|
||||
letterPlaceholder: { type: String, default: '' },
|
||||
value: { type: String, default: '' },
|
||||
df: { type: Object as PropType<Field> },
|
||||
},
|
||||
computed: {
|
||||
imageSizeStyle() {
|
||||
if (this.size === 'form') {
|
||||
return { width: '135px', height: '135px' };
|
||||
}
|
||||
return {};
|
||||
},
|
||||
shouldClear() {
|
||||
return !!this.value;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async handleClick() {
|
||||
if (this.value) {
|
||||
@ -106,17 +118,5 @@ export default defineComponent({
|
||||
this.triggerChange(dataURL);
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
imageSizeStyle() {
|
||||
if (this.size === 'form') {
|
||||
return { width: '135px', height: '135px' };
|
||||
}
|
||||
return {};
|
||||
},
|
||||
shouldClear() {
|
||||
return !!this.value;
|
||||
},
|
||||
},
|
||||
components: { FeatherIcon },
|
||||
});
|
||||
</script>
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user