2
0
mirror of https://github.com/frappe/books.git synced 2025-01-03 15:17:30 +00:00

incr: rem singleton from index.ts cause can't test

- update models to not use singleton export
This commit is contained in:
18alantom 2022-04-18 16:59:20 +05:30
parent ffdacc9637
commit 76bf6cfda5
62 changed files with 1009 additions and 638 deletions

View File

@ -1,4 +1,4 @@
import frappe from 'frappe'; import { NotFoundError } from 'frappe/utils/errors';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
export async function getExchangeRate({ export async function getExchangeRate({
@ -15,7 +15,7 @@ export async function getExchangeRate({
} }
if (!fromCurrency || !toCurrency) { if (!fromCurrency || !toCurrency) {
throw new frappe.errors.NotFoundError( throw new NotFoundError(
'Please provide `fromCurrency` and `toCurrency` to get exchange rate.' 'Please provide `fromCurrency` and `toCurrency` to get exchange rate.'
); );
} }

View File

@ -1,5 +1,6 @@
import frappe from 'frappe'; import { Frappe } from 'frappe';
import Doc from 'frappe/model/doc'; import Doc from 'frappe/model/doc';
import { ValidationError } from 'frappe/utils/errors';
import Money from 'pesa/dist/types/src/money'; import Money from 'pesa/dist/types/src/money';
import { import {
AccountEntry, AccountEntry,
@ -18,7 +19,12 @@ export class LedgerPosting {
reverted: boolean; reverted: boolean;
accountEntries: AccountEntry[]; accountEntries: AccountEntry[];
constructor({ reference, party, date, description }: LedgerPostingOptions) { frappe: Frappe;
constructor(
{ reference, party, date, description }: LedgerPostingOptions,
frappe: Frappe
) {
this.reference = reference; this.reference = reference;
this.party = party; this.party = party;
this.date = date; this.date = date;
@ -28,6 +34,8 @@ export class LedgerPosting {
this.reverted = false; this.reverted = false;
// To change balance while entering ledger entries // To change balance while entering ledger entries
this.accountEntries = []; this.accountEntries = [];
this.frappe = frappe;
} }
async debit( async debit(
@ -58,7 +66,7 @@ export class LedgerPosting {
amount: Money amount: Money
) { ) {
const debitAccounts = ['Asset', 'Expense']; const debitAccounts = ['Asset', 'Expense'];
const accountDoc = await frappe.doc.getDoc('Account', accountName); const accountDoc = await this.frappe.doc.getDoc('Account', accountName);
const rootType = accountDoc.rootType as string; const rootType = accountDoc.rootType as string;
if (debitAccounts.indexOf(rootType) === -1) { if (debitAccounts.indexOf(rootType) === -1) {
@ -86,8 +94,8 @@ export class LedgerPosting {
referenceName: referenceName ?? this.reference.name!, referenceName: referenceName ?? this.reference.name!,
description: this.description, description: this.description,
reverted: this.reverted, reverted: this.reverted,
debit: frappe.pesa(0), debit: this.frappe.pesa(0),
credit: frappe.pesa(0), credit: this.frappe.pesa(0),
}; };
this.entries.push(entry); this.entries.push(entry);
@ -105,7 +113,7 @@ export class LedgerPosting {
async postReverse() { async postReverse() {
this.validateEntries(); this.validateEntries();
const data = await frappe.db.getAll('AccountingLedgerEntry', { const data = await this.frappe.db.getAll('AccountingLedgerEntry', {
fields: ['name'], fields: ['name'],
filters: { filters: {
referenceName: this.reference.name!, referenceName: this.reference.name!,
@ -114,7 +122,7 @@ export class LedgerPosting {
}); });
for (const entry of data) { for (const entry of data) {
const entryDoc = await frappe.doc.getDoc( const entryDoc = await this.frappe.doc.getDoc(
'AccountingLedgerEntry', 'AccountingLedgerEntry',
entry.name as string entry.name as string
); );
@ -157,18 +165,21 @@ export class LedgerPosting {
validateEntries() { validateEntries() {
const { debit, credit } = this.getTotalDebitAndCredit(); const { debit, credit } = this.getTotalDebitAndCredit();
if (debit.neq(credit)) { if (debit.neq(credit)) {
throw new frappe.errors.ValidationError( throw new ValidationError(
`Total Debit: ${frappe.format( `Total Debit: ${this.frappe.format(
debit, debit,
'Currency' 'Currency'
)} must be equal to Total Credit: ${frappe.format(credit, 'Currency')}` )} must be equal to Total Credit: ${this.frappe.format(
credit,
'Currency'
)}`
); );
} }
} }
getTotalDebitAndCredit() { getTotalDebitAndCredit() {
let debit = frappe.pesa(0); let debit = this.frappe.pesa(0);
let credit = frappe.pesa(0); let credit = this.frappe.pesa(0);
for (const entry of this.entries) { for (const entry of this.entries) {
debit = debit.add(entry.debit); debit = debit.add(entry.debit);
@ -180,12 +191,12 @@ export class LedgerPosting {
async insertEntries() { async insertEntries() {
for (const entry of this.entries) { for (const entry of this.entries) {
const entryDoc = frappe.doc.getNewDoc('AccountingLedgerEntry'); const entryDoc = this.frappe.doc.getNewDoc('AccountingLedgerEntry');
Object.assign(entryDoc, entry); Object.assign(entryDoc, entry);
await entryDoc.insert(); await entryDoc.insert();
} }
for (const entry of this.accountEntries) { for (const entry of this.accountEntries) {
const entryDoc = await frappe.doc.getDoc('Account', entry.name); const entryDoc = await this.frappe.doc.getDoc('Account', entry.name);
const balance = entryDoc.get('balance') as Money; const balance = entryDoc.get('balance') as Money;
entryDoc.balance = balance.add(entry.balanceChange); entryDoc.balance = balance.add(entry.balanceChange);
await entryDoc.update(); await entryDoc.update();
@ -193,6 +204,6 @@ export class LedgerPosting {
} }
getRoundOffAccount() { getRoundOffAccount() {
return frappe.singles.AccountingSettings!.roundOffAccount as string; return this.frappe.singles.AccountingSettings!.roundOffAccount as string;
} }
} }

View File

@ -18,7 +18,12 @@ import {
import { getRandomString, getValueMapFromList } from '../../utils'; import { getRandomString, getValueMapFromList } from '../../utils';
import { DatabaseBase, GetAllOptions, QueryFilter } from '../../utils/db/types'; import { DatabaseBase, GetAllOptions, QueryFilter } from '../../utils/db/types';
import { getDefaultMetaFieldValueMap, sqliteTypeMap, SYSTEM } from '../helpers'; import { getDefaultMetaFieldValueMap, sqliteTypeMap, SYSTEM } from '../helpers';
import { ColumnDiff, FieldValueMap, GetQueryBuilderOptions } from './types'; import {
ColumnDiff,
FieldValueMap,
GetQueryBuilderOptions,
SingleValue,
} from './types';
/** /**
* # DatabaseCore * # DatabaseCore
@ -256,7 +261,7 @@ export default class DatabaseCore extends DatabaseBase {
async getSingleValues( async getSingleValues(
...fieldnames: ({ fieldname: string; parent?: string } | string)[] ...fieldnames: ({ fieldname: string; parent?: string } | string)[]
): Promise<{ fieldname: string; parent: string; value: RawValue }[]> { ): Promise<SingleValue<RawValue>> {
const fieldnameList = fieldnames.map((fieldname) => { const fieldnameList = fieldnames.map((fieldname) => {
if (typeof fieldname === 'string') { if (typeof fieldname === 'string') {
return { fieldname }; return { fieldname };

View File

@ -21,8 +21,7 @@ export class DatabaseManager extends DatabaseDemuxBase {
async createNewDatabase(dbPath: string, countryCode: string) { async createNewDatabase(dbPath: string, countryCode: string) {
await this.#unlinkIfExists(dbPath); await this.#unlinkIfExists(dbPath);
await this.connectToDatabase(dbPath, countryCode); return await this.connectToDatabase(dbPath, countryCode);
return countryCode;
} }
async connectToDatabase(dbPath: string, countryCode?: string) { async connectToDatabase(dbPath: string, countryCode?: string) {

View File

@ -46,3 +46,8 @@ export interface SqliteTableInfo {
} }
export type BespokeFunction = (db:DatabaseCore, ...args: unknown[]) => Promise<unknown> export type BespokeFunction = (db:DatabaseCore, ...args: unknown[]) => Promise<unknown>
export type SingleValue<T> = {
fieldname: string;
parent: string;
value: T;
}[];

View File

@ -5,7 +5,7 @@ removed into a separate repo, but as of now it's in gestation.
The reason for maintaining a framework is to allow for varied backends. The reason for maintaining a framework is to allow for varied backends.
Currently Books runs on the electron renderer process and all db stuff happens Currently Books runs on the electron renderer process and all db stuff happens
on the electron main process which has access to node libs. As the development on the electron main process which has access to nodelibs. As the development
of `Fyo` progresses it will allow for a browser frontend and a node server of `Fyo` progresses it will allow for a browser frontend and a node server
backend. backend.
@ -70,6 +70,9 @@ other things.
- Get models and `regionalModels` using `countryCode` from `models/index.ts/getRegionalModels`. - Get models and `regionalModels` using `countryCode` from `models/index.ts/getRegionalModels`.
- Call `fyo.initializeAndRegister` with the all models. - Call `fyo.initializeAndRegister` with the all models.
_Note: since **SystemSettings** are initialized on `fyo.initializeAndRegister`
db needs to be set first else an error will be thrown_
## Testing ## Testing
For testing the `fyo` class, `mocha` is used (`node` side). So for this the For testing the `fyo` class, `mocha` is used (`node` side). So for this the
@ -77,7 +80,10 @@ demux classes are directly replaced by `node` side managers such as
`DatabaseManager`. `DatabaseManager`.
For this to work the class signatures of the demux class and the manager have to For this to work the class signatures of the demux class and the manager have to
be the same. be the same which is maintained by abstract demux classes.
`DatabaseManager` is used as the `DatabaseDemux` for testing without API or IPC
calls. For `AuthDemux` the `DummyAuthDemux` class is used.
## Translations ## Translations

View File

@ -1,4 +1,7 @@
import { Frappe } from 'frappe'; import { Frappe } from 'frappe';
import { AuthDemux } from 'frappe/demux/auth';
import { AuthDemuxBase, TelemetryCreds } from 'utils/auth/types';
import { AuthDemuxConstructor } from './types';
interface AuthConfig { interface AuthConfig {
serverURL: string; serverURL: string;
@ -15,8 +18,9 @@ export class AuthHandler {
#config: AuthConfig; #config: AuthConfig;
#session: Session; #session: Session;
frappe: Frappe; frappe: Frappe;
#demux: AuthDemuxBase;
constructor(frappe: Frappe) { constructor(frappe: Frappe, Demux?: AuthDemuxConstructor) {
this.frappe = frappe; this.frappe = frappe;
this.#config = { this.#config = {
serverURL: '', serverURL: '',
@ -28,6 +32,12 @@ export class AuthHandler {
user: '', user: '',
token: '', token: '',
}; };
if (Demux !== undefined) {
this.#demux = new Demux(frappe.isElectron);
} else {
this.#demux = new AuthDemux(frappe.isElectron);
}
} }
get session(): Readonly<Session> { get session(): Readonly<Session> {
@ -90,4 +100,8 @@ export class AuthHandler {
#getServerURL() { #getServerURL() {
return this.#config.serverURL || ''; return this.#config.serverURL || '';
} }
async getTelemetryCreds(): Promise<TelemetryCreds> {
return await this.#demux.getTelemetryCreds();
}
} }

View File

@ -1,4 +1,4 @@
import frappe from 'frappe'; import { Frappe } from 'frappe';
import Doc from 'frappe/model/doc'; import Doc from 'frappe/model/doc';
import Money from 'pesa/dist/types/src/money'; import Money from 'pesa/dist/types/src/money';
import { FieldType, FieldTypeEnum, RawValue } from 'schemas/types'; import { FieldType, FieldTypeEnum, RawValue } from 'schemas/types';
@ -22,9 +22,11 @@ import { DocValue, DocValueMap, RawValueMap } from './types';
export class Converter { export class Converter {
db: DatabaseHandler; db: DatabaseHandler;
frappe: Frappe;
constructor(db: DatabaseHandler) { constructor(db: DatabaseHandler, frappe: Frappe) {
this.db = db; this.db = db;
this.frappe = frappe;
} }
toDocValueMap( toDocValueMap(
@ -49,7 +51,11 @@ export class Converter {
} }
} }
static toDocValue(value: RawValue, fieldtype: FieldType): DocValue { static toDocValue(
value: RawValue,
fieldtype: FieldType,
frappe: Frappe
): DocValue {
switch (fieldtype) { switch (fieldtype) {
case FieldTypeEnum.Currency: case FieldTypeEnum.Currency:
return frappe.pesa((value ?? 0) as string | number); return frappe.pesa((value ?? 0) as string | number);
@ -112,7 +118,8 @@ export class Converter {
} else { } else {
docValueMap[fieldname] = Converter.toDocValue( docValueMap[fieldname] = Converter.toDocValue(
rawValue, rawValue,
field.fieldtype field.fieldtype,
this.frappe
); );
} }
} }

View File

@ -1,3 +1,4 @@
import { SingleValue } from 'backend/database/types';
import { Frappe } from 'frappe'; import { Frappe } from 'frappe';
import { DatabaseDemux } from 'frappe/demux/db'; import { DatabaseDemux } from 'frappe/demux/db';
import { Field, RawValue, SchemaMap } from 'schemas/types'; import { Field, RawValue, SchemaMap } from 'schemas/types';
@ -9,7 +10,6 @@ import {
DocValue, DocValue,
DocValueMap, DocValueMap,
RawValueMap, RawValueMap,
SingleValue,
} from './types'; } from './types';
// Return types of Bespoke Queries // Return types of Bespoke Queries
@ -27,7 +27,7 @@ export class DatabaseHandler extends DatabaseBase {
constructor(frappe: Frappe, Demux?: DatabaseDemuxConstructor) { constructor(frappe: Frappe, Demux?: DatabaseDemuxConstructor) {
super(); super();
this.#frappe = frappe; this.#frappe = frappe;
this.converter = new Converter(this); this.converter = new Converter(this, this.#frappe);
if (Demux !== undefined) { if (Demux !== undefined) {
this.#demux = new Demux(frappe.isElectron); this.#demux = new Demux(frappe.isElectron);
@ -117,7 +117,7 @@ export class DatabaseHandler extends DatabaseBase {
const docSingleValue: SingleValue<DocValue> = []; const docSingleValue: SingleValue<DocValue> = [];
for (const sv of rawSingleValue) { for (const sv of rawSingleValue) {
const fieldtype = this.fieldValueMap[sv.parent][sv.fieldname].fieldtype; const fieldtype = this.fieldValueMap[sv.parent][sv.fieldname].fieldtype;
const value = Converter.toDocValue(sv.value, fieldtype); const value = Converter.toDocValue(sv.value, fieldtype, this.#frappe);
docSingleValue.push({ docSingleValue.push({
value, value,

View File

@ -164,7 +164,7 @@ export class DocHandler {
throw new Error(`Schema not found for ${schemaName}`); throw new Error(`Schema not found for ${schemaName}`);
} }
const doc = new Model(schema, data); const doc = new Model(schema, data, this.frappe);
doc.setDefaults(); doc.setDefaults();
return doc; return doc;
} }

View File

@ -1,21 +1,45 @@
import Doc from 'frappe/model/doc'; import Doc from 'frappe/model/doc';
import Money from 'pesa/dist/types/src/money'; import Money from 'pesa/dist/types/src/money';
import { RawValue } from 'schemas/types'; import { RawValue } from 'schemas/types';
import { AuthDemuxBase } from 'utils/auth/types';
import { DatabaseDemuxBase } from 'utils/db/types'; import { DatabaseDemuxBase } from 'utils/db/types';
export type DocValue = string | number | boolean | Date | Money | null; export type DocValue = string | number | boolean | Date | Money | null;
export type DocValueMap = Record<string, DocValue | Doc[] | DocValueMap[]>; export type DocValueMap = Record<string, DocValue | Doc[] | DocValueMap[]>;
export type RawValueMap = Record<string, RawValue | RawValueMap[]>; export type RawValueMap = Record<string, RawValue | RawValueMap[]>;
export type SingleValue<T> = {
fieldname: string;
parent: string;
value: T;
}[];
/** /**
* DatabaseDemuxConstructor: type for a constructor that returns a DatabaseDemuxBase * DatabaseDemuxConstructor: type for a constructor that returns a DatabaseDemuxBase
* it's typed this way because `typeof AbstractClass` is invalid as abstract classes * it's typed this way because `typeof AbstractClass` is invalid as abstract classes
* can't be initialized using `new`. * can't be initialized using `new`.
*
* AuthDemuxConstructor: same as the above but for AuthDemuxBase
*/ */
export type DatabaseDemuxConstructor = new (isElectron?: boolean)=> DatabaseDemuxBase
export type DatabaseDemuxConstructor = new (
isElectron?: boolean
) => DatabaseDemuxBase;
export type AuthDemuxConstructor = new (isElectron?: boolean) => AuthDemuxBase;
export enum ConfigKeys {
Files = 'files',
LastSelectedFilePath = 'lastSelectedFilePath',
Language = 'language',
DeviceId = 'deviceId',
Telemetry = 'telemetry',
OpenCount = 'openCount',
}
export interface ConfigFile {
id: string;
companyName: string;
filePath: string;
}
export interface FyoConfig {
DatabaseDemux?: DatabaseDemuxConstructor;
AuthDemux?: AuthDemuxConstructor;
isElectron?: boolean;
isTest?: boolean;
}

22
frappe/demux/auth.ts Normal file
View File

@ -0,0 +1,22 @@
import { ipcRenderer } from 'electron';
import { AuthDemuxBase, TelemetryCreds } from 'utils/auth/types';
import { IPC_ACTIONS } from 'utils/messages';
export class AuthDemux extends AuthDemuxBase {
#isElectron: boolean = false;
constructor(isElectron: boolean) {
super();
this.#isElectron = isElectron;
}
async getTelemetryCreds(): Promise<TelemetryCreds> {
if (this.#isElectron) {
const creds = await ipcRenderer.invoke(IPC_ACTIONS.GET_CREDS);
const url: string = creds?.telemetryUrl ?? '';
const token: string = creds?.tokenString ?? '';
return { url, token };
} else {
return { url: '', token: '' };
}
}
}

55
frappe/demux/config.ts Normal file
View File

@ -0,0 +1,55 @@
import config from 'utils/config';
export class Config {
#isElectron: boolean;
fallback: Map<string, unknown> = new Map();
constructor(isElectron: boolean) {
this.#isElectron = isElectron;
}
get store(): Record<string, unknown> {
if (this.#isElectron) {
return config.store;
} else {
const store: Record<string, unknown> = {};
for (const key of this.fallback.keys()) {
store[key] = this.fallback.get(key);
}
return store;
}
}
get(key: string, defaultValue?: unknown): unknown {
if (this.#isElectron) {
return config.get(key, defaultValue);
} else {
return this.fallback.get(key) ?? defaultValue;
}
}
set(key: string, value: unknown) {
if (this.#isElectron) {
config.set(key, value);
} else {
this.fallback.set(key, value);
}
}
delete(key: string) {
if (this.#isElectron) {
config.delete(key);
} else {
this.fallback.delete(key);
}
}
clear() {
if (this.#isElectron) {
config.clear();
} else {
this.fallback.clear();
}
}
}

View File

@ -1,10 +1,14 @@
import { getMoneyMaker, MoneyMaker } from 'pesa'; import { getMoneyMaker, MoneyMaker } from 'pesa';
import { Field } from 'schemas/types';
import { markRaw } from 'vue'; import { markRaw } from 'vue';
import { AuthHandler } from './core/authHandler'; import { AuthHandler } from './core/authHandler';
import { DatabaseHandler } from './core/dbHandler'; import { DatabaseHandler } from './core/dbHandler';
import { DocHandler } from './core/docHandler'; import { DocHandler } from './core/docHandler';
import { DatabaseDemuxConstructor } from './core/types'; import { DocValue, FyoConfig } from './core/types';
import { Config } from './demux/config';
import Doc from './model/doc';
import { ModelMap } from './model/types'; import { ModelMap } from './model/types';
import { TelemetryManager } from './telemetry/telemetry';
import { import {
DEFAULT_CURRENCY, DEFAULT_CURRENCY,
DEFAULT_DISPLAY_PRECISION, DEFAULT_DISPLAY_PRECISION,
@ -18,10 +22,9 @@ import { ErrorLog } from './utils/types';
export class Frappe { export class Frappe {
t = t; t = t;
T = T; T = T;
format = format;
errors = errors; errors = errors;
isElectron = false; isElectron: boolean;
pesa: MoneyMaker; pesa: MoneyMaker;
@ -38,21 +41,27 @@ export class Frappe {
currencyFormatter?: Intl.NumberFormat; currencyFormatter?: Intl.NumberFormat;
currencySymbols: Record<string, string | undefined> = {}; currencySymbols: Record<string, string | undefined> = {};
constructor(DatabaseDemux?: DatabaseDemuxConstructor) { isTest: boolean;
/** telemetry: TelemetryManager;
* `DatabaseManager` can be passed as the `DatabaseDemux` for config: Config;
* testing this class without API or IPC calls.
*/ constructor(conf: FyoConfig = {}) {
this.auth = new AuthHandler(this); this.isTest = conf.isTest ?? false;
this.db = new DatabaseHandler(this, DatabaseDemux); this.isElectron = conf.isElectron ?? true;
this.auth = new AuthHandler(this, conf.AuthDemux);
this.db = new DatabaseHandler(this, conf.DatabaseDemux);
this.doc = new DocHandler(this); this.doc = new DocHandler(this);
this.pesa = getMoneyMaker({ this.pesa = getMoneyMaker({
currency: 'XXX', currency: DEFAULT_CURRENCY,
precision: DEFAULT_INTERNAL_PRECISION, precision: DEFAULT_INTERNAL_PRECISION,
display: DEFAULT_DISPLAY_PRECISION, display: DEFAULT_DISPLAY_PRECISION,
wrapper: markRaw, wrapper: markRaw,
}); });
this.telemetry = new TelemetryManager(this);
this.config = new Config(this.isElectron);
} }
get initialized() { get initialized() {
@ -75,6 +84,19 @@ export class Frappe {
return this.db.schemaMap; return this.db.schemaMap;
} }
format(value: DocValue, field: string | Field, doc?: Doc) {
return format(value, field, doc ?? null, this);
}
async setIsElectron() {
try {
const { ipcRenderer } = await import('electron');
this.isElectron = Boolean(ipcRenderer);
} catch {
this.isElectron = false;
}
}
async initializeAndRegister( async initializeAndRegister(
models: ModelMap = {}, models: ModelMap = {},
regionalModels: ModelMap = {}, regionalModels: ModelMap = {},
@ -138,9 +160,9 @@ export class Frappe {
}); });
} }
close() { async close() {
this.db.close(); await this.db.close();
this.auth.logout(); await this.auth.logout();
} }
store = { store = {
@ -150,4 +172,3 @@ export class Frappe {
} }
export { T, t }; export { T, t };
export default new Frappe();

View File

@ -1,6 +1,6 @@
import telemetry from '@/telemetry/telemetry'; import { Frappe } from 'frappe';
import { Verb } from '@/telemetry/types';
import { DocValue, DocValueMap } from 'frappe/core/types'; import { DocValue, DocValueMap } from 'frappe/core/types';
import { Verb } from 'frappe/telemetry/types';
import { import {
Conflict, Conflict,
MandatoryError, MandatoryError,
@ -17,7 +17,6 @@ import {
TargetField, TargetField,
} from 'schemas/types'; } from 'schemas/types';
import { getIsNullOrUndef, getMapFromList } from 'utils'; import { getIsNullOrUndef, getMapFromList } from 'utils';
import frappe from '..';
import { getRandomString, isPesa } from '../utils/index'; import { getRandomString, isPesa } from '../utils/index';
import { import {
areDocValuesEqual, areDocValuesEqual,
@ -47,6 +46,7 @@ import { validateSelect } from './validationFunction';
export default class Doc extends Observable<DocValue | Doc[]> { export default class Doc extends Observable<DocValue | Doc[]> {
name?: string; name?: string;
schema: Readonly<Schema>; schema: Readonly<Schema>;
frappe: Frappe;
fieldMap: Record<string, Field>; fieldMap: Record<string, Field>;
/** /**
@ -66,11 +66,16 @@ export default class Doc extends Observable<DocValue | Doc[]> {
revertAction: false, revertAction: false,
}; };
constructor(schema: Schema, data: DocValueMap) { constructor(schema: Schema, data: DocValueMap, frappe: Frappe) {
super(); super();
this.frappe = frappe;
this.schema = schema; this.schema = schema;
this._setInitialValues(data); this._setInitialValues(data);
this.fieldMap = getMapFromList(schema.fields, 'fieldname'); this.fieldMap = getMapFromList(schema.fields, 'fieldname');
if (this.schema.isSingle) {
this.name = this.schemaName;
}
} }
get schemaName(): string { get schemaName(): string {
@ -183,7 +188,10 @@ export default class Doc extends Observable<DocValue | Doc[]> {
continue; continue;
} }
let defaultValue: DocValue | Doc[] = getPreDefaultValues(field.fieldtype); let defaultValue: DocValue | Doc[] = getPreDefaultValues(
field.fieldtype,
this.frappe
);
const defaultFunction = this.defaults[field.fieldname]; const defaultFunction = this.defaults[field.fieldname];
if (defaultFunction !== undefined) { if (defaultFunction !== undefined) {
@ -193,7 +201,7 @@ export default class Doc extends Observable<DocValue | Doc[]> {
} }
if (field.fieldtype === 'Currency' && !isPesa(defaultValue)) { if (field.fieldtype === 'Currency' && !isPesa(defaultValue)) {
defaultValue = frappe.pesa!(defaultValue as string | number); defaultValue = this.frappe.pesa!(defaultValue as string | number);
} }
this[field.fieldname] = defaultValue; this[field.fieldname] = defaultValue;
@ -235,8 +243,8 @@ export default class Doc extends Observable<DocValue | Doc[]> {
} }
const childSchemaName = this.fieldMap[fieldname] as TargetField; const childSchemaName = this.fieldMap[fieldname] as TargetField;
const schema = frappe.db.schemaMap[childSchemaName.target] as Schema; const schema = this.frappe.db.schemaMap[childSchemaName.target] as Schema;
const childDoc = new Doc(schema, data as DocValueMap); const childDoc = new Doc(schema, data as DocValueMap, this.frappe);
childDoc.setDefaults(); childDoc.setDefaults();
return childDoc; return childDoc;
} }
@ -263,7 +271,7 @@ export default class Doc extends Observable<DocValue | Doc[]> {
if (missingMandatoryMessage.length > 0) { if (missingMandatoryMessage.length > 0) {
const fields = missingMandatoryMessage.join('\n'); const fields = missingMandatoryMessage.join('\n');
const message = frappe.t`Value missing for ${fields}`; const message = this.frappe.t`Value missing for ${fields}`;
throw new MandatoryError(message); throw new MandatoryError(message);
} }
} }
@ -322,7 +330,7 @@ export default class Doc extends Observable<DocValue | Doc[]> {
} }
if (!this.createdBy) { if (!this.createdBy) {
this.createdBy = frappe.auth.session.user; this.createdBy = this.frappe.auth.session.user;
} }
if (!this.created) { if (!this.created) {
@ -333,7 +341,7 @@ export default class Doc extends Observable<DocValue | Doc[]> {
} }
updateModified() { updateModified() {
this.modifiedBy = frappe.auth.session.user; this.modifiedBy = this.frappe.auth.session.user;
this.modified = new Date(); this.modified = new Date();
} }
@ -342,7 +350,11 @@ export default class Doc extends Observable<DocValue | Doc[]> {
return; return;
} }
const data = await frappe.db.get(this.schemaName, this.name); const data = await this.frappe.db.get(this.schemaName, this.name);
if (this.schema.isSingle && !data?.name) {
data.name = this.name!;
}
if (data && data.name) { if (data && data.name) {
this.syncValues(data); this.syncValues(data);
if (this.schema.isSingle) { if (this.schema.isSingle) {
@ -375,7 +387,7 @@ export default class Doc extends Observable<DocValue | Doc[]> {
return; return;
} }
this._links[fieldname] = await frappe.doc.getDoc( this._links[fieldname] = await this.frappe.doc.getDoc(
field.target, field.target,
value as string value as string
); );
@ -422,7 +434,7 @@ export default class Doc extends Observable<DocValue | Doc[]> {
return; return;
} }
const currentDoc = await frappe.db.get(this.schemaName, this.name); const currentDoc = await this.frappe.db.get(this.schemaName, this.name);
// check for conflict // check for conflict
if ( if (
@ -430,13 +442,14 @@ export default class Doc extends Observable<DocValue | Doc[]> {
(this.modified as Date) !== (currentDoc.modified as Date) (this.modified as Date) !== (currentDoc.modified as Date)
) { ) {
throw new Conflict( throw new Conflict(
frappe.t`Document ${this.schemaName} ${this.name} has been modified after loading` this.frappe
.t`Document ${this.schemaName} ${this.name} has been modified after loading`
); );
} }
if (this.submitted && !this.schema.isSubmittable) { if (this.submitted && !this.schema.isSubmittable) {
throw new ValidationError( throw new ValidationError(
frappe.t`Document type ${this.schemaName} is not submittable` this.frappe.t`Document type ${this.schemaName} is not submittable`
); );
} }
@ -532,24 +545,27 @@ export default class Doc extends Observable<DocValue | Doc[]> {
} }
async insert() { async insert() {
await setName(this); await setName(this, this.frappe);
this.setBaseMetaValues(); this.setBaseMetaValues();
await this.commit(); await this.commit();
await this.validateInsert(); await this.validateInsert();
await this.trigger('beforeInsert', null); await this.trigger('beforeInsert', null);
const oldName = this.name!; const oldName = this.name!;
const data = await frappe.db.insert(this.schemaName, this.getValidDict()); const data = await this.frappe.db.insert(
this.schemaName,
this.getValidDict()
);
this.syncValues(data); this.syncValues(data);
if (oldName !== this.name) { if (oldName !== this.name) {
frappe.doc.removeFromCache(this.schemaName, oldName); this.frappe.doc.removeFromCache(this.schemaName, oldName);
} }
await this.trigger('afterInsert', null); await this.trigger('afterInsert', null);
await this.trigger('afterSave', null); await this.trigger('afterSave', null);
telemetry.log(Verb.Created, this.schemaName); this.frappe.telemetry.log(Verb.Created, this.schemaName);
return this; return this;
} }
@ -566,7 +582,7 @@ export default class Doc extends Observable<DocValue | Doc[]> {
this.updateModified(); this.updateModified();
const data = this.getValidDict(); const data = this.getValidDict();
await frappe.db.update(this.schemaName, data); await this.frappe.db.update(this.schemaName, data);
this.syncValues(data); this.syncValues(data);
await this.trigger('afterUpdate'); await this.trigger('afterUpdate');
@ -589,10 +605,10 @@ export default class Doc extends Observable<DocValue | Doc[]> {
async delete() { async delete() {
await this.trigger('beforeDelete'); await this.trigger('beforeDelete');
await frappe.db.delete(this.schemaName, this.name!); await this.frappe.db.delete(this.schemaName, this.name!);
await this.trigger('afterDelete'); await this.trigger('afterDelete');
telemetry.log(Verb.Deleted, this.schemaName); this.frappe.telemetry.log(Verb.Deleted, this.schemaName);
} }
async submitOrRevert(isSubmit: boolean) { async submitOrRevert(isSubmit: boolean) {
@ -617,7 +633,7 @@ export default class Doc extends Observable<DocValue | Doc[]> {
async rename(newName: string) { async rename(newName: string) {
await this.trigger('beforeRename'); await this.trigger('beforeRename');
await frappe.db.rename(this.schemaName, this.name!, newName); await this.frappe.db.rename(this.schemaName, this.name!, newName);
this.name = newName; this.name = newName;
await this.trigger('afterRename'); await this.trigger('afterRename');
} }
@ -637,7 +653,7 @@ export default class Doc extends Observable<DocValue | Doc[]> {
const value = d.get(childfield) ?? 0; const value = d.get(childfield) ?? 0;
if (!isPesa(value)) { if (!isPesa(value)) {
try { try {
return frappe.pesa(value as string | number); return this.frappe.pesa(value as string | number);
} catch (err) { } catch (err) {
( (
err as Error err as Error
@ -647,7 +663,7 @@ export default class Doc extends Observable<DocValue | Doc[]> {
} }
return value as Money; return value as Money;
}) })
.reduce((a, b) => a.add(b), frappe.pesa(0)); .reduce((a, b) => a.add(b), this.frappe.pesa(0));
if (convertToFloat) { if (convertToFloat) {
return sum.float; return sum.float;
@ -660,7 +676,7 @@ export default class Doc extends Observable<DocValue | Doc[]> {
return ''; return '';
} }
return frappe.doc.getCachedValue(schemaName, name, fieldname); return this.frappe.doc.getCachedValue(schemaName, name, fieldname);
} }
async duplicate(shouldInsert: boolean = true): Promise<Doc> { async duplicate(shouldInsert: boolean = true): Promise<Doc> {
@ -690,7 +706,7 @@ export default class Doc extends Observable<DocValue | Doc[]> {
updateMap.name = updateMap.name + ' CPY'; updateMap.name = updateMap.name + ' CPY';
} }
const doc = frappe.doc.getEmptyDoc(this.schemaName, false); const doc = this.frappe.doc.getEmptyDoc(this.schemaName, false);
await doc.setMultiple(updateMap); await doc.setMultiple(updateMap);
if (shouldInsert) { if (shouldInsert) {
@ -700,6 +716,16 @@ export default class Doc extends Observable<DocValue | Doc[]> {
return doc; return doc;
} }
async beforeInsert() {}
async afterInsert() {}
async beforeUpdate() {}
async afterUpdate() {}
async afterSave() {}
async beforeDelete() {}
async afterDelete() {}
async beforeRevert() {}
async afterRevert() {}
formulas: FormulaMap = {}; formulas: FormulaMap = {};
defaults: DefaultMap = {}; defaults: DefaultMap = {};
validations: ValidationMap = {}; validations: ValidationMap = {};
@ -711,8 +737,14 @@ export default class Doc extends Observable<DocValue | Doc[]> {
static lists: ListsMap = {}; static lists: ListsMap = {};
static filters: FiltersMap = {}; static filters: FiltersMap = {};
static emptyMessages: EmptyMessageMap = {}; static emptyMessages: EmptyMessageMap = {};
static listSettings: ListViewSettings = {};
static treeSettings?: TreeViewSettings;
static actions: Action[] = []; static getListViewSettings(frappe: Frappe): ListViewSettings {
return {};
}
static getTreeSettings(frappe: Frappe): TreeViewSettings | void {}
static getActions(frappe: Frappe): Action[] {
return [];
}
} }

View File

@ -1,4 +1,4 @@
import frappe from 'frappe'; import { Frappe } from 'frappe';
import { DocValue } from 'frappe/core/types'; import { DocValue } from 'frappe/core/types';
import { isPesa } from 'frappe/utils'; import { isPesa } from 'frappe/utils';
import { isEqual } from 'lodash'; import { isEqual } from 'lodash';
@ -26,7 +26,10 @@ export function areDocValuesEqual(
return isEqual(dvOne, dvTwo); return isEqual(dvOne, dvTwo);
} }
export function getPreDefaultValues(fieldtype: FieldType): DocValue | Doc[] { export function getPreDefaultValues(
fieldtype: FieldType,
frappe: Frappe
): DocValue | Doc[] {
switch (fieldtype) { switch (fieldtype) {
case FieldTypeEnum.Table: case FieldTypeEnum.Table:
return [] as Doc[]; return [] as Doc[];

View File

@ -1,6 +1,7 @@
import frappe from 'frappe'; import { Frappe } from 'frappe';
import NumberSeries from 'frappe/models/NumberSeries'; import NumberSeries from 'frappe/models/NumberSeries';
import { getRandomString } from 'frappe/utils'; import { getRandomString } from 'frappe/utils';
import { DEFAULT_SERIES_START } from 'frappe/utils/consts';
import { BaseError } from 'frappe/utils/errors'; import { BaseError } from 'frappe/utils/errors';
import { Field, Schema } from 'schemas/types'; import { Field, Schema } from 'schemas/types';
import Doc from './doc'; import Doc from './doc';
@ -12,7 +13,7 @@ export function getNumberSeries(schema: Schema): Field | undefined {
return numberSeries; return numberSeries;
} }
export function isNameAutoSet(schemaName: string): boolean { export function isNameAutoSet(schemaName: string, frappe: Frappe): boolean {
const schema = frappe.schemaMap[schemaName]!; const schema = frappe.schemaMap[schemaName]!;
if (schema.naming === 'autoincrement') { if (schema.naming === 'autoincrement') {
return true; return true;
@ -26,17 +27,17 @@ export function isNameAutoSet(schemaName: string): boolean {
return false; return false;
} }
export async function setName(doc: Doc) { export async function setName(doc: Doc, frappe: Frappe) {
// if is server, always name again if autoincrement or other // if is server, always name again if autoincrement or other
if (doc.schema.naming === 'autoincrement') { if (doc.schema.naming === 'autoincrement') {
doc.name = await getNextId(doc.schemaName); doc.name = await getNextId(doc.schemaName, frappe);
return; return;
} }
// Current, per doc number series // Current, per doc number series
const numberSeries = doc.numberSeries as string | undefined; const numberSeries = doc.numberSeries as string | undefined;
if (numberSeries !== undefined) { if (numberSeries !== undefined) {
doc.name = await getSeriesNext(numberSeries, doc.schemaName); doc.name = await getSeriesNext(numberSeries, doc.schemaName, frappe);
return; return;
} }
@ -57,9 +58,9 @@ export async function setName(doc: Doc) {
} }
} }
export async function getNextId(schemaName: string) { export async function getNextId(schemaName: string, frappe: Frappe) {
// get the last inserted row // get the last inserted row
const lastInserted = await getLastInserted(schemaName); const lastInserted = await getLastInserted(schemaName, frappe);
let name = 1; let name = 1;
if (lastInserted) { if (lastInserted) {
let lastNumber = parseInt(lastInserted.name as string); let lastNumber = parseInt(lastInserted.name as string);
@ -69,7 +70,7 @@ export async function getNextId(schemaName: string) {
return (name + '').padStart(9, '0'); return (name + '').padStart(9, '0');
} }
export async function getLastInserted(schemaName: string) { export async function getLastInserted(schemaName: string, frappe: Frappe) {
const lastInserted = await frappe.db.getAll(schemaName, { const lastInserted = await frappe.db.getAll(schemaName, {
fields: ['name'], fields: ['name'],
limit: 1, limit: 1,
@ -79,7 +80,11 @@ export async function getLastInserted(schemaName: string) {
return lastInserted && lastInserted.length ? lastInserted[0] : null; return lastInserted && lastInserted.length ? lastInserted[0] : null;
} }
export async function getSeriesNext(prefix: string, schemaName: string) { export async function getSeriesNext(
prefix: string,
schemaName: string,
frappe: Frappe
) {
let series: NumberSeries; let series: NumberSeries;
try { try {
@ -90,7 +95,7 @@ export async function getSeriesNext(prefix: string, schemaName: string) {
throw e; throw e;
} }
await createNumberSeries(prefix, schemaName); await createNumberSeries(prefix, schemaName, DEFAULT_SERIES_START, frappe);
series = (await frappe.doc.getDoc('NumberSeries', prefix)) as NumberSeries; series = (await frappe.doc.getDoc('NumberSeries', prefix)) as NumberSeries;
} }
@ -100,7 +105,8 @@ export async function getSeriesNext(prefix: string, schemaName: string) {
export async function createNumberSeries( export async function createNumberSeries(
prefix: string, prefix: string,
referenceType: string, referenceType: string,
start = 1001 start: number,
frappe: Frappe
) { ) {
const exists = await frappe.db.exists('NumberSeries', prefix); const exists = await frappe.db.exists('NumberSeries', prefix);
if (exists) { if (exists) {

View File

@ -1,3 +1,4 @@
import { Frappe } from 'frappe';
import { DocValue, DocValueMap } from 'frappe/core/types'; import { DocValue, DocValueMap } from 'frappe/core/types';
import SystemSettings from 'frappe/models/SystemSettings'; import SystemSettings from 'frappe/models/SystemSettings';
import { FieldType } from 'schemas/types'; import { FieldType } from 'schemas/types';
@ -41,8 +42,8 @@ export type ModelMap = Record<string, typeof Doc | undefined>;
export type DocMap = Record<string, Doc | undefined>; export type DocMap = Record<string, Doc | undefined>;
export interface SinglesMap { export interface SinglesMap {
SystemSettings?: SystemSettings SystemSettings?: SystemSettings;
[key: string]: Doc | undefined [key: string]: Doc | undefined;
} }
// Static Config properties // Static Config properties
@ -50,7 +51,7 @@ export interface SinglesMap {
export type FilterFunction = (doc: Doc) => QueryFilter; export type FilterFunction = (doc: Doc) => QueryFilter;
export type FiltersMap = Record<string, FilterFunction>; export type FiltersMap = Record<string, FilterFunction>;
export type EmptyMessageFunction = (doc: Doc) => string; export type EmptyMessageFunction = (doc: Doc, frappe: Frappe) => string;
export type EmptyMessageMap = Record<string, EmptyMessageFunction>; export type EmptyMessageMap = Record<string, EmptyMessageFunction>;
export type ListFunction = (doc?: Doc) => string[]; export type ListFunction = (doc?: Doc) => string[];

View File

@ -1,4 +1,3 @@
import frappe from 'frappe';
import Doc from 'frappe/model/doc'; import Doc from 'frappe/model/doc';
function getPaddedName(prefix: string, next: number, padZeros: number): string { function getPaddedName(prefix: string, next: number, padZeros: number): string {
@ -32,7 +31,7 @@ export default class NumberSeries extends Doc {
} }
const name = this.getPaddedName(this.current as number); const name = this.getPaddedName(this.current as number);
return await frappe.db.exists(schemaName, name); return await this.frappe.db.exists(schemaName, name);
} }
getPaddedName(next: number): string { getPaddedName(next: number): string {

116
frappe/telemetry/helpers.ts Normal file
View File

@ -0,0 +1,116 @@
import { Frappe } from 'frappe';
import { ConfigFile, ConfigKeys } from 'frappe/core/types';
import { DEFAULT_COUNTRY_CODE } from 'frappe/utils/consts';
import { Count, TelemetrySetting, UniqueId } from './types';
export function getId(): string {
let id: string = '';
for (let i = 0; i < 4; i++) {
id += Math.random().toString(36).slice(2, 9);
}
return id;
}
export function getCountry(): string {
// @ts-ignore
return frappe.singles.SystemSettings?.countryCode ?? DEFAULT_COUNTRY_CODE;
}
export function getLanguage(frappe: Frappe): string {
return frappe.config.get('language') as string;
}
export async function getCounts(
interestingDocs: string[],
frappe: Frappe
): Promise<Count> {
const countMap: Count = {};
// @ts-ignore
if (frappe.db === undefined) {
return countMap;
}
for (const name of interestingDocs) {
const count: number = (await frappe.db.getAll(name)).length;
countMap[name] = count;
}
return countMap;
}
export function getDeviceId(frappe: Frappe): UniqueId {
let deviceId = frappe.config.get(ConfigKeys.DeviceId) as string | undefined;
if (deviceId === undefined) {
deviceId = getId();
frappe.config.set(ConfigKeys.DeviceId, deviceId);
}
return deviceId;
}
export function getInstanceId(frappe: Frappe): UniqueId {
const files = frappe.config.get(ConfigKeys.Files) as ConfigFile[];
// @ts-ignore
const companyName = frappe.AccountingSettings?.companyName;
if (companyName === undefined) {
return '';
}
const file = files.find((f) => f.companyName === companyName);
if (file === undefined) {
return addNewFile(companyName, files, frappe);
}
if (file.id === undefined) {
return setInstanceId(companyName, files, frappe);
}
return file.id;
}
function addNewFile(
companyName: string,
files: ConfigFile[],
frappe: Frappe
): UniqueId {
const newFile: ConfigFile = {
companyName,
filePath: frappe.config.get(ConfigKeys.LastSelectedFilePath, '') as string,
id: getId(),
};
files.push(newFile);
frappe.config.set(ConfigKeys.Files, files);
return newFile.id;
}
function setInstanceId(
companyName: string,
files: ConfigFile[],
frappe: Frappe
): UniqueId {
let id = '';
for (const file of files) {
if (file.id) {
continue;
}
file.id = getId();
if (file.companyName === companyName) {
id = file.id;
}
}
frappe.config.set(ConfigKeys.Files, files);
return id;
}
export const getTelemetryOptions = (frappe: Frappe) => ({
[TelemetrySetting.allow]: frappe.t`Allow Telemetry`,
[TelemetrySetting.dontLogUsage]: frappe.t`Don't Log Usage`,
[TelemetrySetting.dontLogAnything]: frappe.t`Don't Log Anything`,
});

View File

@ -1,15 +1,21 @@
import config, { ConfigKeys, TelemetrySetting } from '@/config'; import { Frappe } from 'frappe';
import frappe from 'frappe'; import { ConfigKeys } from 'frappe/core/types';
import { cloneDeep } from 'lodash'; import { cloneDeep } from 'lodash';
import { import {
getCountry, getCountry,
getCounts, getCounts,
getCreds,
getDeviceId, getDeviceId,
getInstanceId, getInstanceId,
getLanguage, getLanguage,
} from './helpers'; } from './helpers';
import { Noun, NounEnum, Platform, Telemetry, Verb } from './types'; import {
Noun,
NounEnum,
Platform,
Telemetry,
TelemetrySetting,
Verb,
} from './types';
/** /**
* # Telemetry * # Telemetry
@ -44,16 +50,26 @@ import { Noun, NounEnum, Platform, Telemetry, Verb } from './types';
* telemetry and not app usage. * telemetry and not app usage.
*/ */
class TelemetryManager { export class TelemetryManager {
#url: string = ''; #url: string = '';
#token: string = ''; #token: string = '';
#started = false; #started = false;
#telemetryObject: Partial<Telemetry> = {}; #telemetryObject: Partial<Telemetry> = {};
#interestingDocs: string[] = [];
frappe: Frappe;
constructor(frappe: Frappe) {
this.frappe = frappe;
}
set platform(value: Platform) { set platform(value: Platform) {
this.#telemetryObject.platform ||= value; this.#telemetryObject.platform ||= value;
} }
set interestingDocs(schemaNames: string[]) {
this.#interestingDocs = schemaNames;
}
get hasCreds() { get hasCreds() {
return !!this.#url && !!this.#token; return !!this.#url && !!this.#token;
} }
@ -68,9 +84,9 @@ class TelemetryManager {
async start() { async start() {
this.#telemetryObject.country ||= getCountry(); this.#telemetryObject.country ||= getCountry();
this.#telemetryObject.language ??= getLanguage(); this.#telemetryObject.language ??= getLanguage(this.frappe);
this.#telemetryObject.deviceId ||= getDeviceId(); this.#telemetryObject.deviceId ||= getDeviceId(this.frappe);
this.#telemetryObject.instanceId ||= getInstanceId(); this.#telemetryObject.instanceId ||= getInstanceId(this.frappe);
this.#telemetryObject.openTime ||= new Date().valueOf(); this.#telemetryObject.openTime ||= new Date().valueOf();
this.#telemetryObject.timeline ??= []; this.#telemetryObject.timeline ??= [];
this.#telemetryObject.errors ??= {}; this.#telemetryObject.errors ??= {};
@ -120,7 +136,10 @@ class TelemetryManager {
this.#clear(); this.#clear();
if (config.get(ConfigKeys.Telemetry) === TelemetrySetting.dontLogAnything) { if (
this.frappe.config.get(ConfigKeys.Telemetry) ===
TelemetrySetting.dontLogAnything
) {
return; return;
} }
navigator.sendBeacon(this.#url, data); navigator.sendBeacon(this.#url, data);
@ -141,7 +160,10 @@ class TelemetryManager {
return; return;
} }
this.#telemetryObject.counts = await getCounts(); this.#telemetryObject.counts = await getCounts(
this.#interestingDocs,
this.frappe
);
} }
async #setCreds() { async #setCreds() {
@ -149,13 +171,15 @@ class TelemetryManager {
return; return;
} }
const { url, token } = await getCreds(); const { url, token } = await this.frappe.auth.getTelemetryCreds();
this.#url = url; this.#url = url;
this.#token = token; this.#token = token;
} }
#getCanLog(): boolean { #getCanLog(): boolean {
const telemetrySetting = config.get(ConfigKeys.Telemetry) as string; const telemetrySetting = this.frappe.config.get(
ConfigKeys.Telemetry
) as string;
return telemetrySetting === TelemetrySetting.allow; return telemetrySetting === TelemetrySetting.allow;
} }
@ -170,5 +194,3 @@ class TelemetryManager {
delete this.#telemetryObject.country; delete this.#telemetryObject.country;
} }
} }
export default new TelemetryManager();

View File

@ -1,5 +1,3 @@
import { DoctypeName } from 'models/types';
export type AppVersion = string; export type AppVersion = string;
export type UniqueId = string; export type UniqueId = string;
export type Timestamp = number; export type Timestamp = number;
@ -11,9 +9,7 @@ export interface InteractionEvent {
more?: Record<string, unknown>; more?: Record<string, unknown>;
} }
export type Count = Partial<{ export type Count = Record<string, number>;
[key in DoctypeName]: number;
}>;
export type Platform = 'Windows' | 'Mac' | 'Linux'; export type Platform = 'Windows' | 'Mac' | 'Linux';
export interface Telemetry { export interface Telemetry {
@ -46,3 +42,9 @@ export enum NounEnum {
} }
export type Noun = string | NounEnum; export type Noun = string | NounEnum;
export enum TelemetrySetting {
allow = 'allow',
dontLogUsage = 'dontLogUsage',
dontLogAnything = 'dontLogAnything',
}

7
frappe/tests/helpers.ts Normal file
View File

@ -0,0 +1,7 @@
import { AuthDemuxBase, TelemetryCreds } from 'utils/auth/types';
export class DummyAuthDemux extends AuthDemuxBase {
async getTelemetryCreds(): Promise<TelemetryCreds> {
return { url: '', token: '' };
}
}

View File

@ -1,10 +1,18 @@
import * as assert from 'assert'; import * as assert from 'assert';
import 'mocha'; import 'mocha';
import models, { getRegionalModels } from 'models';
import { getSchemas } from 'schemas';
import { Frappe } from '..'; import { Frappe } from '..';
import { DatabaseManager } from '../../backend/database/manager'; import { DatabaseManager } from '../../backend/database/manager';
import { DummyAuthDemux } from './helpers';
describe('Frappe', function () { describe('Frappe', function () {
const frappe = new Frappe(DatabaseManager); const frappe = new Frappe({
DatabaseDemux: DatabaseManager,
AuthDemux: DummyAuthDemux,
isTest: true,
isElectron: false,
});
specify('Init', async function () { specify('Init', async function () {
assert.strictEqual( assert.strictEqual(
@ -12,7 +20,7 @@ describe('Frappe', function () {
0, 0,
'zero schemas one' 'zero schemas one'
); );
await frappe.initializeAndRegister();
assert.strictEqual( assert.strictEqual(
Object.keys(frappe.schemaMap).length, Object.keys(frappe.schemaMap).length,
0, 0,
@ -20,7 +28,7 @@ describe('Frappe', function () {
); );
await frappe.db.createNewDatabase(':memory:', 'in'); await frappe.db.createNewDatabase(':memory:', 'in');
await frappe.initializeAndRegister({}, {}, true); await frappe.initializeAndRegister({}, {});
assert.strictEqual( assert.strictEqual(
Object.keys(frappe.schemaMap).length > 0, Object.keys(frappe.schemaMap).length > 0,
true, true,
@ -29,3 +37,28 @@ describe('Frappe', function () {
await frappe.db.close(); await frappe.db.close();
}); });
}); });
describe('Frappe', function () {
const countryCode = 'in';
let frappe: Frappe;
const schemas = getSchemas(countryCode);
this.beforeEach(async function () {
frappe = new Frappe({
DatabaseDemux: DatabaseManager,
isTest: true,
isElectron: false,
});
const regionalModels = await getRegionalModels(countryCode);
await frappe.db.createNewDatabase(':memory:', countryCode);
await frappe.initializeAndRegister(models, regionalModels);
});
this.afterEach(async function () {
await frappe.close();
});
specify('temp', async function () {
frappe.db.schemaMap;
});
});

View File

@ -5,14 +5,4 @@ export const DEFAULT_LOCALE = 'en-IN';
export const DEFAULT_COUNTRY_CODE = 'in'; export const DEFAULT_COUNTRY_CODE = 'in';
export const DEFAULT_CURRENCY = 'INR'; export const DEFAULT_CURRENCY = 'INR';
export const DEFAULT_LANGUAGE = 'English'; export const DEFAULT_LANGUAGE = 'English';
export const DEFAULT_NUMBER_SERIES = { export const DEFAULT_SERIES_START = 1001;
SalesInvoice: 'SINV-',
PurchaseInvoice: 'PINV-',
Payment: 'PAY-',
JournalEntry: 'JV-',
Quotation: 'QTN-',
SalesOrder: 'SO-',
Fulfillment: 'OF-',
PurchaseOrder: 'PO-',
PurchaseReceipt: 'PREC-',
};

View File

@ -1,4 +1,4 @@
import frappe from 'frappe'; import { Frappe } from 'frappe';
import { DocValue } from 'frappe/core/types'; import { DocValue } from 'frappe/core/types';
import Doc from 'frappe/model/doc'; import Doc from 'frappe/model/doc';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
@ -14,8 +14,9 @@ import {
export function format( export function format(
value: DocValue, value: DocValue,
df?: string | Field, df: string | Field | null,
doc?: Doc doc: Doc | null,
frappe: Frappe
): string { ): string {
if (!df) { if (!df) {
return String(value); return String(value);
@ -24,11 +25,11 @@ export function format(
const field: Field = getField(df); const field: Field = getField(df);
if (field.fieldtype === FieldTypeEnum.Currency) { if (field.fieldtype === FieldTypeEnum.Currency) {
return formatCurrency(value, field, doc); return formatCurrency(value, field, doc, frappe);
} }
if (field.fieldtype === FieldTypeEnum.Date) { if (field.fieldtype === FieldTypeEnum.Date) {
return formatDate(value); return formatDate(value, frappe);
} }
if (field.fieldtype === FieldTypeEnum.Check) { if (field.fieldtype === FieldTypeEnum.Check) {
@ -42,7 +43,7 @@ export function format(
return String(value); return String(value);
} }
function formatDate(value: DocValue): string { function formatDate(value: DocValue, frappe: Frappe): string {
const dateFormat = const dateFormat =
(frappe.singles.SystemSettings?.dateFormat as string) ?? (frappe.singles.SystemSettings?.dateFormat as string) ??
DEFAULT_DATE_FORMAT; DEFAULT_DATE_FORMAT;
@ -64,12 +65,17 @@ function formatDate(value: DocValue): string {
return formattedDate; return formattedDate;
} }
function formatCurrency(value: DocValue, field: Field, doc?: Doc): string { function formatCurrency(
const currency = getCurrency(field, doc); value: DocValue,
field: Field,
doc: Doc | null,
frappe: Frappe
): string {
const currency = getCurrency(field, doc, frappe);
let valueString; let valueString;
try { try {
valueString = formatNumber(value); valueString = formatNumber(value, frappe);
} catch (err) { } catch (err) {
(err as Error).message += ` value: '${value}', type: ${typeof value}`; (err as Error).message += ` value: '${value}', type: ${typeof value}`;
throw err; throw err;
@ -83,8 +89,8 @@ function formatCurrency(value: DocValue, field: Field, doc?: Doc): string {
return valueString; return valueString;
} }
function formatNumber(value: DocValue): string { function formatNumber(value: DocValue, frappe: Frappe): string {
const numberFormatter = getNumberFormatter(); const numberFormatter = getNumberFormatter(frappe);
if (typeof value === 'number') { if (typeof value === 'number') {
return numberFormatter.format(value); return numberFormatter.format(value);
} }
@ -106,7 +112,7 @@ function formatNumber(value: DocValue): string {
return formattedNumber; return formattedNumber;
} }
function getNumberFormatter() { function getNumberFormatter(frappe: Frappe) {
if (frappe.currencyFormatter) { if (frappe.currencyFormatter) {
return frappe.currencyFormatter; return frappe.currencyFormatter;
} }
@ -123,7 +129,7 @@ function getNumberFormatter() {
})); }));
} }
function getCurrency(field: Field, doc?: Doc): string { function getCurrency(field: Field, doc: Doc | null, frappe: Frappe): string {
if (doc && doc.getCurrencies[field.fieldname]) { if (doc && doc.getCurrencies[field.fieldname]) {
return doc.getCurrencies[field.fieldname](); return doc.getCurrencies[field.fieldname]();
} }

View File

@ -36,6 +36,9 @@ directly use the the `Frappe` class and will be run using `mocha` on `node`.
Importing frontend code will break all the tests. This also implies that one Importing frontend code will break all the tests. This also implies that one
should be wary about transitive dependencies. should be wary about transitive dependencies.
It should also not import the `frappe` object (singleton) from `src`, where ever
frappe is required in models it should be passed to it.
_Note: Frontend specific code can be imported but they should be done so, only _Note: Frontend specific code can be imported but they should be done so, only
using dynamic imports i.e. `await import('...')`._ using dynamic imports i.e. `await import('...')`._

View File

@ -1,4 +1,4 @@
import frappe from 'frappe'; import { Frappe } from 'frappe';
import Doc from 'frappe/model/doc'; import Doc from 'frappe/model/doc';
import { import {
FiltersMap, FiltersMap,
@ -6,36 +6,45 @@ import {
TreeViewSettings, TreeViewSettings,
} from 'frappe/model/types'; } from 'frappe/model/types';
import { QueryFilter } from 'utils/db/types'; import { QueryFilter } from 'utils/db/types';
import { AccountRootType, AccountType } from './types';
export class Account extends Doc { export class Account extends Doc {
rootType?: AccountRootType;
accountType?: AccountType;
parentAccount?: string;
async beforeInsert() { async beforeInsert() {
if (this.accountType || !this.parentAccount) { if (this.accountType || !this.parentAccount) {
return; return;
} }
const account = await frappe.db.get( const account = await this.frappe.db.get(
'Account', 'Account',
this.parentAccount as string this.parentAccount as string
); );
this.accountType = account.accountType as string; this.accountType = account.accountType as AccountType;
} }
static listSettings: ListViewSettings = { static getListViewSettings(): ListViewSettings {
columns: ['name', 'parentAccount', 'rootType'], return {
}; columns: ['name', 'parentAccount', 'rootType'],
};
}
static treeSettings: TreeViewSettings = { static getTreeSettings(frappe: Frappe): void | TreeViewSettings {
parentField: 'parentAccount', return {
async getRootLabel(): Promise<string> { parentField: 'parentAccount',
const accountingSettings = await frappe.doc.getSingle( async getRootLabel(): Promise<string> {
'AccountingSettings' const accountingSettings = await frappe.doc.getSingle(
); 'AccountingSettings'
return accountingSettings.companyName as string; );
}, return accountingSettings.companyName as string;
}; },
};
}
static filters: FiltersMap = { static filters: FiltersMap = {
parentAccount: (doc: Doc) => { parentAccount: (doc: Account) => {
const filter: QueryFilter = { const filter: QueryFilter = {
isGroup: true, isGroup: true,
}; };

View File

@ -0,0 +1,27 @@
export type AccountType =
| 'Accumulated Depreciation'
| 'Bank'
| 'Cash'
| 'Chargeable'
| 'Cost of Goods Sold'
| 'Depreciation'
| 'Equity'
| 'Expense Account'
| 'Expenses Included In Valuation'
| 'Fixed Asset'
| 'Income Account'
| 'Payable'
| 'Receivable'
| 'Round Off'
| 'Stock'
| 'Stock Adjustment'
| 'Stock Received But Not Billed'
| 'Tax'
| 'Temporary';
export type AccountRootType =
| 'Asset'
| 'Liability'
| 'Equity'
| 'Income'
| 'Expense';

View File

@ -2,7 +2,9 @@ import Doc from 'frappe/model/doc';
import { ListViewSettings } from 'frappe/model/types'; import { ListViewSettings } from 'frappe/model/types';
export class AccountingLedgerEntry extends Doc { export class AccountingLedgerEntry extends Doc {
static listSettings: ListViewSettings = { static getListViewSettings(): ListViewSettings {
columns: ['account', 'party', 'debit', 'credit', 'balance'], return {
}; columns: ['account', 'party', 'debit', 'credit', 'balance'],
};
}
} }

View File

@ -1,4 +1,3 @@
import frappe from 'frappe';
import Doc from 'frappe/model/doc'; import Doc from 'frappe/model/doc';
import { EmptyMessageMap, FormulaMap, ListsMap } from 'frappe/model/types'; import { EmptyMessageMap, FormulaMap, ListsMap } from 'frappe/model/types';
import { stateCodeMap } from 'regional/in'; import { stateCodeMap } from 'regional/in';
@ -37,7 +36,7 @@ export class Address extends Doc {
}; };
static emptyMessages: EmptyMessageMap = { static emptyMessages: EmptyMessageMap = {
state: (doc: Doc) => { state: (doc: Doc, frappe) => {
if (doc.country) { if (doc.country) {
return frappe.t`Enter State`; return frappe.t`Enter State`;
} }

View File

@ -1,5 +1,4 @@
import { LedgerPosting } from 'accounting/ledgerPosting'; import { LedgerPosting } from 'accounting/ledgerPosting';
import frappe from 'frappe';
import { DocValue } from 'frappe/core/types'; import { DocValue } from 'frappe/core/types';
import Doc from 'frappe/model/doc'; import Doc from 'frappe/model/doc';
import { DefaultMap, FiltersMap, FormulaMap } from 'frappe/model/types'; import { DefaultMap, FiltersMap, FormulaMap } from 'frappe/model/types';
@ -28,7 +27,7 @@ export abstract class Invoice extends Doc {
} }
async getPayments() { async getPayments() {
const payments = await frappe.db.getAll('PaymentFor', { const payments = await this.frappe.db.getAll('PaymentFor', {
fields: ['parent'], fields: ['parent'],
filters: { referenceName: this.name! }, filters: { referenceName: this.name! },
orderBy: 'name', orderBy: 'name',
@ -56,12 +55,12 @@ export abstract class Invoice extends Doc {
await entries.post(); await entries.post();
// update outstanding amounts // update outstanding amounts
await frappe.db.update(this.schemaName, { await this.frappe.db.update(this.schemaName, {
name: this.name as string, name: this.name as string,
outstandingAmount: this.baseGrandTotal!, outstandingAmount: this.baseGrandTotal!,
}); });
const party = (await frappe.doc.getDoc('Party', this.party!)) as Party; const party = (await this.frappe.doc.getDoc('Party', this.party!)) as Party;
await party.updateOutstandingAmount(); await party.updateOutstandingAmount();
} }
@ -69,7 +68,7 @@ export abstract class Invoice extends Doc {
const paymentRefList = await this.getPayments(); const paymentRefList = await this.getPayments();
for (const paymentFor of paymentRefList) { for (const paymentFor of paymentRefList) {
const paymentReference = paymentFor.parent; const paymentReference = paymentFor.parent;
const payment = (await frappe.doc.getDoc( const payment = (await this.frappe.doc.getDoc(
'Payment', 'Payment',
paymentReference as string paymentReference as string
)) as Payment; )) as Payment;
@ -80,7 +79,7 @@ export abstract class Invoice extends Doc {
} }
// To set the payment status as unsubmitted. // To set the payment status as unsubmitted.
await frappe.db.update('Payment', { await this.frappe.db.update('Payment', {
name: paymentReference, name: paymentReference,
submitted: false, submitted: false,
cancelled: true, cancelled: true,
@ -93,7 +92,9 @@ export abstract class Invoice extends Doc {
async getExchangeRate() { async getExchangeRate() {
if (!this.currency) return 1.0; if (!this.currency) return 1.0;
const accountingSettings = await frappe.doc.getSingle('AccountingSettings'); const accountingSettings = await this.frappe.doc.getSingle(
'AccountingSettings'
);
const companyCurrency = accountingSettings.currency; const companyCurrency = accountingSettings.currency;
if (this.currency === companyCurrency) { if (this.currency === companyCurrency) {
return 1.0; return 1.0;
@ -129,8 +130,8 @@ export abstract class Invoice extends Doc {
taxes[account] = taxes[account] || { taxes[account] = taxes[account] || {
account, account,
rate, rate,
amount: frappe.pesa(0), amount: this.frappe.pesa(0),
baseAmount: frappe.pesa(0), baseAmount: this.frappe.pesa(0),
}; };
const amount = (row.amount as Money).mul(rate).div(100); const amount = (row.amount as Money).mul(rate).div(100);
@ -149,7 +150,7 @@ export abstract class Invoice extends Doc {
async getTax(tax: string) { async getTax(tax: string) {
if (!this._taxes![tax]) { if (!this._taxes![tax]) {
this._taxes[tax] = await frappe.doc.getDoc('Tax', tax); this._taxes[tax] = await this.frappe.doc.getDoc('Tax', tax);
} }
return this._taxes[tax]; return this._taxes[tax];
@ -166,7 +167,7 @@ export abstract class Invoice extends Doc {
this.getFrom('Party', this.party!, 'defaultAccount') as string, this.getFrom('Party', this.party!, 'defaultAccount') as string,
currency: async () => currency: async () =>
(this.getFrom('Party', this.party!, 'currency') as string) || (this.getFrom('Party', this.party!, 'currency') as string) ||
(frappe.singles.AccountingSettings!.currency as string), (this.frappe.singles.AccountingSettings!.currency as string),
exchangeRate: async () => await this.getExchangeRate(), exchangeRate: async () => await this.getExchangeRate(),
netTotal: async () => this.getSum('items', 'amount', false), netTotal: async () => this.getSum('items', 'amount', false),
baseNetTotal: async () => this.netTotal!.mul(this.exchangeRate!), baseNetTotal: async () => this.netTotal!.mul(this.exchangeRate!),

View File

@ -1,4 +1,3 @@
import frappe from 'frappe';
import { DocValue } from 'frappe/core/types'; import { DocValue } from 'frappe/core/types';
import Doc from 'frappe/model/doc'; import Doc from 'frappe/model/doc';
import { import {
@ -7,6 +6,7 @@ import {
FormulaMap, FormulaMap,
ValidationMap, ValidationMap,
} from 'frappe/model/types'; } from 'frappe/model/types';
import { ValidationError } from 'frappe/utils/errors';
import Money from 'pesa/dist/types/src/money'; import Money from 'pesa/dist/types/src/money';
import { Invoice } from '../Invoice/Invoice'; import { Invoice } from '../Invoice/Invoice';
@ -32,7 +32,7 @@ export abstract class InvoiceItem extends Doc {
'Item', 'Item',
this.item as string, this.item as string,
'rate' 'rate'
)) || frappe.pesa(0)) as Money; )) || this.frappe.pesa(0)) as Money;
return baseRate.div(this.exchangeRate!); return baseRate.div(this.exchangeRate!);
}, },
@ -73,8 +73,8 @@ export abstract class InvoiceItem extends Doc {
return; return;
} }
throw new frappe.errors.ValidationError( throw new ValidationError(
frappe.t`Rate (${frappe.format( this.frappe.t`Rate (${this.frappe.format(
value, value,
'Currency' 'Currency'
)}) cannot be less zero.` )}) cannot be less zero.`

View File

@ -1,4 +1,4 @@
import frappe from 'frappe'; import { Frappe } from 'frappe';
import { DocValue } from 'frappe/core/types'; import { DocValue } from 'frappe/core/types';
import Doc from 'frappe/model/doc'; import Doc from 'frappe/model/doc';
import { import {
@ -9,6 +9,7 @@ import {
ListViewSettings, ListViewSettings,
ValidationMap, ValidationMap,
} from 'frappe/model/types'; } from 'frappe/model/types';
import { ValidationError } from 'frappe/utils/errors';
import Money from 'pesa/dist/types/src/money'; import Money from 'pesa/dist/types/src/money';
export class Item extends Doc { export class Item extends Doc {
@ -19,11 +20,11 @@ export class Item extends Doc {
accountName = 'Sales'; accountName = 'Sales';
} }
const accountExists = await frappe.db.exists('Account', accountName); const accountExists = await this.frappe.db.exists('Account', accountName);
return accountExists ? accountName : ''; return accountExists ? accountName : '';
}, },
expenseAccount: async () => { expenseAccount: async () => {
const cogs = await frappe.db.getAllRaw('Account', { const cogs = await this.frappe.db.getAllRaw('Account', {
filters: { filters: {
accountType: 'Cost of Goods Sold', accountType: 'Cost of Goods Sold',
}, },
@ -56,43 +57,45 @@ export class Item extends Doc {
validations: ValidationMap = { validations: ValidationMap = {
rate: async (value: DocValue) => { rate: async (value: DocValue) => {
if ((value as Money).isNegative()) { if ((value as Money).isNegative()) {
throw new frappe.errors.ValidationError( throw new ValidationError(this.frappe.t`Rate can't be negative.`);
frappe.t`Rate can't be negative.`
);
} }
}, },
}; };
static actions: Action[] = [ static getActions(frappe: Frappe): Action[] {
{ return [
label: frappe.t`New Invoice`, {
condition: (doc) => !doc.isNew, label: frappe.t`New Invoice`,
action: async (doc, router) => { condition: (doc) => !doc.isNew,
const invoice = await frappe.doc.getEmptyDoc('SalesInvoice'); action: async (doc, router) => {
invoice.append('items', { const invoice = await frappe.doc.getEmptyDoc('SalesInvoice');
item: doc.name as string, invoice.append('items', {
rate: doc.rate as Money, item: doc.name as string,
tax: doc.tax as string, rate: doc.rate as Money,
}); tax: doc.tax as string,
router.push(`/edit/SalesInvoice/${invoice.name}`); });
router.push(`/edit/SalesInvoice/${invoice.name}`);
},
}, },
}, {
{ label: frappe.t`New Bill`,
label: frappe.t`New Bill`, condition: (doc) => !doc.isNew,
condition: (doc) => !doc.isNew, action: async (doc, router) => {
action: async (doc, router) => { const invoice = await frappe.doc.getEmptyDoc('PurchaseInvoice');
const invoice = await frappe.doc.getEmptyDoc('PurchaseInvoice'); invoice.append('items', {
invoice.append('items', { item: doc.name as string,
item: doc.name as string, rate: doc.rate as Money,
rate: doc.rate as Money, tax: doc.tax as string,
tax: doc.tax as string, });
}); router.push(`/edit/PurchaseInvoice/${invoice.name}`);
router.push(`/edit/PurchaseInvoice/${invoice.name}`); },
}, },
}, ];
]; }
listSettings: ListViewSettings = { static getListViewSettings(): ListViewSettings {
columns: ['name', 'unit', 'tax', 'rate'], return {
}; columns: ['name', 'unit', 'tax', 'rate'],
};
}
} }

View File

@ -1,4 +1,4 @@
import frappe from 'frappe'; import { Frappe } from 'frappe';
import Doc from 'frappe/model/doc'; import Doc from 'frappe/model/doc';
import { import {
Action, Action,
@ -15,7 +15,7 @@ export class JournalEntry extends Doc {
accounts: Doc[] = []; accounts: Doc[] = [];
getPosting() { getPosting() {
const entries = new LedgerPosting({ reference: this }); const entries = new LedgerPosting({ reference: this }, this.frappe);
for (const row of this.accounts) { for (const row of this.accounts) {
const debit = row.debit as Money; const debit = row.debit as Money;
@ -32,11 +32,11 @@ export class JournalEntry extends Doc {
return entries; return entries;
} }
beforeUpdate() { async beforeUpdate() {
this.getPosting().validateEntries(); this.getPosting().validateEntries();
} }
beforeInsert() { async beforeInsert() {
this.getPosting().validateEntries(); this.getPosting().validateEntries();
} }
@ -56,44 +56,48 @@ export class JournalEntry extends Doc {
numberSeries: () => ({ referenceType: 'JournalEntry' }), numberSeries: () => ({ referenceType: 'JournalEntry' }),
}; };
static actions: Action[] = [getLedgerLinkAction()]; static getActions(frappe: Frappe): Action[] {
return [getLedgerLinkAction(frappe)];
}
static listSettings: ListViewSettings = { static getListViewSettings(frappe: Frappe): ListViewSettings {
formRoute: (name) => `/edit/JournalEntry/${name}`, return {
columns: [ formRoute: (name) => `/edit/JournalEntry/${name}`,
'date', columns: [
{ 'date',
label: frappe.t`Status`, {
fieldtype: 'Select', label: frappe.t`Status`,
size: 'small', fieldtype: 'Select',
render(doc) { size: 'small',
let status = 'Draft'; render(doc) {
let color = 'gray'; let status = 'Draft';
if (doc.submitted) { let color = 'gray';
color = 'green'; if (doc.submitted) {
status = 'Submitted'; color = 'green';
} status = 'Submitted';
}
if (doc.cancelled) { if (doc.cancelled) {
color = 'red'; color = 'red';
status = 'Cancelled'; status = 'Cancelled';
} }
return { return {
template: `<Badge class="text-xs" color="${color}">${status}</Badge>`, template: `<Badge class="text-xs" color="${color}">${status}</Badge>`,
}; };
},
}, },
}, {
{ label: frappe.t`Entry ID`,
label: frappe.t`Entry ID`, fieldtype: 'Data',
fieldtype: 'Data', fieldname: 'name',
fieldname: 'name', getValue(doc) {
getValue(doc) { return doc.name as string;
return doc.name as string; },
}, },
}, 'entryType',
'entryType', 'referenceNumber',
'referenceNumber', ],
], };
}; }
} }

View File

@ -1,4 +1,3 @@
import frappe from 'frappe';
import Doc from 'frappe/model/doc'; import Doc from 'frappe/model/doc';
import { FiltersMap, FormulaMap } from 'frappe/model/types'; import { FiltersMap, FormulaMap } from 'frappe/model/types';
import Money from 'pesa/dist/types/src/money'; import Money from 'pesa/dist/types/src/money';
@ -9,7 +8,7 @@ export class JournalEntryAccount extends Doc {
const otherTypeValue = this.get(otherType) as Money; const otherTypeValue = this.get(otherType) as Money;
if (!otherTypeValue.isZero()) { if (!otherTypeValue.isZero()) {
return frappe.pesa(0); return this.frappe.pesa(0);
} }
const totalType = this.parentdoc!.getSum('accounts', type, false) as Money; const totalType = this.parentdoc!.getSum('accounts', type, false) as Money;

View File

@ -1,4 +1,4 @@
import frappe from 'frappe'; import { Frappe } from 'frappe';
import Doc from 'frappe/model/doc'; import Doc from 'frappe/model/doc';
import { import {
Action, Action,
@ -29,15 +29,17 @@ export class Party extends Doc {
schemaName: 'SalesInvoice' | 'PurchaseInvoice' schemaName: 'SalesInvoice' | 'PurchaseInvoice'
) { ) {
const outstandingAmounts = ( const outstandingAmounts = (
await frappe.db.getAllRaw(schemaName, { await this.frappe.db.getAllRaw(schemaName, {
fields: ['outstandingAmount', 'party'], fields: ['outstandingAmount', 'party'],
filters: { submitted: true }, filters: { submitted: true },
}) })
).filter(({ party }) => party === this.name); ).filter(({ party }) => party === this.name);
const totalOutstanding = outstandingAmounts const totalOutstanding = outstandingAmounts
.map(({ outstandingAmount }) => frappe.pesa(outstandingAmount as number)) .map(({ outstandingAmount }) =>
.reduce((a, b) => a.add(b), frappe.pesa(0)); this.frappe.pesa(outstandingAmount as number)
)
.reduce((a, b) => a.add(b), this.frappe.pesa(0));
await this.set('outstandingAmount', totalOutstanding); await this.set('outstandingAmount', totalOutstanding);
await this.update(); await this.update();
@ -55,10 +57,11 @@ export class Party extends Doc {
accountName = 'Creditors'; accountName = 'Creditors';
} }
const accountExists = await frappe.db.exists('Account', accountName); const accountExists = await this.frappe.db.exists('Account', accountName);
return accountExists ? accountName : ''; return accountExists ? accountName : '';
}, },
currency: async () => frappe.singles.AccountingSettings!.currency as string, currency: async () =>
this.frappe.singles.AccountingSettings!.currency as string,
addressDisplay: async () => { addressDisplay: async () => {
const address = this.address as string | undefined; const address = this.address as string | undefined;
if (address) { if (address) {
@ -85,80 +88,84 @@ export class Party extends Doc {
}, },
}; };
static listSettings: ListViewSettings = { static getListViewSettings(): ListViewSettings {
columns: ['name', 'phone', 'outstandingAmount'], return {
}; columns: ['name', 'phone', 'outstandingAmount'],
};
}
static actions: Action[] = [ static getActions(frappe: Frappe): Action[] {
{ return [
label: frappe.t`Create Bill`, {
condition: (doc: Doc) => label: frappe.t`Create Bill`,
!doc.isNew && (doc.role as PartyRole) !== 'Customer', condition: (doc: Doc) =>
action: async (partyDoc, router) => { !doc.isNew && (doc.role as PartyRole) !== 'Customer',
const doc = await frappe.doc.getEmptyDoc('PurchaseInvoice'); action: async (partyDoc, router) => {
router.push({ const doc = await frappe.doc.getEmptyDoc('PurchaseInvoice');
path: `/edit/PurchaseInvoice/${doc.name}`, router.push({
query: { path: `/edit/PurchaseInvoice/${doc.name}`,
doctype: 'PurchaseInvoice', query: {
values: { doctype: 'PurchaseInvoice',
// @ts-ignore values: {
party: partyDoc.name!, // @ts-ignore
party: partyDoc.name!,
},
}, },
}, });
}); },
}, },
}, {
{ label: frappe.t`View Bills`,
label: frappe.t`View Bills`, condition: (doc: Doc) =>
condition: (doc: Doc) => !doc.isNew && (doc.role as PartyRole) !== 'Customer',
!doc.isNew && (doc.role as PartyRole) !== 'Customer', action: async (partyDoc, router) => {
action: async (partyDoc, router) => { router.push({
router.push({ name: 'ListView',
name: 'ListView', params: {
params: { doctype: 'PurchaseInvoice',
doctype: 'PurchaseInvoice', filters: {
filters: { // @ts-ignore
// @ts-ignore party: partyDoc.name!,
party: partyDoc.name!, },
}, },
}, });
}); },
}, },
}, {
{ label: frappe.t`Create Invoice`,
label: frappe.t`Create Invoice`, condition: (doc: Doc) =>
condition: (doc: Doc) => !doc.isNew && (doc.role as PartyRole) !== 'Supplier',
!doc.isNew && (doc.role as PartyRole) !== 'Supplier', action: async (partyDoc, router) => {
action: async (partyDoc, router) => { const doc = await frappe.doc.getEmptyDoc('SalesInvoice');
const doc = await frappe.doc.getEmptyDoc('SalesInvoice'); router.push({
router.push({ path: `/edit/SalesInvoice/${doc.name}`,
path: `/edit/SalesInvoice/${doc.name}`, query: {
query: { doctype: 'SalesInvoice',
doctype: 'SalesInvoice', values: {
values: { // @ts-ignore
// @ts-ignore party: partyDoc.name!,
party: partyDoc.name!, },
}, },
}, });
}); },
}, },
}, {
{ label: frappe.t`View Invoices`,
label: frappe.t`View Invoices`, condition: (doc: Doc) =>
condition: (doc: Doc) => !doc.isNew && (doc.role as PartyRole) !== 'Supplier',
!doc.isNew && (doc.role as PartyRole) !== 'Supplier', action: async (partyDoc, router) => {
action: async (partyDoc, router) => { router.push({
router.push({ name: 'ListView',
name: 'ListView', params: {
params: { doctype: 'SalesInvoice',
doctype: 'SalesInvoice', filters: {
filters: { // @ts-ignore
// @ts-ignore party: partyDoc.name!,
party: partyDoc.name!, },
}, },
}, });
}); },
}, },
}, ];
]; }
} }

View File

@ -1,5 +1,5 @@
import { LedgerPosting } from 'accounting/ledgerPosting'; import { LedgerPosting } from 'accounting/ledgerPosting';
import frappe from 'frappe'; import { Frappe } from 'frappe';
import { DocValue } from 'frappe/core/types'; import { DocValue } from 'frappe/core/types';
import Doc from 'frappe/model/doc'; import Doc from 'frappe/model/doc';
import { import {
@ -21,6 +21,8 @@ import { PaymentMethod, PaymentType } from './types';
export class Payment extends Doc { export class Payment extends Doc {
party?: string; party?: string;
amount?: Money;
writeoff?: Money;
async change({ changed }: { changed: string }) { async change({ changed }: { changed: string }) {
switch (changed) { switch (changed) {
@ -48,7 +50,10 @@ export class Payment extends Doc {
} }
const schemaName = referenceType as string; const schemaName = referenceType as string;
const doc = await frappe.doc.getDoc(schemaName, referenceName as string); const doc = await this.frappe.doc.getDoc(
schemaName,
referenceName as string
);
let party; let party;
let paymentType: PaymentType; let paymentType: PaymentType;
@ -66,7 +71,7 @@ export class Payment extends Doc {
} }
updateAmountOnReferenceUpdate() { updateAmountOnReferenceUpdate() {
this.amount = frappe.pesa(0); this.amount = this.frappe.pesa(0);
for (const paymentReference of this.for as Doc[]) { for (const paymentReference of this.for as Doc[]) {
this.amount = (this.amount as Money).add( this.amount = (this.amount as Money).add(
paymentReference.amount as Money paymentReference.amount as Money
@ -93,7 +98,7 @@ export class Payment extends Doc {
if (this.paymentAccount !== this.account || !this.account) { if (this.paymentAccount !== this.account || !this.account) {
return; return;
} }
throw new frappe.errors.ValidationError( throw new this.frappe.errors.ValidationError(
`To Account and From Account can't be the same: ${this.account}` `To Account and From Account can't be the same: ${this.account}`
); );
} }
@ -106,7 +111,7 @@ export class Payment extends Doc {
const referenceAmountTotal = forReferences const referenceAmountTotal = forReferences
.map(({ amount }) => amount as Money) .map(({ amount }) => amount as Money)
.reduce((a, b) => a.add(b), frappe.pesa(0)); .reduce((a, b) => a.add(b), this.frappe.pesa(0));
if ( if (
(this.amount as Money) (this.amount as Money)
@ -116,20 +121,20 @@ export class Payment extends Doc {
return; return;
} }
const writeoff = frappe.format(this.writeoff, 'Currency'); const writeoff = this.frappe.format(this.writeoff!, 'Currency');
const payment = frappe.format(this.amount, 'Currency'); const payment = this.frappe.format(this.amount!, 'Currency');
const refAmount = frappe.format(referenceAmountTotal, 'Currency'); const refAmount = this.frappe.format(referenceAmountTotal, 'Currency');
if ((this.writeoff as Money).gt(0)) { if ((this.writeoff as Money).gt(0)) {
throw new frappe.errors.ValidationError( throw new ValidationError(
frappe.t`Amount: ${payment} and writeoff: ${writeoff} this.frappe.t`Amount: ${payment} and writeoff: ${writeoff}
is less than the total amount allocated to is less than the total amount allocated to
references: ${refAmount}.` references: ${refAmount}.`
); );
} }
throw new frappe.errors.ValidationError( throw new ValidationError(
frappe.t`Amount: ${payment} is less than the total this.frappe.t`Amount: ${payment} is less than the total
amount allocated to references: ${refAmount}.` amount allocated to references: ${refAmount}.`
); );
} }
@ -139,9 +144,9 @@ export class Payment extends Doc {
return; return;
} }
if (!frappe.singles.AccountingSettings!.writeOffAccount) { if (!this.frappe.singles.AccountingSettings!.writeOffAccount) {
throw new frappe.errors.ValidationError( throw new ValidationError(
frappe.t`Write Off Account not set. this.frappe.t`Write Off Account not set.
Please set Write Off Account in General Settings` Please set Write Off Account in General Settings`
); );
} }
@ -152,10 +157,13 @@ export class Payment extends Doc {
const paymentAccount = this.paymentAccount as string; const paymentAccount = this.paymentAccount as string;
const amount = this.amount as Money; const amount = this.amount as Money;
const writeoff = this.writeoff as Money; const writeoff = this.writeoff as Money;
const entries = new LedgerPosting({ const entries = new LedgerPosting(
reference: this, {
party: this.party!, reference: this,
}); party: this.party!,
},
this.frappe
);
await entries.debit(paymentAccount as string, amount.sub(writeoff)); await entries.debit(paymentAccount as string, amount.sub(writeoff));
await entries.credit(account as string, amount.sub(writeoff)); await entries.credit(account as string, amount.sub(writeoff));
@ -164,11 +172,14 @@ export class Payment extends Doc {
return [entries]; return [entries];
} }
const writeoffEntry = new LedgerPosting({ const writeoffEntry = new LedgerPosting(
reference: this, {
party: this.party!, reference: this,
}); party: this.party!,
const writeOffAccount = frappe.singles.AccountingSettings! },
this.frappe
);
const writeOffAccount = this.frappe.singles.AccountingSettings!
.writeOffAccount as string; .writeOffAccount as string;
if (this.paymentType === 'Pay') { if (this.paymentType === 'Pay') {
@ -196,7 +207,7 @@ export class Payment extends Doc {
) { ) {
continue; continue;
} }
const referenceDoc = await frappe.doc.getDoc( const referenceDoc = await this.frappe.doc.getDoc(
row.referenceType as string, row.referenceType as string,
row.referenceName as string row.referenceName as string
); );
@ -210,26 +221,30 @@ export class Payment extends Doc {
} }
if (amount.lte(0) || amount.gt(outstandingAmount)) { if (amount.lte(0) || amount.gt(outstandingAmount)) {
let message = frappe.t`Payment amount: ${frappe.format( let message = this.frappe.t`Payment amount: ${this.frappe.format(
this.amount, this.amount!,
'Currency' 'Currency'
)} should be less than Outstanding amount: ${frappe.format( )} should be less than Outstanding amount: ${this.frappe.format(
outstandingAmount, outstandingAmount,
'Currency' 'Currency'
)}.`; )}.`;
if (amount.lte(0)) { if (amount.lte(0)) {
const amt = frappe.format(this.amount, 'Currency'); const amt = this.frappe.format(this.amount!, 'Currency');
message = frappe.t`Payment amount: ${amt} should be greater than 0.`; message = this.frappe
.t`Payment amount: ${amt} should be greater than 0.`;
} }
throw new frappe.errors.ValidationError(message); throw new ValidationError(message);
} else { } else {
// update outstanding amounts in invoice and party // update outstanding amounts in invoice and party
const newOutstanding = outstandingAmount.sub(amount); const newOutstanding = outstandingAmount.sub(amount);
await referenceDoc.set('outstandingAmount', newOutstanding); await referenceDoc.set('outstandingAmount', newOutstanding);
await referenceDoc.update(); await referenceDoc.update();
const party = (await frappe.doc.getDoc('Party', this.party!)) as Party; const party = (await this.frappe.doc.getDoc(
'Party',
this.party!
)) as Party;
await party.updateOutstandingAmount(); await party.updateOutstandingAmount();
} }
@ -254,7 +269,7 @@ export class Payment extends Doc {
async updateReferenceOutstandingAmount() { async updateReferenceOutstandingAmount() {
await (this.for as Doc[]).forEach( await (this.for as Doc[]).forEach(
async ({ amount, referenceType, referenceName }) => { async ({ amount, referenceType, referenceName }) => {
const refDoc = await frappe.doc.getDoc( const refDoc = await this.frappe.doc.getDoc(
referenceType as string, referenceType as string,
referenceName as string referenceName as string
); );
@ -289,7 +304,7 @@ export class Payment extends Doc {
amount: async (value: DocValue) => { amount: async (value: DocValue) => {
if ((value as Money).isNegative()) { if ((value as Money).isNegative()) {
throw new ValidationError( throw new ValidationError(
frappe.t`Payment amount cannot be less than zero.` this.frappe.t`Payment amount cannot be less than zero.`
); );
} }
@ -297,14 +312,14 @@ export class Payment extends Doc {
const amount = this.getSum('for', 'amount', false); const amount = this.getSum('for', 'amount', false);
if ((value as Money).gt(amount)) { if ((value as Money).gt(amount)) {
throw new frappe.errors.ValidationError( throw new ValidationError(
frappe.t`Payment amount cannot this.frappe.t`Payment amount cannot
exceed ${frappe.format(amount, 'Currency')}.` exceed ${this.frappe.format(amount, 'Currency')}.`
); );
} else if ((value as Money).isZero()) { } else if ((value as Money).isZero()) {
throw new frappe.errors.ValidationError( throw new ValidationError(
frappe.t`Payment amount cannot this.frappe.t`Payment amount cannot
be ${frappe.format(value, 'Currency')}.` be ${this.frappe.format(value, 'Currency')}.`
); );
} }
}, },
@ -353,36 +368,40 @@ export class Payment extends Doc {
}, },
}; };
static actions: Action[] = [getLedgerLinkAction()]; static getActions(frappe: Frappe): Action[] {
return [getLedgerLinkAction(frappe)];
}
static listSettings: ListViewSettings = { static getListViewSettings(frappe: Frappe): ListViewSettings {
columns: [ return {
'party', columns: [
{ 'party',
label: frappe.t`Status`, {
fieldname: 'status', label: frappe.t`Status`,
fieldtype: 'Select', fieldname: 'status',
size: 'small', fieldtype: 'Select',
render(doc) { size: 'small',
let status = 'Draft'; render(doc) {
let color = 'gray'; let status = 'Draft';
if (doc.submitted === 1) { let color = 'gray';
color = 'green'; if (doc.submitted === 1) {
status = 'Submitted'; color = 'green';
} status = 'Submitted';
if (doc.cancelled === 1) { }
color = 'red'; if (doc.cancelled === 1) {
status = 'Cancelled'; color = 'red';
} status = 'Cancelled';
}
return { return {
template: `<Badge class="text-xs" color="${color}">${status}</Badge>`, template: `<Badge class="text-xs" color="${color}">${status}</Badge>`,
}; };
},
}, },
}, 'paymentType',
'paymentType', 'date',
'date', 'amount',
'amount', ],
], };
}; }
} }

View File

@ -1,4 +1,3 @@
import frappe from 'frappe';
import Doc from 'frappe/model/doc'; import Doc from 'frappe/model/doc';
import { FiltersMap, FormulaMap } from 'frappe/model/types'; import { FiltersMap, FormulaMap } from 'frappe/model/types';
import Money from 'pesa/dist/types/src/money'; import Money from 'pesa/dist/types/src/money';
@ -16,7 +15,7 @@ export class PaymentFor extends Doc {
return outstandingAmount; return outstandingAmount;
} }
return frappe.pesa(0); return this.frappe.pesa(0);
}, },
}; };

View File

@ -1,4 +1,5 @@
import { LedgerPosting } from 'accounting/ledgerPosting'; import { LedgerPosting } from 'accounting/ledgerPosting';
import { Frappe } from 'frappe';
import { Action, ListViewSettings } from 'frappe/model/types'; import { Action, ListViewSettings } from 'frappe/model/types';
import { import {
getTransactionActions, getTransactionActions,
@ -11,10 +12,13 @@ export class PurchaseInvoice extends Invoice {
items?: PurchaseInvoiceItem[]; items?: PurchaseInvoiceItem[];
async getPosting() { async getPosting() {
const entries: LedgerPosting = new LedgerPosting({ const entries: LedgerPosting = new LedgerPosting(
reference: this, {
party: this.party, reference: this,
}); party: this.party,
},
this.frappe
);
await entries.credit(this.account!, this.baseGrandTotal!); await entries.credit(this.account!, this.baseGrandTotal!);
@ -32,17 +36,21 @@ export class PurchaseInvoice extends Invoice {
return entries; return entries;
} }
static actions: Action[] = getTransactionActions('PurchaseInvoice'); static getActions(frappe: Frappe): Action[] {
return getTransactionActions('PurchaseInvoice', frappe);
}
static listSettings: ListViewSettings = { static getListViewSettings(frappe: Frappe): ListViewSettings {
formRoute: (name) => `/edit/PurchaseInvoice/${name}`, return {
columns: [ formRoute: (name) => `/edit/PurchaseInvoice/${name}`,
'party', columns: [
'name', 'party',
getTransactionStatusColumn(), 'name',
'date', getTransactionStatusColumn(frappe),
'grandTotal', 'date',
'outstandingAmount', 'grandTotal',
], 'outstandingAmount',
}; ],
};
}
} }

View File

@ -1,4 +1,5 @@
import { LedgerPosting } from 'accounting/ledgerPosting'; import { LedgerPosting } from 'accounting/ledgerPosting';
import { Frappe } from 'frappe';
import { Action, ListViewSettings } from 'frappe/model/types'; import { Action, ListViewSettings } from 'frappe/model/types';
import { import {
getTransactionActions, getTransactionActions,
@ -11,10 +12,13 @@ export class SalesInvoice extends Invoice {
items?: SalesInvoiceItem[]; items?: SalesInvoiceItem[];
async getPosting() { async getPosting() {
const entries: LedgerPosting = new LedgerPosting({ const entries: LedgerPosting = new LedgerPosting(
reference: this, {
party: this.party, reference: this,
}); party: this.party,
},
this.frappe
);
await entries.debit(this.account!, this.baseGrandTotal!); await entries.debit(this.account!, this.baseGrandTotal!);
for (const item of this.items!) { for (const item of this.items!) {
@ -30,17 +34,21 @@ export class SalesInvoice extends Invoice {
return entries; return entries;
} }
static actions: Action[] = getTransactionActions('SalesInvoice'); static getActions(frappe: Frappe): Action[] {
return getTransactionActions('SalesInvoice', frappe);
}
static listSettings: ListViewSettings = { static getListViewSettings(frappe: Frappe): ListViewSettings {
formRoute: (name) => `/edit/SalesInvoice/${name}`, return {
columns: [ formRoute: (name) => `/edit/SalesInvoice/${name}`,
'party', columns: [
'name', 'party',
getTransactionStatusColumn(), 'name',
'date', getTransactionStatusColumn(frappe),
'grandTotal', 'date',
'outstandingAmount', 'grandTotal',
], 'outstandingAmount',
}; ],
};
}
} }

View File

@ -1,4 +1,4 @@
import frappe from 'frappe'; import { t } from 'frappe';
import Doc from 'frappe/model/doc'; import Doc from 'frappe/model/doc';
import { FormulaMap, ListsMap } from 'frappe/model/types'; import { FormulaMap, ListsMap } from 'frappe/model/types';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
@ -6,7 +6,7 @@ import countryInfo from '../../../fixtures/countryInfo.json';
export function getCOAList() { export function getCOAList() {
return [ return [
{ name: frappe.t`Standard Chart of Accounts`, countryCode: '' }, { name: t`Standard Chart of Accounts`, countryCode: '' },
{ countryCode: 'ae', name: 'U.A.E - Chart of Accounts' }, { countryCode: 'ae', name: 'U.A.E - Chart of Accounts' },
{ {

View File

@ -2,5 +2,7 @@ import Doc from 'frappe/model/doc';
import { ListViewSettings } from 'frappe/model/types'; import { ListViewSettings } from 'frappe/model/types';
export class Tax extends Doc { export class Tax extends Doc {
static listSettings: ListViewSettings = { columns: ['name'] }; static getListViewSettings(): ListViewSettings {
return { columns: ['name'] };
}
} }

View File

@ -1,12 +1,11 @@
import { openQuickEdit } from '@/utils'; import { Frappe } from 'frappe';
import frappe from 'frappe';
import Doc from 'frappe/model/doc'; import Doc from 'frappe/model/doc';
import { Action, ColumnConfig } from 'frappe/model/types'; import { Action, ColumnConfig } from 'frappe/model/types';
import Money from 'pesa/dist/types/src/money'; import Money from 'pesa/dist/types/src/money';
import { Router } from 'vue-router'; import { Router } from 'vue-router';
import { InvoiceStatus } from './types'; import { InvoiceStatus } from './types';
export function getLedgerLinkAction(): Action { export function getLedgerLinkAction(frappe: Frappe): Action {
return { return {
label: frappe.t`Ledger Entries`, label: frappe.t`Ledger Entries`,
condition: (doc: Doc) => !!doc.submitted, condition: (doc: Doc) => !!doc.submitted,
@ -26,7 +25,10 @@ export function getLedgerLinkAction(): Action {
}; };
} }
export function getTransactionActions(schemaName: string): Action[] { export function getTransactionActions(
schemaName: string,
frappe: Frappe
): Action[] {
return [ return [
{ {
label: frappe.t`Make Payment`, label: frappe.t`Make Payment`,
@ -42,6 +44,7 @@ export function getTransactionActions(schemaName: string): Action[] {
const paymentType = isSales ? 'Receive' : 'Pay'; const paymentType = isSales ? 'Receive' : 'Pay';
const hideAccountField = isSales ? 'account' : 'paymentAccount'; const hideAccountField = isSales ? 'account' : 'paymentAccount';
const { openQuickEdit } = await import('../src/utils');
await openQuickEdit({ await openQuickEdit({
schemaName: 'Payment', schemaName: 'Payment',
name: payment.name as string, name: payment.name as string,
@ -69,11 +72,11 @@ export function getTransactionActions(schemaName: string): Action[] {
router.push({ path: `/print/${doc.doctype}/${doc.name}` }); router.push({ path: `/print/${doc.doctype}/${doc.name}` });
}, },
}, },
getLedgerLinkAction(), getLedgerLinkAction(frappe),
]; ];
} }
export function getTransactionStatusColumn(): ColumnConfig { export function getTransactionStatusColumn(frappe: Frappe): ColumnConfig {
const statusMap = { const statusMap = {
Unpaid: frappe.t`Unpaid`, Unpaid: frappe.t`Unpaid`,
Paid: frappe.t`Paid`, Paid: frappe.t`Paid`,

View File

@ -1,3 +1,4 @@
import { ModelMap } from 'frappe/model/types';
import { Account } from './baseModels/Account/Account'; import { Account } from './baseModels/Account/Account';
import { AccountingLedgerEntry } from './baseModels/AccountingLedgerEntry/AccountingLedgerEntry'; import { AccountingLedgerEntry } from './baseModels/AccountingLedgerEntry/AccountingLedgerEntry';
import { AccountingSettings } from './baseModels/AccountingSettings/AccountingSettings'; import { AccountingSettings } from './baseModels/AccountingSettings/AccountingSettings';
@ -34,9 +35,11 @@ export default {
SetupWizard, SetupWizard,
Tax, Tax,
TaxSummary, TaxSummary,
}; } as ModelMap;
export async function getRegionalModels(countryCode: string) { export async function getRegionalModels(
countryCode: string
): Promise<ModelMap> {
if (countryCode !== 'in') { if (countryCode !== 'in') {
return {}; return {};
} }

View File

@ -3,7 +3,7 @@ import { Party as BaseParty } from 'models/baseModels/Party/Party';
import { GSTType } from './types'; import { GSTType } from './types';
export class Party extends BaseParty { export class Party extends BaseParty {
beforeInsert() { async beforeInsert() {
const gstin = this.get('gstin') as string | undefined; const gstin = this.get('gstin') as string | undefined;
const gstType = this.get('gstType') as GSTType; const gstType = this.get('gstType') as GSTType;

View File

@ -1 +1,31 @@
export type InvoiceStatus = 'Draft' | 'Unpaid' | 'Cancelled' | 'Paid'; export type InvoiceStatus = 'Draft' | 'Unpaid' | 'Cancelled' | 'Paid';
export enum ModelNameEnum {
Account = 'Account',
AccountingLedgerEntry = 'AccountingLedgerEntry',
AccountingSettings = 'AccountingSettings',
Address = 'Address',
Color = 'Color',
CompanySettings = 'CompanySettings',
Currency = 'Currency',
GetStarted = 'GetStarted',
Item = 'Item',
JournalEntry = 'JournalEntry',
JournalEntryAccount = 'JournalEntryAccount',
NumberSeries = 'NumberSeries',
Party = 'Party',
Payment = 'Payment',
PaymentFor = 'PaymentFor',
PrintSettings = 'PrintSettings',
PurchaseInvoice = 'PurchaseInvoice',
PurchaseInvoiceItem = 'PurchaseInvoiceItem',
SalesInvoice = 'SalesInvoice',
SalesInvoiceItem = 'SalesInvoiceItem',
SalesInvoiceSettings = 'SalesInvoiceSettings',
SetupWizard = 'SetupWizard',
Tax = 'Tax',
TaxDetail = 'TaxDetail',
TaxSummary = 'TaxSummary',
PatchRun = 'PatchRun',
SingleValue = 'SingleValue',
SystemSettings = 'SystemSettings',
}

View File

@ -149,8 +149,5 @@
"accountType", "accountType",
"isGroup" "isGroup"
], ],
"keywordFields": ["name", "rootType", "accountType"], "keywordFields": ["name", "rootType", "accountType"]
"treeSettings": {
"parentField": "parentAccount"
}
} }

View File

@ -67,7 +67,7 @@ function deepFreeze(schemaMap: SchemaMap) {
} }
export function addMetaFields(schemaMap: SchemaMap): SchemaMap { export function addMetaFields(schemaMap: SchemaMap): SchemaMap {
const metaSchemaMap = getMapFromList(metaSchemas, 'name'); const metaSchemaMap = getMapFromList(cloneDeep(metaSchemas), 'name');
const base = metaSchemaMap.base; const base = metaSchemaMap.base;
const tree = getCombined(metaSchemaMap.tree, base); const tree = getCombined(metaSchemaMap.tree, base);
@ -115,13 +115,13 @@ function addNameField(schemaMap: SchemaMap) {
} }
function getCoreSchemas(): SchemaMap { function getCoreSchemas(): SchemaMap {
const rawSchemaMap = getMapFromList(coreSchemas, 'name'); const rawSchemaMap = getMapFromList(cloneDeep(coreSchemas), 'name');
const coreSchemaMap = getAbstractCombinedSchemas(rawSchemaMap); const coreSchemaMap = getAbstractCombinedSchemas(rawSchemaMap);
return cleanSchemas(coreSchemaMap); return cleanSchemas(coreSchemaMap);
} }
function getAppSchemas(countryCode: string): SchemaMap { function getAppSchemas(countryCode: string): SchemaMap {
const appSchemaMap = getMapFromList(appSchemas, 'name'); const appSchemaMap = getMapFromList(cloneDeep(appSchemas), 'name');
const regionalSchemaMap = getRegionalSchemaMap(countryCode); const regionalSchemaMap = getRegionalSchemaMap(countryCode);
const combinedSchemas = getRegionalCombinedSchemas( const combinedSchemas = getRegionalCombinedSchemas(
appSchemaMap, appSchemaMap,
@ -225,7 +225,7 @@ export function getRegionalCombinedSchemas(
} }
function getRegionalSchemaMap(countryCode: string): SchemaStubMap { function getRegionalSchemaMap(countryCode: string): SchemaStubMap {
const countrySchemas = regionalSchemas[countryCode] as const countrySchemas = cloneDeep(regionalSchemas[countryCode]) as
| SchemaStub[] | SchemaStub[]
| undefined; | undefined;
if (countrySchemas === undefined) { if (countrySchemas === undefined) {

View File

@ -68,7 +68,6 @@ export type Field =
| DynamicLinkField | DynamicLinkField
| NumberField; | NumberField;
export type TreeSettings = { parentField: string };
export type Naming = 'autoincrement' | 'random' | 'numberSeries' export type Naming = 'autoincrement' | 'random' | 'numberSeries'
export interface Schema { export interface Schema {
@ -84,7 +83,6 @@ export interface Schema {
isSubmittable?: boolean; // For transactional types, values considered only after submit isSubmittable?: boolean; // For transactional types, values considered only after submit
keywordFields?: string[]; // Used to get fields that are to be used for search. keywordFields?: string[]; // Used to get fields that are to be used for search.
quickEditFields?: string[]; // Used to get fields for the quickEditForm quickEditFields?: string[]; // Used to get fields for the quickEditForm
treeSettings?: TreeSettings; // Used to determine root nodes
inlineEditDisplayField?:string;// Display field if inline editable inlineEditDisplayField?:string;// Display field if inline editable
naming?: Naming; // Used for assigning name, default is 'random' else 'numberSeries' if present naming?: Naming; // Used for assigning name, default is 'random' else 'numberSeries' if present
removeFields?: string[]; // Used by the builder to remove fields. removeFields?: string[]; // Used by the builder to remove fields.

View File

@ -1,26 +0,0 @@
import Store from 'electron-store';
import frappe from 'frappe';
const config = new Store();
export default config;
export enum ConfigKeys {
Files = 'files',
LastSelectedFilePath = 'lastSelectedFilePath',
Language = 'language',
DeviceId = 'deviceId',
Telemetry = 'telemetry',
OpenCount = 'openCount',
}
export enum TelemetrySetting {
allow = 'allow',
dontLogUsage = 'dontLogUsage',
dontLogAnything = 'dontLogAnything',
}
export interface ConfigFile {
id: string;
companyName: string;
filePath: string;
}

View File

@ -4,8 +4,8 @@ import Doc from 'frappe/model/doc';
import { isNameAutoSet } from 'frappe/model/naming'; import { isNameAutoSet } from 'frappe/model/naming';
import { FieldType, FieldTypeEnum } from 'schemas/types'; import { FieldType, FieldTypeEnum } from 'schemas/types';
import { parseCSV } from '../utils/csvParser'; import { parseCSV } from '../utils/csvParser';
import telemetry from './telemetry/telemetry'; import telemetry from '../frappe/telemetry/telemetry';
import { Noun, Verb } from './telemetry/types'; import { Noun, Verb } from '../frappe/telemetry/types';
export const importable = [ export const importable = [
'SalesInvoice', 'SalesInvoice',

View File

@ -9,9 +9,9 @@ import {
} from 'frappe/utils/errors'; } from 'frappe/utils/errors';
import { ErrorLog } from 'frappe/utils/types'; import { ErrorLog } from 'frappe/utils/types';
import { IPC_ACTIONS, IPC_MESSAGES } from 'utils/messages'; import { IPC_ACTIONS, IPC_MESSAGES } from 'utils/messages';
import config, { ConfigKeys, TelemetrySetting } from './config'; import telemetry from '../frappe/telemetry/telemetry';
import telemetry from './telemetry/telemetry'; import config, { ConfigKeys, TelemetrySetting } from '../utils/config';
import { showMessageDialog, showToast } from './utils'; import { showMessageDialog, showToast } from './utils.js';
function getCanLog(): boolean { function getCanLog(): boolean {
const telemetrySetting = config.get(ConfigKeys.Telemetry); const telemetrySetting = config.get(ConfigKeys.Telemetry);

3
src/initFyo.ts Normal file
View File

@ -0,0 +1,3 @@
import { Frappe } from 'frappe';
export const fyo = new Frappe({ isTest: false, isElectron: true });

View File

@ -1,6 +1,7 @@
import frappe from 'frappe';
import { createNumberSeries } from 'frappe/model/naming'; import { createNumberSeries } from 'frappe/model/naming';
import { DEFAULT_SERIES_START } from 'frappe/utils/consts';
import { getValueMapFromList } from 'utils'; import { getValueMapFromList } from 'utils';
import { fyo } from './initFyo';
export default async function postStart() { export default async function postStart() {
await createDefaultNumberSeries(); await createDefaultNumberSeries();
@ -9,23 +10,28 @@ export default async function postStart() {
} }
async function createDefaultNumberSeries() { async function createDefaultNumberSeries() {
await createNumberSeries('SINV-', 'SalesInvoice'); await createNumberSeries('SINV-', 'SalesInvoice', DEFAULT_SERIES_START, fyo);
await createNumberSeries('PINV-', 'PurchaseInvoice'); await createNumberSeries(
await createNumberSeries('PAY-', 'Payment'); 'PINV-',
await createNumberSeries('JV-', 'JournalEntry'); 'PurchaseInvoice',
DEFAULT_SERIES_START,
fyo
);
await createNumberSeries('PAY-', 'Payment', DEFAULT_SERIES_START, fyo);
await createNumberSeries('JV-', 'JournalEntry', DEFAULT_SERIES_START, fyo);
} }
async function setSingles() { async function setSingles() {
await frappe.doc.getSingle('AccountingSettings'); await fyo.doc.getSingle('AccountingSettings');
await frappe.doc.getSingle('GetStarted'); await fyo.doc.getSingle('GetStarted');
} }
async function setCurrencySymbols() { async function setCurrencySymbols() {
const currencies = (await frappe.db.getAll('Currency', { const currencies = (await fyo.db.getAll('Currency', {
fields: ['name', 'symbol'], fields: ['name', 'symbol'],
})) as { name: string; symbol: string }[]; })) as { name: string; symbol: string }[];
frappe.currencySymbols = getValueMapFromList( fyo.currencySymbols = getValueMapFromList(
currencies, currencies,
'name', 'name',
'symbol' 'symbol'

View File

@ -1,12 +1,13 @@
import config, { ConfigKeys } from '@/config'; import frappe from 'frappe';
import { ConfigKeys } from 'frappe/core/types';
export function incrementOpenCount() { export function incrementOpenCount() {
let openCount = config.get(ConfigKeys.OpenCount); let openCount = frappe.config.get(ConfigKeys.OpenCount);
if (typeof openCount !== 'number') { if (typeof openCount !== 'number') {
openCount = 1; openCount = 1;
} else { } else {
openCount += 1; openCount += 1;
} }
config.set(ConfigKeys.OpenCount, openCount); frappe.config.set(ConfigKeys.OpenCount, openCount);
} }

View File

@ -1,6 +1,6 @@
import { handleError } from '@/errorHandling'; import { handleError } from '@/errorHandling';
import { IPC_CHANNELS, IPC_MESSAGES } from 'utils/messages'; import { IPC_CHANNELS, IPC_MESSAGES } from 'utils/messages';
import telemetry from '@/telemetry/telemetry'; import telemetry from 'frappe/telemetry/telemetry';
import { showToast } from '@/utils'; import { showToast } from '@/utils';
import { ipcRenderer } from 'electron'; import { ipcRenderer } from 'electron';
import frappe from 'frappe'; import frappe from 'frappe';

View File

@ -10,8 +10,8 @@ import QuickEditForm from '@/pages/QuickEditForm.vue';
import Report from '@/pages/Report.vue'; import Report from '@/pages/Report.vue';
import Settings from '@/pages/Settings/Settings.vue'; import Settings from '@/pages/Settings/Settings.vue';
import { createRouter, createWebHistory } from 'vue-router'; import { createRouter, createWebHistory } from 'vue-router';
import telemetry from './telemetry/telemetry'; import telemetry from '../frappe/telemetry/telemetry';
import { NounEnum, Verb } from './telemetry/types'; import { NounEnum, Verb } from '../frappe/telemetry/types';
const routes = [ const routes = [
{ {

View File

@ -1,129 +0,0 @@
import config, { ConfigFile, ConfigKeys, TelemetrySetting } from '@/config';
import { ipcRenderer } from 'electron';
import frappe, { t } from 'frappe';
import { IPC_ACTIONS } from 'utils/messages';
import { DoctypeName } from '../../models/types';
import { Count, UniqueId } from './types';
export function getId(): string {
let id: string = '';
for (let i = 0; i < 4; i++) {
id += Math.random().toString(36).slice(2, 9);
}
return id;
}
export function getCountry(): string {
// @ts-ignore
return frappe.AccountingSettings?.country ?? '';
}
export function getLanguage(): string {
return config.get('language') as string;
}
export async function getCounts(): Promise<Count> {
const interestingDocs = [
DoctypeName.Payment,
DoctypeName.PaymentFor,
DoctypeName.SalesInvoice,
DoctypeName.SalesInvoiceItem,
DoctypeName.PurchaseInvoice,
DoctypeName.PurchaseInvoiceItem,
DoctypeName.JournalEntry,
DoctypeName.JournalEntryAccount,
DoctypeName.Party,
DoctypeName.Account,
DoctypeName.Tax,
];
const countMap: Count = {};
// @ts-ignore
if (frappe.db === undefined) {
return countMap;
}
type CountResponse = { 'count(*)': number }[];
for (const name of interestingDocs) {
const count: number = (await frappe.db.getAll(name)).length;
countMap[name] = count;
}
return countMap;
}
export function getDeviceId(): UniqueId {
let deviceId = config.get(ConfigKeys.DeviceId) as string | undefined;
if (deviceId === undefined) {
deviceId = getId();
config.set(ConfigKeys.DeviceId, deviceId);
}
return deviceId;
}
export function getInstanceId(): UniqueId {
const files = config.get(ConfigKeys.Files) as ConfigFile[];
// @ts-ignore
const companyName = frappe.AccountingSettings?.companyName;
if (companyName === undefined) {
return '';
}
const file = files.find((f) => f.companyName === companyName);
if (file === undefined) {
return addNewFile(companyName, files);
}
if (file.id === undefined) {
return setInstanceId(companyName, files);
}
return file.id;
}
function addNewFile(companyName: string, files: ConfigFile[]): UniqueId {
const newFile: ConfigFile = {
companyName,
filePath: config.get(ConfigKeys.LastSelectedFilePath, '') as string,
id: getId(),
};
files.push(newFile);
config.set(ConfigKeys.Files, files);
return newFile.id;
}
function setInstanceId(companyName: string, files: ConfigFile[]): UniqueId {
let id = '';
for (const file of files) {
if (file.id) {
continue;
}
file.id = getId();
if (file.companyName === companyName) {
id = file.id;
}
}
config.set(ConfigKeys.Files, files);
return id;
}
export async function getCreds() {
const creds = await ipcRenderer.invoke(IPC_ACTIONS.GET_CREDS);
const url: string = creds?.telemetryUrl ?? '';
const token: string = creds?.tokenString ?? '';
return { url, token };
}
export const getTelemetryOptions = () => ({
[TelemetrySetting.allow]: t`Allow Telemetry`,
[TelemetrySetting.dontLogUsage]: t`Don't Log Usage`,
[TelemetrySetting.dontLogAnything]: t`Don't Log Anything`,
});

View File

@ -44,6 +44,6 @@
"scripts/**/*.ts", "scripts/**/*.ts",
"utils/csvParser.ts" "utils/csvParser.ts"
], , "utils/config.ts" ],
"exclude": ["node_modules"] "exclude": ["node_modules"]
} }

5
utils/auth/types.ts Normal file
View File

@ -0,0 +1,5 @@
export type TelemetryCreds = { url: string; token: string };
export abstract class AuthDemuxBase {
abstract getTelemetryCreds(): Promise<TelemetryCreds>
}

4
utils/config.ts Normal file
View File

@ -0,0 +1,4 @@
import Store from 'electron-store';
const config = new Store();
export default config;