2
0
mirror of https://github.com/frappe/books.git synced 2024-12-22 10:58:59 +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';
export async function getExchangeRate({
@ -15,7 +15,7 @@ export async function getExchangeRate({
}
if (!fromCurrency || !toCurrency) {
throw new frappe.errors.NotFoundError(
throw new NotFoundError(
'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 { ValidationError } from 'frappe/utils/errors';
import Money from 'pesa/dist/types/src/money';
import {
AccountEntry,
@ -18,7 +19,12 @@ export class LedgerPosting {
reverted: boolean;
accountEntries: AccountEntry[];
constructor({ reference, party, date, description }: LedgerPostingOptions) {
frappe: Frappe;
constructor(
{ reference, party, date, description }: LedgerPostingOptions,
frappe: Frappe
) {
this.reference = reference;
this.party = party;
this.date = date;
@ -28,6 +34,8 @@ export class LedgerPosting {
this.reverted = false;
// To change balance while entering ledger entries
this.accountEntries = [];
this.frappe = frappe;
}
async debit(
@ -58,7 +66,7 @@ export class LedgerPosting {
amount: Money
) {
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;
if (debitAccounts.indexOf(rootType) === -1) {
@ -86,8 +94,8 @@ export class LedgerPosting {
referenceName: referenceName ?? this.reference.name!,
description: this.description,
reverted: this.reverted,
debit: frappe.pesa(0),
credit: frappe.pesa(0),
debit: this.frappe.pesa(0),
credit: this.frappe.pesa(0),
};
this.entries.push(entry);
@ -105,7 +113,7 @@ export class LedgerPosting {
async postReverse() {
this.validateEntries();
const data = await frappe.db.getAll('AccountingLedgerEntry', {
const data = await this.frappe.db.getAll('AccountingLedgerEntry', {
fields: ['name'],
filters: {
referenceName: this.reference.name!,
@ -114,7 +122,7 @@ export class LedgerPosting {
});
for (const entry of data) {
const entryDoc = await frappe.doc.getDoc(
const entryDoc = await this.frappe.doc.getDoc(
'AccountingLedgerEntry',
entry.name as string
);
@ -157,18 +165,21 @@ export class LedgerPosting {
validateEntries() {
const { debit, credit } = this.getTotalDebitAndCredit();
if (debit.neq(credit)) {
throw new frappe.errors.ValidationError(
`Total Debit: ${frappe.format(
throw new ValidationError(
`Total Debit: ${this.frappe.format(
debit,
'Currency'
)} must be equal to Total Credit: ${frappe.format(credit, 'Currency')}`
)} must be equal to Total Credit: ${this.frappe.format(
credit,
'Currency'
)}`
);
}
}
getTotalDebitAndCredit() {
let debit = frappe.pesa(0);
let credit = frappe.pesa(0);
let debit = this.frappe.pesa(0);
let credit = this.frappe.pesa(0);
for (const entry of this.entries) {
debit = debit.add(entry.debit);
@ -180,12 +191,12 @@ export class LedgerPosting {
async insertEntries() {
for (const entry of this.entries) {
const entryDoc = frappe.doc.getNewDoc('AccountingLedgerEntry');
const entryDoc = this.frappe.doc.getNewDoc('AccountingLedgerEntry');
Object.assign(entryDoc, entry);
await entryDoc.insert();
}
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;
entryDoc.balance = balance.add(entry.balanceChange);
await entryDoc.update();
@ -193,6 +204,6 @@ export class LedgerPosting {
}
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 { DatabaseBase, GetAllOptions, QueryFilter } from '../../utils/db/types';
import { getDefaultMetaFieldValueMap, sqliteTypeMap, SYSTEM } from '../helpers';
import { ColumnDiff, FieldValueMap, GetQueryBuilderOptions } from './types';
import {
ColumnDiff,
FieldValueMap,
GetQueryBuilderOptions,
SingleValue,
} from './types';
/**
* # DatabaseCore
@ -256,7 +261,7 @@ export default class DatabaseCore extends DatabaseBase {
async getSingleValues(
...fieldnames: ({ fieldname: string; parent?: string } | string)[]
): Promise<{ fieldname: string; parent: string; value: RawValue }[]> {
): Promise<SingleValue<RawValue>> {
const fieldnameList = fieldnames.map((fieldname) => {
if (typeof fieldname === 'string') {
return { fieldname };

View File

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

View File

@ -45,4 +45,9 @@ export interface SqliteTableInfo {
dflt_value: string | null;
}
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.
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
backend.
@ -70,6 +70,9 @@ other things.
- Get models and `regionalModels` using `countryCode` from `models/index.ts/getRegionalModels`.
- 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
For testing the `fyo` class, `mocha` is used (`node` side). So for this the
@ -77,15 +80,18 @@ demux classes are directly replaced by `node` side managers such as
`DatabaseManager`.
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
All translations take place during runtime, for translations to work, a
`LanguageMap` (for def check `utils/types.ts`) has to be set.
`LanguageMap` (for def check `utils/types.ts`) has to be set.
This can be done using `fyo/utils/translation.ts/setLanguageMapOnTranslationString`.
Since translations are runtime, if the code is evaluated before the language map
is loaded, translations won't work. To prevent this, don't maintain translation
strings globally.
strings globally.

View File

@ -1,4 +1,7 @@
import { Frappe } from 'frappe';
import { AuthDemux } from 'frappe/demux/auth';
import { AuthDemuxBase, TelemetryCreds } from 'utils/auth/types';
import { AuthDemuxConstructor } from './types';
interface AuthConfig {
serverURL: string;
@ -15,8 +18,9 @@ export class AuthHandler {
#config: AuthConfig;
#session: Session;
frappe: Frappe;
#demux: AuthDemuxBase;
constructor(frappe: Frappe) {
constructor(frappe: Frappe, Demux?: AuthDemuxConstructor) {
this.frappe = frappe;
this.#config = {
serverURL: '',
@ -28,6 +32,12 @@ export class AuthHandler {
user: '',
token: '',
};
if (Demux !== undefined) {
this.#demux = new Demux(frappe.isElectron);
} else {
this.#demux = new AuthDemux(frappe.isElectron);
}
}
get session(): Readonly<Session> {
@ -90,4 +100,8 @@ export class AuthHandler {
#getServerURL() {
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 Money from 'pesa/dist/types/src/money';
import { FieldType, FieldTypeEnum, RawValue } from 'schemas/types';
@ -22,9 +22,11 @@ import { DocValue, DocValueMap, RawValueMap } from './types';
export class Converter {
db: DatabaseHandler;
frappe: Frappe;
constructor(db: DatabaseHandler) {
constructor(db: DatabaseHandler, frappe: Frappe) {
this.db = db;
this.frappe = frappe;
}
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) {
case FieldTypeEnum.Currency:
return frappe.pesa((value ?? 0) as string | number);
@ -112,7 +118,8 @@ export class Converter {
} else {
docValueMap[fieldname] = Converter.toDocValue(
rawValue,
field.fieldtype
field.fieldtype,
this.frappe
);
}
}

View File

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

View File

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

View File

@ -1,21 +1,45 @@
import Doc from 'frappe/model/doc';
import Money from 'pesa/dist/types/src/money';
import { RawValue } from 'schemas/types';
import { AuthDemuxBase } from 'utils/auth/types';
import { DatabaseDemuxBase } from 'utils/db/types';
export type DocValue = string | number | boolean | Date | Money | null;
export type DocValueMap = Record<string, DocValue | Doc[] | DocValueMap[]>;
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
* it's typed this way because `typeof AbstractClass` is invalid as abstract classes
* 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 { Field } from 'schemas/types';
import { markRaw } from 'vue';
import { AuthHandler } from './core/authHandler';
import { DatabaseHandler } from './core/dbHandler';
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 { TelemetryManager } from './telemetry/telemetry';
import {
DEFAULT_CURRENCY,
DEFAULT_DISPLAY_PRECISION,
@ -18,10 +22,9 @@ import { ErrorLog } from './utils/types';
export class Frappe {
t = t;
T = T;
format = format;
errors = errors;
isElectron = false;
isElectron: boolean;
pesa: MoneyMaker;
@ -38,21 +41,27 @@ export class Frappe {
currencyFormatter?: Intl.NumberFormat;
currencySymbols: Record<string, string | undefined> = {};
constructor(DatabaseDemux?: DatabaseDemuxConstructor) {
/**
* `DatabaseManager` can be passed as the `DatabaseDemux` for
* testing this class without API or IPC calls.
*/
this.auth = new AuthHandler(this);
this.db = new DatabaseHandler(this, DatabaseDemux);
isTest: boolean;
telemetry: TelemetryManager;
config: Config;
constructor(conf: FyoConfig = {}) {
this.isTest = conf.isTest ?? false;
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.pesa = getMoneyMaker({
currency: 'XXX',
currency: DEFAULT_CURRENCY,
precision: DEFAULT_INTERNAL_PRECISION,
display: DEFAULT_DISPLAY_PRECISION,
wrapper: markRaw,
});
this.telemetry = new TelemetryManager(this);
this.config = new Config(this.isElectron);
}
get initialized() {
@ -75,6 +84,19 @@ export class Frappe {
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(
models: ModelMap = {},
regionalModels: ModelMap = {},
@ -138,9 +160,9 @@ export class Frappe {
});
}
close() {
this.db.close();
this.auth.logout();
async close() {
await this.db.close();
await this.auth.logout();
}
store = {
@ -150,4 +172,3 @@ export class Frappe {
}
export { T, t };
export default new Frappe();

View File

@ -1,6 +1,6 @@
import telemetry from '@/telemetry/telemetry';
import { Verb } from '@/telemetry/types';
import { Frappe } from 'frappe';
import { DocValue, DocValueMap } from 'frappe/core/types';
import { Verb } from 'frappe/telemetry/types';
import {
Conflict,
MandatoryError,
@ -17,7 +17,6 @@ import {
TargetField,
} from 'schemas/types';
import { getIsNullOrUndef, getMapFromList } from 'utils';
import frappe from '..';
import { getRandomString, isPesa } from '../utils/index';
import {
areDocValuesEqual,
@ -47,6 +46,7 @@ import { validateSelect } from './validationFunction';
export default class Doc extends Observable<DocValue | Doc[]> {
name?: string;
schema: Readonly<Schema>;
frappe: Frappe;
fieldMap: Record<string, Field>;
/**
@ -66,11 +66,16 @@ export default class Doc extends Observable<DocValue | Doc[]> {
revertAction: false,
};
constructor(schema: Schema, data: DocValueMap) {
constructor(schema: Schema, data: DocValueMap, frappe: Frappe) {
super();
this.frappe = frappe;
this.schema = schema;
this._setInitialValues(data);
this.fieldMap = getMapFromList(schema.fields, 'fieldname');
if (this.schema.isSingle) {
this.name = this.schemaName;
}
}
get schemaName(): string {
@ -183,7 +188,10 @@ export default class Doc extends Observable<DocValue | Doc[]> {
continue;
}
let defaultValue: DocValue | Doc[] = getPreDefaultValues(field.fieldtype);
let defaultValue: DocValue | Doc[] = getPreDefaultValues(
field.fieldtype,
this.frappe
);
const defaultFunction = this.defaults[field.fieldname];
if (defaultFunction !== undefined) {
@ -193,7 +201,7 @@ export default class Doc extends Observable<DocValue | Doc[]> {
}
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;
@ -235,8 +243,8 @@ export default class Doc extends Observable<DocValue | Doc[]> {
}
const childSchemaName = this.fieldMap[fieldname] as TargetField;
const schema = frappe.db.schemaMap[childSchemaName.target] as Schema;
const childDoc = new Doc(schema, data as DocValueMap);
const schema = this.frappe.db.schemaMap[childSchemaName.target] as Schema;
const childDoc = new Doc(schema, data as DocValueMap, this.frappe);
childDoc.setDefaults();
return childDoc;
}
@ -263,7 +271,7 @@ export default class Doc extends Observable<DocValue | Doc[]> {
if (missingMandatoryMessage.length > 0) {
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);
}
}
@ -322,7 +330,7 @@ export default class Doc extends Observable<DocValue | Doc[]> {
}
if (!this.createdBy) {
this.createdBy = frappe.auth.session.user;
this.createdBy = this.frappe.auth.session.user;
}
if (!this.created) {
@ -333,7 +341,7 @@ export default class Doc extends Observable<DocValue | Doc[]> {
}
updateModified() {
this.modifiedBy = frappe.auth.session.user;
this.modifiedBy = this.frappe.auth.session.user;
this.modified = new Date();
}
@ -342,7 +350,11 @@ export default class Doc extends Observable<DocValue | Doc[]> {
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) {
this.syncValues(data);
if (this.schema.isSingle) {
@ -375,7 +387,7 @@ export default class Doc extends Observable<DocValue | Doc[]> {
return;
}
this._links[fieldname] = await frappe.doc.getDoc(
this._links[fieldname] = await this.frappe.doc.getDoc(
field.target,
value as string
);
@ -422,7 +434,7 @@ export default class Doc extends Observable<DocValue | Doc[]> {
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
if (
@ -430,13 +442,14 @@ export default class Doc extends Observable<DocValue | Doc[]> {
(this.modified as Date) !== (currentDoc.modified as Date)
) {
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) {
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() {
await setName(this);
await setName(this, this.frappe);
this.setBaseMetaValues();
await this.commit();
await this.validateInsert();
await this.trigger('beforeInsert', null);
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);
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('afterSave', null);
telemetry.log(Verb.Created, this.schemaName);
this.frappe.telemetry.log(Verb.Created, this.schemaName);
return this;
}
@ -566,7 +582,7 @@ export default class Doc extends Observable<DocValue | Doc[]> {
this.updateModified();
const data = this.getValidDict();
await frappe.db.update(this.schemaName, data);
await this.frappe.db.update(this.schemaName, data);
this.syncValues(data);
await this.trigger('afterUpdate');
@ -589,10 +605,10 @@ export default class Doc extends Observable<DocValue | Doc[]> {
async delete() {
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');
telemetry.log(Verb.Deleted, this.schemaName);
this.frappe.telemetry.log(Verb.Deleted, this.schemaName);
}
async submitOrRevert(isSubmit: boolean) {
@ -617,7 +633,7 @@ export default class Doc extends Observable<DocValue | Doc[]> {
async rename(newName: string) {
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;
await this.trigger('afterRename');
}
@ -637,7 +653,7 @@ export default class Doc extends Observable<DocValue | Doc[]> {
const value = d.get(childfield) ?? 0;
if (!isPesa(value)) {
try {
return frappe.pesa(value as string | number);
return this.frappe.pesa(value as string | number);
} catch (err) {
(
err as Error
@ -647,7 +663,7 @@ export default class Doc extends Observable<DocValue | Doc[]> {
}
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) {
return sum.float;
@ -660,7 +676,7 @@ export default class Doc extends Observable<DocValue | Doc[]> {
return '';
}
return frappe.doc.getCachedValue(schemaName, name, fieldname);
return this.frappe.doc.getCachedValue(schemaName, name, fieldname);
}
async duplicate(shouldInsert: boolean = true): Promise<Doc> {
@ -690,7 +706,7 @@ export default class Doc extends Observable<DocValue | Doc[]> {
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);
if (shouldInsert) {
@ -700,6 +716,16 @@ export default class Doc extends Observable<DocValue | Doc[]> {
return doc;
}
async beforeInsert() {}
async afterInsert() {}
async beforeUpdate() {}
async afterUpdate() {}
async afterSave() {}
async beforeDelete() {}
async afterDelete() {}
async beforeRevert() {}
async afterRevert() {}
formulas: FormulaMap = {};
defaults: DefaultMap = {};
validations: ValidationMap = {};
@ -711,8 +737,14 @@ export default class Doc extends Observable<DocValue | Doc[]> {
static lists: ListsMap = {};
static filters: FiltersMap = {};
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 { isPesa } from 'frappe/utils';
import { isEqual } from 'lodash';
@ -26,7 +26,10 @@ export function areDocValuesEqual(
return isEqual(dvOne, dvTwo);
}
export function getPreDefaultValues(fieldtype: FieldType): DocValue | Doc[] {
export function getPreDefaultValues(
fieldtype: FieldType,
frappe: Frappe
): DocValue | Doc[] {
switch (fieldtype) {
case FieldTypeEnum.Table:
return [] as Doc[];

View File

@ -1,6 +1,7 @@
import frappe from 'frappe';
import { Frappe } from 'frappe';
import NumberSeries from 'frappe/models/NumberSeries';
import { getRandomString } from 'frappe/utils';
import { DEFAULT_SERIES_START } from 'frappe/utils/consts';
import { BaseError } from 'frappe/utils/errors';
import { Field, Schema } from 'schemas/types';
import Doc from './doc';
@ -12,7 +13,7 @@ export function getNumberSeries(schema: Schema): Field | undefined {
return numberSeries;
}
export function isNameAutoSet(schemaName: string): boolean {
export function isNameAutoSet(schemaName: string, frappe: Frappe): boolean {
const schema = frappe.schemaMap[schemaName]!;
if (schema.naming === 'autoincrement') {
return true;
@ -26,17 +27,17 @@ export function isNameAutoSet(schemaName: string): boolean {
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 (doc.schema.naming === 'autoincrement') {
doc.name = await getNextId(doc.schemaName);
doc.name = await getNextId(doc.schemaName, frappe);
return;
}
// Current, per doc number series
const numberSeries = doc.numberSeries as string | undefined;
if (numberSeries !== undefined) {
doc.name = await getSeriesNext(numberSeries, doc.schemaName);
doc.name = await getSeriesNext(numberSeries, doc.schemaName, frappe);
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
const lastInserted = await getLastInserted(schemaName);
const lastInserted = await getLastInserted(schemaName, frappe);
let name = 1;
if (lastInserted) {
let lastNumber = parseInt(lastInserted.name as string);
@ -69,7 +70,7 @@ export async function getNextId(schemaName: string) {
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, {
fields: ['name'],
limit: 1,
@ -79,7 +80,11 @@ export async function getLastInserted(schemaName: string) {
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;
try {
@ -90,7 +95,7 @@ export async function getSeriesNext(prefix: string, schemaName: string) {
throw e;
}
await createNumberSeries(prefix, schemaName);
await createNumberSeries(prefix, schemaName, DEFAULT_SERIES_START, frappe);
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(
prefix: string,
referenceType: string,
start = 1001
start: number,
frappe: Frappe
) {
const exists = await frappe.db.exists('NumberSeries', prefix);
if (exists) {

View File

@ -1,3 +1,4 @@
import { Frappe } from 'frappe';
import { DocValue, DocValueMap } from 'frappe/core/types';
import SystemSettings from 'frappe/models/SystemSettings';
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 interface SinglesMap {
SystemSettings?: SystemSettings
[key: string]: Doc | undefined
SystemSettings?: SystemSettings;
[key: string]: Doc | undefined;
}
// Static Config properties
@ -50,7 +51,7 @@ export interface SinglesMap {
export type FilterFunction = (doc: Doc) => QueryFilter;
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 ListFunction = (doc?: Doc) => string[];

View File

@ -1,4 +1,3 @@
import frappe from 'frappe';
import Doc from 'frappe/model/doc';
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);
return await frappe.db.exists(schemaName, name);
return await this.frappe.db.exists(schemaName, name);
}
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 {
getCountry,
getCounts,
getCreds,
getDeviceId,
getInstanceId,
getLanguage,
} from './helpers';
import { Noun, NounEnum, Platform, Telemetry, Verb } from './types';
import {
Noun,
NounEnum,
Platform,
Telemetry,
TelemetrySetting,
Verb,
} from './types';
/**
* # Telemetry
@ -44,16 +50,26 @@ import { Noun, NounEnum, Platform, Telemetry, Verb } from './types';
* telemetry and not app usage.
*/
class TelemetryManager {
export class TelemetryManager {
#url: string = '';
#token: string = '';
#started = false;
#telemetryObject: Partial<Telemetry> = {};
#interestingDocs: string[] = [];
frappe: Frappe;
constructor(frappe: Frappe) {
this.frappe = frappe;
}
set platform(value: Platform) {
this.#telemetryObject.platform ||= value;
}
set interestingDocs(schemaNames: string[]) {
this.#interestingDocs = schemaNames;
}
get hasCreds() {
return !!this.#url && !!this.#token;
}
@ -68,9 +84,9 @@ class TelemetryManager {
async start() {
this.#telemetryObject.country ||= getCountry();
this.#telemetryObject.language ??= getLanguage();
this.#telemetryObject.deviceId ||= getDeviceId();
this.#telemetryObject.instanceId ||= getInstanceId();
this.#telemetryObject.language ??= getLanguage(this.frappe);
this.#telemetryObject.deviceId ||= getDeviceId(this.frappe);
this.#telemetryObject.instanceId ||= getInstanceId(this.frappe);
this.#telemetryObject.openTime ||= new Date().valueOf();
this.#telemetryObject.timeline ??= [];
this.#telemetryObject.errors ??= {};
@ -120,7 +136,10 @@ class TelemetryManager {
this.#clear();
if (config.get(ConfigKeys.Telemetry) === TelemetrySetting.dontLogAnything) {
if (
this.frappe.config.get(ConfigKeys.Telemetry) ===
TelemetrySetting.dontLogAnything
) {
return;
}
navigator.sendBeacon(this.#url, data);
@ -141,7 +160,10 @@ class TelemetryManager {
return;
}
this.#telemetryObject.counts = await getCounts();
this.#telemetryObject.counts = await getCounts(
this.#interestingDocs,
this.frappe
);
}
async #setCreds() {
@ -149,13 +171,15 @@ class TelemetryManager {
return;
}
const { url, token } = await getCreds();
const { url, token } = await this.frappe.auth.getTelemetryCreds();
this.#url = url;
this.#token = token;
}
#getCanLog(): boolean {
const telemetrySetting = config.get(ConfigKeys.Telemetry) as string;
const telemetrySetting = this.frappe.config.get(
ConfigKeys.Telemetry
) as string;
return telemetrySetting === TelemetrySetting.allow;
}
@ -170,5 +194,3 @@ class TelemetryManager {
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 UniqueId = string;
export type Timestamp = number;
@ -11,9 +9,7 @@ export interface InteractionEvent {
more?: Record<string, unknown>;
}
export type Count = Partial<{
[key in DoctypeName]: number;
}>;
export type Count = Record<string, number>;
export type Platform = 'Windows' | 'Mac' | 'Linux';
export interface Telemetry {
@ -46,3 +42,9 @@ export enum 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 'mocha';
import models, { getRegionalModels } from 'models';
import { getSchemas } from 'schemas';
import { Frappe } from '..';
import { DatabaseManager } from '../../backend/database/manager';
import { DummyAuthDemux } from './helpers';
describe('Frappe', function () {
const frappe = new Frappe(DatabaseManager);
const frappe = new Frappe({
DatabaseDemux: DatabaseManager,
AuthDemux: DummyAuthDemux,
isTest: true,
isElectron: false,
});
specify('Init', async function () {
assert.strictEqual(
@ -12,7 +20,7 @@ describe('Frappe', function () {
0,
'zero schemas one'
);
await frappe.initializeAndRegister();
assert.strictEqual(
Object.keys(frappe.schemaMap).length,
0,
@ -20,7 +28,7 @@ describe('Frappe', function () {
);
await frappe.db.createNewDatabase(':memory:', 'in');
await frappe.initializeAndRegister({}, {}, true);
await frappe.initializeAndRegister({}, {});
assert.strictEqual(
Object.keys(frappe.schemaMap).length > 0,
true,
@ -29,3 +37,28 @@ describe('Frappe', function () {
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_CURRENCY = 'INR';
export const DEFAULT_LANGUAGE = 'English';
export const DEFAULT_NUMBER_SERIES = {
SalesInvoice: 'SINV-',
PurchaseInvoice: 'PINV-',
Payment: 'PAY-',
JournalEntry: 'JV-',
Quotation: 'QTN-',
SalesOrder: 'SO-',
Fulfillment: 'OF-',
PurchaseOrder: 'PO-',
PurchaseReceipt: 'PREC-',
};
export const DEFAULT_SERIES_START = 1001;

View File

@ -1,4 +1,4 @@
import frappe from 'frappe';
import { Frappe } from 'frappe';
import { DocValue } from 'frappe/core/types';
import Doc from 'frappe/model/doc';
import { DateTime } from 'luxon';
@ -14,8 +14,9 @@ import {
export function format(
value: DocValue,
df?: string | Field,
doc?: Doc
df: string | Field | null,
doc: Doc | null,
frappe: Frappe
): string {
if (!df) {
return String(value);
@ -24,11 +25,11 @@ export function format(
const field: Field = getField(df);
if (field.fieldtype === FieldTypeEnum.Currency) {
return formatCurrency(value, field, doc);
return formatCurrency(value, field, doc, frappe);
}
if (field.fieldtype === FieldTypeEnum.Date) {
return formatDate(value);
return formatDate(value, frappe);
}
if (field.fieldtype === FieldTypeEnum.Check) {
@ -42,7 +43,7 @@ export function format(
return String(value);
}
function formatDate(value: DocValue): string {
function formatDate(value: DocValue, frappe: Frappe): string {
const dateFormat =
(frappe.singles.SystemSettings?.dateFormat as string) ??
DEFAULT_DATE_FORMAT;
@ -64,12 +65,17 @@ function formatDate(value: DocValue): string {
return formattedDate;
}
function formatCurrency(value: DocValue, field: Field, doc?: Doc): string {
const currency = getCurrency(field, doc);
function formatCurrency(
value: DocValue,
field: Field,
doc: Doc | null,
frappe: Frappe
): string {
const currency = getCurrency(field, doc, frappe);
let valueString;
try {
valueString = formatNumber(value);
valueString = formatNumber(value, frappe);
} catch (err) {
(err as Error).message += ` value: '${value}', type: ${typeof value}`;
throw err;
@ -83,8 +89,8 @@ function formatCurrency(value: DocValue, field: Field, doc?: Doc): string {
return valueString;
}
function formatNumber(value: DocValue): string {
const numberFormatter = getNumberFormatter();
function formatNumber(value: DocValue, frappe: Frappe): string {
const numberFormatter = getNumberFormatter(frappe);
if (typeof value === 'number') {
return numberFormatter.format(value);
}
@ -106,7 +112,7 @@ function formatNumber(value: DocValue): string {
return formattedNumber;
}
function getNumberFormatter() {
function getNumberFormatter(frappe: Frappe) {
if (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]) {
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
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
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 {
FiltersMap,
@ -6,36 +6,45 @@ import {
TreeViewSettings,
} from 'frappe/model/types';
import { QueryFilter } from 'utils/db/types';
import { AccountRootType, AccountType } from './types';
export class Account extends Doc {
rootType?: AccountRootType;
accountType?: AccountType;
parentAccount?: string;
async beforeInsert() {
if (this.accountType || !this.parentAccount) {
return;
}
const account = await frappe.db.get(
const account = await this.frappe.db.get(
'Account',
this.parentAccount as string
);
this.accountType = account.accountType as string;
this.accountType = account.accountType as AccountType;
}
static listSettings: ListViewSettings = {
columns: ['name', 'parentAccount', 'rootType'],
};
static getListViewSettings(): ListViewSettings {
return {
columns: ['name', 'parentAccount', 'rootType'],
};
}
static treeSettings: TreeViewSettings = {
parentField: 'parentAccount',
async getRootLabel(): Promise<string> {
const accountingSettings = await frappe.doc.getSingle(
'AccountingSettings'
);
return accountingSettings.companyName as string;
},
};
static getTreeSettings(frappe: Frappe): void | TreeViewSettings {
return {
parentField: 'parentAccount',
async getRootLabel(): Promise<string> {
const accountingSettings = await frappe.doc.getSingle(
'AccountingSettings'
);
return accountingSettings.companyName as string;
},
};
}
static filters: FiltersMap = {
parentAccount: (doc: Doc) => {
parentAccount: (doc: Account) => {
const filter: QueryFilter = {
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';
export class AccountingLedgerEntry extends Doc {
static listSettings: ListViewSettings = {
columns: ['account', 'party', 'debit', 'credit', 'balance'],
};
static getListViewSettings(): ListViewSettings {
return {
columns: ['account', 'party', 'debit', 'credit', 'balance'],
};
}
}

View File

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

View File

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

View File

@ -1,4 +1,3 @@
import frappe from 'frappe';
import { DocValue } from 'frappe/core/types';
import Doc from 'frappe/model/doc';
import {
@ -7,6 +6,7 @@ import {
FormulaMap,
ValidationMap,
} from 'frappe/model/types';
import { ValidationError } from 'frappe/utils/errors';
import Money from 'pesa/dist/types/src/money';
import { Invoice } from '../Invoice/Invoice';
@ -32,7 +32,7 @@ export abstract class InvoiceItem extends Doc {
'Item',
this.item as string,
'rate'
)) || frappe.pesa(0)) as Money;
)) || this.frappe.pesa(0)) as Money;
return baseRate.div(this.exchangeRate!);
},
@ -73,8 +73,8 @@ export abstract class InvoiceItem extends Doc {
return;
}
throw new frappe.errors.ValidationError(
frappe.t`Rate (${frappe.format(
throw new ValidationError(
this.frappe.t`Rate (${this.frappe.format(
value,
'Currency'
)}) 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 Doc from 'frappe/model/doc';
import {
@ -9,6 +9,7 @@ import {
ListViewSettings,
ValidationMap,
} from 'frappe/model/types';
import { ValidationError } from 'frappe/utils/errors';
import Money from 'pesa/dist/types/src/money';
export class Item extends Doc {
@ -19,11 +20,11 @@ export class Item extends Doc {
accountName = 'Sales';
}
const accountExists = await frappe.db.exists('Account', accountName);
const accountExists = await this.frappe.db.exists('Account', accountName);
return accountExists ? accountName : '';
},
expenseAccount: async () => {
const cogs = await frappe.db.getAllRaw('Account', {
const cogs = await this.frappe.db.getAllRaw('Account', {
filters: {
accountType: 'Cost of Goods Sold',
},
@ -56,43 +57,45 @@ export class Item extends Doc {
validations: ValidationMap = {
rate: async (value: DocValue) => {
if ((value as Money).isNegative()) {
throw new frappe.errors.ValidationError(
frappe.t`Rate can't be negative.`
);
throw new ValidationError(this.frappe.t`Rate can't be negative.`);
}
},
};
static actions: Action[] = [
{
label: frappe.t`New Invoice`,
condition: (doc) => !doc.isNew,
action: async (doc, router) => {
const invoice = await frappe.doc.getEmptyDoc('SalesInvoice');
invoice.append('items', {
item: doc.name as string,
rate: doc.rate as Money,
tax: doc.tax as string,
});
router.push(`/edit/SalesInvoice/${invoice.name}`);
static getActions(frappe: Frappe): Action[] {
return [
{
label: frappe.t`New Invoice`,
condition: (doc) => !doc.isNew,
action: async (doc, router) => {
const invoice = await frappe.doc.getEmptyDoc('SalesInvoice');
invoice.append('items', {
item: doc.name as string,
rate: doc.rate as Money,
tax: doc.tax as string,
});
router.push(`/edit/SalesInvoice/${invoice.name}`);
},
},
},
{
label: frappe.t`New Bill`,
condition: (doc) => !doc.isNew,
action: async (doc, router) => {
const invoice = await frappe.doc.getEmptyDoc('PurchaseInvoice');
invoice.append('items', {
item: doc.name as string,
rate: doc.rate as Money,
tax: doc.tax as string,
});
router.push(`/edit/PurchaseInvoice/${invoice.name}`);
{
label: frappe.t`New Bill`,
condition: (doc) => !doc.isNew,
action: async (doc, router) => {
const invoice = await frappe.doc.getEmptyDoc('PurchaseInvoice');
invoice.append('items', {
item: doc.name as string,
rate: doc.rate as Money,
tax: doc.tax as string,
});
router.push(`/edit/PurchaseInvoice/${invoice.name}`);
},
},
},
];
];
}
listSettings: ListViewSettings = {
columns: ['name', 'unit', 'tax', 'rate'],
};
static getListViewSettings(): ListViewSettings {
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 {
Action,
@ -15,7 +15,7 @@ export class JournalEntry extends Doc {
accounts: Doc[] = [];
getPosting() {
const entries = new LedgerPosting({ reference: this });
const entries = new LedgerPosting({ reference: this }, this.frappe);
for (const row of this.accounts) {
const debit = row.debit as Money;
@ -32,11 +32,11 @@ export class JournalEntry extends Doc {
return entries;
}
beforeUpdate() {
async beforeUpdate() {
this.getPosting().validateEntries();
}
beforeInsert() {
async beforeInsert() {
this.getPosting().validateEntries();
}
@ -56,44 +56,48 @@ export class JournalEntry extends Doc {
numberSeries: () => ({ referenceType: 'JournalEntry' }),
};
static actions: Action[] = [getLedgerLinkAction()];
static getActions(frappe: Frappe): Action[] {
return [getLedgerLinkAction(frappe)];
}
static listSettings: ListViewSettings = {
formRoute: (name) => `/edit/JournalEntry/${name}`,
columns: [
'date',
{
label: frappe.t`Status`,
fieldtype: 'Select',
size: 'small',
render(doc) {
let status = 'Draft';
let color = 'gray';
if (doc.submitted) {
color = 'green';
status = 'Submitted';
}
static getListViewSettings(frappe: Frappe): ListViewSettings {
return {
formRoute: (name) => `/edit/JournalEntry/${name}`,
columns: [
'date',
{
label: frappe.t`Status`,
fieldtype: 'Select',
size: 'small',
render(doc) {
let status = 'Draft';
let color = 'gray';
if (doc.submitted) {
color = 'green';
status = 'Submitted';
}
if (doc.cancelled) {
color = 'red';
status = 'Cancelled';
}
if (doc.cancelled) {
color = 'red';
status = 'Cancelled';
}
return {
template: `<Badge class="text-xs" color="${color}">${status}</Badge>`,
};
return {
template: `<Badge class="text-xs" color="${color}">${status}</Badge>`,
};
},
},
},
{
label: frappe.t`Entry ID`,
fieldtype: 'Data',
fieldname: 'name',
getValue(doc) {
return doc.name as string;
{
label: frappe.t`Entry ID`,
fieldtype: 'Data',
fieldname: 'name',
getValue(doc) {
return doc.name as string;
},
},
},
'entryType',
'referenceNumber',
],
};
'entryType',
'referenceNumber',
],
};
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
import frappe from 'frappe';
import { t } from 'frappe';
import Doc from 'frappe/model/doc';
import { FormulaMap, ListsMap } from 'frappe/model/types';
import { DateTime } from 'luxon';
@ -6,7 +6,7 @@ import countryInfo from '../../../fixtures/countryInfo.json';
export function getCOAList() {
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' },
{

View File

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

View File

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

View File

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

View File

@ -1 +1,31 @@
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",
"isGroup"
],
"keywordFields": ["name", "rootType", "accountType"],
"treeSettings": {
"parentField": "parentAccount"
}
"keywordFields": ["name", "rootType", "accountType"]
}

View File

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

View File

@ -68,7 +68,6 @@ export type Field =
| DynamicLinkField
| NumberField;
export type TreeSettings = { parentField: string };
export type Naming = 'autoincrement' | 'random' | 'numberSeries'
export interface Schema {
@ -84,7 +83,6 @@ export interface Schema {
isSubmittable?: boolean; // For transactional types, values considered only after submit
keywordFields?: string[]; // Used to get fields that are to be used for search.
quickEditFields?: string[]; // Used to get fields for the quickEditForm
treeSettings?: TreeSettings; // Used to determine root nodes
inlineEditDisplayField?:string;// Display field if inline editable
naming?: Naming; // Used for assigning name, default is 'random' else 'numberSeries' if present
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 { FieldType, FieldTypeEnum } from 'schemas/types';
import { parseCSV } from '../utils/csvParser';
import telemetry from './telemetry/telemetry';
import { Noun, Verb } from './telemetry/types';
import telemetry from '../frappe/telemetry/telemetry';
import { Noun, Verb } from '../frappe/telemetry/types';
export const importable = [
'SalesInvoice',

View File

@ -9,9 +9,9 @@ import {
} from 'frappe/utils/errors';
import { ErrorLog } from 'frappe/utils/types';
import { IPC_ACTIONS, IPC_MESSAGES } from 'utils/messages';
import config, { ConfigKeys, TelemetrySetting } from './config';
import telemetry from './telemetry/telemetry';
import { showMessageDialog, showToast } from './utils';
import telemetry from '../frappe/telemetry/telemetry';
import config, { ConfigKeys, TelemetrySetting } from '../utils/config';
import { showMessageDialog, showToast } from './utils.js';
function getCanLog(): boolean {
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 { DEFAULT_SERIES_START } from 'frappe/utils/consts';
import { getValueMapFromList } from 'utils';
import { fyo } from './initFyo';
export default async function postStart() {
await createDefaultNumberSeries();
@ -9,23 +10,28 @@ export default async function postStart() {
}
async function createDefaultNumberSeries() {
await createNumberSeries('SINV-', 'SalesInvoice');
await createNumberSeries('PINV-', 'PurchaseInvoice');
await createNumberSeries('PAY-', 'Payment');
await createNumberSeries('JV-', 'JournalEntry');
await createNumberSeries('SINV-', 'SalesInvoice', DEFAULT_SERIES_START, fyo);
await createNumberSeries(
'PINV-',
'PurchaseInvoice',
DEFAULT_SERIES_START,
fyo
);
await createNumberSeries('PAY-', 'Payment', DEFAULT_SERIES_START, fyo);
await createNumberSeries('JV-', 'JournalEntry', DEFAULT_SERIES_START, fyo);
}
async function setSingles() {
await frappe.doc.getSingle('AccountingSettings');
await frappe.doc.getSingle('GetStarted');
await fyo.doc.getSingle('AccountingSettings');
await fyo.doc.getSingle('GetStarted');
}
async function setCurrencySymbols() {
const currencies = (await frappe.db.getAll('Currency', {
const currencies = (await fyo.db.getAll('Currency', {
fields: ['name', 'symbol'],
})) as { name: string; symbol: string }[];
frappe.currencySymbols = getValueMapFromList(
fyo.currencySymbols = getValueMapFromList(
currencies,
'name',
'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() {
let openCount = config.get(ConfigKeys.OpenCount);
let openCount = frappe.config.get(ConfigKeys.OpenCount);
if (typeof openCount !== 'number') {
openCount = 1;
} else {
openCount += 1;
}
config.set(ConfigKeys.OpenCount, openCount);
frappe.config.set(ConfigKeys.OpenCount, openCount);
}

View File

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

View File

@ -10,8 +10,8 @@ import QuickEditForm from '@/pages/QuickEditForm.vue';
import Report from '@/pages/Report.vue';
import Settings from '@/pages/Settings/Settings.vue';
import { createRouter, createWebHistory } from 'vue-router';
import telemetry from './telemetry/telemetry';
import { NounEnum, Verb } from './telemetry/types';
import telemetry from '../frappe/telemetry/telemetry';
import { NounEnum, Verb } from '../frappe/telemetry/types';
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",
"utils/csvParser.ts"
],
, "utils/config.ts" ],
"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;