From f243326126d30faf6954998cc66f121ee0387cd9 Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Mon, 21 Mar 2022 11:17:26 +0530 Subject: [PATCH 001/163] incr: type errors, set on frappe --- frappe/common/errors.js | 101 ---------------------------------------- frappe/common/errors.ts | 71 ++++++++++++++++++++++++++++ frappe/common/index.js | 2 - frappe/index.js | 6 ++- 4 files changed, 75 insertions(+), 105 deletions(-) delete mode 100644 frappe/common/errors.js create mode 100644 frappe/common/errors.ts diff --git a/frappe/common/errors.js b/frappe/common/errors.js deleted file mode 100644 index 3d1f4da2..00000000 --- a/frappe/common/errors.js +++ /dev/null @@ -1,101 +0,0 @@ -const frappe = require('frappe'); - -class BaseError extends Error { - constructor(statusCode, message) { - super(message); - this.name = 'BaseError'; - this.statusCode = statusCode; - this.message = message; - } -} - -class ValidationError extends BaseError { - constructor(message) { - super(417, message); - this.name = 'ValidationError'; - } -} - -class NotFoundError extends BaseError { - constructor(message) { - super(404, message); - this.name = 'NotFoundError'; - } -} - -class ForbiddenError extends BaseError { - constructor(message) { - super(403, message); - this.name = 'ForbiddenError'; - } -} - -class DuplicateEntryError extends ValidationError { - constructor(message) { - super(message); - this.name = 'DuplicateEntryError'; - } -} - -class LinkValidationError extends ValidationError { - constructor(message) { - super(message); - this.name = 'LinkValidationError'; - } -} - -class MandatoryError extends ValidationError { - constructor(message) { - super(message); - this.name = 'MandatoryError'; - } -} - -class DatabaseError extends BaseError { - constructor(message) { - super(500, message); - this.name = 'DatabaseError'; - } -} - -class CannotCommitError extends DatabaseError { - constructor(message) { - super(message); - this.name = 'CannotCommitError'; - } -} - -class ValueError extends ValidationError {} -class Conflict extends ValidationError {} -class InvalidFieldError extends ValidationError {} - -function throwError(message, error = 'ValidationError') { - const errorClass = { - ValidationError: ValidationError, - NotFoundError: NotFoundError, - ForbiddenError: ForbiddenError, - ValueError: ValueError, - Conflict: Conflict, - }; - const err = new errorClass[error](message); - frappe.events.trigger('throw', { message, stackTrace: err.stack }); - throw err; -} - -frappe.throw = throwError; - -module.exports = { - BaseError, - ValidationError, - ValueError, - Conflict, - NotFoundError, - ForbiddenError, - DuplicateEntryError, - LinkValidationError, - DatabaseError, - CannotCommitError, - MandatoryError, - InvalidFieldError, - throw: throwError, -}; diff --git a/frappe/common/errors.ts b/frappe/common/errors.ts new file mode 100644 index 00000000..5cf534d1 --- /dev/null +++ b/frappe/common/errors.ts @@ -0,0 +1,71 @@ +export class BaseError extends Error { + message: string; + statusCode: number; + + constructor(statusCode: number, message: string) { + super(message); + this.name = 'BaseError'; + this.statusCode = statusCode; + this.message = message; + } +} + +export class ValidationError extends BaseError { + constructor(message: string) { + super(417, message); + this.name = 'ValidationError'; + } +} + +export class NotFoundError extends BaseError { + constructor(message: string) { + super(404, message); + this.name = 'NotFoundError'; + } +} + +export class ForbiddenError extends BaseError { + constructor(message: string) { + super(403, message); + this.name = 'ForbiddenError'; + } +} + +export class DuplicateEntryError extends ValidationError { + constructor(message: string) { + super(message); + this.name = 'DuplicateEntryError'; + } +} + +export class LinkValidationError extends ValidationError { + constructor(message: string) { + super(message); + this.name = 'LinkValidationError'; + } +} + +export class MandatoryError extends ValidationError { + constructor(message: string) { + super(message); + this.name = 'MandatoryError'; + } +} + +export class DatabaseError extends BaseError { + constructor(message: string) { + super(500, message); + this.name = 'DatabaseError'; + } +} + +export class CannotCommitError extends DatabaseError { + constructor(message: string) { + super(message); + this.name = 'CannotCommitError'; + } +} + +export class ValueError extends ValidationError {} +export class Conflict extends ValidationError {} +export class InvalidFieldError extends ValidationError {} diff --git a/frappe/common/index.js b/frappe/common/index.js index ac11c0ce..2b99b881 100644 --- a/frappe/common/index.js +++ b/frappe/common/index.js @@ -1,13 +1,11 @@ export default async function initLibs(frappe) { const utils = await import('../utils'); const format = await import('../utils/format'); - const errors = await import('./errors'); const BaseMeta = await import('frappe/model/meta'); const BaseDocument = await import('frappe/model/document'); Object.assign(frappe, utils.default); Object.assign(frappe, format.default); - frappe.errors = errors.default; frappe.BaseDocument = BaseDocument.default; frappe.BaseMeta = BaseMeta.default; } diff --git a/frappe/index.js b/frappe/index.js index 454445f2..ebca6054 100644 --- a/frappe/index.js +++ b/frappe/index.js @@ -1,6 +1,7 @@ import initLibs from 'frappe/common'; import { getMoneyMaker } from 'pesa'; import { markRaw } from 'vue'; +import * as errors from './common/errors'; import utils from './utils'; import { DEFAULT_DISPLAY_PRECISION, @@ -10,6 +11,9 @@ import Observable from './utils/observable'; import { t, T } from './utils/translation'; class Frappe { + t = t; + T = T; + errors = errors; isElectron = false; isServer = false; @@ -383,8 +387,6 @@ class Frappe { isDevelopment: false, appVersion: '', }; - t = t; - T = T; } export { T, t }; From 807a5d2da7b13826ea4352fc374992616c29ec46 Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Mon, 21 Mar 2022 11:28:52 +0530 Subject: [PATCH 002/163] incr: remove utils, format from common --- frappe/common/index.js | 4 -- frappe/index.js | 10 +++-- frappe/utils/format.js | 92 ++++++++++++++++++++---------------------- frappe/utils/index.js | 41 +++++-------------- 4 files changed, 61 insertions(+), 86 deletions(-) diff --git a/frappe/common/index.js b/frappe/common/index.js index 2b99b881..269e7ffc 100644 --- a/frappe/common/index.js +++ b/frappe/common/index.js @@ -1,11 +1,7 @@ export default async function initLibs(frappe) { - const utils = await import('../utils'); - const format = await import('../utils/format'); const BaseMeta = await import('frappe/model/meta'); const BaseDocument = await import('frappe/model/document'); - Object.assign(frappe, utils.default); - Object.assign(frappe, format.default); frappe.BaseDocument = BaseDocument.default; frappe.BaseMeta = BaseMeta.default; } diff --git a/frappe/index.js b/frappe/index.js index ebca6054..3db2be5c 100644 --- a/frappe/index.js +++ b/frappe/index.js @@ -2,17 +2,21 @@ import initLibs from 'frappe/common'; import { getMoneyMaker } from 'pesa'; import { markRaw } from 'vue'; import * as errors from './common/errors'; -import utils from './utils'; +import { asyncHandler, getDuplicates, getRandomString } from './utils'; import { DEFAULT_DISPLAY_PRECISION, DEFAULT_INTERNAL_PRECISION, } from './utils/consts'; +import { format } from './utils/format'; import Observable from './utils/observable'; import { t, T } from './utils/translation'; class Frappe { t = t; T = T; + format = format; + getRandomString = getRandomString; + errors = errors; isElectron = false; isServer = false; @@ -114,7 +118,7 @@ class Frappe { let fieldnames = (metaDefinition.fields || []) .map((df) => df.fieldname) .sort(); - let duplicateFieldnames = utils.getDuplicates(fieldnames); + let duplicateFieldnames = getDuplicates(fieldnames); if (duplicateFieldnames.length > 0) { throw new Error( `Duplicate fields in ${doctype}: ${duplicateFieldnames.join(', ')}` @@ -144,7 +148,7 @@ class Frappe { // add to router if client-server this.app.post( `/api/method/${method}`, - this.asyncHandler(async function (request, response) { + asyncHandler(async function (request, response) { let data = await handler(request.body); if (data === undefined) { data = {}; diff --git a/frappe/utils/format.js b/frappe/utils/format.js index a7b0f55d..53c63ebf 100644 --- a/frappe/utils/format.js +++ b/frappe/utils/format.js @@ -2,55 +2,51 @@ import frappe from 'frappe'; import { DateTime } from 'luxon'; import { DEFAULT_DISPLAY_PRECISION, DEFAULT_LOCALE } from './consts'; -export default { - format(value, df, doc) { - if (!df) { - return value; - } - - if (typeof df === 'string') { - df = { fieldtype: df }; - } - - if (df.fieldtype === 'Currency') { - const currency = getCurrency(df, doc); - value = formatCurrency(value, currency); - } else if (df.fieldtype === 'Date') { - let dateFormat; - if (!frappe.SystemSettings) { - dateFormat = 'yyyy-MM-dd'; - } else { - dateFormat = frappe.SystemSettings.dateFormat; - } - - if (typeof value === 'string') { - // ISO String - value = DateTime.fromISO(value); - } else if (Object.prototype.toString.call(value) === '[object Date]') { - // JS Date - value = DateTime.fromJSDate(value); - } - - value = value.toFormat(dateFormat); - if (value === 'Invalid DateTime') { - value = ''; - } - } else if (df.fieldtype === 'Check') { - typeof parseInt(value) === 'number' - ? (value = parseInt(value)) - : (value = Boolean(value)); - } else { - if (value === null || value === undefined) { - value = ''; - } else { - value = value + ''; - } - } +export function format(value, df, doc) { + if (!df) { return value; - }, - formatCurrency, - formatNumber, -}; + } + + if (typeof df === 'string') { + df = { fieldtype: df }; + } + + if (df.fieldtype === 'Currency') { + const currency = getCurrency(df, doc); + value = formatCurrency(value, currency); + } else if (df.fieldtype === 'Date') { + let dateFormat; + if (!frappe.SystemSettings) { + dateFormat = 'yyyy-MM-dd'; + } else { + dateFormat = frappe.SystemSettings.dateFormat; + } + + if (typeof value === 'string') { + // ISO String + value = DateTime.fromISO(value); + } else if (Object.prototype.toString.call(value) === '[object Date]') { + // JS Date + value = DateTime.fromJSDate(value); + } + + value = value.toFormat(dateFormat); + if (value === 'Invalid DateTime') { + value = ''; + } + } else if (df.fieldtype === 'Check') { + typeof parseInt(value) === 'number' + ? (value = parseInt(value)) + : (value = Boolean(value)); + } else { + if (value === null || value === undefined) { + value = ''; + } else { + value = value + ''; + } + } + return value; +} function formatCurrency(value, currency) { let valueString; diff --git a/frappe/utils/index.js b/frappe/utils/index.js index 0abdacf7..23ed1831 100644 --- a/frappe/utils/index.js +++ b/frappe/utils/index.js @@ -1,15 +1,6 @@ -const { pesa } = require('pesa'); +import { pesa } from 'pesa'; -Array.prototype.equals = function (array) { - return ( - this.length == array.length && - this.every(function (item, i) { - return item == array[i]; - }) - ); -}; - -function slug(str) { +export function slug(str) { return str .replace(/(?:^\w|[A-Z]|\b\w)/g, function (letter, index) { return index == 0 ? letter.toLowerCase() : letter.toUpperCase(); @@ -17,17 +8,17 @@ function slug(str) { .replace(/\s+/g, ''); } -function getRandomString() { +export function getRandomString() { return Math.random().toString(36).substr(3); } -async function sleep(seconds) { +export async function sleep(seconds) { return new Promise((resolve) => { setTimeout(resolve, seconds * 1000); }); } -function getQueryString(params) { +export function getQueryString(params) { if (!params) return ''; let parts = []; for (let key in params) { @@ -40,7 +31,7 @@ function getQueryString(params) { return parts.join('&'); } -function asyncHandler(fn) { +export function asyncHandler(fn) { return (req, res, next) => Promise.resolve(fn(req, res, next)).catch((err) => { console.log(err); @@ -53,13 +44,13 @@ function asyncHandler(fn) { * Returns array from 0 to n - 1 * @param {Number} n */ -function range(n) { +export function range(n) { return Array(n) .fill() .map((_, i) => i); } -function unique(list, key = (it) => it) { +export function unique(list, key = (it) => it) { var seen = {}; return list.filter((item) => { var k = key(item); @@ -67,7 +58,7 @@ function unique(list, key = (it) => it) { }); } -function getDuplicates(array) { +export function getDuplicates(array) { let duplicates = []; for (let i in array) { let previous = array[i - 1]; @@ -82,18 +73,6 @@ function getDuplicates(array) { return duplicates; } -function isPesa(value) { +export function isPesa(value) { return value instanceof pesa().constructor; } - -module.exports = { - slug, - getRandomString, - sleep, - getQueryString, - asyncHandler, - range, - unique, - getDuplicates, - isPesa, -}; From 26e030106fece3ae1410b5c95ee49e35ce231834 Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Mon, 21 Mar 2022 12:09:06 +0530 Subject: [PATCH 003/163] incr: rename Meta and Document --- frappe/backends/helpers.ts | 24 ++++++++++++++ frappe/backends/sqlite.js | 32 ++----------------- frappe/common/index.js | 7 ---- frappe/index.js | 14 ++++---- frappe/model/meta.js | 25 ++++++++------- .../NumberSeries/NumberSeriesDocument.js | 4 +-- frappe/utils/noop.js | 1 - models/doctype/Account/AccountDocument.js | 4 +-- models/doctype/Event/EventDocument.js | 4 +-- models/doctype/GSTR3B/GSTR3BDocument.js | 4 +-- .../JournalEntry/JournalEntryServer.js | 4 +-- models/doctype/Party/PartyServer.js | 4 +-- models/doctype/Payment/PaymentServer.js | 4 +-- .../Transaction/TransactionDocument.js | 4 +-- src/initialization.js | 2 +- src/utils.js | 4 +-- 16 files changed, 67 insertions(+), 74 deletions(-) create mode 100644 frappe/backends/helpers.ts delete mode 100644 frappe/common/index.js delete mode 100644 frappe/utils/noop.js diff --git a/frappe/backends/helpers.ts b/frappe/backends/helpers.ts new file mode 100644 index 00000000..1d2d38a4 --- /dev/null +++ b/frappe/backends/helpers.ts @@ -0,0 +1,24 @@ +export const sqliteTypeMap = { + AutoComplete: 'text', + Currency: 'text', + Int: 'integer', + Float: 'float', + Percent: 'float', + Check: 'integer', + Code: 'text', + Date: 'text', + Datetime: 'text', + Time: 'text', + Text: 'text', + Data: 'text', + Link: 'text', + DynamicLink: 'text', + Password: 'text', + Select: 'text', + File: 'text', + Attach: 'text', + AttachImage: 'text', + Color: 'text', +}; + +export const validTypes = Object.keys(sqliteTypeMap); diff --git a/frappe/backends/sqlite.js b/frappe/backends/sqlite.js index 0d76cf5b..f9f8a58a 100644 --- a/frappe/backends/sqlite.js +++ b/frappe/backends/sqlite.js @@ -1,5 +1,6 @@ import frappe from 'frappe'; import Database from './database'; +import { sqliteTypeMap } from './helpers'; export default class SqliteDatabase extends Database { constructor({ dbPath }) { @@ -67,36 +68,7 @@ export default class SqliteDatabase extends Database { } initTypeMap() { - // prettier-ignore - this.typeMap = { - 'AutoComplete': 'text', - 'Currency': 'text', - 'Int': 'integer', - 'Float': 'float', - 'Percent': 'float', - 'Check': 'integer', - 'Small Text': 'text', - 'Long Text': 'text', - 'Code': 'text', - 'Text Editor': 'text', - 'Date': 'text', - 'Datetime': 'text', - 'Time': 'text', - 'Text': 'text', - 'Data': 'text', - 'Link': 'text', - 'DynamicLink': 'text', - 'Password': 'text', - 'Select': 'text', - 'Read Only': 'text', - 'File': 'text', - 'Attach': 'text', - 'AttachImage': 'text', - 'Signature': 'text', - 'Color': 'text', - 'Barcode': 'text', - 'Geolocation': 'text' - }; + this.typeMap = sqliteTypeMap; } getError(err) { diff --git a/frappe/common/index.js b/frappe/common/index.js deleted file mode 100644 index 269e7ffc..00000000 --- a/frappe/common/index.js +++ /dev/null @@ -1,7 +0,0 @@ -export default async function initLibs(frappe) { - const BaseMeta = await import('frappe/model/meta'); - const BaseDocument = await import('frappe/model/document'); - - frappe.BaseDocument = BaseDocument.default; - frappe.BaseMeta = BaseMeta.default; -} diff --git a/frappe/index.js b/frappe/index.js index 3db2be5c..5976dbb2 100644 --- a/frappe/index.js +++ b/frappe/index.js @@ -1,4 +1,3 @@ -import initLibs from 'frappe/common'; import { getMoneyMaker } from 'pesa'; import { markRaw } from 'vue'; import * as errors from './common/errors'; @@ -23,7 +22,10 @@ class Frappe { async initializeAndRegister(customModels = {}, force = false) { this.init(force); - await initLibs(this); + + this.Meta = (await import('frappe/model/meta')).default; + this.Document = (await import('frappe/model/document')).default; + const coreModels = await import('frappe/models'); this.registerModels(coreModels.default); this.registerModels(customModels); @@ -233,7 +235,7 @@ class Frappe { throw new Error(`${doctype} is not a registered doctype`); } - let metaClass = model.metaClass || this.BaseMeta; + let metaClass = model.metaClass || this.Meta; this.metaCache[doctype] = new metaClass(model); } @@ -257,7 +259,7 @@ class Frappe { getDocumentClass(doctype) { const meta = this.getMeta(doctype); - return meta.documentClass || this.BaseDocument; + return meta.documentClass || this.Document; } async getSingle(doctype) { @@ -292,7 +294,7 @@ class Frappe { } async newCustomDoc(fields) { - let doc = new this.BaseDocument({ isCustom: 1, fields }); + let doc = new this.Document({ isCustom: 1, fields }); doc._notInserted = true; doc.name = this.getRandomString(); this.addToCache(doc); @@ -300,7 +302,7 @@ class Frappe { } createMeta(fields) { - let meta = new this.BaseMeta({ isCustom: 1, fields }); + let meta = new this.Meta({ isCustom: 1, fields }); return meta; } diff --git a/frappe/model/meta.js b/frappe/model/meta.js index 5b3dc5c4..f3f0611b 100644 --- a/frappe/model/meta.js +++ b/frappe/model/meta.js @@ -1,9 +1,12 @@ import frappe from 'frappe'; +import { validTypes } from 'frappe/backends/helpers'; +import { ValueError } from 'frappe/common/errors'; import { indicators as indicatorColor } from '../../src/colors'; +import { t } from '../utils/translation'; import Document from './document'; import model from './index'; -export default class BaseMeta extends Document { +export default class Meta extends Document { constructor(data) { if (data.basedOn) { let config = frappe.models[data.basedOn]; @@ -33,7 +36,7 @@ export default class BaseMeta extends Document { if (!this.fields.find((df) => df.fieldname === 'name') && !this.isSingle) { this.fields = [ { - label: frappe.t`ID`, + label: t`ID`, fieldname: 'name', fieldtype: 'Data', required: 1, @@ -149,12 +152,12 @@ export default class BaseMeta extends Document { // fields validation this.fields.forEach((df, i) => { if (!df.fieldname) { - throw new frappe.errors.ValidationError( + throw new ValidationError( `DocType ${this.name}: "fieldname" is required for field at index ${i}` ); } if (!df.fieldtype) { - throw new frappe.errors.ValidationError( + throw new ValidationError( `DocType ${this.name}: "fieldtype" is required for field "${df.fieldname}"` ); } @@ -165,7 +168,7 @@ export default class BaseMeta extends Document { // standard fields for (let field of model.commonFields) { if ( - frappe.db.typeMap[field.fieldtype] && + validTypes.includes(field.fieldtype) && !doctypeFields.includes(field.fieldname) ) { _add(field); @@ -176,7 +179,7 @@ export default class BaseMeta extends Document { _add({ fieldtype: 'Check', fieldname: 'submitted', - label: frappe.t`Submitted`, + label: t`Submitted`, }); } @@ -184,7 +187,7 @@ export default class BaseMeta extends Document { // child fields for (let field of model.childFields) { if ( - frappe.db.typeMap[field.fieldtype] && + validTypes.includes(field.fieldtype) && !doctypeFields.includes(field.fieldname) ) { _add(field); @@ -194,7 +197,7 @@ export default class BaseMeta extends Document { // parent fields for (let field of model.parentFields) { if ( - frappe.db.typeMap[field.fieldtype] && + validTypes.includes(field.fieldtype) && !doctypeFields.includes(field.fieldname) ) { _add(field); @@ -206,7 +209,7 @@ export default class BaseMeta extends Document { // tree fields for (let field of model.treeFields) { if ( - frappe.db.typeMap[field.fieldtype] && + validTypes.includes(field.fieldtype) && !doctypeFields.includes(field.fieldname) ) { _add(field); @@ -216,7 +219,7 @@ export default class BaseMeta extends Document { // doctype fields for (let field of this.fields) { - let include = frappe.db.typeMap[field.fieldtype]; + const include = validTypes.includes(field.fieldtype); if (include) { _add(field); @@ -278,7 +281,7 @@ export default class BaseMeta extends Document { } if (!validValues.includes(value)) { - throw new frappe.errors.ValueError( + throw new ValueError( // prettier-ignore `DocType ${this.name}: Invalid value "${value}" for "${field.label}". Must be one of ${options.join(', ')}` ); diff --git a/frappe/models/doctype/NumberSeries/NumberSeriesDocument.js b/frappe/models/doctype/NumberSeries/NumberSeriesDocument.js index 7b83ab6c..09c6f73c 100644 --- a/frappe/models/doctype/NumberSeries/NumberSeriesDocument.js +++ b/frappe/models/doctype/NumberSeries/NumberSeriesDocument.js @@ -1,8 +1,8 @@ import { getPaddedName } from '@/utils'; import frappe from 'frappe'; -import BaseDocument from 'frappe/model/document'; +import Document from 'frappe/model/document'; -export default class NumberSeries extends BaseDocument { +export default class NumberSeries extends Document { validate() { if (!this.current) { this.current = this.start; diff --git a/frappe/utils/noop.js b/frappe/utils/noop.js deleted file mode 100644 index bd375ffa..00000000 --- a/frappe/utils/noop.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = function () { return function () {}; }; \ No newline at end of file diff --git a/models/doctype/Account/AccountDocument.js b/models/doctype/Account/AccountDocument.js index b16f1463..f2fa2abb 100644 --- a/models/doctype/Account/AccountDocument.js +++ b/models/doctype/Account/AccountDocument.js @@ -1,7 +1,7 @@ import frappe from 'frappe'; -import BaseDocument from 'frappe/model/document'; +import Document from 'frappe/model/document'; -export default class Account extends BaseDocument { +export default class Account extends Document { async validate() { if (!this.accountType && this.parentAccount) { this.accountType = await frappe.db.getValue( diff --git a/models/doctype/Event/EventDocument.js b/models/doctype/Event/EventDocument.js index 077fbad4..c6c06908 100644 --- a/models/doctype/Event/EventDocument.js +++ b/models/doctype/Event/EventDocument.js @@ -1,6 +1,6 @@ -import BaseDocument from 'frappe/model/document'; +import Document from 'frappe/model/document'; -export default class Event extends BaseDocument { +export default class Event extends Document { alertEvent() { alert(this.title); } diff --git a/models/doctype/GSTR3B/GSTR3BDocument.js b/models/doctype/GSTR3B/GSTR3BDocument.js index 31721db5..88cfec2c 100644 --- a/models/doctype/GSTR3B/GSTR3BDocument.js +++ b/models/doctype/GSTR3B/GSTR3BDocument.js @@ -1,8 +1,8 @@ import frappe from 'frappe'; -import BaseDocument from 'frappe/model/document'; +import Document from 'frappe/model/document'; import format from './GSTR3BFormat'; -export default class GSTR3B extends BaseDocument { +export default class GSTR3B extends Document { async getData() { const monthIndex = [ 'January', diff --git a/models/doctype/JournalEntry/JournalEntryServer.js b/models/doctype/JournalEntry/JournalEntryServer.js index c081e720..f10f6ce3 100644 --- a/models/doctype/JournalEntry/JournalEntryServer.js +++ b/models/doctype/JournalEntry/JournalEntryServer.js @@ -1,7 +1,7 @@ -import BaseDocument from 'frappe/model/document'; +import Document from 'frappe/model/document'; import LedgerPosting from '../../../accounting/ledgerPosting'; -export default class JournalEntryServer extends BaseDocument { +export default class JournalEntryServer extends Document { getPosting() { let entries = new LedgerPosting({ reference: this }); diff --git a/models/doctype/Party/PartyServer.js b/models/doctype/Party/PartyServer.js index 174811c5..6145ad88 100644 --- a/models/doctype/Party/PartyServer.js +++ b/models/doctype/Party/PartyServer.js @@ -1,7 +1,7 @@ import frappe from 'frappe'; -import BaseDocument from 'frappe/model/document'; +import Document from 'frappe/model/document'; -export default class PartyServer extends BaseDocument { +export default class PartyServer extends Document { beforeInsert() { if (this.customer && this.supplier) { frappe.call({ diff --git a/models/doctype/Payment/PaymentServer.js b/models/doctype/Payment/PaymentServer.js index 64be0a5c..4f4e6f19 100644 --- a/models/doctype/Payment/PaymentServer.js +++ b/models/doctype/Payment/PaymentServer.js @@ -1,8 +1,8 @@ import frappe from 'frappe'; -import BaseDocument from 'frappe/model/document'; +import Document from 'frappe/model/document'; import LedgerPosting from '../../../accounting/ledgerPosting'; -export default class PaymentServer extends BaseDocument { +export default class PaymentServer extends Document { async change({ changed }) { switch (changed) { case 'for': { diff --git a/models/doctype/Transaction/TransactionDocument.js b/models/doctype/Transaction/TransactionDocument.js index 6564e5c8..49faf74b 100644 --- a/models/doctype/Transaction/TransactionDocument.js +++ b/models/doctype/Transaction/TransactionDocument.js @@ -1,8 +1,8 @@ import frappe from 'frappe'; -import BaseDocument from 'frappe/model/document'; +import Document from 'frappe/model/document'; import { getExchangeRate } from '../../../accounting/exchangeRate'; -export default class TransactionDocument extends BaseDocument { +export default class TransactionDocument extends Document { async getExchangeRate() { if (!this.currency) return 1.0; diff --git a/src/initialization.js b/src/initialization.js index c536ac99..c49a2255 100644 --- a/src/initialization.js +++ b/src/initialization.js @@ -108,7 +108,7 @@ export async function connectToLocalDatabase(filePath) { export async function purgeCache(purgeAll = false) { const filterFunction = purgeAll ? () => true - : (d) => frappe.docs[d][d] instanceof frappe.BaseMeta; + : (d) => frappe.docs[d][d] instanceof frappe.Meta; Object.keys(frappe.docs) .filter(filterFunction) diff --git a/src/utils.js b/src/utils.js index 7ed1f4e6..677639c9 100644 --- a/src/utils.js +++ b/src/utils.js @@ -484,7 +484,7 @@ export function formatXLabels(label) { export function stringifyCircular( obj, ignoreCircular = false, - convertBaseDocument = false + convertDocument = false ) { const cacheKey = []; const cacheValue = []; @@ -504,7 +504,7 @@ export function stringifyCircular( cacheKey.push(key); cacheValue.push(value); - if (convertBaseDocument && value instanceof frappe.BaseDocument) { + if (convertDocument && value instanceof frappe.Document) { return value.getValidDict(); } From 9424b131be8987886b9402d24f63a00dab59bb6e Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Mon, 21 Mar 2022 12:19:19 +0530 Subject: [PATCH 004/163] incr: remove common - utils is frontend safe --- frappe/index.js | 2 +- frappe/model/meta.js | 2 +- frappe/{common => utils}/errors.ts | 0 frappe/utils/translation.js | 2 +- src/errorHandling.ts | 2 +- 5 files changed, 4 insertions(+), 4 deletions(-) rename frappe/{common => utils}/errors.ts (100%) diff --git a/frappe/index.js b/frappe/index.js index 5976dbb2..9ee5495b 100644 --- a/frappe/index.js +++ b/frappe/index.js @@ -1,6 +1,6 @@ import { getMoneyMaker } from 'pesa'; import { markRaw } from 'vue'; -import * as errors from './common/errors'; +import * as errors from './utils/errors'; import { asyncHandler, getDuplicates, getRandomString } from './utils'; import { DEFAULT_DISPLAY_PRECISION, diff --git a/frappe/model/meta.js b/frappe/model/meta.js index f3f0611b..9fa66c87 100644 --- a/frappe/model/meta.js +++ b/frappe/model/meta.js @@ -1,6 +1,6 @@ import frappe from 'frappe'; import { validTypes } from 'frappe/backends/helpers'; -import { ValueError } from 'frappe/common/errors'; +import { ValueError } from 'frappe/utils/errors'; import { indicators as indicatorColor } from '../../src/colors'; import { t } from '../utils/translation'; import Document from './document'; diff --git a/frappe/common/errors.ts b/frappe/utils/errors.ts similarity index 100% rename from frappe/common/errors.ts rename to frappe/utils/errors.ts diff --git a/frappe/utils/translation.js b/frappe/utils/translation.js index dd5e1089..5a0847d4 100644 --- a/frappe/utils/translation.js +++ b/frappe/utils/translation.js @@ -4,7 +4,7 @@ import { getSnippets, getWhitespaceSanitized, } from '../../scripts/helpers'; -import { ValueError } from '../common/errors'; +import { ValueError } from './errors'; class TranslationString { constructor(...args) { diff --git a/src/errorHandling.ts b/src/errorHandling.ts index 3d1a4aba..9e10662d 100644 --- a/src/errorHandling.ts +++ b/src/errorHandling.ts @@ -5,7 +5,7 @@ import { LinkValidationError, MandatoryError, ValidationError, -} from 'frappe/common/errors'; +} from 'frappe/utils/errors'; import Document from 'frappe/model/document'; import config, { ConfigKeys, TelemetrySetting } from './config'; import { IPC_ACTIONS, IPC_MESSAGES } from './messages'; From dc5d705a528f4968ee4feef89252d786c185b2d6 Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Tue, 22 Mar 2022 11:01:03 +0530 Subject: [PATCH 005/163] incr: shift auth out of Frappe to auth --- frappe/core/auth.ts | 88 ++++++++++++++++++++++++++++++++++++++++ frappe/index.js | 78 ++++------------------------------- frappe/model/document.js | 4 +- src/initialization.js | 2 +- 4 files changed, 100 insertions(+), 72 deletions(-) create mode 100644 frappe/core/auth.ts diff --git a/frappe/core/auth.ts b/frappe/core/auth.ts new file mode 100644 index 00000000..bddba15a --- /dev/null +++ b/frappe/core/auth.ts @@ -0,0 +1,88 @@ +interface AuthConfig { + serverURL: string; + backend: string; + port: number; +} + +interface Session { + user: string; + token: string; +} + +export class Auth { + #config: AuthConfig; + #session: Session; + + constructor() { + this.#config = { + serverURL: '', + backend: 'sqlite', + port: 8000, + }; + + this.#session = { + user: '', + token: '', + }; + } + + get session(): Readonly { + return { ...this.#session }; + } + + get config(): Readonly { + return { ...this.#config }; + } + + async login(email: string, password: string) { + if (email === 'Administrator') { + this.#session.user = 'Administrator'; + return; + } + + const response = await fetch(this.#getServerURL() + '/api/login', { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ email, password }), + }); + + if (response.status === 200) { + const res = await response.json(); + + this.#session.user = email; + this.#session.token = res.token; + + return res; + } + + return response; + } + + async signup(email: string, fullName: string, password: string) { + const response = await fetch(this.#getServerURL() + '/api/signup', { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ email, fullName, password }), + }); + + if (response.status === 200) { + return await response.json(); + } + + return response; + } + + async logout() { + // TODO: Implement this with auth flow + } + + #getServerURL() { + return this.#config.serverURL || ''; + } +} diff --git a/frappe/index.js b/frappe/index.js index 9ee5495b..954f150b 100644 --- a/frappe/index.js +++ b/frappe/index.js @@ -1,11 +1,12 @@ import { getMoneyMaker } from 'pesa'; import { markRaw } from 'vue'; -import * as errors from './utils/errors'; +import { Auth } from './core/auth'; import { asyncHandler, getDuplicates, getRandomString } from './utils'; import { DEFAULT_DISPLAY_PRECISION, DEFAULT_INTERNAL_PRECISION, } from './utils/consts'; +import * as errors from './utils/errors'; import { format } from './utils/format'; import Observable from './utils/observable'; import { t, T } from './utils/translation'; @@ -20,6 +21,10 @@ class Frappe { isElectron = false; isServer = false; + constructor() { + this.auth = new Auth(); + } + async initializeAndRegister(customModels = {}, force = false) { this.init(force); @@ -80,25 +85,15 @@ class Frappe { init(force) { if (this._initialized && !force) return; - // Initialize Config - this.config = { - serverURL: '', - backend: 'sqlite', - port: 8000, - }; - // Initialize Globals this.metaCache = {}; this.models = {}; - this.forms = {}; - this.views = {}; - this.flags = {}; + this.methods = {}; this.errorLog = []; // temp params while calling routes this.temp = {}; - this.params = {}; this.docs = new Observable(); this.events = new Observable(); @@ -139,6 +134,7 @@ class Frappe { return filterFunction ? models.filter(filterFunction) : models; } + // del registerView(view, name, module) { if (!this.views[view]) this.views[view] = {}; this.views[view][name] = module; @@ -328,65 +324,9 @@ class Frappe { } } - // only for client side - async login(email, password) { - if (email === 'Administrator') { - this.session = { - user: 'Administrator', - }; - return; - } - - let response = await fetch(this.getServerURL() + '/api/login', { - method: 'POST', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ email, password }), - }); - - if (response.status === 200) { - const res = await response.json(); - - this.session = { - user: email, - token: res.token, - }; - - return res; - } - - return response; - } - - async signup(email, fullName, password) { - let response = await fetch(this.getServerURL() + '/api/signup', { - method: 'POST', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ email, fullName, password }), - }); - - if (response.status === 200) { - return await response.json(); - } - - return response; - } - - getServerURL() { - return this.config.serverURL || ''; - } - close() { this.db.close(); - - if (this.server) { - this.server.close(); - } + this.auth.logout(); } store = { diff --git a/frappe/model/document.js b/frappe/model/document.js index d91f983d..f88c7e66 100644 --- a/frappe/model/document.js +++ b/frappe/model/document.js @@ -321,7 +321,7 @@ export default class Document extends Observable { let now = new Date().toISOString(); if (!this.owner) { - this.owner = frappe.session.user; + this.owner = frappe.auth.session.user; } if (!this.creation) { @@ -335,7 +335,7 @@ export default class Document extends Observable { updateModified() { if (frappe.isServer) { let now = new Date().toISOString(); - this.modifiedBy = frappe.session.user; + this.modifiedBy = frappe.auth.session.user this.modified = now; } } diff --git a/src/initialization.js b/src/initialization.js index c49a2255..a95287e6 100644 --- a/src/initialization.js +++ b/src/initialization.js @@ -39,7 +39,7 @@ export async function connectToLocalDatabase(filePath) { return { connectionSuccess: false, reason: DB_CONN_FAILURE.INVALID_FILE }; } - frappe.login('Administrator'); + frappe.auth.login('Administrator'); try { frappe.db = new SQLiteDatabase({ dbPath: filePath, From 82709165a40661199c2eae9ed4b1802eb54eb252 Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Tue, 22 Mar 2022 11:30:33 +0530 Subject: [PATCH 006/163] chore: rename getNewDoc, newDoc - delete irrelevant tests --- accounting/importCOA.js | 2 +- accounting/ledgerPosting.js | 2 +- frappe/backends/database.js | 9 +- frappe/index.js | 160 ++++++++++------------ frappe/model/document.js | 8 +- frappe/model/naming.js | 4 +- frappe/model/runPatches.js | 2 +- models/doctype/Item/Item.js | 4 +- models/doctype/Party/Customer.js | 2 +- models/doctype/Party/Supplier.js | 2 +- models/doctype/Tax/RegionalEntries.js | 2 +- models/doctype/Transaction/Transaction.js | 2 +- src/components/Controls/Link.vue | 7 +- src/components/FilterDropdown.vue | 6 +- src/components/TwoColumnForm.vue | 6 +- src/dataImport.ts | 2 +- src/pages/ChartOfAccounts.vue | 8 +- src/pages/Dashboard/UnpaidInvoices.vue | 3 +- src/pages/ListView/ListView.vue | 10 +- src/pages/SetupWizard/SetupWizard.vue | 19 ++- src/pages/SetupWizard/setupCompany.js | 2 +- tests/testInvoice.js | 56 -------- 22 files changed, 145 insertions(+), 173 deletions(-) delete mode 100644 tests/testInvoice.js diff --git a/accounting/importCOA.js b/accounting/importCOA.js index d1629840..e9c4bd33 100644 --- a/accounting/importCOA.js +++ b/accounting/importCOA.js @@ -25,7 +25,7 @@ async function importAccounts(children, parentAccount, rootType, rootAccount) { const { accountType, accountNumber } = child; const accountName = getAccountName(rootName, accountNumber); const isGroup = identifyIsGroup(child); - const doc = frappe.newDoc({ + const doc = frappe.getNewDoc({ doctype: 'Account', name: accountName, parentAccount, diff --git a/accounting/ledgerPosting.js b/accounting/ledgerPosting.js index f2f504bf..58a17817 100644 --- a/accounting/ledgerPosting.js +++ b/accounting/ledgerPosting.js @@ -145,7 +145,7 @@ export default class LedgerPosting { async insertEntries() { for (let entry of this.entries) { - let entryDoc = frappe.newDoc({ + let entryDoc = frappe.getNewDoc({ doctype: 'AccountingLedgerEntry', }); Object.assign(entryDoc, entry); diff --git a/frappe/backends/database.js b/frappe/backends/database.js index f1bc6874..af4839b8 100644 --- a/frappe/backends/database.js +++ b/frappe/backends/database.js @@ -2,6 +2,7 @@ import frappe from 'frappe'; import Observable from 'frappe/utils/observable'; import Knex from 'knex'; import CacheManager from '../utils/cacheManager'; +import { getRandomString } from '../utils/index'; export default class Database extends Observable { constructor() { @@ -49,7 +50,7 @@ export default class Database extends Observable { if (await this.singleExists(doctype)) { const singleValues = await this.getSingleFieldsToInsert(doctype); singleValues.forEach(({ fieldname, value }) => { - let singleValue = frappe.newDoc({ + let singleValue = frappe.getNewDoc({ doctype: 'SingleValue', parent: doctype, fieldname, @@ -429,7 +430,7 @@ export default class Database extends Observable { let fields = this.getValidFields(doctype); if (!doc.name) { - doc.name = frappe.getRandomString(); + doc.name = getRandomString(); } let formattedDoc = this.getFormattedDoc(fields, doc); @@ -509,7 +510,7 @@ export default class Database extends Observable { for (let field of meta.getValidFields({ withChildren: false })) { let value = doc[field.fieldname]; if (value != null) { - let singleValue = frappe.newDoc({ + let singleValue = frappe.getNewDoc({ doctype: 'SingleValue', parent: doctype, fieldname: field.fieldname, @@ -540,7 +541,7 @@ export default class Database extends Observable { prepareChild(parenttype, parent, child, field, idx) { if (!child.name) { - child.name = frappe.getRandomString(); + child.name = getRandomString(); } child.parent = parent; child.parenttype = parenttype; diff --git a/frappe/index.js b/frappe/index.js index 954f150b..e627ac86 100644 --- a/frappe/index.js +++ b/frappe/index.js @@ -15,7 +15,6 @@ class Frappe { t = t; T = T; format = format; - getRandomString = getRandomString; errors = errors; isElectron = false; @@ -36,6 +35,67 @@ class Frappe { this.registerModels(customModels); } + init(force) { + if (this._initialized && !force) return; + + // Initialize Globals + this.metaCache = {}; + this.models = {}; + + this.methods = {}; + this.errorLog = []; + + // temp params while calling routes + this.temp = {}; + + this.docs = new Observable(); + this.events = new Observable(); + this._initialized = true; + } + + registerModels(models) { + // register models from app/models/index.js + for (let doctype in models) { + let metaDefinition = models[doctype]; + if (!metaDefinition.name) { + throw new Error(`Name is mandatory for ${doctype}`); + } + if (metaDefinition.name !== doctype) { + throw new Error( + `Model name mismatch for ${doctype}: ${metaDefinition.name}` + ); + } + let fieldnames = (metaDefinition.fields || []) + .map((df) => df.fieldname) + .sort(); + let duplicateFieldnames = getDuplicates(fieldnames); + if (duplicateFieldnames.length > 0) { + throw new Error( + `Duplicate fields in ${doctype}: ${duplicateFieldnames.join(', ')}` + ); + } + + this.models[doctype] = metaDefinition; + } + } + + registerMethod({ method, handler }) { + this.methods[method] = handler; + if (this.app) { + // add to router if client-server + this.app.post( + `/api/method/${method}`, + asyncHandler(async function (request, response) { + let data = await handler(request.body); + if (data === undefined) { + data = {}; + } + return response.json(data); + }) + ); + } + } + async initializeMoneyMaker(currency) { currency ??= 'XXX'; @@ -82,50 +142,6 @@ class Frappe { }); } - init(force) { - if (this._initialized && !force) return; - - // Initialize Globals - this.metaCache = {}; - this.models = {}; - - this.methods = {}; - this.errorLog = []; - - // temp params while calling routes - this.temp = {}; - - this.docs = new Observable(); - this.events = new Observable(); - this._initialized = true; - } - - registerModels(models) { - // register models from app/models/index.js - for (let doctype in models) { - let metaDefinition = models[doctype]; - if (!metaDefinition.name) { - throw new Error(`Name is mandatory for ${doctype}`); - } - if (metaDefinition.name !== doctype) { - throw new Error( - `Model name mismatch for ${doctype}: ${metaDefinition.name}` - ); - } - let fieldnames = (metaDefinition.fields || []) - .map((df) => df.fieldname) - .sort(); - let duplicateFieldnames = getDuplicates(fieldnames); - if (duplicateFieldnames.length > 0) { - throw new Error( - `Duplicate fields in ${doctype}: ${duplicateFieldnames.join(', ')}` - ); - } - - this.models[doctype] = metaDefinition; - } - } - getModels(filterFunction) { let models = []; for (let doctype in this.models) { @@ -134,29 +150,6 @@ class Frappe { return filterFunction ? models.filter(filterFunction) : models; } - // del - registerView(view, name, module) { - if (!this.views[view]) this.views[view] = {}; - this.views[view][name] = module; - } - - registerMethod({ method, handler }) { - this.methods[method] = handler; - if (this.app) { - // add to router if client-server - this.app.post( - `/api/method/${method}`, - asyncHandler(async function (request, response) { - let data = await handler(request.body); - if (data === undefined) { - data = {}; - } - return response.json(data); - }) - ); - } - } - async call({ method, args }) { if (this.isServer) { if (this.methods[method]) { @@ -263,7 +256,7 @@ class Frappe { } async getDuplicate(doc) { - const newDoc = await this.getNewDoc(doc.doctype); + const newDoc = await this.getEmptyDoc(doc.doctype); for (let field of this.getMeta(doc.doctype).getValidFields()) { if (['name', 'submitted'].includes(field.fieldname)) continue; if (field.fieldtype === 'Table') { @@ -279,37 +272,26 @@ class Frappe { return newDoc; } - getNewDoc(doctype, cacheDoc = true) { - let doc = this.newDoc({ doctype: doctype }); + getEmptyDoc(doctype, cacheDoc = true) { + let doc = this.getNewDoc({ doctype: doctype }); doc._notInserted = true; - doc.name = frappe.getRandomString(); + doc.name = getRandomString(); + if (cacheDoc) { this.addToCache(doc); } + return doc; } - async newCustomDoc(fields) { - let doc = new this.Document({ isCustom: 1, fields }); - doc._notInserted = true; - doc.name = this.getRandomString(); - this.addToCache(doc); - return doc; - } - - createMeta(fields) { - let meta = new this.Meta({ isCustom: 1, fields }); - return meta; - } - - newDoc(data) { + getNewDoc(data) { let doc = new (this.getDocumentClass(data.doctype))(data); doc.setDefaults(); return doc; } - async insert(data) { - return await this.newDoc(data).insert(); + createMeta(fields) { + return new this.Meta({ isCustom: 1, fields }); } async syncDoc(data) { @@ -319,7 +301,7 @@ class Frappe { Object.assign(doc, data); await doc.update(); } else { - doc = this.newDoc(data); + doc = this.getNewDoc(data); await doc.insert(); } } diff --git a/frappe/model/document.js b/frappe/model/document.js index f88c7e66..fc7303c6 100644 --- a/frappe/model/document.js +++ b/frappe/model/document.js @@ -3,7 +3,7 @@ import { Verb } from '@/telemetry/types'; import frappe from 'frappe'; import Observable from 'frappe/utils/observable'; import { DEFAULT_INTERNAL_PRECISION } from '../utils/consts'; -import { isPesa } from '../utils/index'; +import { getRandomString, isPesa } from '../utils/index'; import { setName } from './naming'; export default class Document extends Observable { @@ -190,7 +190,7 @@ export default class Document extends Observable { } if (!data.name) { - data.name = frappe.getRandomString(); + data.name = getRandomString(); } const childDoc = new Document(data); @@ -335,7 +335,7 @@ export default class Document extends Observable { updateModified() { if (frappe.isServer) { let now = new Date().toISOString(); - this.modifiedBy = frappe.auth.session.user + this.modifiedBy = frappe.auth.session.user; this.modified = now; } } @@ -741,7 +741,7 @@ export default class Document extends Observable { updateMap.name = updateMap.name + ' CPY'; } - const doc = frappe.getNewDoc(this.doctype, false); + const doc = frappe.getEmptyDoc(this.doctype, false); await doc.set(updateMap); await doc.insert(); } diff --git a/frappe/model/naming.js b/frappe/model/naming.js index 1a333cbf..11646197 100644 --- a/frappe/model/naming.js +++ b/frappe/model/naming.js @@ -2,7 +2,7 @@ import frappe from 'frappe'; import { getRandomString } from 'frappe/utils'; export async function isNameAutoSet(doctype) { - const doc = frappe.getNewDoc(doctype); + const doc = frappe.getEmptyDoc(doctype); if (doc.meta.naming === 'autoincrement') { return true; } @@ -108,7 +108,7 @@ export async function createNumberSeries(prefix, referenceType, start = 1001) { return; } - const series = frappe.newDoc({ + const series = frappe.getNewDoc({ doctype: 'NumberSeries', name: prefix, start, diff --git a/frappe/model/runPatches.js b/frappe/model/runPatches.js index dfdcceec..b0bebb7b 100644 --- a/frappe/model/runPatches.js +++ b/frappe/model/runPatches.js @@ -17,7 +17,7 @@ export default async function runPatches(patchList) { async function runPatch({ patchName, patchFunction }) { try { await patchFunction(); - const patchRun = frappe.getNewDoc('PatchRun'); + const patchRun = frappe.getEmptyDoc('PatchRun'); patchRun.name = patchName; await patchRun.insert(); } catch (error) { diff --git a/models/doctype/Item/Item.js b/models/doctype/Item/Item.js index 19cf9503..0ef9fdf6 100644 --- a/models/doctype/Item/Item.js +++ b/models/doctype/Item/Item.js @@ -140,7 +140,7 @@ export default { label: t`New Invoice`, condition: (doc) => !doc.isNew(), action: async (doc, router) => { - const invoice = await frappe.getNewDoc('SalesInvoice'); + const invoice = await frappe.getEmptyDoc('SalesInvoice'); invoice.append('items', { item: doc.name, rate: doc.rate, @@ -153,7 +153,7 @@ export default { label: t`New Bill`, condition: (doc) => !doc.isNew(), action: async (doc, router) => { - const invoice = await frappe.getNewDoc('PurchaseInvoice'); + const invoice = await frappe.getEmptyDoc('PurchaseInvoice'); invoice.append('items', { item: doc.name, rate: doc.rate, diff --git a/models/doctype/Party/Customer.js b/models/doctype/Party/Customer.js index 404d3d9a..197ddf5e 100644 --- a/models/doctype/Party/Customer.js +++ b/models/doctype/Party/Customer.js @@ -15,7 +15,7 @@ export default { label: t`Create Invoice`, condition: (doc) => !doc.isNew(), action: async (customer) => { - let doc = await frappe.getNewDoc('SalesInvoice'); + let doc = await frappe.getEmptyDoc('SalesInvoice'); router.push({ path: `/edit/SalesInvoice/${doc.name}`, query: { diff --git a/models/doctype/Party/Supplier.js b/models/doctype/Party/Supplier.js index f10566b2..b5f09ea8 100644 --- a/models/doctype/Party/Supplier.js +++ b/models/doctype/Party/Supplier.js @@ -15,7 +15,7 @@ export default { label: t`Create Bill`, condition: (doc) => !doc.isNew(), action: async (supplier) => { - let doc = await frappe.getNewDoc('PurchaseInvoice'); + let doc = await frappe.getEmptyDoc('PurchaseInvoice'); router.push({ path: `/edit/PurchaseInvoice/${doc.name}`, query: { diff --git a/models/doctype/Tax/RegionalEntries.js b/models/doctype/Tax/RegionalEntries.js index fdb89931..9e0be5cf 100644 --- a/models/doctype/Tax/RegionalEntries.js +++ b/models/doctype/Tax/RegionalEntries.js @@ -8,7 +8,7 @@ export default async function generateTaxes(country) { 'Exempt-GST': [0], 'Exempt-IGST': [0], }; - let newTax = await frappe.getNewDoc('Tax'); + let newTax = await frappe.getEmptyDoc('Tax'); for (const type of Object.keys(GSTs)) { for (const percent of GSTs[type]) { diff --git a/models/doctype/Transaction/Transaction.js b/models/doctype/Transaction/Transaction.js index e81bed84..6e6c302f 100644 --- a/models/doctype/Transaction/Transaction.js +++ b/models/doctype/Transaction/Transaction.js @@ -33,7 +33,7 @@ export function getActions(doctype) { label: t`Make Payment`, condition: (doc) => doc.submitted && doc.outstandingAmount > 0, action: async function makePayment(doc) { - let payment = await frappe.getNewDoc('Payment'); + let payment = await frappe.getEmptyDoc('Payment'); payment.once('afterInsert', async () => { await payment.submit(); }); diff --git a/src/components/Controls/Link.vue b/src/components/Controls/Link.vue index b9203848..c757d8e9 100644 --- a/src/components/Controls/Link.vue +++ b/src/components/Controls/Link.vue @@ -1,3 +1,8 @@ +import Badge from '@/components/Badge'; +import { openQuickEdit } from '@/utils'; +import frappe, { t } from 'frappe'; +import { markRaw } from 'vue'; +import AutoComplete from './AutoComplete'; - - diff --git a/models/doctype/GSTR3B/GSTR3BServer.js b/models/doctype/GSTR3B/GSTR3BServer.js deleted file mode 100644 index fe7c006f..00000000 --- a/models/doctype/GSTR3B/GSTR3BServer.js +++ /dev/null @@ -1,12 +0,0 @@ -import GSTR3B from './GSTR3BDocument'; - -export default class GSTR3BServer extends GSTR3B { - async validate() { - if (this.month.length === 0 || this.year.length != 4) { - throw new Error('Month or Year is not valid'); - } - } - async beforeInsert() { - this.name = `${this.doctype} Report ${this.month} ${this.year}`; - } -}; diff --git a/models/doctype/PurchaseOrder/PurchaseOrder.js b/models/doctype/PurchaseOrder/PurchaseOrder.js deleted file mode 100644 index a47960d9..00000000 --- a/models/doctype/PurchaseOrder/PurchaseOrder.js +++ /dev/null @@ -1,33 +0,0 @@ -import { t } from 'frappe'; -import model from 'frappe/model'; -import { DEFAULT_NUMBER_SERIES } from '../../../frappe/utils/consts'; -import PurchaseInvoice from '../PurchaseInvoice/PurchaseInvoice'; - -export default model.extend( - PurchaseInvoice, - { - name: 'PurchaseOrder', - label: t`Purchase Order`, - settings: 'PurchaseOrderSettings', - fields: [ - { - fieldname: 'items', - childtype: 'PurchaseOrderItem', - }, - { - fieldname: 'numberSeries', - label: t`Number Series`, - fieldtype: 'Link', - target: 'NumberSeries', - required: 1, - getFilters: () => { - return { referenceType: 'PurchaseOrder' }; - }, - default: DEFAULT_NUMBER_SERIES['PurchaseOrder'], - }, - ], - }, - { - skipFields: ['account'], - } -); diff --git a/models/doctype/PurchaseOrderItem/PurchaseOrderItem.js b/models/doctype/PurchaseOrderItem/PurchaseOrderItem.js deleted file mode 100644 index 215580f7..00000000 --- a/models/doctype/PurchaseOrderItem/PurchaseOrderItem.js +++ /dev/null @@ -1,6 +0,0 @@ -import model from 'frappe/model'; -import PurchaseInvoiceItem from '../PurchaseInvoiceItem/PurchaseInvoiceItem'; - -export default model.extend(PurchaseInvoiceItem, { - name: 'PurchaseOrderItem', -}); diff --git a/models/doctype/PurchaseOrderSettings/PurchaseOrderSettings.js b/models/doctype/PurchaseOrderSettings/PurchaseOrderSettings.js deleted file mode 100644 index 4aab3f3d..00000000 --- a/models/doctype/PurchaseOrderSettings/PurchaseOrderSettings.js +++ /dev/null @@ -1,9 +0,0 @@ -import { t } from 'frappe'; -import model from 'frappe/model'; -import PurchaseInvoiceSettings from '../PurchaseInvoiceSettings/PurchaseInvoiceSettings'; - -export default model.extend(PurchaseInvoiceSettings, { - name: 'PurchaseOrderSettings', - label: t`Purchase Order Settings`, - fields: [], -}); diff --git a/models/doctype/PurchaseReceipt/PurchaseReceipt.js b/models/doctype/PurchaseReceipt/PurchaseReceipt.js deleted file mode 100644 index d3c31ee7..00000000 --- a/models/doctype/PurchaseReceipt/PurchaseReceipt.js +++ /dev/null @@ -1,27 +0,0 @@ -import { t } from 'frappe'; -import model from 'frappe/model'; -import { DEFAULT_NUMBER_SERIES } from '../../../frappe/utils/consts'; -import PurchaseOrder from '../PurchaseOrder/PurchaseOrder'; - -export default model.extend(PurchaseOrder, { - name: 'PurchaseReceipt', - label: t`Purchase Receipt`, - settings: 'PurchaseReceiptSettings', - fields: [ - { - fieldname: 'items', - childtype: 'PurchaseReceiptItem', - }, - { - fieldname: 'numberSeries', - label: t`Number Series`, - fieldtype: 'Link', - target: 'NumberSeries', - required: 1, - getFilters: () => { - return { referenceType: 'PurchaseReceipt' }; - }, - default: DEFAULT_NUMBER_SERIES['PurchaseReceipt'], - }, - ], -}); diff --git a/models/doctype/PurchaseReceiptItem/PurchaseReceiptItem.js b/models/doctype/PurchaseReceiptItem/PurchaseReceiptItem.js deleted file mode 100644 index 15eef59b..00000000 --- a/models/doctype/PurchaseReceiptItem/PurchaseReceiptItem.js +++ /dev/null @@ -1,21 +0,0 @@ -import { t } from 'frappe'; -import model from 'frappe/model'; -import PurchaseOrderItem from '../PurchaseOrderItem/PurchaseOrderItem'; - -export default model.extend( - PurchaseOrderItem, - { - name: 'PurchaseReceiptItem', - fields: [ - { - fieldname: 'acceptedQuantity', - label: t`Accepted Quantity`, - fieldtype: 'Float', - required: 1, - }, - ], - }, - { - skipFields: ['expenseAccount'], - } -); diff --git a/models/doctype/PurchaseReceiptSettings/PurchaseReceiptSettings.js b/models/doctype/PurchaseReceiptSettings/PurchaseReceiptSettings.js deleted file mode 100644 index b2c441c2..00000000 --- a/models/doctype/PurchaseReceiptSettings/PurchaseReceiptSettings.js +++ /dev/null @@ -1,9 +0,0 @@ -import { t } from 'frappe'; -import model from 'frappe/model'; -import PurchaseOrderSettings from '../PurchaseOrderSettings/PurchaseOrderSettings'; - -export default model.extend(PurchaseOrderSettings, { - name: 'PurchaseReceiptSettings', - label: t`Purchase Receipt Settings`, - fields: [], -}); diff --git a/models/doctype/Quotation/Quotation.js b/models/doctype/Quotation/Quotation.js deleted file mode 100644 index d2401a63..00000000 --- a/models/doctype/Quotation/Quotation.js +++ /dev/null @@ -1,37 +0,0 @@ -import { t } from 'frappe'; -import model from 'frappe/model'; -import { DEFAULT_NUMBER_SERIES } from '../../../frappe/utils/consts'; -import SalesInvoice from '../SalesInvoice/SalesInvoice'; - -const Quotation = model.extend( - SalesInvoice, - { - name: 'Quotation', - label: t`Quotation`, - settings: 'QuotationSettings', - fields: [ - { - fieldname: 'items', - childtype: 'QuotationItem', - }, - { - fieldname: 'numberSeries', - label: t`Number Series`, - fieldtype: 'Link', - target: 'NumberSeries', - required: 1, - getFilters: () => { - return { referenceType: 'Quotation' }; - }, - default: DEFAULT_NUMBER_SERIES['Quotation'], - }, - ], - links: [], - }, - { - skipFields: ['account'], - overrideProps: ['links'], - } -); - -export default Quotation; diff --git a/models/doctype/Quotation/QuotationDocument.js b/models/doctype/Quotation/QuotationDocument.js deleted file mode 100644 index 2e1f3484..00000000 --- a/models/doctype/Quotation/QuotationDocument.js +++ /dev/null @@ -1,3 +0,0 @@ -import SalesInvoiceDocument from '../SalesInvoice/SalesInvoiceDocument'; - -export default class Quotation extends SalesInvoiceDocument {}; diff --git a/models/doctype/QuotationItem/QuotationItem.js b/models/doctype/QuotationItem/QuotationItem.js deleted file mode 100644 index db71423d..00000000 --- a/models/doctype/QuotationItem/QuotationItem.js +++ /dev/null @@ -1,6 +0,0 @@ -import model from 'frappe/model'; -import SalesInvoiceItem from '../SalesInvoiceItem/SalesInvoiceItem'; - -export default model.extend(SalesInvoiceItem, { - name: 'QuotationItem', -}); diff --git a/models/doctype/QuotationItem/RegionalChanges.js b/models/doctype/QuotationItem/RegionalChanges.js deleted file mode 100644 index 8a163ee5..00000000 --- a/models/doctype/QuotationItem/RegionalChanges.js +++ /dev/null @@ -1,25 +0,0 @@ -import { t } from 'frappe'; -import { cloneDeep } from 'lodash'; -import QuotationItemOriginal from './QuotationItem'; - -export default function getAugmentedQuotationItem({ country }) { - const QuotationItem = cloneDeep(QuotationItemOriginal); - if (!country) { - return QuotationItem; - } - - if (country === 'India') { - QuotationItem.fields = [ - ...QuotationItem.fields, - { - fieldname: 'hsnCode', - label: t`HSN/SAC`, - fieldtype: 'Int', - formula: (row, doc) => doc.getFrom('Item', row.item, 'hsnCode'), - formulaDependsOn: ['item'], - }, - ]; - } - - return QuotationItem; -} diff --git a/models/doctype/QuotationSettings/QuotationSettings.js b/models/doctype/QuotationSettings/QuotationSettings.js deleted file mode 100644 index 2128dbf1..00000000 --- a/models/doctype/QuotationSettings/QuotationSettings.js +++ /dev/null @@ -1,9 +0,0 @@ -import { t } from 'frappe'; -import model from 'frappe/model'; -import SalesInvoiceSettings from '../SalesInvoiceSettings/SalesInvoiceSettings'; - -export default model.extend(SalesInvoiceSettings, { - name: 'QuotationSettings', - label: t`Quotation Settings`, - fields: [], -}); diff --git a/models/doctype/SalesOrder/SalesOrder.js b/models/doctype/SalesOrder/SalesOrder.js deleted file mode 100644 index b8ed2eb6..00000000 --- a/models/doctype/SalesOrder/SalesOrder.js +++ /dev/null @@ -1,27 +0,0 @@ -import { t } from 'frappe'; -import model from 'frappe/model'; -import { DEFAULT_NUMBER_SERIES } from '../../../frappe/utils/consts'; -import Quotation from '../Quotation/Quotation'; - -export default model.extend(Quotation, { - name: 'SalesOrder', - label: t`Sales Order`, - settings: 'SalesOrderSettings', - fields: [ - { - fieldname: 'items', - childtype: 'SalesOrderItem', - }, - { - fieldname: 'numberSeries', - label: t`Number Series`, - fieldtype: 'Link', - target: 'NumberSeries', - required: 1, - getFilters: () => { - return { referenceType: 'SalesOrder' }; - }, - default: DEFAULT_NUMBER_SERIES['SalesOrder'], - }, - ], -}); diff --git a/models/doctype/SalesOrderItem/RegionalChanges.js b/models/doctype/SalesOrderItem/RegionalChanges.js deleted file mode 100644 index 13e59fb0..00000000 --- a/models/doctype/SalesOrderItem/RegionalChanges.js +++ /dev/null @@ -1,25 +0,0 @@ -import { t } from 'frappe'; -import { cloneDeep } from 'lodash'; -import SalesOrderItemOriginal from './SalesOrderItem'; - -export default function getAugmentedSalesOrderItem({ country }) { - const SalesOrderItem = cloneDeep(SalesOrderItemOriginal); - if (!country) { - return SalesOrderItem; - } - - if (country === 'India') { - SalesOrderItem.fields = [ - ...SalesOrderItem.fields, - { - fieldname: 'hsnCode', - label: t`HSN/SAC`, - fieldtype: 'Int', - formula: (row, doc) => doc.getFrom('Item', row.item, 'hsnCode'), - formulaDependsOn: ['item'], - }, - ]; - } - - return SalesOrderItem; -} diff --git a/models/doctype/SalesOrderItem/SalesOrderItem.js b/models/doctype/SalesOrderItem/SalesOrderItem.js deleted file mode 100644 index 9a498fc5..00000000 --- a/models/doctype/SalesOrderItem/SalesOrderItem.js +++ /dev/null @@ -1,6 +0,0 @@ -import model from 'frappe/model'; -import QuotationItem from '../QuotationItem/QuotationItem'; - -export default model.extend(QuotationItem, { - name: 'SalesOrderItem', -}); diff --git a/models/doctype/SalesOrderSettings/SalesOrderSettings.js b/models/doctype/SalesOrderSettings/SalesOrderSettings.js deleted file mode 100644 index 755f2c91..00000000 --- a/models/doctype/SalesOrderSettings/SalesOrderSettings.js +++ /dev/null @@ -1,9 +0,0 @@ -import { t } from 'frappe'; -import model from 'frappe/model'; -import QuotationSettings from '../QuotationSettings/QuotationSettings'; - -export default model.extend(QuotationSettings, { - name: 'SalesOrderSettings', - label: t`Sales Order Settings`, - fields: [], -}); diff --git a/models/index.js b/models/index.js index 0288d118..3ddd0c4f 100644 --- a/models/index.js +++ b/models/index.js @@ -1,54 +1,33 @@ -import SetupWizard from './doctype/SetupWizard/SetupWizard.js'; -import Currency from './doctype/Currency/Currency.js'; -import Color from './doctype/Color/Color.js'; import Account from './doctype/Account/Account.js'; -import AccountingSettings from './doctype/AccountingSettings/AccountingSettings.js'; -import CompanySettings from './doctype/CompanySettings/CompanySettings.js'; import AccountingLedgerEntry from './doctype/AccountingLedgerEntry/AccountingLedgerEntry.js'; -import Party from './doctype/Party/Party.js'; +import AccountingSettings from './doctype/AccountingSettings/AccountingSettings.js'; +import Address from './doctype/Address/Address.js'; +import Color from './doctype/Color/Color.js'; +import CompanySettings from './doctype/CompanySettings/CompanySettings.js'; +import Contact from './doctype/Contact/Contact.js'; +import Currency from './doctype/Currency/Currency.js'; +import GetStarted from './doctype/GetStarted/GetStarted.js'; +import Item from './doctype/Item/Item.js'; +import JournalEntry from './doctype/JournalEntry/JournalEntry.js'; +import JournalEntryAccount from './doctype/JournalEntryAccount/JournalEntryAccount.js'; +import JournalEntrySettings from './doctype/JournalEntrySettings/JournalEntrySettings.js'; import Customer from './doctype/Party/Customer.js'; +import Party from './doctype/Party/Party.js'; import Supplier from './doctype/Party/Supplier.js'; import Payment from './doctype/Payment/Payment.js'; import PaymentFor from './doctype/PaymentFor/PaymentFor.js'; import PaymentSettings from './doctype/PaymentSettings/PaymentSettings.js'; -import Item from './doctype/Item/Item.js'; -import SalesInvoice from './doctype/SalesInvoice/SalesInvoice.js'; -import SalesInvoiceItem from './doctype/SalesInvoiceItem/SalesInvoiceItem.js'; -import SalesInvoiceSettings from './doctype/SalesInvoiceSettings/SalesInvoiceSettings.js'; +import PrintSettings from './doctype/PrintSettings/PrintSettings.js'; import PurchaseInvoice from './doctype/PurchaseInvoice/PurchaseInvoice.js'; import PurchaseInvoiceItem from './doctype/PurchaseInvoiceItem/PurchaseInvoiceItem.js'; import PurchaseInvoiceSettings from './doctype/PurchaseInvoiceSettings/PurchaseInvoiceSettings.js'; +import SalesInvoice from './doctype/SalesInvoice/SalesInvoice.js'; +import SalesInvoiceItem from './doctype/SalesInvoiceItem/SalesInvoiceItem.js'; +import SalesInvoiceSettings from './doctype/SalesInvoiceSettings/SalesInvoiceSettings.js'; +import SetupWizard from './doctype/SetupWizard/SetupWizard.js'; import Tax from './doctype/Tax/Tax.js'; import TaxDetail from './doctype/TaxDetail/TaxDetail.js'; import TaxSummary from './doctype/TaxSummary/TaxSummary.js'; -import GSTR3B from './doctype/GSTR3B/GSTR3B.js'; -import Address from './doctype/Address/Address.js'; -import Contact from './doctype/Contact/Contact.js'; -import JournalEntry from './doctype/JournalEntry/JournalEntry.js'; -import JournalEntryAccount from './doctype/JournalEntryAccount/JournalEntryAccount.js'; -import JournalEntrySettings from './doctype/JournalEntrySettings/JournalEntrySettings.js'; -import Quotation from './doctype/Quotation/Quotation.js'; -import QuotationItem from './doctype/QuotationItem/QuotationItem.js'; -import QuotationSettings from './doctype/QuotationSettings/QuotationSettings.js'; -import SalesOrder from './doctype/SalesOrder/SalesOrder.js'; -import SalesOrderItem from './doctype/SalesOrderItem/SalesOrderItem.js'; -import SalesOrderSettings from './doctype/SalesOrderSettings/SalesOrderSettings.js'; -import Fulfillment from './doctype/Fulfillment/Fulfillment.js'; -import FulfillmentItem from './doctype/FulfillmentItem/FulfillmentItem.js'; -import FulfillmentSettings from './doctype/FulfillmentSettings/FulfillmentSettings.js'; -import PurchaseOrder from './doctype/PurchaseOrder/PurchaseOrder.js'; -import PurchaseOrderItem from './doctype/PurchaseOrderItem/PurchaseOrderItem.js'; -import PurchaseOrderSettings from './doctype/PurchaseOrderSettings/PurchaseOrderSettings.js'; -import PurchaseReceipt from './doctype/PurchaseReceipt/PurchaseReceipt.js'; -import PurchaseReceiptItem from './doctype/PurchaseReceiptItem/PurchaseReceiptItem.js'; -import PurchaseReceiptSettings from './doctype/PurchaseReceiptSettings/PurchaseReceiptSettings.js'; -import Event from './doctype/Event/Event.js'; -import EventSchedule from './doctype/EventSchedule/EventSchedule.js'; -import EventSettings from './doctype/EventSettings/EventSettings.js'; -import Email from './doctype/Email/Email.js'; -import EmailAccount from './doctype/EmailAccount/EmailAccount.js'; -import PrintSettings from './doctype/PrintSettings/PrintSettings.js'; -import GetStarted from './doctype/GetStarted/GetStarted.js'; export default { SetupWizard, @@ -74,32 +53,11 @@ export default { Tax, TaxDetail, TaxSummary, - GSTR3B, Address, Contact, JournalEntry, JournalEntryAccount, JournalEntrySettings, - Quotation, - QuotationItem, - QuotationSettings, - SalesOrder, - SalesOrderItem, - SalesOrderSettings, - Fulfillment, - FulfillmentItem, - FulfillmentSettings, - PurchaseOrder, - PurchaseOrderItem, - PurchaseOrderSettings, - PurchaseReceipt, - PurchaseReceiptItem, - PurchaseReceiptSettings, - Event, - EventSchedule, - EventSettings, - Email, - EmailAccount, PrintSettings, GetStarted, }; diff --git a/models/types.ts b/models/types.ts index 72f59c63..296d171f 100644 --- a/models/types.ts +++ b/models/types.ts @@ -22,7 +22,6 @@ export enum DoctypeName { Tax = 'Tax', TaxDetail = 'TaxDetail', TaxSummary = 'TaxSummary', - GSTR3B = 'GSTR3B', Address = 'Address', Contact = 'Contact', JournalEntry = 'JournalEntry', diff --git a/reports/GoodsAndServiceTax/BaseViewConfig.js b/reports/GoodsAndServiceTax/BaseViewConfig.js index acc07c86..54e297e9 100644 --- a/reports/GoodsAndServiceTax/BaseViewConfig.js +++ b/reports/GoodsAndServiceTax/BaseViewConfig.js @@ -3,8 +3,6 @@ import { DateTime } from 'luxon'; import { stateCodeMap } from '../../accounting/gst'; import { titleCase } from '../../src/utils'; -const stateList = Object.keys(stateCodeMap).map(titleCase).sort(); - export default { filterFields: [ { @@ -13,7 +11,7 @@ export default { size: 'small', placeholder: t`Place`, fieldname: 'place', - getList: () => stateList, + getList: () => Object.keys(stateCodeMap).map(titleCase).sort(), }, { fieldtype: 'Date', diff --git a/reports/view.js b/reports/view.js index c4dade3d..d73df6e1 100644 --- a/reports/view.js +++ b/reports/view.js @@ -1,21 +1,19 @@ -import GeneralLedgerViewConfig from './GeneralLedger/viewConfig'; -import SalesRegisterViewConfig from './SalesRegister/viewConfig'; -import PurchaseRegisterViewConfig from './PurchaseRegister/viewConfig'; import BalanceSheetViewConfig from './BalanceSheet/viewConfig'; -import ProfitAndLossViewConfig from './ProfitAndLoss/viewConfig'; -import TrialBalanceViewConfig from './TrialBalance/viewConfig'; -// import BankReconciliationViewConfig from './BankReconciliation/viewConfig'; +import GeneralLedgerViewConfig from './GeneralLedger/viewConfig'; import GoodsAndServiceTaxGSTR1View from './GoodsAndServiceTax/GSTR1View'; import GoodsAndServiceTaxGSTR2View from './GoodsAndServiceTax/GSTR2View'; +import ProfitAndLossViewConfig from './ProfitAndLoss/viewConfig'; +import PurchaseRegisterViewConfig from './PurchaseRegister/viewConfig'; +import SalesRegisterViewConfig from './SalesRegister/viewConfig'; +import TrialBalanceViewConfig from './TrialBalance/viewConfig'; export default { - 'general-ledger' : GeneralLedgerViewConfig, - 'sales-register' : SalesRegisterViewConfig, - 'purchase-register' : PurchaseRegisterViewConfig, - 'balance-sheet' : BalanceSheetViewConfig, - 'profit-and-loss' : ProfitAndLossViewConfig, - 'trial-balance' : TrialBalanceViewConfig, - // 'bank-reconciliation' : BankReconciliationViewConfig, - 'gstr-1' : GoodsAndServiceTaxGSTR1View, - 'gstr-2' : GoodsAndServiceTaxGSTR2View, + 'general-ledger': GeneralLedgerViewConfig, + 'sales-register': SalesRegisterViewConfig, + 'purchase-register': PurchaseRegisterViewConfig, + 'balance-sheet': BalanceSheetViewConfig, + 'profit-and-loss': ProfitAndLossViewConfig, + 'trial-balance': TrialBalanceViewConfig, + 'gstr-1': GoodsAndServiceTaxGSTR1View, + 'gstr-2': GoodsAndServiceTaxGSTR2View, }; diff --git a/server/postStart.js b/server/postStart.js index c574e911..7383a004 100644 --- a/server/postStart.js +++ b/server/postStart.js @@ -1,6 +1,5 @@ import frappe from 'frappe'; import { createNumberSeries } from 'frappe/model/naming'; -import GSTR3BServer from '../models/doctype/GSTR3B/GSTR3BServer.js'; import JournalEntryServer from '../models/doctype/JournalEntry/JournalEntryServer.js'; import PartyServer from '../models/doctype/Party/PartyServer.js'; import PaymentServer from '../models/doctype/Payment/PaymentServer.js'; @@ -14,7 +13,6 @@ export default async function postStart() { frappe.models.Party.documentClass = PartyServer; frappe.models.PurchaseInvoice.documentClass = PurchaseInvoiceServer; frappe.models.JournalEntry.documentClass = JournalEntryServer; - frappe.models.GSTR3B.documentClass = GSTR3BServer; frappe.metaCache = {}; @@ -23,11 +21,6 @@ export default async function postStart() { await createNumberSeries('PINV-', 'PurchaseInvoice'); await createNumberSeries('PAY-', 'Payment'); await createNumberSeries('JV-', 'JournalEntry'); - // await naming.createNumberSeries('QTN-', 'QuotationSettings'); - // await naming.createNumberSeries('SO-', 'SalesOrderSettings'); - // await naming.createNumberSeries('OF-', 'FulfillmentSettings'); - // await naming.createNumberSeries('PO-', 'PurchaseOrderSettings'); - // await naming.createNumberSeries('PREC-', 'PurchaseReceiptSettings'); // fetch singles // so that they are available synchronously diff --git a/src/pages/ListView/listConfig.js b/src/pages/ListView/listConfig.js index 9d329468..97eba638 100644 --- a/src/pages/ListView/listConfig.js +++ b/src/pages/ListView/listConfig.js @@ -1,15 +1,14 @@ -import SalesInvoice from '../../../models/doctype/SalesInvoice/SalesInvoiceList'; -import PurchaseInvoice from '../../../models/doctype/PurchaseInvoice/PurchaseInvoiceList'; -import Customer from '../../../models/doctype/Party/CustomerList'; -import Supplier from '../../../models/doctype/Party/SupplierList'; -import Party from '../../../models/doctype/Party/PartyList'; -import Item from '../../../models/doctype/Item/ItemList'; -import Payment from '../../../models/doctype/Payment/PaymentList'; -import Tax from '../../../models/doctype/Tax/TaxList'; -import JournalEntry from '../../../models/doctype/JournalEntry/JournalEntryList'; -import AccountingLedgerEntry from '../../../models/doctype/AccountingLedgerEntry/AccountingLedgerEntryList'; import Account from '../../../models/doctype/Account/AccountList'; -import GSTR3B from '../../../models/doctype/GSTR3B/GSTR3BList'; +import AccountingLedgerEntry from '../../../models/doctype/AccountingLedgerEntry/AccountingLedgerEntryList'; +import Item from '../../../models/doctype/Item/ItemList'; +import JournalEntry from '../../../models/doctype/JournalEntry/JournalEntryList'; +import Customer from '../../../models/doctype/Party/CustomerList'; +import Party from '../../../models/doctype/Party/PartyList'; +import Supplier from '../../../models/doctype/Party/SupplierList'; +import Payment from '../../../models/doctype/Payment/PaymentList'; +import PurchaseInvoice from '../../../models/doctype/PurchaseInvoice/PurchaseInvoiceList'; +import SalesInvoice from '../../../models/doctype/SalesInvoice/SalesInvoiceList'; +import Tax from '../../../models/doctype/Tax/TaxList'; export default { SalesInvoice, @@ -22,6 +21,5 @@ export default { Tax, JournalEntry, Account, - GSTR3B, - AccountingLedgerEntry + AccountingLedgerEntry, }; From 02cacd569739b348fdb271637c110aa7d5318205 Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Wed, 23 Mar 2022 13:01:15 +0530 Subject: [PATCH 011/163] incr: add schemas --- schemas/app/Account.json | 88 +++++++++ schemas/app/AccountingLedgerEntry.json | 87 +++++++++ schemas/app/AccountingSettings.json | 102 +++++++++++ schemas/app/Address.json | 95 ++++++++++ schemas/app/Color.json | 17 ++ schemas/app/CompanySettings.json | 27 +++ schemas/app/Contact.json | 87 +++++++++ schemas/app/Currency.json | 42 +++++ schemas/app/Customer.json | 121 +++++++++++++ schemas/app/GetStarted.json | 66 +++++++ schemas/app/Item.json | 134 ++++++++++++++ schemas/app/JournalEntry.json | 97 ++++++++++ schemas/app/JournalEntryAccount.json | 30 ++++ schemas/app/JournalEntrySettings.json | 9 + schemas/app/Party.json | 109 +++++++++++ schemas/app/Payment.json | 219 +++++++++++++++++++++++ schemas/app/PaymentFor.json | 43 +++++ schemas/app/PaymentSettings.json | 8 + schemas/app/PrintSettings.json | 131 ++++++++++++++ schemas/app/PurchaseInvoice.json | 142 +++++++++++++++ schemas/app/PurchaseInvoiceItem.json | 73 ++++++++ schemas/app/PurchaseInvoiceSettings.json | 9 + schemas/app/SalesInvoice.json | 142 +++++++++++++++ schemas/app/SalesInvoiceItem.json | 93 ++++++++++ schemas/app/SalesInvoiceSettings.json | 45 +++++ schemas/app/SetupWizard.json | 101 +++++++++++ schemas/app/Supplier.json | 121 +++++++++++++ schemas/app/Tax.json | 28 +++ schemas/app/TaxDetail.json | 28 +++ schemas/app/TaxSummary.json | 32 ++++ schemas/core/NumberSeries.json | 75 ++++++++ schemas/core/PatchRun.json | 11 ++ schemas/core/SingleValue.json | 27 +++ schemas/core/SystemSettings.json | 86 +++++++++ 34 files changed, 2525 insertions(+) create mode 100644 schemas/app/Account.json create mode 100644 schemas/app/AccountingLedgerEntry.json create mode 100644 schemas/app/AccountingSettings.json create mode 100644 schemas/app/Address.json create mode 100644 schemas/app/Color.json create mode 100644 schemas/app/CompanySettings.json create mode 100644 schemas/app/Contact.json create mode 100644 schemas/app/Currency.json create mode 100644 schemas/app/Customer.json create mode 100644 schemas/app/GetStarted.json create mode 100644 schemas/app/Item.json create mode 100644 schemas/app/JournalEntry.json create mode 100644 schemas/app/JournalEntryAccount.json create mode 100644 schemas/app/JournalEntrySettings.json create mode 100644 schemas/app/Party.json create mode 100644 schemas/app/Payment.json create mode 100644 schemas/app/PaymentFor.json create mode 100644 schemas/app/PaymentSettings.json create mode 100644 schemas/app/PrintSettings.json create mode 100644 schemas/app/PurchaseInvoice.json create mode 100644 schemas/app/PurchaseInvoiceItem.json create mode 100644 schemas/app/PurchaseInvoiceSettings.json create mode 100644 schemas/app/SalesInvoice.json create mode 100644 schemas/app/SalesInvoiceItem.json create mode 100644 schemas/app/SalesInvoiceSettings.json create mode 100644 schemas/app/SetupWizard.json create mode 100644 schemas/app/Supplier.json create mode 100644 schemas/app/Tax.json create mode 100644 schemas/app/TaxDetail.json create mode 100644 schemas/app/TaxSummary.json create mode 100644 schemas/core/NumberSeries.json create mode 100644 schemas/core/PatchRun.json create mode 100644 schemas/core/SingleValue.json create mode 100644 schemas/core/SystemSettings.json diff --git a/schemas/app/Account.json b/schemas/app/Account.json new file mode 100644 index 00000000..42416775 --- /dev/null +++ b/schemas/app/Account.json @@ -0,0 +1,88 @@ +{ + "name": "Account", + "label": "Account", + "doctype": "DocType", + "isSingle": false, + "isTree": true, + "keywordFields": [ + "name", + "rootType", + "accountType" + ], + "fields": [ + { + "fieldname": "name", + "label": "Account Name", + "fieldtype": "Data", + "required": true + }, + { + "fieldname": "rootType", + "label": "Root Type", + "fieldtype": "Select", + "placeholder": "Root Type", + "options": [ + "Asset", + "Liability", + "Equity", + "Income", + "Expense" + ], + "required": true + }, + { + "fieldname": "parentAccount", + "label": "Parent Account", + "fieldtype": "Link", + "target": "Account" + }, + { + "fieldname": "accountType", + "label": "Account Type", + "placeholder": "Account Type", + "fieldtype": "Select", + "options": [ + "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" + ] + }, + { + "fieldname": "balance", + "label": "Balance", + "fieldtype": "Currency", + "readOnly": true + }, + { + "fieldname": "isGroup", + "label": "Is Group", + "fieldtype": "Check" + } + ], + "quickEditFields": [ + "name", + "rootType", + "parentAccount", + "accountType", + "isGroup" + ], + "treeSettings": { + "parentField": "parentAccount" + } +} \ No newline at end of file diff --git a/schemas/app/AccountingLedgerEntry.json b/schemas/app/AccountingLedgerEntry.json new file mode 100644 index 00000000..6de20eae --- /dev/null +++ b/schemas/app/AccountingLedgerEntry.json @@ -0,0 +1,87 @@ +{ + "name": "AccountingLedgerEntry", + "label": "Ledger Entry", + "naming": "autoincrement", + "doctype": "DocType", + "isSingle": false, + "isChild": false, + "keywordFields": [ + "account", + "party", + "referenceName" + ], + "fields": [ + { + "fieldname": "date", + "label": "Date", + "fieldtype": "Date" + }, + { + "fieldname": "account", + "label": "Account", + "fieldtype": "Link", + "target": "Account", + "required": true + }, + { + "fieldname": "description", + "label": "Description", + "fieldtype": "Text" + }, + { + "fieldname": "party", + "label": "Party", + "fieldtype": "Link", + "target": "Party" + }, + { + "fieldname": "debit", + "label": "Debit", + "fieldtype": "Currency" + }, + { + "fieldname": "credit", + "label": "Credit", + "fieldtype": "Currency" + }, + { + "fieldname": "againstAccount", + "label": "Against Account", + "fieldtype": "Text" + }, + { + "fieldname": "referenceType", + "label": "Ref. Type", + "fieldtype": "Data" + }, + { + "fieldname": "referenceName", + "label": "Ref. Name", + "fieldtype": "DynamicLink", + "references": "referenceType" + }, + { + "fieldname": "balance", + "label": "Balance", + "fieldtype": "Currency" + }, + { + "fieldname": "reverted", + "label": "Reverted", + "fieldtype": "Check", + "default": 0 + } + ], + "quickEditFields": [ + "date", + "account", + "description", + "party", + "debit", + "credit", + "againstAccount", + "referenceType", + "referenceName", + "balance" + ] +} \ No newline at end of file diff --git a/schemas/app/AccountingSettings.json b/schemas/app/AccountingSettings.json new file mode 100644 index 00000000..7b2998dc --- /dev/null +++ b/schemas/app/AccountingSettings.json @@ -0,0 +1,102 @@ +{ + "name": "AccountingSettings", + "label": "Accounting Settings", + "naming": "name", + "isSingle": true, + "isChild": false, + "isSubmittable": false, + "settings": null, + "keywordFields": [], + "fields": [ + { + "label": "Company Name", + "fieldname": "companyName", + "fieldtype": "Data", + "required": true + }, + { + "label": "Write Off Account", + "fieldname": "writeOffAccount", + "fieldtype": "Link", + "target": "Account", + "default": "Write Off" + }, + { + "label": "Round Off Account", + "fieldname": "roundOffAccount", + "fieldtype": "Link", + "target": "Account", + "default": "Rounded Off" + }, + { + "fieldname": "country", + "label": "Country", + "fieldtype": "AutoComplete", + "placeholder": "Select Country", + "readOnly": true, + "required": true + }, + { + "fieldname": "currency", + "label": "Currency", + "fieldtype": "Data", + "readOnly": true, + "required": false + }, + { + "fieldname": "fullname", + "label": "Name", + "fieldtype": "Data", + "required": true + }, + { + "fieldname": "email", + "label": "Email", + "fieldtype": "Data", + "required": true, + "validate": { + "type": "email" + } + }, + { + "fieldname": "bankName", + "label": "Bank Name", + "fieldtype": "Data", + "required": true + }, + { + "fieldname": "fiscalYearStart", + "label": "Fiscal Year Start Date", + "fieldtype": "Date", + "required": true + }, + { + "fieldname": "fiscalYearEnd", + "label": "Fiscal Year End Date", + "fieldtype": "Date", + "required": true + }, + { + "fieldname": "setupComplete", + "label": "Setup Complete", + "fieldtype": "Check", + "default": 0 + }, + { + "fieldname": "gstin", + "label": "GSTIN", + "fieldtype": "Data", + "placeholder": "27AAAAA0000A1Z5" + } + ], + "quickEditFields": [ + "fullname", + "email", + "companyName", + "country", + "currency", + "fiscalYearStart", + "fiscalYearEnd", + "gstin" + ] +} \ No newline at end of file diff --git a/schemas/app/Address.json b/schemas/app/Address.json new file mode 100644 index 00000000..c1134297 --- /dev/null +++ b/schemas/app/Address.json @@ -0,0 +1,95 @@ +{ + "name": "Address", + "doctype": "DocType", + "regional": 1, + "isSingle": false, + "keywordFields": [ + "addressLine1", + "addressLine2", + "city", + "state", + "country", + "postalCode" + ], + "fields": [ + { + "fieldname": "addressLine1", + "label": "Address Line 1", + "placeholder": "Address Line 1", + "fieldtype": "Data", + "required": true + }, + { + "fieldname": "addressLine2", + "label": "Address Line 2", + "placeholder": "Address Line 2", + "fieldtype": "Data" + }, + { + "fieldname": "city", + "label": "City / Town", + "placeholder": "City / Town", + "fieldtype": "Data", + "required": true + }, + { + "fieldname": "state", + "label": "State", + "placeholder": "State", + "fieldtype": "AutoComplete" + }, + { + "fieldname": "country", + "label": "Country", + "placeholder": "Country", + "fieldtype": "AutoComplete", + "required": true + }, + { + "fieldname": "postalCode", + "label": "Postal Code", + "placeholder": "Postal Code", + "fieldtype": "Data" + }, + { + "fieldname": "emailAddress", + "label": "Email Address", + "placeholder": "Email Address", + "fieldtype": "Data" + }, + { + "fieldname": "phone", + "label": "Phone", + "placeholder": "Phone", + "fieldtype": "Data" + }, + { + "fieldname": "fax", + "label": "Fax", + "fieldtype": "Data" + }, + { + "fieldname": "addressDisplay", + "fieldtype": "Text", + "label": "Address Display", + "readOnly": true + }, + { + "fieldname": "pos", + "label": "Place of Supply", + "fieldtype": "AutoComplete", + "placeholder": "Place of Supply" + } + ], + "quickEditFields": [ + "addressLine1", + "addressLine2", + "city", + "state", + "country", + "postalCode", + "pos" + ], + "inlineEditDisplayField": "addressDisplay", + "augmented": 1 +} \ No newline at end of file diff --git a/schemas/app/Color.json b/schemas/app/Color.json new file mode 100644 index 00000000..8f6a2eb1 --- /dev/null +++ b/schemas/app/Color.json @@ -0,0 +1,17 @@ +{ + "name": "Color", + "doctype": "DocType", + "fields": [ + { + "fieldname": "name", + "fieldtype": "Data", + "label": "Color", + "required": true + }, + { + "fieldname": "hexvalue", + "fieldtype": "Data", + "label": "Hex Value" + } + ] +} \ No newline at end of file diff --git a/schemas/app/CompanySettings.json b/schemas/app/CompanySettings.json new file mode 100644 index 00000000..5f6ad6c6 --- /dev/null +++ b/schemas/app/CompanySettings.json @@ -0,0 +1,27 @@ +{ + "name": "CompanySettings", + "label": "Company Settings", + "naming": "autoincrement", + "isSingle": true, + "isChild": false, + "keywordFields": [ + "companyName" + ], + "fields": [ + { + "fieldname": "companyName", + "label": "Company Name", + "fieldtype": "Data", + "disabled": false, + "required": true + }, + { + "fieldname": "companyAddress", + "label": "Company Address", + "fieldtype": "Link", + "disabled": false, + "required": true, + "target": "Address" + } + ] +} \ No newline at end of file diff --git a/schemas/app/Contact.json b/schemas/app/Contact.json new file mode 100644 index 00000000..d9e18d31 --- /dev/null +++ b/schemas/app/Contact.json @@ -0,0 +1,87 @@ +{ + "name": "Contact", + "doctype": "DocType", + "isSingle": false, + "naming": "autoincrement", + "pageSettings": { + "hideTitle": true + }, + "titleField": "fullName", + "keywordFields": [ + "fullName" + ], + "fields": [ + { + "fieldname": "fullName", + "label": "Full Name", + "fieldtype": "Data", + "required": true + }, + { + "fieldname": "emailAddress", + "label": "Email Address", + "fieldtype": "Data" + }, + { + "fieldname": "userId", + "label": "User ID", + "fieldtype": "Link", + "target": "User", + "hidden": true + }, + { + "fieldname": "status", + "label": "Status", + "fieldtype": "Select", + "options": [ + "Passive", + "Open", + "Replied" + ] + }, + { + "fieldname": "gender", + "label": "Gender", + "fieldtype": "Select", + "options": [ + "Male", + "Female", + "Gender" + ] + }, + { + "fieldname": "mobileNumber", + "label": "Mobile Number", + "fieldtype": "Data" + }, + { + "fieldname": "phone", + "label": "Phone", + "fieldtype": "Data" + } + ], + "events": {}, + "listSettings": {}, + "layout": [ + { + "columns": [ + { + "fields": [ + "fullName", + "emailAddress", + "userId", + "status" + ] + }, + { + "fields": [ + "postalCode", + "gender", + "phone", + "mobileNumber" + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/schemas/app/Currency.json b/schemas/app/Currency.json new file mode 100644 index 00000000..91055a4c --- /dev/null +++ b/schemas/app/Currency.json @@ -0,0 +1,42 @@ +{ + "name": "Currency", + "label": "Currency", + "doctype": "DocType", + "isSingle": false, + "keywordFields": [ + "name", + "symbol" + ], + "quickEditFields": [ + "name", + "symbol" + ], + "fields": [ + { + "fieldname": "name", + "label": "Currency Name", + "fieldtype": "Data", + "required": true + }, + { + "fieldname": "fraction", + "label": "Fraction", + "fieldtype": "Data" + }, + { + "fieldname": "fractionUnits", + "label": "Fraction Units", + "fieldtype": "Int" + }, + { + "label": "Smallest Currency Fraction Value", + "fieldname": "smallestValue", + "fieldtype": "Currency" + }, + { + "label": "Symbol", + "fieldname": "symbol", + "fieldtype": "Data" + } + ] +} \ No newline at end of file diff --git a/schemas/app/Customer.json b/schemas/app/Customer.json new file mode 100644 index 00000000..ac72af24 --- /dev/null +++ b/schemas/app/Customer.json @@ -0,0 +1,121 @@ +{ + "name": "Customer", + "label": "Customer", + "basedOn": "Party", + "filters": { + "customer": 1 + }, + "actions": [ + { + "label": "Create Invoice" + }, + { + "label": "View Invoices" + } + ], + "regional": 1, + "keywordFields": [ + "name" + ], + "fields": [ + { + "fieldname": "name", + "label": "Name", + "fieldtype": "Data", + "required": true, + "placeholder": "Full Name" + }, + { + "fieldname": "image", + "label": "Image", + "fieldtype": "AttachImage" + }, + { + "fieldname": "customer", + "label": "Is Customer", + "fieldtype": "Check" + }, + { + "fieldname": "gstin", + "label": "GSTIN No.", + "fieldtype": "Data" + }, + { + "fieldname": "gstType", + "label": "GST Registration", + "placeholder": "GST Registration", + "fieldtype": "Select", + "default": "Unregistered", + "options": [ + "Unregistered", + "Registered Regular", + "Consumer" + ] + }, + { + "fieldname": "supplier", + "label": "Is Supplier", + "fieldtype": "Check" + }, + { + "fieldname": "defaultAccount", + "label": "Default Account", + "fieldtype": "Link", + "target": "Account" + }, + { + "fieldname": "outstandingAmount", + "label": "Outstanding Amount", + "fieldtype": "Currency" + }, + { + "fieldname": "currency", + "label": "Currency", + "fieldtype": "Link", + "target": "Currency", + "placeholder": "INR" + }, + { + "fieldname": "email", + "label": "Email", + "fieldtype": "Data", + "placeholder": "john@doe.com", + "validate": { + "type": "email" + } + }, + { + "fieldname": "phone", + "label": "Phone", + "fieldtype": "Data", + "placeholder": "Phone", + "validate": { + "type": "phone" + } + }, + { + "fieldname": "address", + "label": "Address", + "fieldtype": "Link", + "target": "Address", + "placeholder": "Click to create", + "inline": true + }, + { + "fieldname": "addressDisplay", + "label": "Address Display", + "fieldtype": "Text", + "readOnly": true + } + ], + "quickEditFields": [ + "email", + "phone", + "address", + "defaultAccount", + "currency", + "gstType", + "gstin" + ], + "augmented": 1 +} \ No newline at end of file diff --git a/schemas/app/GetStarted.json b/schemas/app/GetStarted.json new file mode 100644 index 00000000..9e4bf4f4 --- /dev/null +++ b/schemas/app/GetStarted.json @@ -0,0 +1,66 @@ +{ + "name": "GetStarted", + "isSingle": true, + "fields": [ + { + "fieldname": "onboardingComplete", + "label": "Onboarding Complete", + "fieldtype": "Check" + }, + { + "fieldname": "companySetup", + "label": "Company Setup", + "fieldtype": "Check" + }, + { + "fieldname": "systemSetup", + "label": "System Setup", + "fieldtype": "Check" + }, + { + "fieldname": "invoiceSetup", + "label": "Invoice Setup", + "fieldtype": "Check" + }, + { + "fieldname": "itemCreated", + "label": "Item Created", + "fieldtype": "Check" + }, + { + "fieldname": "customerCreated", + "label": "Customer Created", + "fieldtype": "Check" + }, + { + "fieldname": "supplierCreated", + "label": "Supplier Created", + "fieldtype": "Check" + }, + { + "fieldname": "invoiceCreated", + "label": "Invoice Created", + "fieldtype": "Check" + }, + { + "fieldname": "billCreated", + "label": "Bill Created", + "fieldtype": "Check" + }, + { + "fieldname": "chartOfAccountsReviewed", + "label": "Chart Of Accounts Reviewed", + "fieldtype": "Check" + }, + { + "fieldname": "openingBalanceChecked", + "label": "Opening Balances", + "fieldtype": "Check" + }, + { + "fieldname": "taxesAdded", + "label": "Add Taxes", + "fieldtype": "Check" + } + ] +} \ No newline at end of file diff --git a/schemas/app/Item.json b/schemas/app/Item.json new file mode 100644 index 00000000..5053af5e --- /dev/null +++ b/schemas/app/Item.json @@ -0,0 +1,134 @@ +{ + "name": "Item", + "label": "Item", + "doctype": "DocType", + "isSingle": false, + "regional": 1, + "keywordFields": [ + "name", + "description" + ], + "fields": [ + { + "fieldname": "name", + "label": "Item Name", + "fieldtype": "Data", + "placeholder": "Item Name", + "required": true + }, + { + "fieldname": "hsnCode", + "label": "HSN/SAC", + "fieldtype": "Int", + "placeholder": "HSN/SAC Code" + }, + { + "fieldname": "image", + "label": "Image", + "fieldtype": "AttachImage" + }, + { + "fieldname": "description", + "label": "Description", + "placeholder": "Item Description", + "fieldtype": "Text" + }, + { + "fieldname": "unit", + "label": "Unit Type", + "fieldtype": "Select", + "placeholder": "Unit Type", + "default": "Unit", + "options": [ + "Unit", + "Kg", + "Gram", + "Hour", + "Day" + ] + }, + { + "fieldname": "itemType", + "label": "Type", + "placeholder": "Type", + "fieldtype": "Select", + "default": "Product", + "options": [ + "Product", + "Service" + ] + }, + { + "fieldname": "for", + "label": "For", + "fieldtype": "Select", + "options": [ + "purchases", + "sales", + "both" + ], + "map": { + "purchases": "Purchases", + "sales": "Sales", + "both": "Both" + }, + "default": "both" + }, + { + "fieldname": "incomeAccount", + "label": "Income", + "fieldtype": "Link", + "target": "Account", + "placeholder": "Income", + "required": true, + "disableCreation": true, + "formulaDependsOn": [ + "itemType" + ] + }, + { + "fieldname": "expenseAccount", + "label": "Expense", + "fieldtype": "Link", + "target": "Account", + "placeholder": "Expense", + "required": true, + "disableCreation": true, + "formulaDependsOn": [ + "itemType" + ] + }, + { + "fieldname": "tax", + "label": "Tax", + "fieldtype": "Link", + "target": "Tax", + "placeholder": "Tax" + }, + { + "fieldname": "rate", + "label": "Rate", + "fieldtype": "Currency" + } + ], + "quickEditFields": [ + "hsnCode", + "rate", + "unit", + "itemType", + "for", + "tax", + "description", + "incomeAccount", + "expenseAccount" + ], + "actions": [ + { + "label": "New Invoice" + }, + { + "label": "New Bill" + } + ], + "augmented": 1 +} \ No newline at end of file diff --git a/schemas/app/JournalEntry.json b/schemas/app/JournalEntry.json new file mode 100644 index 00000000..bb84ed13 --- /dev/null +++ b/schemas/app/JournalEntry.json @@ -0,0 +1,97 @@ +{ + "label": "Journal Entry", + "name": "JournalEntry", + "doctype": "DocType", + "isSubmittable": true, + "settings": "JournalEntrySettings", + "fields": [ + { + "fieldname": "entryType", + "label": "Entry Type", + "fieldtype": "Select", + "placeholder": "Entry Type", + "options": [ + "Journal Entry", + "Bank Entry", + "Cash Entry", + "Credit Card Entry", + "Debit Note", + "Credit Note", + "Contra Entry", + "Excise Entry", + "Write Off Entry", + "Opening Entry", + "Depreciation Entry" + ], + "map": { + "Journal Entry": "Journal Entry", + "Bank Entry": "Bank Entry", + "Cash Entry": "Cash Entry", + "Credit Card Entry": "Credit Card Entry", + "Debit Note": "Debit Note", + "Credit Note": "Credit Note", + "Contra Entry": "Contra Entry", + "Excise Entry": "Excise Entry", + "Write Off Entry": "Write Off Entry", + "Opening Entry": "Opening Entry", + "Depreciation Entry": "Depreciation Entry" + }, + "required": true + }, + { + "label": "Entry No", + "fieldname": "name", + "fieldtype": "Data", + "required": true, + "readOnly": true + }, + { + "fieldname": "date", + "label": "Date", + "fieldtype": "Date" + }, + { + "fieldname": "accounts", + "label": "Account Entries", + "fieldtype": "Table", + "childtype": "JournalEntryAccount", + "required": true + }, + { + "fieldname": "referenceNumber", + "label": "Reference Number", + "fieldtype": "Data" + }, + { + "fieldname": "referenceDate", + "label": "Reference Date", + "fieldtype": "Date" + }, + { + "fieldname": "userRemark", + "label": "User Remark", + "fieldtype": "Text", + "placeholder": "User Remark" + }, + { + "fieldname": "cancelled", + "label": "Cancelled", + "fieldtype": "Check", + "default": 0, + "readOnly": true + }, + { + "fieldname": "numberSeries", + "label": "Number Series", + "fieldtype": "Link", + "target": "NumberSeries", + "required": true, + "default": "JV-" + } + ], + "actions": [ + { + "label": "Ledger Entries" + } + ] +} \ No newline at end of file diff --git a/schemas/app/JournalEntryAccount.json b/schemas/app/JournalEntryAccount.json new file mode 100644 index 00000000..2e3b6f4c --- /dev/null +++ b/schemas/app/JournalEntryAccount.json @@ -0,0 +1,30 @@ +{ + "name": "JournalEntryAccount", + "isChild": true, + "fields": [ + { + "fieldname": "account", + "label": "Account", + "placeholder": "Account", + "fieldtype": "Link", + "target": "Account", + "required": true, + "groupBy": "rootType" + }, + { + "fieldname": "debit", + "label": "Debit", + "fieldtype": "Currency" + }, + { + "fieldname": "credit", + "label": "Credit", + "fieldtype": "Currency" + } + ], + "tableFields": [ + "account", + "debit", + "credit" + ] +} \ No newline at end of file diff --git a/schemas/app/JournalEntrySettings.json b/schemas/app/JournalEntrySettings.json new file mode 100644 index 00000000..9a365dfc --- /dev/null +++ b/schemas/app/JournalEntrySettings.json @@ -0,0 +1,9 @@ +{ + "name": "JournalEntrySettings", + "label": "Journal Entry Setting", + "doctype": "DocType", + "isSingle": true, + "isChild": false, + "keywordFields": [], + "fields": [] +} \ No newline at end of file diff --git a/schemas/app/Party.json b/schemas/app/Party.json new file mode 100644 index 00000000..effe5835 --- /dev/null +++ b/schemas/app/Party.json @@ -0,0 +1,109 @@ +{ + "name": "Party", + "label": "Party", + "regional": 1, + "keywordFields": [ + "name" + ], + "fields": [ + { + "fieldname": "name", + "label": "Name", + "fieldtype": "Data", + "required": true, + "placeholder": "Full Name" + }, + { + "fieldname": "image", + "label": "Image", + "fieldtype": "AttachImage" + }, + { + "fieldname": "customer", + "label": "Is Customer", + "fieldtype": "Check" + }, + { + "fieldname": "gstin", + "label": "GSTIN No.", + "fieldtype": "Data" + }, + { + "fieldname": "gstType", + "label": "GST Registration", + "placeholder": "GST Registration", + "fieldtype": "Select", + "default": "Unregistered", + "options": [ + "Unregistered", + "Registered Regular", + "Consumer" + ] + }, + { + "fieldname": "supplier", + "label": "Is Supplier", + "fieldtype": "Check" + }, + { + "fieldname": "defaultAccount", + "label": "Default Account", + "fieldtype": "Link", + "target": "Account" + }, + { + "fieldname": "outstandingAmount", + "label": "Outstanding Amount", + "fieldtype": "Currency" + }, + { + "fieldname": "currency", + "label": "Currency", + "fieldtype": "Link", + "target": "Currency", + "placeholder": "INR" + }, + { + "fieldname": "email", + "label": "Email", + "fieldtype": "Data", + "placeholder": "john@doe.com", + "validate": { + "type": "email" + } + }, + { + "fieldname": "phone", + "label": "Phone", + "fieldtype": "Data", + "placeholder": "Phone", + "validate": { + "type": "phone" + } + }, + { + "fieldname": "address", + "label": "Address", + "fieldtype": "Link", + "target": "Address", + "placeholder": "Click to create", + "inline": true + }, + { + "fieldname": "addressDisplay", + "label": "Address Display", + "fieldtype": "Text", + "readOnly": true + } + ], + "quickEditFields": [ + "email", + "phone", + "address", + "defaultAccount", + "currency", + "gstType", + "gstin" + ], + "augmented": 1 +} \ No newline at end of file diff --git a/schemas/app/Payment.json b/schemas/app/Payment.json new file mode 100644 index 00000000..e2009c75 --- /dev/null +++ b/schemas/app/Payment.json @@ -0,0 +1,219 @@ +{ + "name": "Payment", + "label": "Payment", + "isSingle": false, + "isChild": false, + "isSubmittable": true, + "keywordFields": [], + "settings": "PaymentSettings", + "fields": [ + { + "label": "Payment No", + "fieldname": "name", + "fieldtype": "Data", + "required": true, + "readOnly": true + }, + { + "fieldname": "party", + "label": "Party", + "fieldtype": "Link", + "target": "Party", + "required": true + }, + { + "fieldname": "date", + "label": "Posting Date", + "fieldtype": "Date" + }, + { + "fieldname": "account", + "label": "From Account", + "fieldtype": "Link", + "target": "Account", + "required": true + }, + { + "fieldname": "paymentType", + "label": "Payment Type", + "fieldtype": "Select", + "placeholder": "Payment Type", + "options": [ + "Receive", + "Pay" + ], + "map": { + "Receive": "Receive", + "Pay": "Pay" + }, + "required": true + }, + { + "fieldname": "paymentAccount", + "label": "To Account", + "placeholder": "To Account", + "fieldtype": "Link", + "target": "Account", + "required": true + }, + { + "fieldname": "numberSeries", + "label": "Number Series", + "fieldtype": "Link", + "target": "NumberSeries", + "required": true, + "default": "PAY-" + }, + { + "fieldname": "paymentMethod", + "label": "Payment Method", + "placeholder": "Payment Method", + "fieldtype": "Select", + "options": [ + "Cash", + "Cheque", + "Transfer" + ], + "default": "Cash", + "required": true + }, + { + "fieldname": "referenceId", + "label": "Ref. / Cheque No.", + "placeholder": "Ref. / Cheque No.", + "fieldtype": "Data" + }, + { + "fieldname": "referenceDate", + "label": "Ref. Date", + "placeholder": "Ref. Date", + "fieldtype": "Date" + }, + { + "fieldname": "clearanceDate", + "label": "Clearance Date", + "placeholder": "Clearance Date", + "fieldtype": "Date" + }, + { + "fieldname": "amount", + "label": "Amount", + "fieldtype": "Currency", + "required": true + }, + { + "fieldname": "writeoff", + "label": "Write Off / Refund", + "fieldtype": "Currency" + }, + { + "fieldname": "for", + "label": "Payment Reference", + "fieldtype": "Table", + "childtype": "PaymentFor", + "required": false + }, + { + "fieldname": "cancelled", + "label": "Cancelled", + "fieldtype": "Check", + "default": 0, + "readOnly": true + } + ], + "quickEditFields": [ + "numberSeries", + "party", + "date", + "paymentMethod", + "account", + "paymentType", + "paymentAccount", + "referenceId", + "referenceDate", + "clearanceDate", + "amount", + "writeoff", + "for" + ], + "layout": [ + { + "columns": [ + { + "fields": [ + "party", + "account" + ] + }, + { + "fields": [ + "date", + "paymentAccount" + ] + } + ] + }, + { + "columns": [ + { + "fields": [ + "paymentMethod" + ] + }, + { + "fields": [ + "paymentType" + ] + }, + { + "fields": [ + "referenceId" + ] + } + ] + }, + { + "columns": [ + { + "fields": [ + "referenceDate" + ] + }, + { + "fields": [ + "clearanceDate" + ] + } + ] + }, + { + "columns": [ + { + "fields": [ + "for" + ] + } + ] + }, + { + "columns": [ + { + "fields": [ + "amount", + "writeoff" + ] + } + ] + } + ], + "actions": [ + { + "label": "Ledger Entries" + } + ], + "links": [ + { + "label": "Ledger Entries" + } + ] +} \ No newline at end of file diff --git a/schemas/app/PaymentFor.json b/schemas/app/PaymentFor.json new file mode 100644 index 00000000..8f475ca3 --- /dev/null +++ b/schemas/app/PaymentFor.json @@ -0,0 +1,43 @@ +{ + "name": "PaymentFor", + "label": "Payment For", + "isSingle": false, + "isChild": true, + "keywordFields": [], + "tableFields": [ + "referenceType", + "referenceName", + "amount" + ], + "fields": [ + { + "fieldname": "referenceType", + "label": "Reference Type", + "placeholder": "Type", + "fieldtype": "Select", + "options": [ + "SalesInvoice", + "PurchaseInvoice" + ], + "map": { + "SalesInvoice": "Invoice", + "PurchaseInvoice": "Bill" + }, + "required": true + }, + { + "fieldname": "referenceName", + "label": "Reference Name", + "fieldtype": "DynamicLink", + "references": "referenceType", + "placeholder": "Name", + "required": true + }, + { + "fieldname": "amount", + "label": "Allocated Amount", + "fieldtype": "Currency", + "required": true + } + ] +} \ No newline at end of file diff --git a/schemas/app/PaymentSettings.json b/schemas/app/PaymentSettings.json new file mode 100644 index 00000000..964ccef4 --- /dev/null +++ b/schemas/app/PaymentSettings.json @@ -0,0 +1,8 @@ +{ + "name": "PaymentSettings", + "label": "Payment Settings", + "isSingle": true, + "isChild": false, + "keywordFields": [], + "fields": [] +} \ No newline at end of file diff --git a/schemas/app/PrintSettings.json b/schemas/app/PrintSettings.json new file mode 100644 index 00000000..32d3b02d --- /dev/null +++ b/schemas/app/PrintSettings.json @@ -0,0 +1,131 @@ +{ + "name": "PrintSettings", + "label": "Print Settings", + "isSingle": true, + "fields": [ + { + "fieldname": "logo", + "label": "Logo", + "fieldtype": "AttachImage" + }, + { + "fieldname": "companyName", + "label": "Company Name", + "fieldtype": "Data" + }, + { + "fieldname": "email", + "label": "Email", + "fieldtype": "Data", + "placeholder": "john@doe.com", + "validate": { + "type": "email" + } + }, + { + "fieldname": "displayLogo", + "label": "Display Logo in Invoice", + "fieldtype": "Check" + }, + { + "fieldname": "phone", + "label": "Phone", + "fieldtype": "Data", + "placeholder": "9888900000", + "validate": { + "type": "phone" + } + }, + { + "fieldname": "address", + "label": "Address", + "fieldtype": "Link", + "target": "Address", + "placeholder": "Click to create", + "inline": true + }, + { + "fieldname": "template", + "label": "Template", + "placeholder": "Template", + "fieldtype": "Select", + "options": [ + "Basic", + "Minimal", + "Business" + ], + "default": "Basic" + }, + { + "fieldname": "color", + "label": "Color", + "placeholder": "Select Color", + "fieldtype": "Color", + "colors": [ + { + "label": "Red", + "value": "#f56565" + }, + { + "label": "Orange", + "value": "#ed8936" + }, + { + "label": "Yellow", + "value": "#ecc94b" + }, + { + "label": "Green", + "value": "#48bb78" + }, + { + "label": "Teal", + "value": "#38b2ac" + }, + { + "label": "Blue", + "value": "#33a1ff" + }, + { + "label": "Indigo", + "value": "#667eea" + }, + { + "label": "Purple", + "value": "#9f7aea" + }, + { + "label": "Pink", + "value": "#ed64a6" + }, + { + "label": "Black", + "value": "#112B42" + } + ] + }, + { + "fieldname": "font", + "label": "Font", + "placeholder": "Font", + "fieldtype": "Select", + "options": [ + "Inter", + "Times New Roman", + "Arial", + "Courier" + ], + "default": "Inter" + } + ], + "quickEditFields": [ + "logo", + "displayLogo", + "template", + "color", + "font", + "email", + "phone", + "address" + ] +} \ No newline at end of file diff --git a/schemas/app/PurchaseInvoice.json b/schemas/app/PurchaseInvoice.json new file mode 100644 index 00000000..9aecaab6 --- /dev/null +++ b/schemas/app/PurchaseInvoice.json @@ -0,0 +1,142 @@ +{ + "name": "PurchaseInvoice", + "doctype": "DocType", + "label": "Bill", + "printTemplate": { + "name": "InvoiceTemplate", + "props": [ + "doc", + "printSettings" + ], + "computed": {}, + "__file": "models/doctype/SalesInvoice/InvoiceTemplate.vue", + "__hmrId": "3d0cd49d" + }, + "isSingle": false, + "isChild": false, + "isSubmittable": true, + "keywordFields": [ + "name", + "supplier" + ], + "settings": "PurchaseInvoiceSettings", + "showTitle": true, + "fields": [ + { + "label": "Bill No", + "fieldname": "name", + "fieldtype": "Data", + "required": true, + "readOnly": true + }, + { + "fieldname": "date", + "label": "Date", + "fieldtype": "Date" + }, + { + "fieldname": "supplier", + "label": "Supplier", + "fieldtype": "Link", + "target": "Supplier", + "required": true + }, + { + "fieldname": "account", + "label": "Account", + "fieldtype": "Link", + "target": "Account" + }, + { + "fieldname": "currency", + "label": "Supplier Currency", + "fieldtype": "Link", + "target": "Currency", + "hidden": true, + "formulaDependsOn": [ + "supplier" + ] + }, + { + "fieldname": "exchangeRate", + "label": "Exchange Rate", + "fieldtype": "Float", + "default": 1 + }, + { + "fieldname": "items", + "label": "Items", + "fieldtype": "Table", + "childtype": "PurchaseInvoiceItem", + "required": true + }, + { + "fieldname": "netTotal", + "label": "Net Total", + "fieldtype": "Currency", + "readOnly": true + }, + { + "fieldname": "baseNetTotal", + "label": "Net Total (Company Currency)", + "fieldtype": "Currency", + "readOnly": true + }, + { + "fieldname": "taxes", + "label": "Taxes", + "fieldtype": "Table", + "childtype": "TaxSummary", + "readOnly": true + }, + { + "fieldname": "grandTotal", + "label": "Grand Total", + "fieldtype": "Currency", + "readOnly": true + }, + { + "fieldname": "baseGrandTotal", + "label": "Grand Total (Company Currency)", + "fieldtype": "Currency", + "readOnly": true + }, + { + "fieldname": "outstandingAmount", + "label": "Outstanding Amount", + "fieldtype": "Currency", + "readOnly": true + }, + { + "fieldname": "terms", + "label": "Terms", + "fieldtype": "Text" + }, + { + "fieldname": "cancelled", + "label": "Cancelled", + "fieldtype": "Check", + "default": 0, + "readOnly": true + }, + { + "fieldname": "numberSeries", + "label": "Number Series", + "fieldtype": "Link", + "target": "NumberSeries", + "required": true, + "default": "PINV-" + } + ], + "actions": [ + { + "label": "Make Payment" + }, + { + "label": "Print" + }, + { + "label": "Ledger Entries" + } + ] +} \ No newline at end of file diff --git a/schemas/app/PurchaseInvoiceItem.json b/schemas/app/PurchaseInvoiceItem.json new file mode 100644 index 00000000..985013a1 --- /dev/null +++ b/schemas/app/PurchaseInvoiceItem.json @@ -0,0 +1,73 @@ +{ + "name": "PurchaseInvoiceItem", + "doctype": "DocType", + "isChild": true, + "keywordFields": [], + "tableFields": [ + "item", + "tax", + "quantity", + "rate", + "amount" + ], + "fields": [ + { + "fieldname": "item", + "label": "Item", + "fieldtype": "Link", + "target": "Item", + "required": true + }, + { + "fieldname": "description", + "label": "Description", + "fieldtype": "Text", + "hidden": true + }, + { + "fieldname": "quantity", + "label": "Quantity", + "fieldtype": "Float", + "required": true, + "default": 1 + }, + { + "fieldname": "rate", + "label": "Rate", + "fieldtype": "Currency", + "required": true + }, + { + "fieldname": "baseRate", + "label": "Rate (Company Currency)", + "fieldtype": "Currency", + "readOnly": true + }, + { + "fieldname": "account", + "label": "Account", + "fieldtype": "Link", + "target": "Account", + "required": true, + "readOnly": true + }, + { + "fieldname": "tax", + "label": "Tax", + "fieldtype": "Link", + "target": "Tax" + }, + { + "fieldname": "amount", + "label": "Amount", + "fieldtype": "Currency", + "readOnly": true + }, + { + "fieldname": "baseAmount", + "label": "Amount (Company Currency)", + "fieldtype": "Currency", + "readOnly": true + } + ] +} \ No newline at end of file diff --git a/schemas/app/PurchaseInvoiceSettings.json b/schemas/app/PurchaseInvoiceSettings.json new file mode 100644 index 00000000..1b21c546 --- /dev/null +++ b/schemas/app/PurchaseInvoiceSettings.json @@ -0,0 +1,9 @@ +{ + "name": "PurchaseInvoiceSettings", + "label": "Bills Settings", + "doctype": "DocType", + "isSingle": true, + "isChild": false, + "keywordFields": [], + "fields": [] +} \ No newline at end of file diff --git a/schemas/app/SalesInvoice.json b/schemas/app/SalesInvoice.json new file mode 100644 index 00000000..b1437a46 --- /dev/null +++ b/schemas/app/SalesInvoice.json @@ -0,0 +1,142 @@ +{ + "name": "SalesInvoice", + "label": "Invoice", + "doctype": "DocType", + "printTemplate": { + "name": "InvoiceTemplate", + "props": [ + "doc", + "printSettings" + ], + "computed": {}, + "__file": "models/doctype/SalesInvoice/InvoiceTemplate.vue", + "__hmrId": "3d0cd49d" + }, + "isSingle": false, + "isChild": false, + "isSubmittable": true, + "keywordFields": [ + "name", + "customer" + ], + "settings": "SalesInvoiceSettings", + "fields": [ + { + "label": "Invoice No", + "fieldname": "name", + "fieldtype": "Data", + "required": true, + "readOnly": true + }, + { + "fieldname": "date", + "label": "Date", + "fieldtype": "Date" + }, + { + "fieldname": "customer", + "label": "Customer", + "fieldtype": "Link", + "target": "Customer", + "required": true + }, + { + "fieldname": "account", + "label": "Account", + "fieldtype": "Link", + "target": "Account", + "disableCreation": true + }, + { + "fieldname": "currency", + "label": "Customer Currency", + "fieldtype": "Link", + "target": "Currency", + "formulaDependsOn": [ + "customer" + ] + }, + { + "fieldname": "exchangeRate", + "label": "Exchange Rate", + "fieldtype": "Float", + "default": 1, + "readOnly": true + }, + { + "fieldname": "items", + "label": "Items", + "fieldtype": "Table", + "childtype": "SalesInvoiceItem", + "required": true + }, + { + "fieldname": "netTotal", + "label": "Net Total", + "fieldtype": "Currency", + "readOnly": true + }, + { + "fieldname": "baseNetTotal", + "label": "Net Total (Company Currency)", + "fieldtype": "Currency", + "readOnly": true + }, + { + "fieldname": "taxes", + "label": "Taxes", + "fieldtype": "Table", + "childtype": "TaxSummary", + "readOnly": true + }, + { + "fieldname": "grandTotal", + "label": "Grand Total", + "fieldtype": "Currency", + "readOnly": true + }, + { + "fieldname": "baseGrandTotal", + "label": "Grand Total (Company Currency)", + "fieldtype": "Currency", + "readOnly": true + }, + { + "fieldname": "outstandingAmount", + "label": "Outstanding Amount", + "fieldtype": "Currency", + "readOnly": true + }, + { + "fieldname": "terms", + "label": "Notes", + "fieldtype": "Text" + }, + { + "fieldname": "cancelled", + "label": "Cancelled", + "fieldtype": "Check", + "default": 0, + "readOnly": true + }, + { + "fieldname": "numberSeries", + "label": "Number Series", + "fieldtype": "Link", + "target": "NumberSeries", + "required": true, + "default": "SINV-" + } + ], + "actions": [ + { + "label": "Make Payment" + }, + { + "label": "Print" + }, + { + "label": "Ledger Entries" + } + ] +} \ No newline at end of file diff --git a/schemas/app/SalesInvoiceItem.json b/schemas/app/SalesInvoiceItem.json new file mode 100644 index 00000000..d6d05c50 --- /dev/null +++ b/schemas/app/SalesInvoiceItem.json @@ -0,0 +1,93 @@ +{ + "name": "SalesInvoiceItem", + "doctype": "DocType", + "isChild": true, + "regional": 1, + "keywordFields": [], + "tableFields": [ + "item", + "tax", + "quantity", + "rate", + "amount" + ], + "fields": [ + { + "fieldname": "item", + "label": "Item", + "fieldtype": "Link", + "target": "Item", + "required": true + }, + { + "fieldname": "description", + "label": "Description", + "fieldtype": "Text", + "hidden": true, + "formulaDependsOn": [ + "item" + ] + }, + { + "fieldname": "quantity", + "label": "Quantity", + "fieldtype": "Float", + "required": true, + "default": 1 + }, + { + "fieldname": "rate", + "label": "Rate", + "fieldtype": "Currency", + "required": true, + "formulaDependsOn": [ + "item" + ] + }, + { + "fieldname": "baseRate", + "label": "Rate (Company Currency)", + "fieldtype": "Currency", + "readOnly": true + }, + { + "fieldname": "account", + "label": "Account", + "hidden": true, + "fieldtype": "Link", + "target": "Account", + "required": true, + "readOnly": true + }, + { + "fieldname": "tax", + "label": "Tax", + "fieldtype": "Link", + "target": "Tax", + "formulaDependsOn": [ + "item" + ] + }, + { + "fieldname": "amount", + "label": "Amount", + "fieldtype": "Currency", + "readOnly": true + }, + { + "fieldname": "baseAmount", + "label": "Amount (Company Currency)", + "fieldtype": "Currency", + "readOnly": true + }, + { + "fieldname": "hsnCode", + "label": "HSN/SAC", + "fieldtype": "Int", + "formulaDependsOn": [ + "item" + ] + } + ], + "augmented": 1 +} \ No newline at end of file diff --git a/schemas/app/SalesInvoiceSettings.json b/schemas/app/SalesInvoiceSettings.json new file mode 100644 index 00000000..20ff6ce8 --- /dev/null +++ b/schemas/app/SalesInvoiceSettings.json @@ -0,0 +1,45 @@ +{ + "name": "SalesInvoiceSettings", + "label": "SalesInvoice Settings", + "doctype": "DocType", + "isSingle": true, + "isChild": false, + "keywordFields": [], + "fields": [ + { + "fieldname": "template", + "label": "Template", + "placeholder": "Template", + "fieldtype": "Select", + "options": [ + "Basic I", + "Basic II", + "Modern" + ], + "required": true, + "default": "Basic I" + }, + { + "fieldname": "font", + "label": "Font", + "placeholder": "Font", + "fieldtype": "Select", + "options": [ + "Montserrat", + "Open Sans", + "Oxygen", + "Merriweather" + ], + "required": true, + "default": "Montserrat" + }, + { + "fieldname": "themeColor", + "label": "Theme Color", + "fieldtype": "Data", + "required": true, + "default": "#000000", + "hidden": true + } + ] +} \ No newline at end of file diff --git a/schemas/app/SetupWizard.json b/schemas/app/SetupWizard.json new file mode 100644 index 00000000..d70a6aae --- /dev/null +++ b/schemas/app/SetupWizard.json @@ -0,0 +1,101 @@ +{ + "name": "SetupWizard", + "label": "Setup Wizard", + "naming": "name", + "isSingle": true, + "isChild": false, + "isSubmittable": false, + "settings": null, + "keywordFields": [], + "fields": [ + { + "fieldname": "companyLogo", + "label": "Company Logo", + "fieldtype": "AttachImage" + }, + { + "fieldname": "country", + "label": "Country", + "fieldtype": "AutoComplete", + "placeholder": "Select Country", + "required": true + }, + { + "fieldname": "fullname", + "label": "Your Name", + "fieldtype": "Data", + "placeholder": "John Doe", + "required": true + }, + { + "fieldname": "email", + "label": "Email", + "fieldtype": "Data", + "placeholder": "john@doe.com", + "required": true, + "validate": { + "type": "email" + } + }, + { + "fieldname": "companyName", + "label": "Company Name", + "placeholder": "Company Name", + "fieldtype": "Data", + "required": true + }, + { + "fieldname": "bankName", + "label": "Bank Name", + "fieldtype": "Data", + "placeholder": "Prime Bank", + "required": true + }, + { + "fieldname": "fiscalYearStart", + "label": "Fiscal Year Start Date", + "placeholder": "Fiscal Year Start Date", + "fieldtype": "Date", + "formulaDependsOn": ["country"], + "required": true + }, + { + "fieldname": "fiscalYearEnd", + "label": "Fiscal Year End Date", + "placeholder": "Fiscal Year End Date", + "fieldtype": "Date", + "formulaDependsOn": ["country"], + "required": true + }, + { + "fieldname": "currency", + "label": "Currency", + "fieldtype": "Data", + "placeholder": "Currency", + "formulaDependsOn": ["country"], + "required": true + }, + { + "fieldname": "completed", + "label": "Completed", + "fieldtype": "Check", + "readOnly": true + }, + { + "fieldname": "chartOfAccounts", + "label": "Chart of Accounts", + "fieldtype": "AutoComplete", + "placeholder": "Select CoA", + "formulaDependsOn": ["country"] + } + ], + "quickEditFields": [ + "fullname", + "bankName", + "country", + "currency", + "chartOfAccounts", + "fiscalYearStart", + "fiscalYearEnd" + ] +} diff --git a/schemas/app/Supplier.json b/schemas/app/Supplier.json new file mode 100644 index 00000000..d67f746e --- /dev/null +++ b/schemas/app/Supplier.json @@ -0,0 +1,121 @@ +{ + "name": "Supplier", + "label": "Supplier", + "basedOn": "Party", + "filters": { + "supplier": 1 + }, + "actions": [ + { + "label": "Create Bill" + }, + { + "label": "View Bills" + } + ], + "regional": 1, + "keywordFields": [ + "name" + ], + "fields": [ + { + "fieldname": "name", + "label": "Name", + "fieldtype": "Data", + "required": true, + "placeholder": "Full Name" + }, + { + "fieldname": "image", + "label": "Image", + "fieldtype": "AttachImage" + }, + { + "fieldname": "customer", + "label": "Is Customer", + "fieldtype": "Check" + }, + { + "fieldname": "gstin", + "label": "GSTIN No.", + "fieldtype": "Data" + }, + { + "fieldname": "gstType", + "label": "GST Registration", + "placeholder": "GST Registration", + "fieldtype": "Select", + "default": "Unregistered", + "options": [ + "Unregistered", + "Registered Regular", + "Consumer" + ] + }, + { + "fieldname": "supplier", + "label": "Is Supplier", + "fieldtype": "Check" + }, + { + "fieldname": "defaultAccount", + "label": "Default Account", + "fieldtype": "Link", + "target": "Account" + }, + { + "fieldname": "outstandingAmount", + "label": "Outstanding Amount", + "fieldtype": "Currency" + }, + { + "fieldname": "currency", + "label": "Currency", + "fieldtype": "Link", + "target": "Currency", + "placeholder": "INR" + }, + { + "fieldname": "email", + "label": "Email", + "fieldtype": "Data", + "placeholder": "john@doe.com", + "validate": { + "type": "email" + } + }, + { + "fieldname": "phone", + "label": "Phone", + "fieldtype": "Data", + "placeholder": "Phone", + "validate": { + "type": "phone" + } + }, + { + "fieldname": "address", + "label": "Address", + "fieldtype": "Link", + "target": "Address", + "placeholder": "Click to create", + "inline": true + }, + { + "fieldname": "addressDisplay", + "label": "Address Display", + "fieldtype": "Text", + "readOnly": true + } + ], + "quickEditFields": [ + "email", + "phone", + "address", + "defaultAccount", + "currency", + "gstType", + "gstin" + ], + "augmented": 1 +} \ No newline at end of file diff --git a/schemas/app/Tax.json b/schemas/app/Tax.json new file mode 100644 index 00000000..1fa77c95 --- /dev/null +++ b/schemas/app/Tax.json @@ -0,0 +1,28 @@ +{ + "name": "Tax", + "label": "Tax", + "doctype": "DocType", + "isSingle": false, + "isChild": false, + "keywordFields": [ + "name" + ], + "fields": [ + { + "fieldname": "name", + "label": "Name", + "fieldtype": "Data", + "required": true + }, + { + "fieldname": "details", + "label": "Details", + "fieldtype": "Table", + "childtype": "TaxDetail", + "required": true + } + ], + "quickEditFields": [ + "details" + ] +} \ No newline at end of file diff --git a/schemas/app/TaxDetail.json b/schemas/app/TaxDetail.json new file mode 100644 index 00000000..d866d373 --- /dev/null +++ b/schemas/app/TaxDetail.json @@ -0,0 +1,28 @@ +{ + "name": "TaxDetail", + "label": "Tax Detail", + "doctype": "DocType", + "isSingle": false, + "isChild": true, + "keywordFields": [], + "fields": [ + { + "fieldname": "account", + "label": "Tax Account", + "fieldtype": "Link", + "target": "Account", + "required": true + }, + { + "fieldname": "rate", + "label": "Rate", + "fieldtype": "Float", + "required": true, + "placeholder": "0%" + } + ], + "tableFields": [ + "account", + "rate" + ] +} \ No newline at end of file diff --git a/schemas/app/TaxSummary.json b/schemas/app/TaxSummary.json new file mode 100644 index 00000000..67cc63af --- /dev/null +++ b/schemas/app/TaxSummary.json @@ -0,0 +1,32 @@ +{ + "name": "TaxSummary", + "doctype": "DocType", + "isChild": true, + "fields": [ + { + "fieldname": "account", + "label": "Tax Account", + "fieldtype": "Link", + "target": "Account", + "required": true + }, + { + "fieldname": "rate", + "label": "Rate", + "fieldtype": "Float", + "required": true + }, + { + "fieldname": "amount", + "label": "Amount", + "fieldtype": "Currency", + "required": true + }, + { + "fieldname": "baseAmount", + "label": "Amount (Company Currency)", + "fieldtype": "Currency", + "readOnly": true + } + ] +} \ No newline at end of file diff --git a/schemas/core/NumberSeries.json b/schemas/core/NumberSeries.json new file mode 100644 index 00000000..b9dbafbd --- /dev/null +++ b/schemas/core/NumberSeries.json @@ -0,0 +1,75 @@ +{ + "name": "NumberSeries", + "label": "Number Series", + "doctype": "DocType", + "isSingle": false, + "isChild": false, + "keywordFields": [], + "fields": [ + { + "fieldname": "name", + "label": "Prefix", + "fieldtype": "Data", + "required": true + }, + { + "fieldname": "start", + "label": "Start", + "fieldtype": "Int", + "default": 1001, + "required": true, + "minvalue": 0 + }, + { + "fieldname": "padZeros", + "label": "Pad Zeros", + "fieldtype": "Int", + "default": 4, + "required": true + }, + { + "fieldname": "referenceType", + "label": "Reference Type", + "fieldtype": "Select", + "options": [ + "SalesInvoice", + "PurchaseInvoice", + "Payment", + "JournalEntry", + "Quotation", + "SalesOrder", + "Fulfillment", + "PurchaseOrder", + "PurchaseReceipt", + "-" + ], + "map": { + "SalesInvoice": "Invoice", + "PurchaseInvoice": "Bill", + "Payment": "Payment", + "JournalEntry": "Journal Entry", + "Quotation": "Quotation", + "SalesOrder": "SalesOrder", + "Fulfillment": "Fulfillment", + "PurchaseOrder": "PurchaseOrder", + "PurchaseReceipt": "PurchaseReceipt", + "-": "None" + }, + "default": "-", + "required": true, + "readOnly": true + }, + { + "fieldname": "current", + "label": "Current", + "fieldtype": "Int", + "required": true, + "readOnly": true + } + ], + "quickEditFields": [ + "start", + "padZeros", + "referenceType" + ] +} \ No newline at end of file diff --git a/schemas/core/PatchRun.json b/schemas/core/PatchRun.json new file mode 100644 index 00000000..89383032 --- /dev/null +++ b/schemas/core/PatchRun.json @@ -0,0 +1,11 @@ +{ + "name": "PatchRun", + "fields": [ + { + "fieldname": "name", + "fieldtype": "Data", + "label": "Name", + "required": true + } + ] +} \ No newline at end of file diff --git a/schemas/core/SingleValue.json b/schemas/core/SingleValue.json new file mode 100644 index 00000000..459081c2 --- /dev/null +++ b/schemas/core/SingleValue.json @@ -0,0 +1,27 @@ +{ + "name": "SingleValue", + "doctype": "DocType", + "isSingle": false, + "isChild": false, + "keywordFields": [], + "fields": [ + { + "fieldname": "parent", + "label": "Parent", + "fieldtype": "Data", + "required": true + }, + { + "fieldname": "fieldname", + "label": "Fieldname", + "fieldtype": "Data", + "required": true + }, + { + "fieldname": "value", + "label": "Value", + "fieldtype": "Data", + "required": true + } + ] +} \ No newline at end of file diff --git a/schemas/core/SystemSettings.json b/schemas/core/SystemSettings.json new file mode 100644 index 00000000..38f6d939 --- /dev/null +++ b/schemas/core/SystemSettings.json @@ -0,0 +1,86 @@ +{ + "name": "SystemSettings", + "label": "System Settings", + "doctype": "DocType", + "isSingle": true, + "isChild": false, + "keywordFields": [], + "fields": [ + { + "fieldname": "dateFormat", + "label": "Date Format", + "fieldtype": "Select", + "options": [ + { + "label": "23/03/2022", + "value": "dd/MM/yyyy" + }, + { + "label": "03/23/2022", + "value": "MM/dd/yyyy" + }, + { + "label": "23-03-2022", + "value": "dd-MM-yyyy" + }, + { + "label": "03-23-2022", + "value": "MM-dd-yyyy" + }, + { + "label": "2022-03-23", + "value": "yyyy-MM-dd" + }, + { + "label": "23 Mar, 2022", + "value": "d MMM, y" + }, + { + "label": "Mar 23, 2022", + "value": "MMM d, y" + } + ], + "default": "MMM d, y", + "required": true, + "description": "Sets the app-wide date display format." + }, + { + "fieldname": "locale", + "label": "Locale", + "fieldtype": "Data", + "default": "en-IN", + "description": "Set the local code. This is used for number formatting." + }, + { + "fieldname": "displayPrecision", + "label": "Display Precision", + "fieldtype": "Int", + "default": 2, + "required": true, + "minValue": 0, + "maxValue": 9, + "description": "Sets how many digits are shown after the decimal point." + }, + { + "fieldname": "internalPrecision", + "label": "Internal Precision", + "fieldtype": "Int", + "minValue": 0, + "default": 11, + "description": "Sets the internal precision used for monetary calculations. Above 6 should be sufficient for most currencies." + }, + { + "fieldname": "hideGetStarted", + "label": "Hide Get Started", + "fieldtype": "Check", + "default": 0, + "description": "Hides the Get Started section from the sidebar. Change will be visible on restart or refreshing the app." + } + ], + "quickEditFields": [ + "locale", + "dateFormat", + "displayPrecision", + "hideGetStarted" + ] +} \ No newline at end of file From 879b677b3b68355ba9db1a71764ff98a64392efc Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Wed, 23 Mar 2022 14:07:00 +0530 Subject: [PATCH 012/163] incr: cleanup and import schemas --- schemas/app/Account.json | 128 +++++++++++++++----- schemas/app/AccountingLedgerEntry.json | 24 +--- schemas/app/AccountingSettings.json | 21 +--- schemas/app/Address.json | 34 ++---- schemas/app/Color.json | 7 +- schemas/app/CompanySettings.json | 10 +- schemas/app/Contact.json | 87 ------------- schemas/app/Currency.json | 15 +-- schemas/app/Customer.json | 118 +----------------- schemas/app/GetStarted.json | 3 +- schemas/app/Item.json | 101 +++++++-------- schemas/app/JournalEntry.json | 84 +++++++------ schemas/app/JournalEntryAccount.json | 7 +- schemas/app/JournalEntrySettings.json | 9 -- schemas/{core => app}/NumberSeries.json | 49 +++----- schemas/app/Party.json | 56 +-------- schemas/app/Payment.json | 123 ++++--------------- schemas/app/PaymentFor.json | 22 ++-- schemas/app/PaymentSettings.json | 8 -- schemas/app/PrintSettings.json | 53 +++++--- schemas/app/PurchaseInvoice.json | 50 ++------ schemas/app/PurchaseInvoiceItem.json | 14 +-- schemas/app/PurchaseInvoiceSettings.json | 9 -- schemas/app/SalesInvoice.json | 51 ++------ schemas/app/SalesInvoiceItem.json | 39 ++---- schemas/app/SalesInvoiceSettings.json | 41 +++++-- schemas/app/SetupWizard.json | 14 +-- schemas/app/Supplier.json | 118 +----------------- schemas/app/Tax.json | 9 +- schemas/app/TaxDetail.json | 7 +- schemas/app/TaxSummary.json | 2 +- schemas/core/PatchRun.json | 3 +- schemas/core/SingleValue.json | 6 +- schemas/core/SystemSettings.json | 7 +- schemas/regional/in/AccountingSettings.json | 22 ++++ schemas/regional/in/Address.json | 20 +++ schemas/regional/in/Party.json | 27 +++++ schemas/schemas.ts | 77 ++++++++++++ schemas/types.ts | 81 +++++++++++++ 39 files changed, 626 insertions(+), 930 deletions(-) delete mode 100644 schemas/app/Contact.json delete mode 100644 schemas/app/JournalEntrySettings.json rename schemas/{core => app}/NumberSeries.json (54%) delete mode 100644 schemas/app/PaymentSettings.json delete mode 100644 schemas/app/PurchaseInvoiceSettings.json create mode 100644 schemas/regional/in/AccountingSettings.json create mode 100644 schemas/regional/in/Address.json create mode 100644 schemas/regional/in/Party.json create mode 100644 schemas/schemas.ts create mode 100644 schemas/types.ts diff --git a/schemas/app/Account.json b/schemas/app/Account.json index 42416775..75a03c30 100644 --- a/schemas/app/Account.json +++ b/schemas/app/Account.json @@ -4,11 +4,6 @@ "doctype": "DocType", "isSingle": false, "isTree": true, - "keywordFields": [ - "name", - "rootType", - "accountType" - ], "fields": [ { "fieldname": "name", @@ -22,11 +17,26 @@ "fieldtype": "Select", "placeholder": "Root Type", "options": [ - "Asset", - "Liability", - "Equity", - "Income", - "Expense" + { + "value": "Asset", + "label": "Asset" + }, + { + "value": "Liability", + "label": "Liability" + }, + { + "value": "Equity", + "label": "Equity" + }, + { + "value": "Income", + "label": "Income" + }, + { + "value": "Expense", + "label": "Expense" + } ], "required": true }, @@ -42,25 +52,82 @@ "placeholder": "Account Type", "fieldtype": "Select", "options": [ - "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" + { + "value": "Accumulated Depreciation", + "label": "Accumulated Depreciation" + }, + { + "value": "Bank", + "label": "Bank" + }, + { + "value": "Cash", + "label": "Cash" + }, + { + "value": "Chargeable", + "label": "Chargeable" + }, + { + "value": "Cost of Goods Sold", + "label": "Cost of Goods Sold" + }, + { + "value": "Depreciation", + "label": "Depreciation" + }, + { + "value": "Equity", + "label": "Equity" + }, + { + "value": "Expense Account", + "label": "Expense Account" + }, + { + "value": "Expenses Included In Valuation", + "label": "Expenses Included In Valuation" + }, + { + "value": "Fixed Asset", + "label": "Fixed Asset" + }, + { + "value": "Income Account", + "label": "Income Account" + }, + { + "value": "Payable", + "label": "Payable" + }, + { + "value": "Receivable", + "label": "Receivable" + }, + { + "value": "Round Off", + "label": "Round Off" + }, + { + "value": "Stock", + "label": "Stock" + }, + { + "value": "Stock Adjustment", + "label": "Stock Adjustment" + }, + { + "value": "Stock Received But Not Billed", + "label": "Stock Received But Not Billed" + }, + { + "value": "Tax", + "label": "Tax" + }, + { + "value": "Temporary", + "label": "Temporary" + } ] }, { @@ -82,7 +149,8 @@ "accountType", "isGroup" ], + "keywordFields": ["name", "rootType", "accountType"], "treeSettings": { "parentField": "parentAccount" } -} \ No newline at end of file +} diff --git a/schemas/app/AccountingLedgerEntry.json b/schemas/app/AccountingLedgerEntry.json index 6de20eae..a8480b0c 100644 --- a/schemas/app/AccountingLedgerEntry.json +++ b/schemas/app/AccountingLedgerEntry.json @@ -1,15 +1,8 @@ { "name": "AccountingLedgerEntry", "label": "Ledger Entry", - "naming": "autoincrement", - "doctype": "DocType", "isSingle": false, "isChild": false, - "keywordFields": [ - "account", - "party", - "referenceName" - ], "fields": [ { "fieldname": "date", @@ -23,11 +16,6 @@ "target": "Account", "required": true }, - { - "fieldname": "description", - "label": "Description", - "fieldtype": "Text" - }, { "fieldname": "party", "label": "Party", @@ -44,11 +32,6 @@ "label": "Credit", "fieldtype": "Currency" }, - { - "fieldname": "againstAccount", - "label": "Against Account", - "fieldtype": "Text" - }, { "fieldname": "referenceType", "label": "Ref. Type", @@ -69,19 +52,18 @@ "fieldname": "reverted", "label": "Reverted", "fieldtype": "Check", - "default": 0 + "default": false } ], "quickEditFields": [ "date", "account", - "description", "party", "debit", "credit", - "againstAccount", "referenceType", "referenceName", "balance" - ] + ], + "keywordFields": ["account", "party", "referenceName"] } \ No newline at end of file diff --git a/schemas/app/AccountingSettings.json b/schemas/app/AccountingSettings.json index 7b2998dc..c147814a 100644 --- a/schemas/app/AccountingSettings.json +++ b/schemas/app/AccountingSettings.json @@ -1,12 +1,9 @@ { "name": "AccountingSettings", "label": "Accounting Settings", - "naming": "name", "isSingle": true, "isChild": false, "isSubmittable": false, - "settings": null, - "keywordFields": [], "fields": [ { "label": "Company Name", @@ -53,10 +50,7 @@ "fieldname": "email", "label": "Email", "fieldtype": "Data", - "required": true, - "validate": { - "type": "email" - } + "required": true }, { "fieldname": "bankName", @@ -80,13 +74,7 @@ "fieldname": "setupComplete", "label": "Setup Complete", "fieldtype": "Check", - "default": 0 - }, - { - "fieldname": "gstin", - "label": "GSTIN", - "fieldtype": "Data", - "placeholder": "27AAAAA0000A1Z5" + "default": false } ], "quickEditFields": [ @@ -96,7 +84,6 @@ "country", "currency", "fiscalYearStart", - "fiscalYearEnd", - "gstin" + "fiscalYearEnd" ] -} \ No newline at end of file +} diff --git a/schemas/app/Address.json b/schemas/app/Address.json index c1134297..12fd1071 100644 --- a/schemas/app/Address.json +++ b/schemas/app/Address.json @@ -1,16 +1,7 @@ { "name": "Address", - "doctype": "DocType", - "regional": 1, + "label": "Address", "isSingle": false, - "keywordFields": [ - "addressLine1", - "addressLine2", - "city", - "state", - "country", - "postalCode" - ], "fields": [ { "fieldname": "addressLine1", @@ -36,6 +27,7 @@ "fieldname": "state", "label": "State", "placeholder": "State", + "options": [], "fieldtype": "AutoComplete" }, { @@ -43,6 +35,7 @@ "label": "Country", "placeholder": "Country", "fieldtype": "AutoComplete", + "options": [], "required": true }, { @@ -73,12 +66,6 @@ "fieldtype": "Text", "label": "Address Display", "readOnly": true - }, - { - "fieldname": "pos", - "label": "Place of Supply", - "fieldtype": "AutoComplete", - "placeholder": "Place of Supply" } ], "quickEditFields": [ @@ -87,9 +74,14 @@ "city", "state", "country", - "postalCode", - "pos" + "postalCode" ], - "inlineEditDisplayField": "addressDisplay", - "augmented": 1 -} \ No newline at end of file + "keywordFields": [ + "addressLine1", + "addressLine2", + "city", + "state", + "country", + "postalCode" + ] +} diff --git a/schemas/app/Color.json b/schemas/app/Color.json index 8f6a2eb1..0e88ca16 100644 --- a/schemas/app/Color.json +++ b/schemas/app/Color.json @@ -1,6 +1,6 @@ { "name": "Color", - "doctype": "DocType", + "label": "Color", "fields": [ { "fieldname": "name", @@ -11,7 +11,8 @@ { "fieldname": "hexvalue", "fieldtype": "Data", - "label": "Hex Value" + "label": "Hex Value", + "required": true } ] -} \ No newline at end of file +} diff --git a/schemas/app/CompanySettings.json b/schemas/app/CompanySettings.json index 5f6ad6c6..db405430 100644 --- a/schemas/app/CompanySettings.json +++ b/schemas/app/CompanySettings.json @@ -4,24 +4,20 @@ "naming": "autoincrement", "isSingle": true, "isChild": false, - "keywordFields": [ - "companyName" - ], "fields": [ { "fieldname": "companyName", "label": "Company Name", "fieldtype": "Data", - "disabled": false, "required": true }, { "fieldname": "companyAddress", "label": "Company Address", "fieldtype": "Link", - "disabled": false, "required": true, "target": "Address" } - ] -} \ No newline at end of file + ], + "keywordFields": ["companyName"] +} diff --git a/schemas/app/Contact.json b/schemas/app/Contact.json deleted file mode 100644 index d9e18d31..00000000 --- a/schemas/app/Contact.json +++ /dev/null @@ -1,87 +0,0 @@ -{ - "name": "Contact", - "doctype": "DocType", - "isSingle": false, - "naming": "autoincrement", - "pageSettings": { - "hideTitle": true - }, - "titleField": "fullName", - "keywordFields": [ - "fullName" - ], - "fields": [ - { - "fieldname": "fullName", - "label": "Full Name", - "fieldtype": "Data", - "required": true - }, - { - "fieldname": "emailAddress", - "label": "Email Address", - "fieldtype": "Data" - }, - { - "fieldname": "userId", - "label": "User ID", - "fieldtype": "Link", - "target": "User", - "hidden": true - }, - { - "fieldname": "status", - "label": "Status", - "fieldtype": "Select", - "options": [ - "Passive", - "Open", - "Replied" - ] - }, - { - "fieldname": "gender", - "label": "Gender", - "fieldtype": "Select", - "options": [ - "Male", - "Female", - "Gender" - ] - }, - { - "fieldname": "mobileNumber", - "label": "Mobile Number", - "fieldtype": "Data" - }, - { - "fieldname": "phone", - "label": "Phone", - "fieldtype": "Data" - } - ], - "events": {}, - "listSettings": {}, - "layout": [ - { - "columns": [ - { - "fields": [ - "fullName", - "emailAddress", - "userId", - "status" - ] - }, - { - "fields": [ - "postalCode", - "gender", - "phone", - "mobileNumber" - ] - } - ] - } - ] -} \ No newline at end of file diff --git a/schemas/app/Currency.json b/schemas/app/Currency.json index 91055a4c..44c13664 100644 --- a/schemas/app/Currency.json +++ b/schemas/app/Currency.json @@ -1,16 +1,7 @@ { "name": "Currency", "label": "Currency", - "doctype": "DocType", "isSingle": false, - "keywordFields": [ - "name", - "symbol" - ], - "quickEditFields": [ - "name", - "symbol" - ], "fields": [ { "fieldname": "name", @@ -38,5 +29,7 @@ "fieldname": "symbol", "fieldtype": "Data" } - ] -} \ No newline at end of file + ], + "keywordFields": ["name", "symbol"], + "quickEditFields": ["name", "symbol"] +} diff --git a/schemas/app/Customer.json b/schemas/app/Customer.json index ac72af24..e19524c0 100644 --- a/schemas/app/Customer.json +++ b/schemas/app/Customer.json @@ -1,121 +1,5 @@ { "name": "Customer", "label": "Customer", - "basedOn": "Party", - "filters": { - "customer": 1 - }, - "actions": [ - { - "label": "Create Invoice" - }, - { - "label": "View Invoices" - } - ], - "regional": 1, - "keywordFields": [ - "name" - ], - "fields": [ - { - "fieldname": "name", - "label": "Name", - "fieldtype": "Data", - "required": true, - "placeholder": "Full Name" - }, - { - "fieldname": "image", - "label": "Image", - "fieldtype": "AttachImage" - }, - { - "fieldname": "customer", - "label": "Is Customer", - "fieldtype": "Check" - }, - { - "fieldname": "gstin", - "label": "GSTIN No.", - "fieldtype": "Data" - }, - { - "fieldname": "gstType", - "label": "GST Registration", - "placeholder": "GST Registration", - "fieldtype": "Select", - "default": "Unregistered", - "options": [ - "Unregistered", - "Registered Regular", - "Consumer" - ] - }, - { - "fieldname": "supplier", - "label": "Is Supplier", - "fieldtype": "Check" - }, - { - "fieldname": "defaultAccount", - "label": "Default Account", - "fieldtype": "Link", - "target": "Account" - }, - { - "fieldname": "outstandingAmount", - "label": "Outstanding Amount", - "fieldtype": "Currency" - }, - { - "fieldname": "currency", - "label": "Currency", - "fieldtype": "Link", - "target": "Currency", - "placeholder": "INR" - }, - { - "fieldname": "email", - "label": "Email", - "fieldtype": "Data", - "placeholder": "john@doe.com", - "validate": { - "type": "email" - } - }, - { - "fieldname": "phone", - "label": "Phone", - "fieldtype": "Data", - "placeholder": "Phone", - "validate": { - "type": "phone" - } - }, - { - "fieldname": "address", - "label": "Address", - "fieldtype": "Link", - "target": "Address", - "placeholder": "Click to create", - "inline": true - }, - { - "fieldname": "addressDisplay", - "label": "Address Display", - "fieldtype": "Text", - "readOnly": true - } - ], - "quickEditFields": [ - "email", - "phone", - "address", - "defaultAccount", - "currency", - "gstType", - "gstin" - ], - "augmented": 1 + "extends": "Party" } \ No newline at end of file diff --git a/schemas/app/GetStarted.json b/schemas/app/GetStarted.json index 9e4bf4f4..a0c0ce40 100644 --- a/schemas/app/GetStarted.json +++ b/schemas/app/GetStarted.json @@ -1,5 +1,6 @@ { "name": "GetStarted", + "label": "Get Started", "isSingle": true, "fields": [ { @@ -63,4 +64,4 @@ "fieldtype": "Check" } ] -} \ No newline at end of file +} diff --git a/schemas/app/Item.json b/schemas/app/Item.json index 5053af5e..5f94526e 100644 --- a/schemas/app/Item.json +++ b/schemas/app/Item.json @@ -1,13 +1,7 @@ { "name": "Item", "label": "Item", - "doctype": "DocType", "isSingle": false, - "regional": 1, - "keywordFields": [ - "name", - "description" - ], "fields": [ { "fieldname": "name", @@ -16,12 +10,6 @@ "placeholder": "Item Name", "required": true }, - { - "fieldname": "hsnCode", - "label": "HSN/SAC", - "fieldtype": "Int", - "placeholder": "HSN/SAC Code" - }, { "fieldname": "image", "label": "Image", @@ -40,11 +28,26 @@ "placeholder": "Unit Type", "default": "Unit", "options": [ - "Unit", - "Kg", - "Gram", - "Hour", - "Day" + { + "value": "Unit", + "name": "Unit" + }, + { + "value": "Kg", + "name": "Kg" + }, + { + "value": "Gram", + "label": "Gram" + }, + { + "value": "Hour", + "label": "Hour" + }, + { + "value": "Day", + "label": "Day" + } ] }, { @@ -54,8 +57,14 @@ "fieldtype": "Select", "default": "Product", "options": [ - "Product", - "Service" + { + "value": "Product", + "label": "Product" + }, + { + "value": "Service", + "label": "Service" + } ] }, { @@ -63,15 +72,19 @@ "label": "For", "fieldtype": "Select", "options": [ - "purchases", - "sales", - "both" + { + "value": "purchases", + "label": "Purchases" + }, + { + "value": "sales", + "label": "Sales" + }, + { + "value": "both", + "label": "Both" + } ], - "map": { - "purchases": "Purchases", - "sales": "Sales", - "both": "Both" - }, "default": "both" }, { @@ -80,11 +93,7 @@ "fieldtype": "Link", "target": "Account", "placeholder": "Income", - "required": true, - "disableCreation": true, - "formulaDependsOn": [ - "itemType" - ] + "required": true }, { "fieldname": "expenseAccount", @@ -92,11 +101,7 @@ "fieldtype": "Link", "target": "Account", "placeholder": "Expense", - "required": true, - "disableCreation": true, - "formulaDependsOn": [ - "itemType" - ] + "required": true }, { "fieldname": "tax", @@ -109,10 +114,15 @@ "fieldname": "rate", "label": "Rate", "fieldtype": "Currency" + }, + { + "fieldname": "hsnCode", + "label": "HSN/SAC", + "fieldtype": "Int", + "placeholder": "HSN/SAC Code" } ], "quickEditFields": [ - "hsnCode", "rate", "unit", "itemType", @@ -120,15 +130,8 @@ "tax", "description", "incomeAccount", - "expenseAccount" + "expenseAccount", + "hsnCode" ], - "actions": [ - { - "label": "New Invoice" - }, - { - "label": "New Bill" - } - ], - "augmented": 1 -} \ No newline at end of file + "keywordFields": ["name", "description"] +} diff --git a/schemas/app/JournalEntry.json b/schemas/app/JournalEntry.json index bb84ed13..a53ba3dc 100644 --- a/schemas/app/JournalEntry.json +++ b/schemas/app/JournalEntry.json @@ -1,9 +1,7 @@ { "label": "Journal Entry", "name": "JournalEntry", - "doctype": "DocType", "isSubmittable": true, - "settings": "JournalEntrySettings", "fields": [ { "fieldname": "entryType", @@ -11,31 +9,51 @@ "fieldtype": "Select", "placeholder": "Entry Type", "options": [ - "Journal Entry", - "Bank Entry", - "Cash Entry", - "Credit Card Entry", - "Debit Note", - "Credit Note", - "Contra Entry", - "Excise Entry", - "Write Off Entry", - "Opening Entry", - "Depreciation Entry" + { + "value": "Journal Entry", + "label": "Journal Entry" + }, + { + "value": "Bank Entry", + "label": "Bank Entry" + }, + { + "value": "Cash Entry", + "label": "Cash Entry" + }, + { + "value": "Credit Card Entry", + "label": "Credit Card Entry" + }, + { + "value": "Debit Note", + "label": "Debit Note" + }, + { + "value": "Credit Note", + "label": "Credit Note" + }, + { + "value": "Contra Entry", + "label": "Contra Entry" + }, + { + "value": "Excise Entry", + "label": "Excise Entry" + }, + { + "value": "Write Off Entry", + "label": "Write Off Entry" + }, + { + "value": "Opening Entry", + "label": "Opening Entry" + }, + { + "value": "Depreciation Entry", + "label": "Depreciation Entry" + } ], - "map": { - "Journal Entry": "Journal Entry", - "Bank Entry": "Bank Entry", - "Cash Entry": "Cash Entry", - "Credit Card Entry": "Credit Card Entry", - "Debit Note": "Debit Note", - "Credit Note": "Credit Note", - "Contra Entry": "Contra Entry", - "Excise Entry": "Excise Entry", - "Write Off Entry": "Write Off Entry", - "Opening Entry": "Opening Entry", - "Depreciation Entry": "Depreciation Entry" - }, "required": true }, { @@ -54,7 +72,7 @@ "fieldname": "accounts", "label": "Account Entries", "fieldtype": "Table", - "childtype": "JournalEntryAccount", + "target": "JournalEntryAccount", "required": true }, { @@ -73,13 +91,6 @@ "fieldtype": "Text", "placeholder": "User Remark" }, - { - "fieldname": "cancelled", - "label": "Cancelled", - "fieldtype": "Check", - "default": 0, - "readOnly": true - }, { "fieldname": "numberSeries", "label": "Number Series", @@ -88,10 +99,5 @@ "required": true, "default": "JV-" } - ], - "actions": [ - { - "label": "Ledger Entries" - } ] } \ No newline at end of file diff --git a/schemas/app/JournalEntryAccount.json b/schemas/app/JournalEntryAccount.json index 2e3b6f4c..810caadc 100644 --- a/schemas/app/JournalEntryAccount.json +++ b/schemas/app/JournalEntryAccount.json @@ -1,5 +1,6 @@ { "name": "JournalEntryAccount", + "label": "Journal Entry Account", "isChild": true, "fields": [ { @@ -22,9 +23,5 @@ "fieldtype": "Currency" } ], - "tableFields": [ - "account", - "debit", - "credit" - ] + "tableFields": ["account", "debit", "credit"] } \ No newline at end of file diff --git a/schemas/app/JournalEntrySettings.json b/schemas/app/JournalEntrySettings.json deleted file mode 100644 index 9a365dfc..00000000 --- a/schemas/app/JournalEntrySettings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "JournalEntrySettings", - "label": "Journal Entry Setting", - "doctype": "DocType", - "isSingle": true, - "isChild": false, - "keywordFields": [], - "fields": [] -} \ No newline at end of file diff --git a/schemas/core/NumberSeries.json b/schemas/app/NumberSeries.json similarity index 54% rename from schemas/core/NumberSeries.json rename to schemas/app/NumberSeries.json index b9dbafbd..90ede4e4 100644 --- a/schemas/core/NumberSeries.json +++ b/schemas/app/NumberSeries.json @@ -1,10 +1,8 @@ { "name": "NumberSeries", "label": "Number Series", - "doctype": "DocType", "isSingle": false, "isChild": false, - "keywordFields": [], "fields": [ { "fieldname": "name", @@ -32,29 +30,23 @@ "label": "Reference Type", "fieldtype": "Select", "options": [ - "SalesInvoice", - "PurchaseInvoice", - "Payment", - "JournalEntry", - "Quotation", - "SalesOrder", - "Fulfillment", - "PurchaseOrder", - "PurchaseReceipt", - "-" + { + "value": "SalesInvoice", + "label": "Invoice" + }, + { + "value": "PurchaseInvoice", + "label": "Bill" + }, + { + "value": "Payment", + "label": "Payment" + }, + { + "value": "JournalEntry", + "label": "Journal Entry" + } ], - "map": { - "SalesInvoice": "Invoice", - "PurchaseInvoice": "Bill", - "Payment": "Payment", - "JournalEntry": "Journal Entry", - "Quotation": "Quotation", - "SalesOrder": "SalesOrder", - "Fulfillment": "Fulfillment", - "PurchaseOrder": "PurchaseOrder", - "PurchaseReceipt": "PurchaseReceipt", - "-": "None" - }, "default": "-", "required": true, "readOnly": true @@ -67,9 +59,6 @@ "readOnly": true } ], - "quickEditFields": [ - "start", - "padZeros", - "referenceType" - ] -} \ No newline at end of file + "quickEditFields": ["start", "padZeros", "referenceType"], + "keywordFields": [] +} diff --git a/schemas/app/Party.json b/schemas/app/Party.json index effe5835..ae18d0fc 100644 --- a/schemas/app/Party.json +++ b/schemas/app/Party.json @@ -1,10 +1,7 @@ { "name": "Party", "label": "Party", - "regional": 1, - "keywordFields": [ - "name" - ], + "isAbstract": true, "fields": [ { "fieldname": "name", @@ -18,33 +15,6 @@ "label": "Image", "fieldtype": "AttachImage" }, - { - "fieldname": "customer", - "label": "Is Customer", - "fieldtype": "Check" - }, - { - "fieldname": "gstin", - "label": "GSTIN No.", - "fieldtype": "Data" - }, - { - "fieldname": "gstType", - "label": "GST Registration", - "placeholder": "GST Registration", - "fieldtype": "Select", - "default": "Unregistered", - "options": [ - "Unregistered", - "Registered Regular", - "Consumer" - ] - }, - { - "fieldname": "supplier", - "label": "Is Supplier", - "fieldtype": "Check" - }, { "fieldname": "defaultAccount", "label": "Default Account", @@ -67,19 +37,13 @@ "fieldname": "email", "label": "Email", "fieldtype": "Data", - "placeholder": "john@doe.com", - "validate": { - "type": "email" - } + "placeholder": "john@doe.com" }, { "fieldname": "phone", "label": "Phone", "fieldtype": "Data", - "placeholder": "Phone", - "validate": { - "type": "phone" - } + "placeholder": "Phone" }, { "fieldname": "address", @@ -88,12 +52,6 @@ "target": "Address", "placeholder": "Click to create", "inline": true - }, - { - "fieldname": "addressDisplay", - "label": "Address Display", - "fieldtype": "Text", - "readOnly": true } ], "quickEditFields": [ @@ -101,9 +59,7 @@ "phone", "address", "defaultAccount", - "currency", - "gstType", - "gstin" + "currency" ], - "augmented": 1 -} \ No newline at end of file + "keywordFields": ["name"] +} diff --git a/schemas/app/Payment.json b/schemas/app/Payment.json index e2009c75..3e3344eb 100644 --- a/schemas/app/Payment.json +++ b/schemas/app/Payment.json @@ -4,7 +4,6 @@ "isSingle": false, "isChild": false, "isSubmittable": true, - "keywordFields": [], "settings": "PaymentSettings", "fields": [ { @@ -18,7 +17,7 @@ "fieldname": "party", "label": "Party", "fieldtype": "Link", - "target": "Party", + "target": ["Customer", "Supplier"], "required": true }, { @@ -39,13 +38,15 @@ "fieldtype": "Select", "placeholder": "Payment Type", "options": [ - "Receive", - "Pay" + { + "value": "Receive", + "label": "Receive" + }, + { + "value": "Pay", + "label": "Pay" + } ], - "map": { - "Receive": "Receive", - "Pay": "Pay" - }, "required": true }, { @@ -70,9 +71,18 @@ "placeholder": "Payment Method", "fieldtype": "Select", "options": [ - "Cash", - "Cheque", - "Transfer" + { + "value": "Cash", + "label": "Cash" + }, + { + "value": "Cheque", + "label": "Cheque" + }, + { + "value": "Transfer", + "label": "Transfer" + } ], "default": "Cash", "required": true @@ -110,15 +120,8 @@ "fieldname": "for", "label": "Payment Reference", "fieldtype": "Table", - "childtype": "PaymentFor", + "target": "PaymentFor", "required": false - }, - { - "fieldname": "cancelled", - "label": "Cancelled", - "fieldtype": "Check", - "default": 0, - "readOnly": true } ], "quickEditFields": [ @@ -135,85 +138,5 @@ "amount", "writeoff", "for" - ], - "layout": [ - { - "columns": [ - { - "fields": [ - "party", - "account" - ] - }, - { - "fields": [ - "date", - "paymentAccount" - ] - } - ] - }, - { - "columns": [ - { - "fields": [ - "paymentMethod" - ] - }, - { - "fields": [ - "paymentType" - ] - }, - { - "fields": [ - "referenceId" - ] - } - ] - }, - { - "columns": [ - { - "fields": [ - "referenceDate" - ] - }, - { - "fields": [ - "clearanceDate" - ] - } - ] - }, - { - "columns": [ - { - "fields": [ - "for" - ] - } - ] - }, - { - "columns": [ - { - "fields": [ - "amount", - "writeoff" - ] - } - ] - } - ], - "actions": [ - { - "label": "Ledger Entries" - } - ], - "links": [ - { - "label": "Ledger Entries" - } ] -} \ No newline at end of file +} diff --git a/schemas/app/PaymentFor.json b/schemas/app/PaymentFor.json index 8f475ca3..712ccf13 100644 --- a/schemas/app/PaymentFor.json +++ b/schemas/app/PaymentFor.json @@ -3,12 +3,6 @@ "label": "Payment For", "isSingle": false, "isChild": true, - "keywordFields": [], - "tableFields": [ - "referenceType", - "referenceName", - "amount" - ], "fields": [ { "fieldname": "referenceType", @@ -16,13 +10,15 @@ "placeholder": "Type", "fieldtype": "Select", "options": [ - "SalesInvoice", - "PurchaseInvoice" + { + "value": "SalesInvoice", + "label": "Sales Invoice" + }, + { + "value": "PurchaseInvoice", + "label": "Purchase Invoice" + } ], - "map": { - "SalesInvoice": "Invoice", - "PurchaseInvoice": "Bill" - }, "required": true }, { @@ -40,4 +36,4 @@ "required": true } ] -} \ No newline at end of file +} diff --git a/schemas/app/PaymentSettings.json b/schemas/app/PaymentSettings.json deleted file mode 100644 index 964ccef4..00000000 --- a/schemas/app/PaymentSettings.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "PaymentSettings", - "label": "Payment Settings", - "isSingle": true, - "isChild": false, - "keywordFields": [], - "fields": [] -} \ No newline at end of file diff --git a/schemas/app/PrintSettings.json b/schemas/app/PrintSettings.json index 32d3b02d..7c2570b3 100644 --- a/schemas/app/PrintSettings.json +++ b/schemas/app/PrintSettings.json @@ -17,10 +17,7 @@ "fieldname": "email", "label": "Email", "fieldtype": "Data", - "placeholder": "john@doe.com", - "validate": { - "type": "email" - } + "placeholder": "john@doe.com" }, { "fieldname": "displayLogo", @@ -31,18 +28,14 @@ "fieldname": "phone", "label": "Phone", "fieldtype": "Data", - "placeholder": "9888900000", - "validate": { - "type": "phone" - } + "placeholder": "9888900000" }, { "fieldname": "address", "label": "Address", "fieldtype": "Link", "target": "Address", - "placeholder": "Click to create", - "inline": true + "placeholder": "Click to create" }, { "fieldname": "template", @@ -50,9 +43,18 @@ "placeholder": "Template", "fieldtype": "Select", "options": [ - "Basic", - "Minimal", - "Business" + { + "value": "Basic", + "label": "Basic" + }, + { + "value": "Minimal", + "label": "Minimal" + }, + { + "value": "Business", + "label": "Business" + } ], "default": "Basic" }, @@ -61,7 +63,8 @@ "label": "Color", "placeholder": "Select Color", "fieldtype": "Color", - "colors": [ + "default": "#112B42", + "options": [ { "label": "Red", "value": "#f56565" @@ -110,10 +113,22 @@ "placeholder": "Font", "fieldtype": "Select", "options": [ - "Inter", - "Times New Roman", - "Arial", - "Courier" + { + "value": "Inter", + "label": "Inter" + }, + { + "value": "Times New Roman", + "label": "Times New Roman" + }, + { + "value": "Arial", + "label": "Arial" + }, + { + "value": "Courier", + "label": "Courier" + } ], "default": "Inter" } @@ -128,4 +143,4 @@ "phone", "address" ] -} \ No newline at end of file +} diff --git a/schemas/app/PurchaseInvoice.json b/schemas/app/PurchaseInvoice.json index 9aecaab6..73415488 100644 --- a/schemas/app/PurchaseInvoice.json +++ b/schemas/app/PurchaseInvoice.json @@ -1,25 +1,9 @@ { "name": "PurchaseInvoice", - "doctype": "DocType", - "label": "Bill", - "printTemplate": { - "name": "InvoiceTemplate", - "props": [ - "doc", - "printSettings" - ], - "computed": {}, - "__file": "models/doctype/SalesInvoice/InvoiceTemplate.vue", - "__hmrId": "3d0cd49d" - }, + "label": "Purchase Invoice", "isSingle": false, "isChild": false, "isSubmittable": true, - "keywordFields": [ - "name", - "supplier" - ], - "settings": "PurchaseInvoiceSettings", "showTitle": true, "fields": [ { @@ -52,10 +36,7 @@ "label": "Supplier Currency", "fieldtype": "Link", "target": "Currency", - "hidden": true, - "formulaDependsOn": [ - "supplier" - ] + "hidden": true }, { "fieldname": "exchangeRate", @@ -67,38 +48,42 @@ "fieldname": "items", "label": "Items", "fieldtype": "Table", - "childtype": "PurchaseInvoiceItem", + "target": "PurchaseInvoiceItem", "required": true }, { "fieldname": "netTotal", "label": "Net Total", "fieldtype": "Currency", + "computed": true, "readOnly": true }, { "fieldname": "baseNetTotal", "label": "Net Total (Company Currency)", "fieldtype": "Currency", + "computed": true, "readOnly": true }, { "fieldname": "taxes", "label": "Taxes", "fieldtype": "Table", - "childtype": "TaxSummary", + "target": "TaxSummary", "readOnly": true }, { "fieldname": "grandTotal", "label": "Grand Total", "fieldtype": "Currency", + "computed": true, "readOnly": true }, { "fieldname": "baseGrandTotal", "label": "Grand Total (Company Currency)", "fieldtype": "Currency", + "computed": true, "readOnly": true }, { @@ -112,13 +97,6 @@ "label": "Terms", "fieldtype": "Text" }, - { - "fieldname": "cancelled", - "label": "Cancelled", - "fieldtype": "Check", - "default": 0, - "readOnly": true - }, { "fieldname": "numberSeries", "label": "Number Series", @@ -128,15 +106,5 @@ "default": "PINV-" } ], - "actions": [ - { - "label": "Make Payment" - }, - { - "label": "Print" - }, - { - "label": "Ledger Entries" - } - ] + "keywordFields": ["name", "supplier", "numberSeries"] } \ No newline at end of file diff --git a/schemas/app/PurchaseInvoiceItem.json b/schemas/app/PurchaseInvoiceItem.json index 985013a1..c109ddbe 100644 --- a/schemas/app/PurchaseInvoiceItem.json +++ b/schemas/app/PurchaseInvoiceItem.json @@ -1,15 +1,8 @@ { "name": "PurchaseInvoiceItem", - "doctype": "DocType", + "label": "Purchase Invoice Item", "isChild": true, - "keywordFields": [], - "tableFields": [ - "item", - "tax", - "quantity", - "rate", - "amount" - ], + "tableFields": ["item", "tax", "quantity", "rate", "amount"], "fields": [ { "fieldname": "item", @@ -41,6 +34,7 @@ "fieldname": "baseRate", "label": "Rate (Company Currency)", "fieldtype": "Currency", + "computed": true, "readOnly": true }, { @@ -61,12 +55,14 @@ "fieldname": "amount", "label": "Amount", "fieldtype": "Currency", + "computed": true, "readOnly": true }, { "fieldname": "baseAmount", "label": "Amount (Company Currency)", "fieldtype": "Currency", + "computed": true, "readOnly": true } ] diff --git a/schemas/app/PurchaseInvoiceSettings.json b/schemas/app/PurchaseInvoiceSettings.json deleted file mode 100644 index 1b21c546..00000000 --- a/schemas/app/PurchaseInvoiceSettings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "PurchaseInvoiceSettings", - "label": "Bills Settings", - "doctype": "DocType", - "isSingle": true, - "isChild": false, - "keywordFields": [], - "fields": [] -} \ No newline at end of file diff --git a/schemas/app/SalesInvoice.json b/schemas/app/SalesInvoice.json index b1437a46..24f7d4f2 100644 --- a/schemas/app/SalesInvoice.json +++ b/schemas/app/SalesInvoice.json @@ -1,24 +1,10 @@ { "name": "SalesInvoice", - "label": "Invoice", - "doctype": "DocType", - "printTemplate": { - "name": "InvoiceTemplate", - "props": [ - "doc", - "printSettings" - ], - "computed": {}, - "__file": "models/doctype/SalesInvoice/InvoiceTemplate.vue", - "__hmrId": "3d0cd49d" - }, + "label": "Sales Invoice", "isSingle": false, "isChild": false, "isSubmittable": true, - "keywordFields": [ - "name", - "customer" - ], + "keywordFields": ["name", "customer"], "settings": "SalesInvoiceSettings", "fields": [ { @@ -44,17 +30,13 @@ "fieldname": "account", "label": "Account", "fieldtype": "Link", - "target": "Account", - "disableCreation": true + "target": "Account" }, { "fieldname": "currency", "label": "Customer Currency", "fieldtype": "Link", - "target": "Currency", - "formulaDependsOn": [ - "customer" - ] + "target": "Currency" }, { "fieldname": "exchangeRate", @@ -74,37 +56,42 @@ "fieldname": "netTotal", "label": "Net Total", "fieldtype": "Currency", + "computed": true, "readOnly": true }, { "fieldname": "baseNetTotal", "label": "Net Total (Company Currency)", "fieldtype": "Currency", + "computed": true, "readOnly": true }, { "fieldname": "taxes", "label": "Taxes", "fieldtype": "Table", - "childtype": "TaxSummary", + "target": "TaxSummary", "readOnly": true }, { "fieldname": "grandTotal", "label": "Grand Total", "fieldtype": "Currency", + "computed": true, "readOnly": true }, { "fieldname": "baseGrandTotal", "label": "Grand Total (Company Currency)", "fieldtype": "Currency", + "computed": true, "readOnly": true }, { "fieldname": "outstandingAmount", "label": "Outstanding Amount", "fieldtype": "Currency", + "computed": true, "readOnly": true }, { @@ -112,13 +99,6 @@ "label": "Notes", "fieldtype": "Text" }, - { - "fieldname": "cancelled", - "label": "Cancelled", - "fieldtype": "Check", - "default": 0, - "readOnly": true - }, { "fieldname": "numberSeries", "label": "Number Series", @@ -127,16 +107,5 @@ "required": true, "default": "SINV-" } - ], - "actions": [ - { - "label": "Make Payment" - }, - { - "label": "Print" - }, - { - "label": "Ledger Entries" - } ] } \ No newline at end of file diff --git a/schemas/app/SalesInvoiceItem.json b/schemas/app/SalesInvoiceItem.json index d6d05c50..2be59cd5 100644 --- a/schemas/app/SalesInvoiceItem.json +++ b/schemas/app/SalesInvoiceItem.json @@ -1,16 +1,7 @@ { "name": "SalesInvoiceItem", - "doctype": "DocType", + "label": "Sales Invoice Item", "isChild": true, - "regional": 1, - "keywordFields": [], - "tableFields": [ - "item", - "tax", - "quantity", - "rate", - "amount" - ], "fields": [ { "fieldname": "item", @@ -23,10 +14,7 @@ "fieldname": "description", "label": "Description", "fieldtype": "Text", - "hidden": true, - "formulaDependsOn": [ - "item" - ] + "hidden": true }, { "fieldname": "quantity", @@ -39,15 +27,13 @@ "fieldname": "rate", "label": "Rate", "fieldtype": "Currency", - "required": true, - "formulaDependsOn": [ - "item" - ] + "required": true }, { "fieldname": "baseRate", "label": "Rate (Company Currency)", "fieldtype": "Currency", + "computed": true, "readOnly": true }, { @@ -63,31 +49,22 @@ "fieldname": "tax", "label": "Tax", "fieldtype": "Link", - "target": "Tax", - "formulaDependsOn": [ - "item" - ] + "target": "Tax" }, { "fieldname": "amount", "label": "Amount", "fieldtype": "Currency", + "computed": true, "readOnly": true }, { "fieldname": "baseAmount", "label": "Amount (Company Currency)", "fieldtype": "Currency", + "computed": true, "readOnly": true - }, - { - "fieldname": "hsnCode", - "label": "HSN/SAC", - "fieldtype": "Int", - "formulaDependsOn": [ - "item" - ] } ], - "augmented": 1 + "tableFields": ["item", "tax", "quantity", "rate", "amount"] } \ No newline at end of file diff --git a/schemas/app/SalesInvoiceSettings.json b/schemas/app/SalesInvoiceSettings.json index 20ff6ce8..6779c82a 100644 --- a/schemas/app/SalesInvoiceSettings.json +++ b/schemas/app/SalesInvoiceSettings.json @@ -1,10 +1,8 @@ { "name": "SalesInvoiceSettings", - "label": "SalesInvoice Settings", - "doctype": "DocType", + "label": "Sales Invoice Settings", "isSingle": true, "isChild": false, - "keywordFields": [], "fields": [ { "fieldname": "template", @@ -12,9 +10,18 @@ "placeholder": "Template", "fieldtype": "Select", "options": [ - "Basic I", - "Basic II", - "Modern" + { + "value": "Basic I", + "label": "Basic I" + }, + { + "value": "Basic II", + "label": "Basic II" + }, + { + "value": "Modern", + "label": "Modern" + } ], "required": true, "default": "Basic I" @@ -25,10 +32,22 @@ "placeholder": "Font", "fieldtype": "Select", "options": [ - "Montserrat", - "Open Sans", - "Oxygen", - "Merriweather" + { + "value": "Montserrat", + "label": "Montserrat" + }, + { + "value": "Open Sans", + "label": "Open Sans" + }, + { + "value": "Oxygen", + "label": "Oxygen" + }, + { + "value": "Merriweather", + "label": "Merriweather" + } ], "required": true, "default": "Montserrat" @@ -42,4 +61,4 @@ "hidden": true } ] -} \ No newline at end of file +} diff --git a/schemas/app/SetupWizard.json b/schemas/app/SetupWizard.json index d70a6aae..a7caab4c 100644 --- a/schemas/app/SetupWizard.json +++ b/schemas/app/SetupWizard.json @@ -1,12 +1,9 @@ { "name": "SetupWizard", "label": "Setup Wizard", - "naming": "name", "isSingle": true, "isChild": false, "isSubmittable": false, - "settings": null, - "keywordFields": [], "fields": [ { "fieldname": "companyLogo", @@ -32,10 +29,7 @@ "label": "Email", "fieldtype": "Data", "placeholder": "john@doe.com", - "required": true, - "validate": { - "type": "email" - } + "required": true }, { "fieldname": "companyName", @@ -56,7 +50,6 @@ "label": "Fiscal Year Start Date", "placeholder": "Fiscal Year Start Date", "fieldtype": "Date", - "formulaDependsOn": ["country"], "required": true }, { @@ -64,7 +57,6 @@ "label": "Fiscal Year End Date", "placeholder": "Fiscal Year End Date", "fieldtype": "Date", - "formulaDependsOn": ["country"], "required": true }, { @@ -72,7 +64,6 @@ "label": "Currency", "fieldtype": "Data", "placeholder": "Currency", - "formulaDependsOn": ["country"], "required": true }, { @@ -85,8 +76,7 @@ "fieldname": "chartOfAccounts", "label": "Chart of Accounts", "fieldtype": "AutoComplete", - "placeholder": "Select CoA", - "formulaDependsOn": ["country"] + "placeholder": "Select CoA" } ], "quickEditFields": [ diff --git a/schemas/app/Supplier.json b/schemas/app/Supplier.json index d67f746e..7db93805 100644 --- a/schemas/app/Supplier.json +++ b/schemas/app/Supplier.json @@ -1,121 +1,5 @@ { "name": "Supplier", "label": "Supplier", - "basedOn": "Party", - "filters": { - "supplier": 1 - }, - "actions": [ - { - "label": "Create Bill" - }, - { - "label": "View Bills" - } - ], - "regional": 1, - "keywordFields": [ - "name" - ], - "fields": [ - { - "fieldname": "name", - "label": "Name", - "fieldtype": "Data", - "required": true, - "placeholder": "Full Name" - }, - { - "fieldname": "image", - "label": "Image", - "fieldtype": "AttachImage" - }, - { - "fieldname": "customer", - "label": "Is Customer", - "fieldtype": "Check" - }, - { - "fieldname": "gstin", - "label": "GSTIN No.", - "fieldtype": "Data" - }, - { - "fieldname": "gstType", - "label": "GST Registration", - "placeholder": "GST Registration", - "fieldtype": "Select", - "default": "Unregistered", - "options": [ - "Unregistered", - "Registered Regular", - "Consumer" - ] - }, - { - "fieldname": "supplier", - "label": "Is Supplier", - "fieldtype": "Check" - }, - { - "fieldname": "defaultAccount", - "label": "Default Account", - "fieldtype": "Link", - "target": "Account" - }, - { - "fieldname": "outstandingAmount", - "label": "Outstanding Amount", - "fieldtype": "Currency" - }, - { - "fieldname": "currency", - "label": "Currency", - "fieldtype": "Link", - "target": "Currency", - "placeholder": "INR" - }, - { - "fieldname": "email", - "label": "Email", - "fieldtype": "Data", - "placeholder": "john@doe.com", - "validate": { - "type": "email" - } - }, - { - "fieldname": "phone", - "label": "Phone", - "fieldtype": "Data", - "placeholder": "Phone", - "validate": { - "type": "phone" - } - }, - { - "fieldname": "address", - "label": "Address", - "fieldtype": "Link", - "target": "Address", - "placeholder": "Click to create", - "inline": true - }, - { - "fieldname": "addressDisplay", - "label": "Address Display", - "fieldtype": "Text", - "readOnly": true - } - ], - "quickEditFields": [ - "email", - "phone", - "address", - "defaultAccount", - "currency", - "gstType", - "gstin" - ], - "augmented": 1 + "extends": "Party" } \ No newline at end of file diff --git a/schemas/app/Tax.json b/schemas/app/Tax.json index 1fa77c95..8ceaa260 100644 --- a/schemas/app/Tax.json +++ b/schemas/app/Tax.json @@ -1,12 +1,8 @@ { "name": "Tax", "label": "Tax", - "doctype": "DocType", "isSingle": false, "isChild": false, - "keywordFields": [ - "name" - ], "fields": [ { "fieldname": "name", @@ -22,7 +18,6 @@ "required": true } ], - "quickEditFields": [ - "details" - ] + "keywordFields": ["name"], + "quickEditFields": ["details"] } \ No newline at end of file diff --git a/schemas/app/TaxDetail.json b/schemas/app/TaxDetail.json index d866d373..d98a1a1e 100644 --- a/schemas/app/TaxDetail.json +++ b/schemas/app/TaxDetail.json @@ -1,10 +1,8 @@ { "name": "TaxDetail", "label": "Tax Detail", - "doctype": "DocType", "isSingle": false, "isChild": true, - "keywordFields": [], "fields": [ { "fieldname": "account", @@ -21,8 +19,5 @@ "placeholder": "0%" } ], - "tableFields": [ - "account", - "rate" - ] + "tableFields": ["account", "rate"] } \ No newline at end of file diff --git a/schemas/app/TaxSummary.json b/schemas/app/TaxSummary.json index 67cc63af..71e23921 100644 --- a/schemas/app/TaxSummary.json +++ b/schemas/app/TaxSummary.json @@ -1,6 +1,6 @@ { "name": "TaxSummary", - "doctype": "DocType", + "label": "Tax Summary", "isChild": true, "fields": [ { diff --git a/schemas/core/PatchRun.json b/schemas/core/PatchRun.json index 89383032..a4617873 100644 --- a/schemas/core/PatchRun.json +++ b/schemas/core/PatchRun.json @@ -1,5 +1,6 @@ { "name": "PatchRun", + "label": "Patch Run", "fields": [ { "fieldname": "name", @@ -8,4 +9,4 @@ "required": true } ] -} \ No newline at end of file +} diff --git a/schemas/core/SingleValue.json b/schemas/core/SingleValue.json index 459081c2..c34d2570 100644 --- a/schemas/core/SingleValue.json +++ b/schemas/core/SingleValue.json @@ -1,9 +1,8 @@ { "name": "SingleValue", - "doctype": "DocType", + "label": "Single Value", "isSingle": false, "isChild": false, - "keywordFields": [], "fields": [ { "fieldname": "parent", @@ -23,5 +22,6 @@ "fieldtype": "Data", "required": true } - ] + ], + "keywordFields": [] } \ No newline at end of file diff --git a/schemas/core/SystemSettings.json b/schemas/core/SystemSettings.json index 38f6d939..730fad42 100644 --- a/schemas/core/SystemSettings.json +++ b/schemas/core/SystemSettings.json @@ -1,10 +1,8 @@ { "name": "SystemSettings", "label": "System Settings", - "doctype": "DocType", "isSingle": true, "isChild": false, - "keywordFields": [], "fields": [ { "fieldname": "dateFormat", @@ -82,5 +80,6 @@ "dateFormat", "displayPrecision", "hideGetStarted" - ] -} \ No newline at end of file + ], + "keywordFields": [] +} diff --git a/schemas/regional/in/AccountingSettings.json b/schemas/regional/in/AccountingSettings.json new file mode 100644 index 00000000..32391b55 --- /dev/null +++ b/schemas/regional/in/AccountingSettings.json @@ -0,0 +1,22 @@ +{ + "name": "AccountingSettings", + "fields": [ + { + "fieldname": "gstin", + "label": "GSTIN", + "fieldtype": "Data", + "placeholder": "27AAAAA0000A1Z5" + } + ], + "keywordFields": [], + "quickEditFields": [ + "fullname", + "email", + "companyName", + "country", + "currency", + "fiscalYearStart", + "fiscalYearEnd", + "gstin" + ] +} diff --git a/schemas/regional/in/Address.json b/schemas/regional/in/Address.json new file mode 100644 index 00000000..376ae771 --- /dev/null +++ b/schemas/regional/in/Address.json @@ -0,0 +1,20 @@ +{ + "name": "Address", + "fields": [ + { + "fieldname": "pos", + "label": "Place of Supply", + "fieldtype": "AutoComplete", + "placeholder": "Place of Supply" + } + ], + "quickEditFields": [ + "addressLine1", + "addressLine2", + "city", + "state", + "country", + "postalCode", + "pos" + ] +} diff --git a/schemas/regional/in/Party.json b/schemas/regional/in/Party.json new file mode 100644 index 00000000..82bd30f2 --- /dev/null +++ b/schemas/regional/in/Party.json @@ -0,0 +1,27 @@ +{ + "name": "Party", + "fields": [ + { + "fieldname": "gstin", + "label": "GSTIN No.", + "fieldtype": "Data" + }, + { + "fieldname": "gstType", + "label": "GST Registration", + "placeholder": "GST Registration", + "fieldtype": "Select", + "default": "Unregistered", + "options": ["Unregistered", "Registered Regular", "Consumer"] + } + ], + "quickEditFields": [ + "email", + "phone", + "address", + "defaultAccount", + "currency", + "gstType", + "gstin" + ] +} diff --git a/schemas/schemas.ts b/schemas/schemas.ts new file mode 100644 index 00000000..8eefb8c7 --- /dev/null +++ b/schemas/schemas.ts @@ -0,0 +1,77 @@ +import Account from './app/Account.json'; +import AccountingLedgerEntry from './app/AccountingLedgerEntry.json'; +import AccountingSettings from './app/AccountingSettings.json'; +import Address from './app/Address.json'; +import Color from './app/Color.json'; +import CompanySettings from './app/CompanySettings.json'; +import Currency from './app/Currency.json'; +import Customer from './app/Customer.json'; +import GetStarted from './app/GetStarted.json'; +import Item from './app/Item.json'; +import JournalEntry from './app/JournalEntry.json'; +import JournalEntryAccount from './app/JournalEntryAccount.json'; +import NumberSeries from './app/NumberSeries.json'; +import Party from './app/Party.json'; +import Payment from './app/Payment.json'; +import PaymentFor from './app/PaymentFor.json'; +import PrintSettings from './app/PrintSettings.json'; +import PurchaseInvoice from './app/PurchaseInvoice.json'; +import PurchaseInvoiceItem from './app/PurchaseInvoiceItem.json'; +import SalesInvoice from './app/SalesInvoice.json'; +import SalesInvoiceItem from './app/SalesInvoiceItem.json'; +import SalesInvoiceSettings from './app/SalesInvoiceSettings.json'; +import SetupWizard from './app/SetupWizard.json'; +import Supplier from './app/Supplier.json'; +import Tax from './app/Tax.json'; +import TaxDetail from './app/TaxDetail.json'; +import TaxSummary from './app/TaxSummary.json'; +import PatchRun from './core/PatchRun.json'; +import SingleValue from './core/SingleValue.json'; +import SystemSettings from './core/SystemSettings.json'; +import { Schema } from './types'; + +export const coreSchemas: Schema[] = [ + PatchRun as Schema, + SingleValue as Schema, + SystemSettings as Schema, +]; + +export const appSchemas: Schema[] = [ + SetupWizard as Schema, + GetStarted as Schema, + + Color as Schema, + Currency as Schema, + NumberSeries as Schema, + + PrintSettings as Schema, + CompanySettings as Schema, + + Account as Schema, + AccountingSettings as Schema, + AccountingLedgerEntry as Schema, + + Party as Schema, + Supplier as Schema, + Customer as Schema, + + Address as Schema, + Item as Schema, + + Payment as Schema, + PaymentFor as Schema, + + JournalEntry as Schema, + JournalEntryAccount as Schema, + + PurchaseInvoice as Schema, + PurchaseInvoiceItem as Schema, + + SalesInvoice as Schema, + SalesInvoiceItem as Schema, + SalesInvoiceSettings as Schema, + + Tax as Schema, + TaxDetail as Schema, + TaxSummary as Schema, +]; diff --git a/schemas/types.ts b/schemas/types.ts new file mode 100644 index 00000000..4ba11b1f --- /dev/null +++ b/schemas/types.ts @@ -0,0 +1,81 @@ +export enum FieldTypeEnum { + Data = 'Data', + Select = 'Select', + Link = 'Link', + Date = 'Date', + Table = 'Table', + AutoComplete = 'AutoComplete', + Check = 'Check', + AttachImage = 'AttachImage', + DynamicLink = 'DynamicLink', + Int = 'Int', + Float = 'Float', + Currency = 'Currency', + Text = 'Text', + Color = 'Color', + Code = 'Code', +} + +export type FieldType = keyof typeof FieldTypeEnum; +export type RawValue = string | number | boolean; + +export interface BaseField { + fieldname: string; + fieldtype: FieldType; + label: string; + hidden?: boolean; + required?: boolean; + readOnly?: boolean; + description?: string; + default?: RawValue; + placeholder?: string; + groupBy?: string; + computed?: boolean; +} + +export type SelectOption = { value: string; label: string }; +export interface OptionField extends BaseField { + fieldtype: + | FieldTypeEnum.Select + | FieldTypeEnum.AutoComplete + | FieldTypeEnum.Color; + options: SelectOption[]; +} + +export interface TargetField extends BaseField { + fieldtype: FieldTypeEnum.Table | FieldTypeEnum.Link; + target: string | string[]; +} + +export interface DynamicLinkField extends BaseField { + fieldtype: FieldTypeEnum.DynamicLink; + references: string; +} + +export interface NumberField extends BaseField { + fieldtype: FieldTypeEnum.Float | FieldTypeEnum.Int; + minvalue?: number; + maxvalue?: number; +} + +export type Field = + | BaseField + | OptionField + | TargetField + | DynamicLinkField + | NumberField; + +export type TreeSettings = { parentField: string }; +export interface Schema { + name: string; + label: string; + fields: Field[]; + isTree?: boolean; + extends?: string; + isChild?: boolean; + isSingle?: boolean; + isAbstract?: boolean; + isSubmittable?: boolean; + keywordFields?: string[]; + treeSettings?: TreeSettings; +} From ef6e8d44abb93752a7624940dfc8c1ab2d19718d Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Wed, 23 Mar 2022 20:16:19 +0530 Subject: [PATCH 013/163] incr: build schemas, add some notes --- schemas/core/SystemSettings.json | 2 +- schemas/helpers.ts | 19 +++ schemas/index.ts | 125 ++++++++++++++++++++ schemas/regional/in/AccountingSettings.json | 1 - schemas/regional/in/Party.json | 15 ++- schemas/regional/in/index.ts | 6 + schemas/regional/index.ts | 6 + schemas/schemas.ts | 8 +- schemas/types.ts | 99 ++++++++++++---- src/main.js | 3 + 10 files changed, 251 insertions(+), 33 deletions(-) create mode 100644 schemas/helpers.ts create mode 100644 schemas/index.ts create mode 100644 schemas/regional/in/index.ts create mode 100644 schemas/regional/index.ts diff --git a/schemas/core/SystemSettings.json b/schemas/core/SystemSettings.json index 730fad42..de638d9d 100644 --- a/schemas/core/SystemSettings.json +++ b/schemas/core/SystemSettings.json @@ -71,7 +71,7 @@ "fieldname": "hideGetStarted", "label": "Hide Get Started", "fieldtype": "Check", - "default": 0, + "default": false, "description": "Hides the Get Started section from the sidebar. Change will be visible on restart or refreshing the app." } ], diff --git a/schemas/helpers.ts b/schemas/helpers.ts new file mode 100644 index 00000000..65e7a570 --- /dev/null +++ b/schemas/helpers.ts @@ -0,0 +1,19 @@ +export function getMapFromList( + list: T[], + name: string = 'name' +): Record { + const acc: Record = {}; + for (const t of list) { + const key = t[name] as string | undefined; + if (key === undefined) { + continue; + } + + acc[key] = t; + } + return acc; +} + +export function getListFromMap(map: Record): T[] { + return Object.keys(map).map((n) => map[n]); +} diff --git a/schemas/index.ts b/schemas/index.ts new file mode 100644 index 00000000..17291fe9 --- /dev/null +++ b/schemas/index.ts @@ -0,0 +1,125 @@ +import { cloneDeep } from 'lodash'; +import { getListFromMap, getMapFromList } from './helpers'; +import regional from './regional'; +import { appSchemas, coreSchemas } from './schemas'; +import { Schema, SchemaMap, SchemaStub, SchemaStubMap } from './types'; + +export function getSchemas(countryCode: string = '-'): SchemaMap { + const builtCoreSchemas = getCoreSchemas(); + const builtAppSchemas = getAppSchemas(countryCode); + + return Object.assign({}, builtAppSchemas, builtCoreSchemas); +} + +function getCoreSchemas(): SchemaMap { + const rawSchemaMap = getMapFromList(coreSchemas); + const coreSchemaMap = getAbstractCombinedSchemas(rawSchemaMap); + return cleanSchemas(coreSchemaMap); +} + +function getAppSchemas(countryCode: string): SchemaMap { + const combinedSchemas = getRegionalCombinedSchemas(countryCode); + const schemaMap = getAbstractCombinedSchemas(combinedSchemas); + return cleanSchemas(schemaMap); +} + +function cleanSchemas(schemaMap: SchemaMap): SchemaMap { + for (const name in schemaMap) { + const schema = schemaMap[name]; + if (schema.isAbstract && !schema.extends) { + delete schemaMap[name]; + continue; + } + + delete schema.extends; + delete schema.isAbstract; + } + + return schemaMap; +} + +function getCombined( + extendingSchema: SchemaStub, + abstractSchema: SchemaStub +): SchemaStub { + abstractSchema = cloneDeep(abstractSchema); + extendingSchema = cloneDeep(extendingSchema); + + const abstractFields = getMapFromList( + abstractSchema.fields ?? [], + 'fieldname' + ); + const extendingFields = getMapFromList( + extendingSchema.fields ?? [], + 'fieldname' + ); + + const combined = Object.assign(abstractSchema, extendingSchema); + + for (const fieldname in extendingFields) { + abstractFields[fieldname] = extendingFields[fieldname]; + } + + combined.fields = getListFromMap(abstractFields); + return combined; +} + +function getAbstractCombinedSchemas(schemas: SchemaStubMap): SchemaMap { + const abstractSchemaNames: string[] = Object.keys(schemas).filter( + (n) => schemas[n].isAbstract + ); + + const extendingSchemaNames: string[] = Object.keys(schemas).filter((n) => + abstractSchemaNames.includes(schemas[n].extends) + ); + + const completeSchemas: Schema[] = Object.keys(schemas) + .filter( + (n) => + !abstractSchemaNames.includes(n) && !extendingSchemaNames.includes(n) + ) + .map((n) => schemas[n] as Schema); + + const schemaMap = getMapFromList(completeSchemas) as SchemaMap; + + for (const name of extendingSchemaNames) { + const extendingSchema = schemas[name] as Schema; + const abstractSchema = schemas[extendingSchema.extends] as SchemaStub; + + schemaMap[name] = getCombined(extendingSchema, abstractSchema) as Schema; + } + + for (const name in abstractSchemaNames) { + delete schemaMap[name]; + } + + return schemaMap; +} + +function getRegionalCombinedSchemas(countryCode: string): SchemaStubMap { + const regionalSchemaMap = getRegionalSchema(countryCode); + const appSchemaMap = getMapFromList(appSchemas); + const combined = { ...appSchemaMap }; + + for (const name in regionalSchemaMap) { + const regionalSchema = regionalSchemaMap[name]; + + if (!combined.hasOwnProperty(name)) { + combined[name] = regionalSchema; + continue; + } + + combined[name] = getCombined(regionalSchema, combined[name]); + } + + return combined; +} + +function getRegionalSchema(countryCode: string): SchemaStubMap { + const regionalSchemas = regional[countryCode] as SchemaStub[] | undefined; + if (regionalSchemas === undefined) { + return {}; + } + + return getMapFromList(regionalSchemas); +} diff --git a/schemas/regional/in/AccountingSettings.json b/schemas/regional/in/AccountingSettings.json index 32391b55..66de67ac 100644 --- a/schemas/regional/in/AccountingSettings.json +++ b/schemas/regional/in/AccountingSettings.json @@ -8,7 +8,6 @@ "placeholder": "27AAAAA0000A1Z5" } ], - "keywordFields": [], "quickEditFields": [ "fullname", "email", diff --git a/schemas/regional/in/Party.json b/schemas/regional/in/Party.json index 82bd30f2..7076ab27 100644 --- a/schemas/regional/in/Party.json +++ b/schemas/regional/in/Party.json @@ -12,7 +12,20 @@ "placeholder": "GST Registration", "fieldtype": "Select", "default": "Unregistered", - "options": ["Unregistered", "Registered Regular", "Consumer"] + "options": [ + { + "value": "Unregistered", + "label": "Unregistered" + }, + { + "value": "Registered Regular", + "label": "Registered Regular" + }, + { + "value": "Consumer", + "label": "Consumer" + } + ] } ], "quickEditFields": [ diff --git a/schemas/regional/in/index.ts b/schemas/regional/in/index.ts new file mode 100644 index 00000000..835ca90a --- /dev/null +++ b/schemas/regional/in/index.ts @@ -0,0 +1,6 @@ +import { SchemaStub } from '../../types'; +import AccountingSettings from './AccountingSettings.json'; +import Address from './Address.json'; +import Party from './Party.json'; + +export default [AccountingSettings, Address, Party] as SchemaStub[]; diff --git a/schemas/regional/index.ts b/schemas/regional/index.ts new file mode 100644 index 00000000..3616cca1 --- /dev/null +++ b/schemas/regional/index.ts @@ -0,0 +1,6 @@ +import IndianSchemas from './in'; + +/** + * Regional Schemas are exported by country code. + */ +export default { in: IndianSchemas }; diff --git a/schemas/schemas.ts b/schemas/schemas.ts index 8eefb8c7..171f1265 100644 --- a/schemas/schemas.ts +++ b/schemas/schemas.ts @@ -28,7 +28,7 @@ import TaxSummary from './app/TaxSummary.json'; import PatchRun from './core/PatchRun.json'; import SingleValue from './core/SingleValue.json'; import SystemSettings from './core/SystemSettings.json'; -import { Schema } from './types'; +import { Schema, SchemaStub } from './types'; export const coreSchemas: Schema[] = [ PatchRun as Schema, @@ -36,7 +36,7 @@ export const coreSchemas: Schema[] = [ SystemSettings as Schema, ]; -export const appSchemas: Schema[] = [ +export const appSchemas: Schema[] | SchemaStub[] = [ SetupWizard as Schema, GetStarted as Schema, @@ -52,8 +52,8 @@ export const appSchemas: Schema[] = [ AccountingLedgerEntry as Schema, Party as Schema, - Supplier as Schema, - Customer as Schema, + Supplier as SchemaStub, + Customer as SchemaStub, Address as Schema, Item as Schema, diff --git a/schemas/types.ts b/schemas/types.ts index 4ba11b1f..44095baa 100644 --- a/schemas/types.ts +++ b/schemas/types.ts @@ -1,3 +1,38 @@ +/** + * # Schema + * + * Main purpose of this is to describe the shape of the Models table in the + * database. But there is some irrelevant information in the schemas with + * respect to this goal. This is information is allowed as long as it is not + * dynamic, which is impossible anyways as the files are data (.json !.js) + * + * If any field has to have a dynamic value, it should be added to the controller + * file by the same name. + * + * + * There are a few types of schemas: + * - _Regional_: Schemas that are in the '../regional' subdirectories + * these can be of any of the below types. + * - _Abstract_: Schemas that are not used as they are but only after they are + * extended by Stub schemas. Indentified by the `isAbstract` field + * - _Subclass_: Schemas that have an "extends" field on them, the value of which + * points to an Abstract schema. + * - _Complete_: Schemas which are neither abstract nor stub. + * + * + * ## Final Schema + * + * This is the schema which is used by the database and app code. + * + * The order in which a schema is built is: + * 1. Build _Regional_ schemas by overriding the fields and other properties of the + * non regional variants. + * 2. Combine _Subclass_ schemas with _Abstract_ schemas to get complete schemas. + * + * Note: if a Regional schema is not present as a non regional variant it's used + * as it is. + */ + export enum FieldTypeEnum { Data = 'Data', Select = 'Select', @@ -19,18 +54,19 @@ export enum FieldTypeEnum { export type FieldType = keyof typeof FieldTypeEnum; export type RawValue = string | number | boolean; +// prettier-ignore export interface BaseField { - fieldname: string; - fieldtype: FieldType; - label: string; - hidden?: boolean; - required?: boolean; - readOnly?: boolean; - description?: string; - default?: RawValue; - placeholder?: string; - groupBy?: string; - computed?: boolean; + fieldname: string; // Column name in the db + fieldtype: FieldType; // UI Descriptive field types that map to column types + label: string; // Translateable UI facing name + required?: boolean; // Implies Not Null + hidden?: boolean; // UI Facing config, whether field is shown in a form + readOnly?: boolean; // UI Facing config, whether field is editable + description?: string; // UI Facing, translateable, used for inline documentation + default?: RawValue; // Default value of a field, should match the db type + placeholder?: string; // UI Facing config, form field placeholder + groupBy?: string; // UI Facing used in dropdowns fields + computed?: boolean; // Indicates whether a value is computed, implies readonly } export type SelectOption = { value: string; label: string }; @@ -42,20 +78,23 @@ export interface OptionField extends BaseField { options: SelectOption[]; } +// prettier-ignore export interface TargetField extends BaseField { fieldtype: FieldTypeEnum.Table | FieldTypeEnum.Link; - target: string | string[]; + target: string | string[]; // Name of the table or group of tables to fetch values } +// prettier-ignore export interface DynamicLinkField extends BaseField { fieldtype: FieldTypeEnum.DynamicLink; - references: string; + references: string; // Reference to an option field that links to schema } +// prettier-ignore export interface NumberField extends BaseField { fieldtype: FieldTypeEnum.Float | FieldTypeEnum.Int; - minvalue?: number; - maxvalue?: number; + minvalue?: number; // UI Facing used to restrict lower bound + maxvalue?: number; // UI Facing used to restrict upper bound } export type Field = @@ -66,16 +105,24 @@ export type Field = | NumberField; export type TreeSettings = { parentField: string }; + +// prettier-ignore export interface Schema { - name: string; - label: string; - fields: Field[]; - isTree?: boolean; - extends?: string; - isChild?: boolean; - isSingle?: boolean; - isAbstract?: boolean; - isSubmittable?: boolean; - keywordFields?: string[]; - treeSettings?: TreeSettings; + name: string; // Table PK + label: string; // Translateable UI facing name + fields: Field[]; // Maps to database columns + isTree?: boolean; // Used for nested set, eg for Chart of Accounts + extends?: string; // Value points to an Abstract schema. Indicates Subclass schema + isChild?: boolean; // Indicates a child table, i.e table with "parent" FK column + isSingle?: boolean; // Fields will be values in SingleValue, i.e. an Entity Attr. Value + isAbstract?: boolean; // Not entered into db, used to extend a Subclass schema + isSubmittable?: boolean; // For transactional types, values considered only after submit + keywordFields?: string[]; // Used for fields that are to be used for search. + treeSettings?: TreeSettings; // Used to determine root nodes } + +export interface SchemaStub extends Partial { + name: string; +} +export type SchemaMap = Record; +export type SchemaStubMap = Record; diff --git a/src/main.js b/src/main.js index 1ddf31b2..a0264169 100644 --- a/src/main.js +++ b/src/main.js @@ -1,6 +1,7 @@ import { ipcRenderer } from 'electron'; import frappe from 'frappe'; import { createApp } from 'vue'; +import { getSchemas } from '../schemas'; import App from './App'; import FeatherIcon from './components/FeatherIcon'; import config, { ConfigKeys } from './config'; @@ -101,3 +102,5 @@ import { setLanguageMap, stringifyCircular } from './utils'; handleError(true, error, {}, () => process.exit(1)); }); })(); + +window.gs = getSchemas; From 19278e92d60bacbbe6a810a857c16dcacb5aec77 Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Wed, 23 Mar 2022 22:10:36 +0530 Subject: [PATCH 014/163] incr: add meta fields --- schemas/app/AccountingLedgerEntry.json | 6 ++-- schemas/app/SalesInvoice.json | 2 +- schemas/app/Tax.json | 2 +- schemas/index.ts | 47 ++++++++++++++++++++++---- schemas/meta/base.json | 33 ++++++++++++++++++ schemas/meta/child.json | 29 ++++++++++++++++ schemas/meta/submittable.json | 19 +++++++++++ schemas/meta/tree.json | 19 +++++++++++ schemas/schemas.ts | 12 +++++++ schemas/types.ts | 9 ++--- 10 files changed, 163 insertions(+), 15 deletions(-) create mode 100644 schemas/meta/base.json create mode 100644 schemas/meta/child.json create mode 100644 schemas/meta/submittable.json create mode 100644 schemas/meta/tree.json diff --git a/schemas/app/AccountingLedgerEntry.json b/schemas/app/AccountingLedgerEntry.json index a8480b0c..bacffc0a 100644 --- a/schemas/app/AccountingLedgerEntry.json +++ b/schemas/app/AccountingLedgerEntry.json @@ -19,8 +19,8 @@ { "fieldname": "party", "label": "Party", - "fieldtype": "Link", - "target": "Party" + "fieldtype": "DynamicLink", + "target": ["Customer", "Supplier"] }, { "fieldname": "debit", @@ -66,4 +66,4 @@ "balance" ], "keywordFields": ["account", "party", "referenceName"] -} \ No newline at end of file +} diff --git a/schemas/app/SalesInvoice.json b/schemas/app/SalesInvoice.json index 24f7d4f2..76a74b62 100644 --- a/schemas/app/SalesInvoice.json +++ b/schemas/app/SalesInvoice.json @@ -49,7 +49,7 @@ "fieldname": "items", "label": "Items", "fieldtype": "Table", - "childtype": "SalesInvoiceItem", + "target": "SalesInvoiceItem", "required": true }, { diff --git a/schemas/app/Tax.json b/schemas/app/Tax.json index 8ceaa260..5f7d7b1e 100644 --- a/schemas/app/Tax.json +++ b/schemas/app/Tax.json @@ -14,7 +14,7 @@ "fieldname": "details", "label": "Details", "fieldtype": "Table", - "childtype": "TaxDetail", + "target": "TaxDetail", "required": true } ], diff --git a/schemas/index.ts b/schemas/index.ts index 17291fe9..a71611cc 100644 --- a/schemas/index.ts +++ b/schemas/index.ts @@ -1,14 +1,47 @@ import { cloneDeep } from 'lodash'; import { getListFromMap, getMapFromList } from './helpers'; -import regional from './regional'; -import { appSchemas, coreSchemas } from './schemas'; +import regionalSchemas from './regional'; +import { appSchemas, coreSchemas, metaSchemas } from './schemas'; import { Schema, SchemaMap, SchemaStub, SchemaStubMap } from './types'; export function getSchemas(countryCode: string = '-'): SchemaMap { const builtCoreSchemas = getCoreSchemas(); const builtAppSchemas = getAppSchemas(countryCode); - return Object.assign({}, builtAppSchemas, builtCoreSchemas); + let schemaMap = Object.assign({}, builtAppSchemas, builtCoreSchemas); + schemaMap = addMetaFields(schemaMap); + return schemaMap; +} + +function addMetaFields(schemaMap: SchemaMap): SchemaMap { + const metaSchemaMap = getMapFromList(metaSchemas); + + const base = metaSchemaMap.base; + const tree = getCombined(metaSchemaMap.tree, base); + const child = metaSchemaMap.child; + const submittable = getCombined(metaSchemaMap.submittable, base); + const submittableTree = getCombined(tree, metaSchemaMap.submittable); + + for (const name in schemaMap) { + const schema = schemaMap[name]; + if (schema.isSingle) { + continue; + } + + if (schema.isTree && schema.isSubmittable) { + schema.fields = [...schema.fields, ...submittableTree.fields]; + } else if (schema.isTree) { + schema.fields = [...schema.fields, ...tree.fields]; + } else if (schema.isSubmittable) { + schema.fields = [...schema.fields, ...submittable.fields]; + } else if (schema.isChild) { + schema.fields = [...schema.fields, ...child.fields]; + } else { + schema.fields = [...schema.fields, ...base.fields]; + } + } + + return schemaMap; } function getCoreSchemas(): SchemaMap { @@ -116,10 +149,12 @@ function getRegionalCombinedSchemas(countryCode: string): SchemaStubMap { } function getRegionalSchema(countryCode: string): SchemaStubMap { - const regionalSchemas = regional[countryCode] as SchemaStub[] | undefined; - if (regionalSchemas === undefined) { + const countrySchemas = regionalSchemas[countryCode] as + | SchemaStub[] + | undefined; + if (countrySchemas === undefined) { return {}; } - return getMapFromList(regionalSchemas); + return getMapFromList(countrySchemas); } diff --git a/schemas/meta/base.json b/schemas/meta/base.json new file mode 100644 index 00000000..c596594b --- /dev/null +++ b/schemas/meta/base.json @@ -0,0 +1,33 @@ +{ + "name": "base", + "fields": [ + { + "fieldname": "createdBy", + "label": "Created By", + "fieldtype": "Data", + "required": true, + "meta": true + }, + { + "fieldname": "modifiedBy", + "label": "Modified By", + "fieldtype": "Data", + "required": true, + "meta": true + }, + { + "fieldname": "created", + "label": "Created", + "fieldtype": "Datetime", + "required": true, + "meta": true + }, + { + "fieldname": "modified", + "label": "Modified", + "fieldtype": "Datetime", + "required": true, + "meta": true + } + ] +} diff --git a/schemas/meta/child.json b/schemas/meta/child.json new file mode 100644 index 00000000..89b522a8 --- /dev/null +++ b/schemas/meta/child.json @@ -0,0 +1,29 @@ +{ + "name": "child", + "fields": [ + { + "fieldname": "idx", + "fieldtype": "Int", + "required": true, + "meta": true + }, + { + "fieldname": "parent", + "fieldtype": "Data", + "required": true, + "meta": true + }, + { + "fieldname": "parentName", + "fieldtype": "Data", + "required": true, + "meta": true + }, + { + "fieldname": "parentFieldname", + "fieldtype": "Data", + "required": true, + "meta": true + } + ] +} diff --git a/schemas/meta/submittable.json b/schemas/meta/submittable.json new file mode 100644 index 00000000..dbe6e524 --- /dev/null +++ b/schemas/meta/submittable.json @@ -0,0 +1,19 @@ +{ + "name": "submittable", + "fields": [ + { + "fieldname": "submitted", + "label": "Submitted", + "fieldtype": "Check", + "required": true, + "meta": true + }, + { + "fieldname": "cancelled", + "label": "Cancelled", + "fieldtype": "Check", + "required": true, + "meta": true + } + ] +} diff --git a/schemas/meta/tree.json b/schemas/meta/tree.json new file mode 100644 index 00000000..bddd83e8 --- /dev/null +++ b/schemas/meta/tree.json @@ -0,0 +1,19 @@ +{ + "name": "tree", + "fields": [ + { + "fieldname": "lft", + "label": "Left Index", + "fieldtype": "Int", + "required": true, + "meta": true + }, + { + "fieldname": "rgt", + "label": "Right Index", + "fieldtype": "Int", + "required": true, + "meta": true + } + ] +} diff --git a/schemas/schemas.ts b/schemas/schemas.ts index 171f1265..189d012d 100644 --- a/schemas/schemas.ts +++ b/schemas/schemas.ts @@ -28,6 +28,11 @@ import TaxSummary from './app/TaxSummary.json'; import PatchRun from './core/PatchRun.json'; import SingleValue from './core/SingleValue.json'; import SystemSettings from './core/SystemSettings.json'; +import base from './meta/base.json'; +import submittable from './meta/submittable.json'; +//asdf +import child from './meta/child.json'; +import tree from './meta/tree.json'; import { Schema, SchemaStub } from './types'; export const coreSchemas: Schema[] = [ @@ -36,6 +41,13 @@ export const coreSchemas: Schema[] = [ SystemSettings as Schema, ]; +export const metaSchemas: SchemaStub[] = [ + base as SchemaStub, + child as SchemaStub, + submittable as SchemaStub, + tree as SchemaStub, +]; + export const appSchemas: Schema[] | SchemaStub[] = [ SetupWizard as Schema, GetStarted as Schema, diff --git a/schemas/types.ts b/schemas/types.ts index 44095baa..53f82371 100644 --- a/schemas/types.ts +++ b/schemas/types.ts @@ -54,7 +54,7 @@ export enum FieldTypeEnum { export type FieldType = keyof typeof FieldTypeEnum; export type RawValue = string | number | boolean; -// prettier-ignore +// @formatter:off export interface BaseField { fieldname: string; // Column name in the db fieldtype: FieldType; // UI Descriptive field types that map to column types @@ -67,6 +67,7 @@ export interface BaseField { placeholder?: string; // UI Facing config, form field placeholder groupBy?: string; // UI Facing used in dropdowns fields computed?: boolean; // Indicates whether a value is computed, implies readonly + meta?: boolean; // Field is a meta field, i.e. only for the db, not UI } export type SelectOption = { value: string; label: string }; @@ -78,19 +79,19 @@ export interface OptionField extends BaseField { options: SelectOption[]; } -// prettier-ignore +// @formatter:off export interface TargetField extends BaseField { fieldtype: FieldTypeEnum.Table | FieldTypeEnum.Link; target: string | string[]; // Name of the table or group of tables to fetch values } -// prettier-ignore +// @formatter:off export interface DynamicLinkField extends BaseField { fieldtype: FieldTypeEnum.DynamicLink; references: string; // Reference to an option field that links to schema } -// prettier-ignore +// @formatter:off export interface NumberField extends BaseField { fieldtype: FieldTypeEnum.Float | FieldTypeEnum.Int; minvalue?: number; // UI Facing used to restrict lower bound From 74d173f682efb499db87881cbfeee5ff3cb7d76e Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Thu, 24 Mar 2022 15:20:40 +0530 Subject: [PATCH 015/163] incr: refactor core.ts to use schema !meta --- backend/common.ts | 28 +- backend/database/core.ts | 1463 +++++++++++------------- backend/database/types.ts | 21 +- frappe/models/index.js | 2 - schemas/app/AccountingLedgerEntry.json | 14 +- schemas/app/Payment.json | 13 +- schemas/meta/child.json | 2 +- schemas/types.ts | 4 +- 8 files changed, 748 insertions(+), 799 deletions(-) diff --git a/backend/common.ts b/backend/common.ts index 03b50842..58078c41 100644 --- a/backend/common.ts +++ b/backend/common.ts @@ -1,21 +1,4 @@ -import Document from '../frappe/model/document'; -import Meta from '../frappe/model/meta'; -import { Model } from '../src/types/model'; - -export function getModels(): Record { - // TODO: complete this function - return {}; -} - -export function getMeta(doctype: string): Meta { - // TODO: compete this function - return new Meta(); -} - -export function getNewDoc(data: Record): Document { - // TODO: compete this function - return new Document(); -} +import { Field, FieldTypeEnum, SchemaMap } from '../schemas/types'; export const sqliteTypeMap = { AutoComplete: 'text', @@ -41,3 +24,12 @@ export const sqliteTypeMap = { }; export const validTypes = Object.keys(sqliteTypeMap); + +export function getFieldsByType( + schemaName: string, + schemaMap: SchemaMap, + type: FieldTypeEnum +): Field[] { + const fields = schemaMap[schemaName].fields ?? []; + return fields.filter((f) => f.fieldtype === type); +} diff --git a/backend/database/core.ts b/backend/database/core.ts index a9e6d4ac..d9d0f989 100644 --- a/backend/database/core.ts +++ b/backend/database/core.ts @@ -1,5 +1,4 @@ import { knex, Knex } from 'knex'; -import { pesa } from 'pesa'; import { getRandomString } from '../../frappe/utils'; import CacheManager from '../../frappe/utils/cacheManager'; import { @@ -7,30 +6,35 @@ import { DatabaseError, DuplicateEntryError, LinkValidationError, + NotFoundError, ValueError, } from '../../frappe/utils/errors'; -import Observable from '../../frappe/utils/observable'; -import { getMeta, getModels, getNewDoc, sqliteTypeMap } from '../common'; -import { GetQueryBuilderOptions, QueryFilter, RawData } from './types'; +import { + Field, + FieldTypeEnum, + RawValue, + SchemaMap, + TargetField, +} from '../../schemas/types'; +import { getFieldsByType, sqliteTypeMap } from '../common'; +import { + ColumnDiff, + FieldValueMap, + GetAllOptions, + GetQueryBuilderOptions, + QueryFilter, +} from './types'; -interface GetAllOptions { - doctype?: string; - fields?: string[]; - filters?: Record; - start?: number; - limit?: number; - groupBy?: string; - orderBy?: string; - order?: 'asc' | 'desc'; -} - -export default class Database extends Observable { +export default class Database { knex: Knex; cache: CacheManager; typeMap = sqliteTypeMap; + dbPath: string; + schemaMap: SchemaMap; + connectionParams: Knex.Config; - constructor(dbPath: string) { - super(); + constructor(dbPath: string, schemaMap: SchemaMap) { + this.schemaMap = schemaMap; this.cache = new CacheManager(); this.dbPath = dbPath; this.connectionParams = { @@ -57,756 +61,28 @@ export default class Database extends Observable { } close() { - // + this.knex.destroy(); } - /** - * TODO: Refactor this - * - * essentially it's not models that are required by the database - * but the schema, all the information that is relevant to building - * tables needs to given to this function. - */ - async migrate() { - const models = getModels(); - for (const doctype in models) { - // check if controller module - const meta = getMeta(doctype); - const baseDoctype = meta.getBaseDocType(); - - if (!meta.isSingle) { - if (await this.tableExists(baseDoctype)) { - await this.alterTable(baseDoctype); - } else { - await this.createTable(baseDoctype); - } - } - } - - await this.commit(); - await this.initializeSingles(); + async tableExists(schemaName: string) { + return await this.knex.schema.hasTable(schemaName); } - async initializeSingles() { - const models = getModels(); - const singleDoctypes = Object.keys(models) - .filter((n) => models[n].isSingle) - .map((n) => models[n].name); - - for (const doctype of singleDoctypes) { - if (await this.singleExists(doctype)) { - const singleValues = await this.getSingleFieldsToInsert(doctype); - singleValues.forEach(({ fieldname, value }) => { - const singleValue = getNewDoc({ - doctype: 'SingleValue', - parent: doctype, - fieldname, - value, - }); - singleValue.insert(); - }); - continue; - } - const meta = getMeta(doctype); - if (meta.fields.every((df) => df.default == null)) { - continue; - } - const defaultValues = meta.fields.reduce((doc, df) => { - if (df.default != null) { - doc[df.fieldname] = df.default; - } - return doc; - }, {}); - await this.updateSingle(doctype, defaultValues); - } - } - - async getSingleFieldsToInsert(doctype) { - const existingFields = ( - await this.knex('SingleValue') - .where({ parent: doctype }) - .select('fieldname') - ).map(({ fieldname }) => fieldname); - - return getMeta(doctype) - .fields.map(({ fieldname, default: value }) => ({ - fieldname, - value, - })) - .filter( - ({ fieldname, value }) => - !existingFields.includes(fieldname) && value !== undefined - ); - } - - async createTable(doctype, tableName = null) { - const fields = this.getValidFields(doctype); - return await this.runCreateTableQuery(tableName || doctype, fields); - } - - runCreateTableQuery(doctype, fields) { - return this.knex.schema.createTable(doctype, (table) => { - for (const field of fields) { - this.buildColumnForTable(table, field); - } - }); - } - - async alterTable(doctype) { - // get columns - const diff = await this.getColumnDiff(doctype); - const newForeignKeys = await this.getNewForeignKeys(doctype); - - return this.knex.schema - .table(doctype, (table) => { - if (diff.added.length) { - for (const field of diff.added) { - this.buildColumnForTable(table, field); - } - } - - if (diff.removed.length) { - this.removeColumns(doctype, diff.removed); - } - }) - .then(() => { - if (newForeignKeys.length) { - return this.addForeignKeys(doctype, newForeignKeys); - } - }); - } - - buildColumnForTable(table, field) { - const columnType = this.typeMap[field.fieldtype]; - if (!columnType) { - // In case columnType is "Table" - // childTable links are handled using the childTable's "parent" field - return; - } - - const column = table[columnType](field.fieldname); - - // primary key - if (field.fieldname === 'name') { - column.primary(); - } - - // default value - if (!!field.default && !(field.default instanceof Function)) { - column.defaultTo(field.default); - } - - // required - if ( - (!!field.required && !(field.required instanceof Function)) || - field.fieldtype === 'Currency' - ) { - column.notNullable(); - } - - // link - if (field.fieldtype === 'Link' && field.target) { - const meta = getMeta(field.target); - table - .foreign(field.fieldname) - .references('name') - .inTable(meta.getBaseDocType()) - .onUpdate('CASCADE') - .onDelete('RESTRICT'); - } - } - - async getColumnDiff(doctype) { - const tableColumns = await this.getTableColumns(doctype); - const validFields = this.getValidFields(doctype); - const diff = { added: [], removed: [] }; - - for (const field of validFields) { - if ( - !tableColumns.includes(field.fieldname) && - this.typeMap[field.fieldtype] - ) { - diff.added.push(field); - } - } - - const validFieldNames = validFields.map((field) => field.fieldname); - for (const column of tableColumns) { - if (!validFieldNames.includes(column)) { - diff.removed.push(column); - } - } - - return diff; - } - - async getNewForeignKeys(doctype) { - const foreignKeys = await this.getForeignKeys(doctype); - const newForeignKeys = []; - const meta = getMeta(doctype); - for (const field of meta.getValidFields({ withChildren: false })) { - if ( - field.fieldtype === 'Link' && - !foreignKeys.includes(field.fieldname) - ) { - newForeignKeys.push(field); - } - } - return newForeignKeys; - } - - async get(doctype, name = null, fields = '*') { - const meta = getMeta(doctype); - let doc; - if (meta.isSingle) { - doc = await this.getSingle(doctype); - doc.name = doctype; - } else { - if (!name) { - throw new ValueError('name is mandatory'); - } - doc = await this.getOne(doctype, name, fields); - } - if (!doc) { - return; - } - await this.loadChildren(doc, meta); - return doc; - } - - async loadChildren(doc, meta) { - // load children - const tableFields = meta.getTableFields(); - for (const field of tableFields) { - doc[field.fieldname] = await this.getAll({ - doctype: field.childtype, - fields: ['*'], - filters: { parent: doc.name }, - orderBy: 'idx', - order: 'asc', - }); - } - } - - async getSingle(doctype) { - const values = await this.getAll({ - doctype: 'SingleValue', - fields: ['fieldname', 'value'], - filters: { parent: doctype }, - orderBy: 'fieldname', - order: 'asc', - }); - const doc = {}; - for (const row of values) { - doc[row.fieldname] = row.value; - } - return doc; - } - - /** - * Get list of values from the singles table. - * @param {...string | Object} fieldnames list of fieldnames to get the values of - * @returns {Array} array of {parent, value, fieldname}. - * @example - * Database.getSingleValues('internalPrecision'); - * // returns [{ fieldname: 'internalPrecision', parent: 'SystemSettings', value: '12' }] - * @example - * Database.getSingleValues({fieldname:'internalPrecision', parent: 'SystemSettings'}); - * // returns [{ fieldname: 'internalPrecision', parent: 'SystemSettings', value: '12' }] - */ - async getSingleValues(...fieldnames) { - fieldnames = fieldnames.map((fieldname) => { - if (typeof fieldname === 'string') { - return { fieldname }; - } - return fieldname; - }); - - let builder = this.knex('SingleValue'); - builder = builder.where(fieldnames[0]); - - fieldnames.slice(1).forEach(({ fieldname, parent }) => { - if (typeof parent === 'undefined') { - builder = builder.orWhere({ fieldname }); - } else { - builder = builder.orWhere({ fieldname, parent }); - } - }); - - let values = []; - try { - values = await builder.select('fieldname', 'value', 'parent'); - } catch (error) { - if (error.message.includes('no such table')) { - return []; - } - throw error; - } - - return values.map((value) => { - const fields = getMeta(value.parent).fields; - return this.getDocFormattedDoc(fields, values); - }); - } - - async getOne(doctype, name, fields = '*') { - const meta = getMeta(doctype); - const baseDoctype = meta.getBaseDocType(); - - const doc = await this.knex - .select(fields) - .from(baseDoctype) - .where('name', name) - .first(); - - if (!doc) { - return doc; - } - - return this.getDocFormattedDoc(meta.fields, doc); - } - - getDocFormattedDoc(fields, doc) { - // format for usage, not going into the db - const docFields = Object.keys(doc); - const filteredFields = fields.filter(({ fieldname }) => - docFields.includes(fieldname) - ); - - const formattedValues = filteredFields.reduce((d, field) => { - const { fieldname } = field; - d[fieldname] = this.getDocFormattedValues(field, doc[fieldname]); - return d; - }, {}); - - return Object.assign(doc, formattedValues); - } - - getDocFormattedValues(field, value) { - // format for usage, not going into the db - try { - if (field.fieldtype === 'Currency') { - return pesa(value); - } - } catch (err) { - err.message += ` value: '${value}' of type: ${typeof value}, fieldname: '${ - field.fieldname - }', label: '${field.label}'`; - throw err; - } - return value; - } - - triggerChange(doctype, name) { - this.trigger(`change:${doctype}`, { name }, 500); - this.trigger(`change`, { doctype, name }, 500); - // also trigger change for basedOn doctype - const meta = getMeta(doctype); - if (meta.basedOn) { - this.triggerChange(meta.basedOn, name); - } - } - - async insert(doctype, doc) { - const meta = getMeta(doctype); - const baseDoctype = meta.getBaseDocType(); - doc = this.applyBaseDocTypeFilters(doctype, doc); - - // insert parent - if (meta.isSingle) { - await this.updateSingle(doctype, doc); - } else { - await this.insertOne(baseDoctype, doc); - } - - // insert children - await this.insertChildren(meta, doc, baseDoctype); - - this.triggerChange(doctype, doc.name); - - return doc; - } - - async insertChildren(meta, doc, doctype) { - const tableFields = meta.getTableFields(); - for (const field of tableFields) { - let idx = 0; - for (const child of doc[field.fieldname] || []) { - this.prepareChild(doctype, doc.name, child, field, idx); - await this.insertOne(field.childtype, child); - idx++; - } - } - } - - insertOne(doctype, doc) { - const fields = this.getValidFields(doctype); - - if (!doc.name) { - doc.name = getRandomString(); - } - - const formattedDoc = this.getFormattedDoc(fields, doc); - return this.knex(doctype).insert(formattedDoc); - } - - async update(doctype, doc) { - const meta = getMeta(doctype); - const baseDoctype = meta.getBaseDocType(); - doc = this.applyBaseDocTypeFilters(doctype, doc); - - // update parent - if (meta.isSingle) { - await this.updateSingle(doctype, doc); - } else { - await this.updateOne(baseDoctype, doc); - } - - // insert or update children - await this.updateChildren(meta, doc, baseDoctype); - - this.triggerChange(doctype, doc.name); - - return doc; - } - - async updateChildren(meta, doc, doctype) { - const tableFields = meta.getTableFields(); - for (const field of tableFields) { - const added = []; - for (const child of doc[field.fieldname] || []) { - this.prepareChild(doctype, doc.name, child, field, added.length); - if (await this.exists(field.childtype, child.name)) { - await this.updateOne(field.childtype, child); - } else { - await this.insertOne(field.childtype, child); - } - added.push(child.name); - } - await this.runDeleteOtherChildren(field, doc.name, added); - } - } - - updateOne(doctype, doc) { - const validFields = this.getValidFields(doctype); - const fieldsToUpdate = Object.keys(doc).filter((f) => f !== 'name'); - const fields = validFields.filter((df) => - fieldsToUpdate.includes(df.fieldname) - ); - const formattedDoc = this.getFormattedDoc(fields, doc); - - return this.knex(doctype) - .where('name', doc.name) - .update(formattedDoc) - .then(() => { - const cacheKey = `${doctype}:${doc.name}`; - if (this.cache.hexists(cacheKey)) { - for (const fieldname in formattedDoc) { - const value = formattedDoc[fieldname]; - this.cache.hset(cacheKey, fieldname, value); - } - } - }); - } - - runDeleteOtherChildren(field, parent, added) { - // delete other children - return this.knex(field.childtype) - .where('parent', parent) - .andWhere('name', 'not in', added) - .delete(); - } - - async updateSingle(doctype, doc) { - const meta = getMeta(doctype); - await this.deleteSingleValues(doctype); - for (const field of meta.getValidFields({ withChildren: false })) { - const value = doc[field.fieldname]; - if (value != null) { - const singleValue = getNewDoc({ - doctype: 'SingleValue', - parent: doctype, - fieldname: field.fieldname, - value: value, - }); - await singleValue.insert(); - } - } - } - - async rename(doctype, oldName, newName) { - const meta = getMeta(doctype); - const baseDoctype = meta.getBaseDocType(); - await this.knex(baseDoctype) - .update({ name: newName }) - .where('name', oldName) - .then(() => { - this.clearValueCache(doctype, oldName); - }); - await this.commit(); - - this.triggerChange(doctype, newName); - } - - prepareChild(parenttype, parent, child, field, idx) { - if (!child.name) { - child.name = getRandomString(); - } - child.parent = parent; - child.parenttype = parenttype; - child.parentfield = field.fieldname; - child.idx = idx; - } - - getValidFields(doctype) { - return getMeta(doctype).getValidFields({ withChildren: false }); - } - - getFormattedDoc(fields, doc) { - // format for storage, going into the db - const formattedDoc = {}; - fields.map((field) => { - const value = doc[field.fieldname]; - formattedDoc[field.fieldname] = this.getFormattedValue(field, value); - }); - return formattedDoc; - } - - getFormattedValue(field, value) { - // format for storage, going into the db - const type = typeof value; - if (field.fieldtype === 'Currency') { - let currency = value; - - if (type === 'number' || type === 'string') { - currency = pesa(value); - } - - const currencyValue = currency.store; - if (typeof currencyValue !== 'string') { - throw new Error( - `invalid currencyValue '${currencyValue}' of type '${typeof currencyValue}' on converting from '${value}' of type '${type}'` - ); - } - - return currencyValue; - } - - if (value instanceof Date) { - if (field.fieldtype === 'Date') { - // date - return value.toISOString().substr(0, 10); - } else { - // datetime - return value.toISOString(); - } - } else if (field.fieldtype === 'Link' && !value) { - // empty value must be null to satisfy - // foreign key constraint - return null; - } else { - return value; - } - } - - applyBaseDocTypeFilters(doctype, doc) { - const meta = getMeta(doctype); - if (meta.filters) { - for (const fieldname in meta.filters) { - const value = meta.filters[fieldname]; - if (typeof value !== 'object') { - doc[fieldname] = value; - } - } - } - return doc; - } - - async deleteMany(doctype, names) { - for (const name of names) { - await this.delete(doctype, name); - } - } - - async delete(doctype, name) { - const meta = getMeta(doctype); - const baseDoctype = meta.getBaseDocType(); - await this.deleteOne(baseDoctype, name); - - // delete children - const tableFields = getMeta(doctype).getTableFields(); - for (const field of tableFields) { - await this.deleteChildren(field.childtype, name); - } - - this.triggerChange(doctype, name); - } - - async deleteOne(doctype, name) { - return this.knex(doctype) - .where('name', name) - .delete() - .then(() => { - this.clearValueCache(doctype, name); - }); - } - - deleteChildren(parenttype, parent) { - return this.knex(parenttype).where('parent', parent).delete(); - } - - async exists(doctype, name) { - return (await this.getValue(doctype, name)) ? true : false; - } - - async getValue(doctype, filters, fieldname = 'name') { - const meta = getMeta(doctype); - const baseDoctype = meta.getBaseDocType(); - if (typeof filters === 'string') { - filters = { name: filters }; - } - if (meta.filters) { - Object.assign(filters, meta.filters); - } - - const row = await this.getAll({ - doctype: baseDoctype, - fields: [fieldname], - filters: filters, - start: 0, - limit: 1, - orderBy: 'name', - order: 'asc', - }); - return row.length ? row[0][fieldname] : null; - } - - async setValue(doctype, name, fieldname, value) { - return await this.setValues(doctype, name, { - [fieldname]: value, - }); - } - - async setValues( - doctype: string, - name: string, - fieldValuePair: Record - ) { - const doc = Object.assign({}, fieldValuePair, { name }); - return this.updateOne(doctype, doc); - } - - async getCachedValue(doctype: string, name: string, fieldname: string) { - let value = this.cache.hget(`${doctype}:${name}`, fieldname); - if (value == null) { - value = await this.getValue(doctype, name, fieldname); - } - return value; - } - - // TODO: Not in core - async getAll({ - doctype, - fields, - filters, - start, - limit, - groupBy, - orderBy = 'creation', - order = 'desc', - }: GetAllOptions = {}) { - const meta = getMeta(doctype); - const baseDoctype = meta.getBaseDocType(); - if (!fields) { - fields = meta.getKeywordFields(); - fields.push('name'); - } - if (typeof fields === 'string') { - fields = [fields]; - } - if (meta.filters) { - filters = Object.assign({}, filters, meta.filters); - } - - const docs = await this.getQueryBuilder(doctype, fields, filters, { - offset: start, - limit, - groupBy, - orderBy, - order, - }); - return docs.map((doc) => this.getDocFormattedDoc(meta.fields, doc)); - } - - clearValueCache(doctype: string, name: string) { - const cacheKey = `${doctype}:${name}`; - this.cache.hclear(cacheKey); - } - - async addForeignKeys(tableName: string, newForeignKeys: string[]) { - await this.sql('PRAGMA foreign_keys=OFF'); - await this.sql('BEGIN TRANSACTION'); - - const tempName = 'TEMP' + tableName; - - // create temp table - await this.createTable(tableName, tempName); - - try { - // copy from old to new table - await this.knex(tempName).insert(this.knex.select().from(tableName)); - } catch (err) { - await this.sql('ROLLBACK'); - await this.sql('PRAGMA foreign_keys=ON'); - - const rows = await this.knex.select().from(tableName); - await this.prestigeTheTable(tableName, rows); - return; - } - - // drop old table - await this.knex.schema.dropTable(tableName); - - // rename new table - await this.knex.schema.renameTable(tempName, tableName); - - await this.sql('COMMIT'); - await this.sql('PRAGMA foreign_keys=ON'); - } - - /** - * - * - * - * - * Everything below this is a leaf node function in this file - * and is safe, doesn't reuqire refactors - * - * - * - * - * - */ - - async tableExists(tableName: string) { - return await this.knex.schema.hasTable(tableName); - } - - async singleExists(singleDoctypeName: string) { + async singleExists(singleSchemaName: string) { const res = await this.knex('SingleValue') .count('parent as count') - .where('parent', singleDoctypeName) + .where('parent', singleSchemaName) .first(); return res.count > 0; } - async removeColumns(tableName: string, targetColumns: string[]) { + async removeColumns(schemaName: string, targetColumns: string[]) { // TODO: Implement this for sqlite } - async deleteSingleValues(singleDoctypeName: string) { + async deleteSingleValues(singleSchemaName: string) { return await this.knex('SingleValue') - .where('parent', singleDoctypeName) + .where('parent', singleSchemaName) .delete(); } @@ -826,6 +102,9 @@ export default class Database extends Observable { getError(err: Error) { let errorType = DatabaseError; + if (err.message.includes('SQLITE_ERROR: no such table')) { + errorType = NotFoundError; + } if (err.message.includes('FOREIGN KEY')) { errorType = LinkValidationError; } @@ -838,15 +117,15 @@ export default class Database extends Observable { return errorType; } - async prestigeTheTable(tableName: string, tableRows: RawData) { + async prestigeTheTable(schemaName: string, tableRows: FieldValueMap[]) { const max = 200; // Alter table hacx for sqlite in case of schema change. - const tempName = `__${tableName}`; + const tempName = `__${schemaName}`; await this.knex.schema.dropTableIfExists(tempName); await this.knex.raw('PRAGMA foreign_keys=OFF'); - await this.createTable(tableName, tempName); + await this.createTable(schemaName, tempName); if (tableRows.length > 200) { const fi = Math.floor(tableRows.length / max); @@ -861,30 +140,30 @@ export default class Database extends Observable { await this.knex.batchInsert(tempName, tableRows); } - await this.knex.schema.dropTable(tableName); - await this.knex.schema.renameTable(tempName, tableName); + await this.knex.schema.dropTable(schemaName); + await this.knex.schema.renameTable(tempName, schemaName); await this.knex.raw('PRAGMA foreign_keys=ON'); } - async getTableColumns(doctype: string) { - const info = await this.sql(`PRAGMA table_info(${doctype})`); + async getTableColumns(schemaName: string): Promise { + const info = await this.sql(`PRAGMA table_info(${schemaName})`); return info.map((d) => d.name); } - async getForeignKeys(doctype: string) { + async getForeignKeys(schemaName: string): Promise { const foreignKeyList = await this.sql( - `PRAGMA foreign_key_list(${doctype})` + `PRAGMA foreign_key_list(${schemaName})` ); return foreignKeyList.map((d) => d.from); } getQueryBuilder( - tableName: string, + schemaName: string, fields: string[], filters: QueryFilter, options: GetQueryBuilderOptions - ) { - const builder = this.knex.select(fields).from(tableName); + ): Knex.QueryBuilder { + const builder = this.knex.select(fields).from(schemaName); this.applyFiltersToBuilder(builder, filters); @@ -956,4 +235,656 @@ export default class Database extends Observable { } }); } + + async getColumnDiff(schemaName: string): Promise { + const tableColumns = await this.getTableColumns(schemaName); + const validFields = this.schemaMap[schemaName].fields; + const diff: ColumnDiff = { added: [], removed: [] }; + + for (const field of validFields) { + if ( + !tableColumns.includes(field.fieldname) && + this.typeMap[field.fieldtype] + ) { + diff.added.push(field); + } + } + + const validFieldNames = validFields.map((field) => field.fieldname); + for (const column of tableColumns) { + if (!validFieldNames.includes(column)) { + diff.removed.push(column); + } + } + + return diff; + } + + async getNewForeignKeys(schemaName): Promise { + const foreignKeys = await this.getForeignKeys(schemaName); + const newForeignKeys: Field[] = []; + const schema = this.schemaMap[schemaName]; + for (const field of schema.fields) { + if ( + field.fieldtype === 'Link' && + !foreignKeys.includes(field.fieldname) + ) { + newForeignKeys.push(field); + } + } + return newForeignKeys; + } + + buildColumnForTable(table: Knex.AlterTableBuilder, field: Field) { + if (field.fieldtype === FieldTypeEnum.Table) { + // In case columnType is "Table" + // childTable links are handled using the childTable's "parent" field + return; + } + + const columnType = this.typeMap[field.fieldtype]; + if (!columnType) { + return; + } + + const column = table[columnType](field.fieldname); + + // primary key + if (field.fieldname === 'name') { + column.primary(); + } + + // iefault value + if (field.default !== undefined) { + column.defaultTo(field.default); + } + + // required + if (field.required || field.fieldtype === 'Currency') { + column.notNullable(); + } + + // link + if (field.fieldtype === 'Link' && (field as TargetField).target) { + const targetschemaName = (field as TargetField).target as string; + const schema = this.schemaMap[targetschemaName]; + table + .foreign(field.fieldname) + .references('name') + .inTable(schema.name) + .onUpdate('CASCADE') + .onDelete('RESTRICT'); + } + } + + async alterTable(schemaName: string) { + // get columns + const diff: ColumnDiff = await this.getColumnDiff(schemaName); + const newForeignKeys: Field[] = await this.getNewForeignKeys(schemaName); + + return this.knex.schema + .table(schemaName, (table) => { + if (diff.added.length) { + for (const field of diff.added) { + this.buildColumnForTable(table, field); + } + } + + if (diff.removed.length) { + this.removeColumns(schemaName, diff.removed); + } + }) + .then(() => { + if (newForeignKeys.length) { + return this.addForeignKeys(schemaName, newForeignKeys); + } + }); + } + + async createTable(schemaName: string, tableName?: string) { + tableName ??= schemaName; + const fields = this.schemaMap[schemaName].fields; + return await this.runCreateTableQuery(tableName, fields); + } + + runCreateTableQuery(schemaName: string, fields: Field[]) { + return this.knex.schema.createTable(schemaName, (table) => { + for (const field of fields) { + this.buildColumnForTable(table, field); + } + }); + } + + async getNonExtantSingleValues(singleSchemaName: string) { + const existingFields = ( + await this.knex('SingleValue') + .where({ parent: singleSchemaName }) + .select('fieldname') + ).map(({ fieldname }) => fieldname); + + return this.schemaMap[singleSchemaName].fields + .map(({ fieldname, default: value }) => ({ + fieldname, + value: value as RawValue | undefined, + })) + .filter( + ({ fieldname, value }) => + !existingFields.includes(fieldname) && value !== undefined + ); + } + + async delete(schemaName: string, name: string) { + await this.deleteOne(schemaName, name); + + // delete children + const tableFields = getFieldsByType( + schemaName, + this.schemaMap, + FieldTypeEnum.Table + ) as TargetField[]; + + for (const field of tableFields) { + await this.deleteChildren(field.target, name); + } + } + + async deleteMany(schemaName: string, names: string[]) { + for (const name of names) { + await this.delete(schemaName, name); + } + } + + async deleteOne(schemaName: string, name: string) { + return this.knex(schemaName) + .where('name', name) + .delete() + .then(() => { + this.clearValueCache(schemaName, name); + }); + } + + deleteChildren(schemaName: string, parentName: string) { + return this.knex(schemaName).where('parent', parentName).delete(); + } + + runDeleteOtherChildren( + field: TargetField, + parentName: string, + added: string[] + ) { + // delete other children + return this.knex(field.target) + .where('parent', parentName) + .andWhere('name', 'not in', added) + .delete(); + } + + async rename(schemaName: string, oldName: string, newName: string) { + await this.knex(schemaName) + .update({ name: newName }) + .where('name', oldName) + .then(() => { + this.clearValueCache(schemaName, oldName); + }); + await this.commit(); + } + + prepareChild( + parentSchemaName: string, + parentName: string, + child: FieldValueMap, + field: Field, + idx: number + ) { + if (!child.name) { + child.name = getRandomString(); + } + child.parentName = parentName; + child.parentSchemaName = parentSchemaName; + child.parentFieldname = field.fieldname; + child.idx = idx; + } + + clearValueCache(schemaName: string, name: string) { + const cacheKey = `${schemaName}:${name}`; + this.cache.hclear(cacheKey); + } + + async addForeignKeys(schemaName: string, newForeignKeys: Field[]) { + await this.sql('PRAGMA foreign_keys=OFF'); + await this.sql('BEGIN TRANSACTION'); + + const tempName = 'TEMP' + schemaName; + + // create temp table + await this.createTable(schemaName, tempName); + + try { + // copy from old to new table + await this.knex(tempName).insert(this.knex.select().from(schemaName)); + } catch (err) { + await this.sql('ROLLBACK'); + await this.sql('PRAGMA foreign_keys=ON'); + + const rows = await this.knex.select().from(schemaName); + await this.prestigeTheTable(schemaName, rows); + return; + } + + // drop old table + await this.knex.schema.dropTable(schemaName); + + // rename new table + await this.knex.schema.renameTable(tempName, schemaName); + + await this.sql('COMMIT'); + await this.sql('PRAGMA foreign_keys=ON'); + } + + async getAll({ + schemaName, + fields, + filters, + start, + limit, + groupBy, + orderBy = 'creation', + order = 'desc', + }: GetAllOptions = {}): Promise { + const schema = this.schemaMap[schemaName]; + if (!fields) { + fields = ['name', ...(schema.keywordFields ?? [])]; + } + + if (typeof fields === 'string') { + fields = [fields]; + } + + return (await this.getQueryBuilder(schemaName, fields, filters, { + offset: start, + limit, + groupBy, + orderBy, + order, + })) as FieldValueMap[]; + } + + async get( + schemaName: string, + name: string = '', + fields: string | string[] = '*' + ) { + const schema = this.schemaMap[schemaName]; + let fieldValueMap: FieldValueMap; + if (schema.isSingle) { + fieldValueMap = await this.getSingle(schemaName); + fieldValueMap.name = schemaName; + } else { + if (!name) { + throw new ValueError('name is mandatory'); + } + fieldValueMap = await this.getOne(schemaName, name, fields); + } + if (!fieldValueMap) { + return; + } + await this.loadChildren(fieldValueMap, schemaName); + return fieldValueMap; + } + + async getOne( + schemaName: string, + name: string, + fields: string | string[] = '*' + ) { + const fieldValueMap: FieldValueMap = await this.knex + .select(fields) + .from(schemaName) + .where('name', name) + .first(); + return fieldValueMap; + } + + async getSingle(schemaName: string): Promise { + const values = await this.getAll({ + schemaName: 'SingleValue', + fields: ['fieldname', 'value'], + filters: { parent: schemaName }, + orderBy: 'fieldname', + order: 'asc', + }); + + const fieldValueMap: FieldValueMap = {}; + for (const row of values) { + fieldValueMap[row.fieldname as string] = row.value as RawValue; + } + + return fieldValueMap; + } + + async loadChildren(fieldValueMap: FieldValueMap, schemaName: string) { + // Sets children on a field + const tableFields = getFieldsByType( + schemaName, + this.schemaMap, + FieldTypeEnum.Table + ) as TargetField[]; + + for (const field of tableFields) { + fieldValueMap[field.fieldname] = await this.getAll({ + schemaName: field.target, + fields: ['*'], + filters: { parent: fieldValueMap.name as string }, + orderBy: 'idx', + order: 'asc', + }); + } + } + + async insert(schemaName: string, fieldValueMap: FieldValueMap) { + // insert parent + if (this.schemaMap[schemaName].isSingle) { + await this.updateSingleValues(schemaName, fieldValueMap); + } else { + await this.insertOne(schemaName, fieldValueMap); + } + + // insert children + await this.insertOrUpdateChildren(schemaName, fieldValueMap, false); + return fieldValueMap; + } + + insertOne(schemaName: string, fieldValueMap: FieldValueMap) { + const fields = this.schemaMap[schemaName].fields; + if (!fieldValueMap.name) { + fieldValueMap.name = getRandomString(); + } + + const validMap = {}; + for (const { fieldname, fieldtype } of fields) { + if (fieldtype === FieldTypeEnum.Table) { + continue; + } + + validMap[fieldname] = fieldValueMap[fieldname]; + } + + return this.knex(schemaName).insert(fieldValueMap); + } + + async updateSingleValues( + singleSchemaName: string, + fieldValueMap: FieldValueMap + ) { + const fields = this.schemaMap[singleSchemaName].fields; + await this.deleteSingleValues(singleSchemaName); + + for (const field of fields) { + const value = fieldValueMap[field.fieldname] as RawValue | undefined; + if (value === undefined) { + continue; + } + + await this.updateSingleValue(singleSchemaName, field.fieldname, value); + } + } + + async updateSingleValue( + singleSchemaName: string, + fieldname: string, + value: RawValue + ) { + return await this.knex('SingleValue') + .where({ + parent: singleSchemaName, + fieldname, + }) + .update({ value }); + } + + async migrate() { + for (const schemaName in this.schemaMap) { + const schema = this.schemaMap[schemaName]; + if (schema.isSingle) { + continue; + } + + if (await this.tableExists(schemaName)) { + await this.alterTable(schemaName); + } else { + await this.createTable(schemaName); + } + } + + await this.commit(); + await this.initializeSingles(); + } + + async initializeSingles() { + const singleSchemaNames = Object.keys(this.schemaMap).filter( + (n) => this.schemaMap[n].isSingle + ); + + for (const schemaName of singleSchemaNames) { + if (await this.singleExists(schemaName)) { + await this.updateNonExtantSingleValues(schemaName); + continue; + } + + const fields = this.schemaMap[schemaName].fields; + if (fields.every((f) => f.default === undefined)) { + continue; + } + + const defaultValues: FieldValueMap = fields.reduce((acc, f) => { + if (f.default !== undefined) { + acc[f.fieldname] = f.default; + } + + return acc; + }, {}); + + await this.updateSingleValues(schemaName, defaultValues); + } + } + + async updateNonExtantSingleValues(schemaName: string) { + const singleValues = await this.getNonExtantSingleValues(schemaName); + for (const sv of singleValues) { + await this.updateSingleValue(schemaName, sv.fieldname, sv.value); + } + } + + async updateOne(schemaName: string, fieldValueMap: FieldValueMap) { + const updateMap = { ...fieldValueMap }; + delete updateMap.name; + + return await this.knex(schemaName) + .where('name', fieldValueMap.name as string) + .update(updateMap) + .then(() => { + const cacheKey = `${schemaName}:${fieldValueMap.name}`; + if (!this.cache.hexists(cacheKey)) { + return; + } + + for (const fieldname in updateMap) { + const value = updateMap[fieldname]; + this.cache.hset(cacheKey, fieldname, value); + } + }); + } + + async update(schemaName: string, fieldValueMap: FieldValueMap) { + // update parent + if (this.schemaMap[schemaName].isSingle) { + await this.updateSingleValues(schemaName, fieldValueMap); + } else { + await this.updateOne(schemaName, fieldValueMap); + } + + // insert or update children + await this.insertOrUpdateChildren(schemaName, fieldValueMap, true); + } + + async insertOrUpdateChildren( + schemaName: string, + fieldValueMap: FieldValueMap, + isUpdate: boolean + ) { + const tableFields = getFieldsByType( + schemaName, + this.schemaMap, + FieldTypeEnum.Table + ) as TargetField[]; + + const parentName = fieldValueMap.name as string; + for (const field of tableFields) { + const added: string[] = []; + + const tableFieldValue = (fieldValueMap[field.fieldname] ?? + []) as FieldValueMap[]; + for (const child of tableFieldValue) { + this.prepareChild(schemaName, parentName, child, field, added.length); + + if ( + isUpdate && + (await this.exists(field.target, child.name as string)) + ) { + await this.updateOne(field.target, child); + } else { + await this.insertOne(field.target, child); + } + + added.push(child.name as string); + } + + if (isUpdate) { + await this.runDeleteOtherChildren(field, parentName, added); + } + } + } + + async exists(schemaName: string, name?: string): Promise { + const schema = this.schemaMap[schemaName]; + if (schema.isSingle) { + return this.singleExists(schemaName); + } + + let row = []; + try { + const qb = this.knex(schemaName); + if (name !== undefined) { + qb.where({ name }); + } + row = await qb.limit(1); + } catch (err) { + if (this.getError(err as Error) !== NotFoundError) { + throw err; + } + } + return row.length > 0; + } + + /** + * Get list of values from the singles table. + * @param {...string | Object} fieldnames list of fieldnames to get the values of + * @returns {Array} array of {parent, value, fieldname}. + * @example + * Database.getSingleValues('internalPrecision'); + * // returns [{ fieldname: 'internalPrecision', parent: 'SystemSettings', value: '12' }] + * @example + * Database.getSingleValues({fieldname:'internalPrecision', parent: 'SystemSettings'}); + * // returns [{ fieldname: 'internalPrecision', parent: 'SystemSettings', value: '12' }] + */ + async getSingleValues( + ...fieldnames: { fieldname: string; parent?: string }[] + ) { + fieldnames = fieldnames.map((fieldname) => { + if (typeof fieldname === 'string') { + return { fieldname }; + } + return fieldname; + }); + + let builder = this.knex('SingleValue'); + builder = builder.where(fieldnames[0]); + + fieldnames.slice(1).forEach(({ fieldname, parent }) => { + if (typeof parent === 'undefined') { + builder = builder.orWhere({ fieldname }); + } else { + builder = builder.orWhere({ fieldname, parent }); + } + }); + + let values: { fieldname: string; parent: string; value: RawValue }[] = []; + try { + values = await builder.select('fieldname', 'value', 'parent'); + } catch (err) { + if (this.getError(err as Error) === NotFoundError) { + return []; + } + + throw err; + } + + return values; + } + + async getValue( + schemaName: string, + filters: string | Record, + fieldname = 'name' + ): Promise { + if (typeof filters === 'string') { + filters = { name: filters }; + } + + const row = await this.getAll({ + schemaName, + fields: [fieldname], + filters: filters, + start: 0, + limit: 1, + orderBy: 'name', + order: 'asc', + }); + + if (row.length === 1) { + return row[0][fieldname] as RawValue; + } + + return undefined; + } + + async setValue( + schemaName: string, + name: string, + fieldname: string, + value: RawValue + ) { + return await this.setValues(schemaName, name, { + [fieldname]: value, + }); + } + + async setValues( + schemaName: string, + name: string, + fieldValueMap: FieldValueMap + ) { + return this.updateOne( + schemaName, + Object.assign({}, fieldValueMap, { name }) + ); + } + + async getCachedValue(schemaName: string, name: string, fieldname: string) { + let value = this.cache.hget(`${schemaName}:${name}`, fieldname); + if (value == null) { + value = await this.getValue(schemaName, name, fieldname); + } + return value; + } } diff --git a/backend/database/types.ts b/backend/database/types.ts index 39e62812..25f30fbc 100644 --- a/backend/database/types.ts +++ b/backend/database/types.ts @@ -1,5 +1,5 @@ -export type RawType = string | number | boolean; -export type RawData = Record[]; +import { Field, RawValue } from '../../schemas/types'; + export type QueryFilter = Record; export interface GetQueryBuilderOptions { @@ -9,3 +9,20 @@ export interface GetQueryBuilderOptions { orderBy: string; order: 'desc' | 'asc'; } + +export interface GetAllOptions { + schemaName?: string; + fields?: string[]; + filters?: Record; + start?: number; + limit?: number; + groupBy?: string; + orderBy?: string; + order?: 'asc' | 'desc'; +} + +export type ColumnDiff = { added: Field[]; removed: string[] }; +export type FieldValueMap = Record< + string, + RawValue | undefined | FieldValueMap[] +>; diff --git a/frappe/models/index.js b/frappe/models/index.js index 49fa70a0..b24843d0 100644 --- a/frappe/models/index.js +++ b/frappe/models/index.js @@ -1,12 +1,10 @@ import NumberSeries from './doctype/NumberSeries/NumberSeries.js'; import PatchRun from './doctype/PatchRun/PatchRun.js'; -import PrintFormat from './doctype/PrintFormat/PrintFormat.js'; import SingleValue from './doctype/SingleValue/SingleValue.js'; import SystemSettings from './doctype/SystemSettings/SystemSettings.js'; export default { NumberSeries, - PrintFormat, SingleValue, SystemSettings, PatchRun, diff --git a/schemas/app/AccountingLedgerEntry.json b/schemas/app/AccountingLedgerEntry.json index bacffc0a..ac540180 100644 --- a/schemas/app/AccountingLedgerEntry.json +++ b/schemas/app/AccountingLedgerEntry.json @@ -17,10 +17,16 @@ "required": true }, { - "fieldname": "party", - "label": "Party", - "fieldtype": "DynamicLink", - "target": ["Customer", "Supplier"] + "fieldname": "customer", + "label": "Customer", + "fieldtype": "Link", + "target": "Customer" + }, + { + "fieldname": "supplier", + "label": "Supplier", + "fieldtype": "Link", + "target": "Supplier" }, { "fieldname": "debit", diff --git a/schemas/app/Payment.json b/schemas/app/Payment.json index 3e3344eb..7d614385 100644 --- a/schemas/app/Payment.json +++ b/schemas/app/Payment.json @@ -14,11 +14,16 @@ "readOnly": true }, { - "fieldname": "party", - "label": "Party", + "fieldname": "customer", + "label": "Customer", "fieldtype": "Link", - "target": ["Customer", "Supplier"], - "required": true + "target": "Customer" + }, + { + "fieldname": "supplier", + "label": "Supplier", + "fieldtype": "Link", + "target": "Supplier" }, { "fieldname": "date", diff --git a/schemas/meta/child.json b/schemas/meta/child.json index 89b522a8..290ca5ef 100644 --- a/schemas/meta/child.json +++ b/schemas/meta/child.json @@ -14,7 +14,7 @@ "meta": true }, { - "fieldname": "parentName", + "fieldname": "parentSchemaName", "fieldtype": "Data", "required": true, "meta": true diff --git a/schemas/types.ts b/schemas/types.ts index 53f82371..1e531ac7 100644 --- a/schemas/types.ts +++ b/schemas/types.ts @@ -79,10 +79,10 @@ export interface OptionField extends BaseField { options: SelectOption[]; } -// @formatter:off +// prettier-ignore export interface TargetField extends BaseField { fieldtype: FieldTypeEnum.Table | FieldTypeEnum.Link; - target: string | string[]; // Name of the table or group of tables to fetch values + target: string; // Name of the table or group of tables to fetch values } // @formatter:off From e9fab0cf83058f4d09654f9922a363d1b66333c3 Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Thu, 24 Mar 2022 18:43:59 +0530 Subject: [PATCH 016/163] incr: add databaseManager, privitize methods --- backend/common.ts | 17 +- backend/database/core.ts | 680 ++++++------- backend/database/database.ts | 925 ------------------ backend/database/manager.ts | 79 ++ backend/database/runPatch.ts | 31 + backend/database/types.ts | 9 + backend/patches/index.ts | 6 + backend/patches/testPatch.ts | 10 + models/doctype/Account/AccountDocument.js | 7 +- .../doctype/Transaction/TransactionServer.js | 10 +- schemas/core/SystemSettings.json | 6 + src/initialization.js | 4 +- src/utils.js | 3 +- 13 files changed, 463 insertions(+), 1324 deletions(-) delete mode 100644 backend/database/database.ts create mode 100644 backend/database/manager.ts create mode 100644 backend/database/runPatch.ts create mode 100644 backend/patches/index.ts create mode 100644 backend/patches/testPatch.ts diff --git a/backend/common.ts b/backend/common.ts index 58078c41..a5832ebe 100644 --- a/backend/common.ts +++ b/backend/common.ts @@ -1,5 +1,3 @@ -import { Field, FieldTypeEnum, SchemaMap } from '../schemas/types'; - export const sqliteTypeMap = { AutoComplete: 'text', Currency: 'text', @@ -24,12 +22,11 @@ export const sqliteTypeMap = { }; export const validTypes = Object.keys(sqliteTypeMap); - -export function getFieldsByType( - schemaName: string, - schemaMap: SchemaMap, - type: FieldTypeEnum -): Field[] { - const fields = schemaMap[schemaName].fields ?? []; - return fields.filter((f) => f.fieldtype === type); +export function getDefaultMetaFieldValueMap() { + return { + createdBy: '__SYSTEM__', + modifiedBy: '__SYSTEM__', + created: new Date().toISOString(), + modified: new Date().toISOString(), + }; } diff --git a/backend/database/core.ts b/backend/database/core.ts index d9d0f989..063bb5e6 100644 --- a/backend/database/core.ts +++ b/backend/database/core.ts @@ -1,6 +1,5 @@ import { knex, Knex } from 'knex'; import { getRandomString } from '../../frappe/utils'; -import CacheManager from '../../frappe/utils/cacheManager'; import { CannotCommitError, DatabaseError, @@ -16,7 +15,7 @@ import { SchemaMap, TargetField, } from '../../schemas/types'; -import { getFieldsByType, sqliteTypeMap } from '../common'; +import { sqliteTypeMap } from '../common'; import { ColumnDiff, FieldValueMap, @@ -25,18 +24,15 @@ import { QueryFilter, } from './types'; -export default class Database { +export default class DatabaseCore { knex: Knex; - cache: CacheManager; typeMap = sqliteTypeMap; dbPath: string; - schemaMap: SchemaMap; + schemaMap: SchemaMap = {}; connectionParams: Knex.Config; - constructor(dbPath: string, schemaMap: SchemaMap) { - this.schemaMap = schemaMap; - this.cache = new CacheManager(); - this.dbPath = dbPath; + constructor(dbPath?: string) { + this.dbPath = dbPath ?? ':memory:'; this.connectionParams = { client: 'sqlite3', connection: { @@ -53,10 +49,14 @@ export default class Database { }; } + setSchemaMap(schemaMap: SchemaMap) { + this.schemaMap = schemaMap; + } + connect() { this.knex = knex(this.connectionParams); this.knex.on('query-error', (error) => { - error.type = this.getError(error); + error.type = this.#getError(error); }); } @@ -64,11 +64,228 @@ export default class Database { this.knex.destroy(); } - async tableExists(schemaName: string) { + async commit() { + try { + await this.raw('commit'); + } catch (err) { + if (err.type !== CannotCommitError) { + throw err; + } + } + } + + async raw(query: string, params: Knex.RawBinding[] = []) { + return await this.knex.raw(query, params); + } + + async migrate() { + for (const schemaName in this.schemaMap) { + const schema = this.schemaMap[schemaName]; + if (schema.isSingle) { + continue; + } + + if (await this.#tableExists(schemaName)) { + await this.#alterTable(schemaName); + } else { + await this.#createTable(schemaName); + } + } + + await this.commit(); + await this.#initializeSingles(); + } + + async exists(schemaName: string, name?: string): Promise { + const schema = this.schemaMap[schemaName]; + if (schema.isSingle) { + return this.#singleExists(schemaName); + } + + let row = []; + try { + const qb = this.knex(schemaName); + if (name !== undefined) { + qb.where({ name }); + } + row = await qb.limit(1); + } catch (err) { + if (this.#getError(err as Error) !== NotFoundError) { + throw err; + } + } + return row.length > 0; + } + + async insert(schemaName: string, fieldValueMap: FieldValueMap) { + // insert parent + if (this.schemaMap[schemaName].isSingle) { + await this.#updateSingleValues(schemaName, fieldValueMap); + } else { + await this.#insertOne(schemaName, fieldValueMap); + } + + // insert children + await this.#insertOrUpdateChildren(schemaName, fieldValueMap, false); + return fieldValueMap; + } + + async get( + schemaName: string, + name: string = '', + fields: string | string[] = '*' + ): Promise { + const isSingle = this.schemaMap[schemaName].isSingle; + if (!isSingle && !name) { + throw new ValueError('name is mandatory'); + } + + /** + * If schema is single return all the values + * of the single type schema, in this case field + * is ignored. + */ + let fieldValueMap: FieldValueMap = {}; + if (isSingle) { + fieldValueMap = await this.#getSingle(schemaName); + fieldValueMap.name = schemaName; + return fieldValueMap; + } + + if (fields !== '*' && typeof fields === 'string') { + fields = [fields]; + } + + /** + * Separate table fields and non table fields + */ + const allTableFields = this.#getTableFields(schemaName); + const allTableFieldNames = allTableFields.map((f) => f.fieldname); + + let tableFields: TargetField[] = []; + let nonTableFieldNames: string[] = []; + + if (Array.isArray(fields)) { + tableFields = tableFields.filter((f) => fields.includes(f.fieldname)); + nonTableFieldNames = fields.filter( + (f) => !allTableFieldNames.includes(f) + ); + } else if (fields === '*') { + tableFields = allTableFields; + } + + /** + * If schema is not single then return specific fields + * if child fields are selected, all child fields are returned. + */ + if (nonTableFieldNames.length) { + fieldValueMap = (await this.#getOne(schemaName, name, fields)) ?? {}; + } + + if (tableFields.length) { + await this.#loadChildren(fieldValueMap, tableFields); + } + return fieldValueMap; + } + + async getAll({ + schemaName, + fields, + filters, + start, + limit, + groupBy, + orderBy = 'creation', + order = 'desc', + }: GetAllOptions = {}): Promise { + const schema = this.schemaMap[schemaName]; + if (!fields) { + fields = ['name', ...(schema.keywordFields ?? [])]; + } + + if (typeof fields === 'string') { + fields = [fields]; + } + + return (await this.#getQueryBuilder(schemaName, fields, filters, { + offset: start, + limit, + groupBy, + orderBy, + order, + })) as FieldValueMap[]; + } + + async getSingleValues( + ...fieldnames: { fieldname: string; parent?: string }[] + ): Promise<{ fieldname: string; parent: string; value: RawValue }[]> { + fieldnames = fieldnames.map((fieldname) => { + if (typeof fieldname === 'string') { + return { fieldname }; + } + return fieldname; + }); + + let builder = this.knex('SingleValue'); + builder = builder.where(fieldnames[0]); + + fieldnames.slice(1).forEach(({ fieldname, parent }) => { + if (typeof parent === 'undefined') { + builder = builder.orWhere({ fieldname }); + } else { + builder = builder.orWhere({ fieldname, parent }); + } + }); + + let values: { fieldname: string; parent: string; value: RawValue }[] = []; + try { + values = await builder.select('fieldname', 'value', 'parent'); + } catch (err) { + if (this.#getError(err as Error) === NotFoundError) { + return []; + } + + throw err; + } + + return values; + } + + async rename(schemaName: string, oldName: string, newName: string) { + await this.knex(schemaName) + .update({ name: newName }) + .where('name', oldName); + await this.commit(); + } + + async update(schemaName: string, fieldValueMap: FieldValueMap) { + // update parent + if (this.schemaMap[schemaName].isSingle) { + await this.#updateSingleValues(schemaName, fieldValueMap); + } else { + await this.#updateOne(schemaName, fieldValueMap); + } + + // insert or update children + await this.#insertOrUpdateChildren(schemaName, fieldValueMap, true); + } + + async delete(schemaName: string, name: string) { + await this.#deleteOne(schemaName, name); + + // delete children + const tableFields = this.#getTableFields(schemaName); + + for (const field of tableFields) { + await this.#deleteChildren(field.target, name); + } + } + + async #tableExists(schemaName: string) { return await this.knex.schema.hasTable(schemaName); } - async singleExists(singleSchemaName: string) { + async #singleExists(singleSchemaName: string) { const res = await this.knex('SingleValue') .count('parent as count') .where('parent', singleSchemaName) @@ -76,31 +293,17 @@ export default class Database { return res.count > 0; } - async removeColumns(schemaName: string, targetColumns: string[]) { + async #removeColumns(schemaName: string, targetColumns: string[]) { // TODO: Implement this for sqlite } - async deleteSingleValues(singleSchemaName: string) { + async #deleteSingleValues(singleSchemaName: string) { return await this.knex('SingleValue') .where('parent', singleSchemaName) .delete(); } - async sql(query: string, params: Knex.RawBinding[] = []) { - return await this.knex.raw(query, params); - } - - async commit() { - try { - await this.sql('commit'); - } catch (e) { - if (e.type !== CannotCommitError) { - throw e; - } - } - } - - getError(err: Error) { + #getError(err: Error) { let errorType = DatabaseError; if (err.message.includes('SQLITE_ERROR: no such table')) { errorType = NotFoundError; @@ -125,7 +328,7 @@ export default class Database { await this.knex.schema.dropTableIfExists(tempName); await this.knex.raw('PRAGMA foreign_keys=OFF'); - await this.createTable(schemaName, tempName); + await this.#createTable(schemaName, tempName); if (tableRows.length > 200) { const fi = Math.floor(tableRows.length / max); @@ -145,19 +348,19 @@ export default class Database { await this.knex.raw('PRAGMA foreign_keys=ON'); } - async getTableColumns(schemaName: string): Promise { - const info = await this.sql(`PRAGMA table_info(${schemaName})`); + async #getTableColumns(schemaName: string): Promise { + const info = await this.raw(`PRAGMA table_info(${schemaName})`); return info.map((d) => d.name); } - async getForeignKeys(schemaName: string): Promise { - const foreignKeyList = await this.sql( + async #getForeignKeys(schemaName: string): Promise { + const foreignKeyList = await this.raw( `PRAGMA foreign_key_list(${schemaName})` ); return foreignKeyList.map((d) => d.from); } - getQueryBuilder( + #getQueryBuilder( schemaName: string, fields: string[], filters: QueryFilter, @@ -165,7 +368,7 @@ export default class Database { ): Knex.QueryBuilder { const builder = this.knex.select(fields).from(schemaName); - this.applyFiltersToBuilder(builder, filters); + this.#applyFiltersToBuilder(builder, filters); if (options.orderBy) { builder.orderBy(options.orderBy, options.order); @@ -186,7 +389,7 @@ export default class Database { return builder; } - applyFiltersToBuilder(builder: Knex.QueryBuilder, filters: QueryFilter) { + #applyFiltersToBuilder(builder: Knex.QueryBuilder, filters: QueryFilter) { // {"status": "Open"} => `status = "Open"` // {"status": "Open", "name": ["like", "apple%"]} @@ -236,8 +439,8 @@ export default class Database { }); } - async getColumnDiff(schemaName: string): Promise { - const tableColumns = await this.getTableColumns(schemaName); + async #getColumnDiff(schemaName: string): Promise { + const tableColumns = await this.#getTableColumns(schemaName); const validFields = this.schemaMap[schemaName].fields; const diff: ColumnDiff = { added: [], removed: [] }; @@ -260,8 +463,8 @@ export default class Database { return diff; } - async getNewForeignKeys(schemaName): Promise { - const foreignKeys = await this.getForeignKeys(schemaName); + async #getNewForeignKeys(schemaName): Promise { + const foreignKeys = await this.#getForeignKeys(schemaName); const newForeignKeys: Field[] = []; const schema = this.schemaMap[schemaName]; for (const field of schema.fields) { @@ -275,7 +478,7 @@ export default class Database { return newForeignKeys; } - buildColumnForTable(table: Knex.AlterTableBuilder, field: Field) { + #buildColumnForTable(table: Knex.AlterTableBuilder, field: Field) { if (field.fieldtype === FieldTypeEnum.Table) { // In case columnType is "Table" // childTable links are handled using the childTable's "parent" field @@ -317,45 +520,45 @@ export default class Database { } } - async alterTable(schemaName: string) { + async #alterTable(schemaName: string) { // get columns - const diff: ColumnDiff = await this.getColumnDiff(schemaName); - const newForeignKeys: Field[] = await this.getNewForeignKeys(schemaName); + const diff: ColumnDiff = await this.#getColumnDiff(schemaName); + const newForeignKeys: Field[] = await this.#getNewForeignKeys(schemaName); return this.knex.schema .table(schemaName, (table) => { if (diff.added.length) { for (const field of diff.added) { - this.buildColumnForTable(table, field); + this.#buildColumnForTable(table, field); } } if (diff.removed.length) { - this.removeColumns(schemaName, diff.removed); + this.#removeColumns(schemaName, diff.removed); } }) .then(() => { if (newForeignKeys.length) { - return this.addForeignKeys(schemaName, newForeignKeys); + return this.#addForeignKeys(schemaName, newForeignKeys); } }); } - async createTable(schemaName: string, tableName?: string) { + async #createTable(schemaName: string, tableName?: string) { tableName ??= schemaName; const fields = this.schemaMap[schemaName].fields; - return await this.runCreateTableQuery(tableName, fields); + return await this.#runCreateTableQuery(tableName, fields); } - runCreateTableQuery(schemaName: string, fields: Field[]) { + #runCreateTableQuery(schemaName: string, fields: Field[]) { return this.knex.schema.createTable(schemaName, (table) => { for (const field of fields) { - this.buildColumnForTable(table, field); + this.#buildColumnForTable(table, field); } }); } - async getNonExtantSingleValues(singleSchemaName: string) { + async #getNonExtantSingleValues(singleSchemaName: string) { const existingFields = ( await this.knex('SingleValue') .where({ parent: singleSchemaName }) @@ -373,41 +576,15 @@ export default class Database { ); } - async delete(schemaName: string, name: string) { - await this.deleteOne(schemaName, name); - - // delete children - const tableFields = getFieldsByType( - schemaName, - this.schemaMap, - FieldTypeEnum.Table - ) as TargetField[]; - - for (const field of tableFields) { - await this.deleteChildren(field.target, name); - } + async #deleteOne(schemaName: string, name: string) { + return this.knex(schemaName).where('name', name).delete(); } - async deleteMany(schemaName: string, names: string[]) { - for (const name of names) { - await this.delete(schemaName, name); - } - } - - async deleteOne(schemaName: string, name: string) { - return this.knex(schemaName) - .where('name', name) - .delete() - .then(() => { - this.clearValueCache(schemaName, name); - }); - } - - deleteChildren(schemaName: string, parentName: string) { + #deleteChildren(schemaName: string, parentName: string) { return this.knex(schemaName).where('parent', parentName).delete(); } - runDeleteOtherChildren( + #runDeleteOtherChildren( field: TargetField, parentName: string, added: string[] @@ -419,17 +596,7 @@ export default class Database { .delete(); } - async rename(schemaName: string, oldName: string, newName: string) { - await this.knex(schemaName) - .update({ name: newName }) - .where('name', oldName) - .then(() => { - this.clearValueCache(schemaName, oldName); - }); - await this.commit(); - } - - prepareChild( + #prepareChild( parentSchemaName: string, parentName: string, child: FieldValueMap, @@ -445,26 +612,21 @@ export default class Database { child.idx = idx; } - clearValueCache(schemaName: string, name: string) { - const cacheKey = `${schemaName}:${name}`; - this.cache.hclear(cacheKey); - } - - async addForeignKeys(schemaName: string, newForeignKeys: Field[]) { - await this.sql('PRAGMA foreign_keys=OFF'); - await this.sql('BEGIN TRANSACTION'); + async #addForeignKeys(schemaName: string, newForeignKeys: Field[]) { + await this.raw('PRAGMA foreign_keys=OFF'); + await this.raw('BEGIN TRANSACTION'); const tempName = 'TEMP' + schemaName; // create temp table - await this.createTable(schemaName, tempName); + await this.#createTable(schemaName, tempName); try { // copy from old to new table await this.knex(tempName).insert(this.knex.select().from(schemaName)); } catch (err) { - await this.sql('ROLLBACK'); - await this.sql('PRAGMA foreign_keys=ON'); + await this.raw('ROLLBACK'); + await this.raw('PRAGMA foreign_keys=ON'); const rows = await this.knex.select().from(schemaName); await this.prestigeTheTable(schemaName, rows); @@ -477,62 +639,26 @@ export default class Database { // rename new table await this.knex.schema.renameTable(tempName, schemaName); - await this.sql('COMMIT'); - await this.sql('PRAGMA foreign_keys=ON'); + await this.raw('COMMIT'); + await this.raw('PRAGMA foreign_keys=ON'); } - async getAll({ - schemaName, - fields, - filters, - start, - limit, - groupBy, - orderBy = 'creation', - order = 'desc', - }: GetAllOptions = {}): Promise { - const schema = this.schemaMap[schemaName]; - if (!fields) { - fields = ['name', ...(schema.keywordFields ?? [])]; - } - - if (typeof fields === 'string') { - fields = [fields]; - } - - return (await this.getQueryBuilder(schemaName, fields, filters, { - offset: start, - limit, - groupBy, - orderBy, - order, - })) as FieldValueMap[]; - } - - async get( - schemaName: string, - name: string = '', - fields: string | string[] = '*' + async #loadChildren( + fieldValueMap: FieldValueMap, + tableFields: TargetField[] ) { - const schema = this.schemaMap[schemaName]; - let fieldValueMap: FieldValueMap; - if (schema.isSingle) { - fieldValueMap = await this.getSingle(schemaName); - fieldValueMap.name = schemaName; - } else { - if (!name) { - throw new ValueError('name is mandatory'); - } - fieldValueMap = await this.getOne(schemaName, name, fields); + for (const field of tableFields) { + fieldValueMap[field.fieldname] = await this.getAll({ + schemaName: field.target, + fields: ['*'], + filters: { parent: fieldValueMap.name as string }, + orderBy: 'idx', + order: 'asc', + }); } - if (!fieldValueMap) { - return; - } - await this.loadChildren(fieldValueMap, schemaName); - return fieldValueMap; } - async getOne( + async #getOne( schemaName: string, name: string, fields: string | string[] = '*' @@ -545,7 +671,7 @@ export default class Database { return fieldValueMap; } - async getSingle(schemaName: string): Promise { + async #getSingle(schemaName: string): Promise { const values = await this.getAll({ schemaName: 'SingleValue', fields: ['fieldname', 'value'], @@ -562,39 +688,7 @@ export default class Database { return fieldValueMap; } - async loadChildren(fieldValueMap: FieldValueMap, schemaName: string) { - // Sets children on a field - const tableFields = getFieldsByType( - schemaName, - this.schemaMap, - FieldTypeEnum.Table - ) as TargetField[]; - - for (const field of tableFields) { - fieldValueMap[field.fieldname] = await this.getAll({ - schemaName: field.target, - fields: ['*'], - filters: { parent: fieldValueMap.name as string }, - orderBy: 'idx', - order: 'asc', - }); - } - } - - async insert(schemaName: string, fieldValueMap: FieldValueMap) { - // insert parent - if (this.schemaMap[schemaName].isSingle) { - await this.updateSingleValues(schemaName, fieldValueMap); - } else { - await this.insertOne(schemaName, fieldValueMap); - } - - // insert children - await this.insertOrUpdateChildren(schemaName, fieldValueMap, false); - return fieldValueMap; - } - - insertOne(schemaName: string, fieldValueMap: FieldValueMap) { + #insertOne(schemaName: string, fieldValueMap: FieldValueMap) { const fields = this.schemaMap[schemaName].fields; if (!fieldValueMap.name) { fieldValueMap.name = getRandomString(); @@ -612,12 +706,12 @@ export default class Database { return this.knex(schemaName).insert(fieldValueMap); } - async updateSingleValues( + async #updateSingleValues( singleSchemaName: string, fieldValueMap: FieldValueMap ) { const fields = this.schemaMap[singleSchemaName].fields; - await this.deleteSingleValues(singleSchemaName); + await this.#deleteSingleValues(singleSchemaName); for (const field of fields) { const value = fieldValueMap[field.fieldname] as RawValue | undefined; @@ -625,11 +719,11 @@ export default class Database { continue; } - await this.updateSingleValue(singleSchemaName, field.fieldname, value); + await this.#updateSingleValue(singleSchemaName, field.fieldname, value); } } - async updateSingleValue( + async #updateSingleValue( singleSchemaName: string, fieldname: string, value: RawValue @@ -642,32 +736,14 @@ export default class Database { .update({ value }); } - async migrate() { - for (const schemaName in this.schemaMap) { - const schema = this.schemaMap[schemaName]; - if (schema.isSingle) { - continue; - } - - if (await this.tableExists(schemaName)) { - await this.alterTable(schemaName); - } else { - await this.createTable(schemaName); - } - } - - await this.commit(); - await this.initializeSingles(); - } - - async initializeSingles() { + async #initializeSingles() { const singleSchemaNames = Object.keys(this.schemaMap).filter( (n) => this.schemaMap[n].isSingle ); for (const schemaName of singleSchemaNames) { - if (await this.singleExists(schemaName)) { - await this.updateNonExtantSingleValues(schemaName); + if (await this.#singleExists(schemaName)) { + await this.#updateNonExtantSingleValues(schemaName); continue; } @@ -684,59 +760,32 @@ export default class Database { return acc; }, {}); - await this.updateSingleValues(schemaName, defaultValues); + await this.#updateSingleValues(schemaName, defaultValues); } } - async updateNonExtantSingleValues(schemaName: string) { - const singleValues = await this.getNonExtantSingleValues(schemaName); + async #updateNonExtantSingleValues(schemaName: string) { + const singleValues = await this.#getNonExtantSingleValues(schemaName); for (const sv of singleValues) { - await this.updateSingleValue(schemaName, sv.fieldname, sv.value); + await this.#updateSingleValue(schemaName, sv.fieldname, sv.value); } } - async updateOne(schemaName: string, fieldValueMap: FieldValueMap) { + async #updateOne(schemaName: string, fieldValueMap: FieldValueMap) { const updateMap = { ...fieldValueMap }; delete updateMap.name; return await this.knex(schemaName) .where('name', fieldValueMap.name as string) - .update(updateMap) - .then(() => { - const cacheKey = `${schemaName}:${fieldValueMap.name}`; - if (!this.cache.hexists(cacheKey)) { - return; - } - - for (const fieldname in updateMap) { - const value = updateMap[fieldname]; - this.cache.hset(cacheKey, fieldname, value); - } - }); + .update(updateMap); } - async update(schemaName: string, fieldValueMap: FieldValueMap) { - // update parent - if (this.schemaMap[schemaName].isSingle) { - await this.updateSingleValues(schemaName, fieldValueMap); - } else { - await this.updateOne(schemaName, fieldValueMap); - } - - // insert or update children - await this.insertOrUpdateChildren(schemaName, fieldValueMap, true); - } - - async insertOrUpdateChildren( + async #insertOrUpdateChildren( schemaName: string, fieldValueMap: FieldValueMap, isUpdate: boolean ) { - const tableFields = getFieldsByType( - schemaName, - this.schemaMap, - FieldTypeEnum.Table - ) as TargetField[]; + const tableFields = this.#getTableFields(schemaName); const parentName = fieldValueMap.name as string; for (const field of tableFields) { @@ -745,146 +794,29 @@ export default class Database { const tableFieldValue = (fieldValueMap[field.fieldname] ?? []) as FieldValueMap[]; for (const child of tableFieldValue) { - this.prepareChild(schemaName, parentName, child, field, added.length); + this.#prepareChild(schemaName, parentName, child, field, added.length); if ( isUpdate && (await this.exists(field.target, child.name as string)) ) { - await this.updateOne(field.target, child); + await this.#updateOne(field.target, child); } else { - await this.insertOne(field.target, child); + await this.#insertOne(field.target, child); } added.push(child.name as string); } if (isUpdate) { - await this.runDeleteOtherChildren(field, parentName, added); + await this.#runDeleteOtherChildren(field, parentName, added); } } } - async exists(schemaName: string, name?: string): Promise { - const schema = this.schemaMap[schemaName]; - if (schema.isSingle) { - return this.singleExists(schemaName); - } - - let row = []; - try { - const qb = this.knex(schemaName); - if (name !== undefined) { - qb.where({ name }); - } - row = await qb.limit(1); - } catch (err) { - if (this.getError(err as Error) !== NotFoundError) { - throw err; - } - } - return row.length > 0; - } - - /** - * Get list of values from the singles table. - * @param {...string | Object} fieldnames list of fieldnames to get the values of - * @returns {Array} array of {parent, value, fieldname}. - * @example - * Database.getSingleValues('internalPrecision'); - * // returns [{ fieldname: 'internalPrecision', parent: 'SystemSettings', value: '12' }] - * @example - * Database.getSingleValues({fieldname:'internalPrecision', parent: 'SystemSettings'}); - * // returns [{ fieldname: 'internalPrecision', parent: 'SystemSettings', value: '12' }] - */ - async getSingleValues( - ...fieldnames: { fieldname: string; parent?: string }[] - ) { - fieldnames = fieldnames.map((fieldname) => { - if (typeof fieldname === 'string') { - return { fieldname }; - } - return fieldname; - }); - - let builder = this.knex('SingleValue'); - builder = builder.where(fieldnames[0]); - - fieldnames.slice(1).forEach(({ fieldname, parent }) => { - if (typeof parent === 'undefined') { - builder = builder.orWhere({ fieldname }); - } else { - builder = builder.orWhere({ fieldname, parent }); - } - }); - - let values: { fieldname: string; parent: string; value: RawValue }[] = []; - try { - values = await builder.select('fieldname', 'value', 'parent'); - } catch (err) { - if (this.getError(err as Error) === NotFoundError) { - return []; - } - - throw err; - } - - return values; - } - - async getValue( - schemaName: string, - filters: string | Record, - fieldname = 'name' - ): Promise { - if (typeof filters === 'string') { - filters = { name: filters }; - } - - const row = await this.getAll({ - schemaName, - fields: [fieldname], - filters: filters, - start: 0, - limit: 1, - orderBy: 'name', - order: 'asc', - }); - - if (row.length === 1) { - return row[0][fieldname] as RawValue; - } - - return undefined; - } - - async setValue( - schemaName: string, - name: string, - fieldname: string, - value: RawValue - ) { - return await this.setValues(schemaName, name, { - [fieldname]: value, - }); - } - - async setValues( - schemaName: string, - name: string, - fieldValueMap: FieldValueMap - ) { - return this.updateOne( - schemaName, - Object.assign({}, fieldValueMap, { name }) - ); - } - - async getCachedValue(schemaName: string, name: string, fieldname: string) { - let value = this.cache.hget(`${schemaName}:${name}`, fieldname); - if (value == null) { - value = await this.getValue(schemaName, name, fieldname); - } - return value; + #getTableFields(schemaName: string): TargetField[] { + return this.schemaMap[schemaName].fields.filter( + (f) => f.fieldtype === FieldTypeEnum.Table + ) as TargetField[]; } } diff --git a/backend/database/database.ts b/backend/database/database.ts deleted file mode 100644 index 183a2841..00000000 --- a/backend/database/database.ts +++ /dev/null @@ -1,925 +0,0 @@ -import { knex, Knex } from 'knex'; -import { pesa } from 'pesa'; -import { getRandomString } from '../../frappe/utils'; -import CacheManager from '../../frappe/utils/cacheManager'; -import { - CannotCommitError, - DatabaseError, - DuplicateEntryError, - LinkValidationError, - ValueError, -} from '../../frappe/utils/errors'; -import Observable from '../../frappe/utils/observable'; -import { getMeta, getModels, getNewDoc, sqliteTypeMap } from '../common'; - -interface GetAllOptions { - doctype?: string; - fields?: string[]; - filters?: Record; - start?: number; - limit?: number; - groupBy?: string; - orderBy?: string; - order?: string; -} - -export default class Database extends Observable { - knex: Knex; - cache: CacheManager; - - constructor(dbPath: string) { - super(); - this.typeMap = sqliteTypeMap; - this.cache = new CacheManager(); - this.dbPath = dbPath; - this.connectionParams = { - client: 'sqlite3', - connection: { - filename: this.dbPath, - }, - pool: { - afterCreate(conn, done) { - conn.run('PRAGMA foreign_keys=ON'); - done(); - }, - }, - useNullAsDefault: true, - asyncStackTraces: process.env.NODE_ENV === 'development', - }; - } - - connect() { - this.knex = knex(this.connectionParams); - this.knex.on('query-error', (error) => { - error.type = this.getError(error); - }); - } - - close() { - // - } - - /** - * TODO: Refactor this - * - * essentially it's not models that are required by the database - * but the schema, all the information that is relevant to building - * tables needs to given to this function. - */ - async migrate() { - const models = getModels(); - for (const doctype in models) { - // check if controller module - const meta = getMeta(doctype); - const baseDoctype = meta.getBaseDocType(); - - if (!meta.isSingle) { - if (await this.tableExists(baseDoctype)) { - await this.alterTable(baseDoctype); - } else { - await this.createTable(baseDoctype); - } - } - } - - await this.commit(); - await this.initializeSingles(); - } - - async initializeSingles() { - const models = getModels(); - const singleDoctypes = Object.keys(models) - .filter((n) => models[n].isSingle) - .map((n) => models[n].name); - - for (const doctype of singleDoctypes) { - if (await this.singleExists(doctype)) { - const singleValues = await this.getSingleFieldsToInsert(doctype); - singleValues.forEach(({ fieldname, value }) => { - const singleValue = getNewDoc({ - doctype: 'SingleValue', - parent: doctype, - fieldname, - value, - }); - singleValue.insert(); - }); - continue; - } - const meta = getMeta(doctype); - if (meta.fields.every((df) => df.default == null)) { - continue; - } - const defaultValues = meta.fields.reduce((doc, df) => { - if (df.default != null) { - doc[df.fieldname] = df.default; - } - return doc; - }, {}); - await this.updateSingle(doctype, defaultValues); - } - } - - async singleExists(doctype) { - const res = await this.knex('SingleValue') - .count('parent as count') - .where('parent', doctype) - .first(); - return res.count > 0; - } - - async getSingleFieldsToInsert(doctype) { - const existingFields = ( - await this.knex('SingleValue') - .where({ parent: doctype }) - .select('fieldname') - ).map(({ fieldname }) => fieldname); - - return getMeta(doctype) - .fields.map(({ fieldname, default: value }) => ({ - fieldname, - value, - })) - .filter( - ({ fieldname, value }) => - !existingFields.includes(fieldname) && value !== undefined - ); - } - - tableExists(table) { - return this.knex.schema.hasTable(table); - } - - async createTable(doctype, tableName = null) { - const fields = this.getValidFields(doctype); - return await this.runCreateTableQuery(tableName || doctype, fields); - } - - runCreateTableQuery(doctype, fields) { - return this.knex.schema.createTable(doctype, (table) => { - for (const field of fields) { - this.buildColumnForTable(table, field); - } - }); - } - - async alterTable(doctype) { - // get columns - const diff = await this.getColumnDiff(doctype); - const newForeignKeys = await this.getNewForeignKeys(doctype); - - return this.knex.schema - .table(doctype, (table) => { - if (diff.added.length) { - for (const field of diff.added) { - this.buildColumnForTable(table, field); - } - } - - if (diff.removed.length) { - this.removeColumns(doctype, diff.removed); - } - }) - .then(() => { - if (newForeignKeys.length) { - return this.addForeignKeys(doctype, newForeignKeys); - } - }); - } - - buildColumnForTable(table, field) { - const columnType = this.getColumnType(field); - if (!columnType) { - // In case columnType is "Table" - // childTable links are handled using the childTable's "parent" field - return; - } - - const column = table[columnType](field.fieldname); - - // primary key - if (field.fieldname === 'name') { - column.primary(); - } - - // default value - if (!!field.default && !(field.default instanceof Function)) { - column.defaultTo(field.default); - } - - // required - if ( - (!!field.required && !(field.required instanceof Function)) || - field.fieldtype === 'Currency' - ) { - column.notNullable(); - } - - // link - if (field.fieldtype === 'Link' && field.target) { - const meta = getMeta(field.target); - table - .foreign(field.fieldname) - .references('name') - .inTable(meta.getBaseDocType()) - .onUpdate('CASCADE') - .onDelete('RESTRICT'); - } - } - - async getColumnDiff(doctype) { - const tableColumns = await this.getTableColumns(doctype); - const validFields = this.getValidFields(doctype); - const diff = { added: [], removed: [] }; - - for (const field of validFields) { - if ( - !tableColumns.includes(field.fieldname) && - this.getColumnType(field) - ) { - diff.added.push(field); - } - } - - const validFieldNames = validFields.map((field) => field.fieldname); - for (const column of tableColumns) { - if (!validFieldNames.includes(column)) { - diff.removed.push(column); - } - } - - return diff; - } - - async removeColumns(doctype: string, removed: string[]) { - // TODO: Implement this for sqlite - } - - async getNewForeignKeys(doctype) { - const foreignKeys = await this.getForeignKeys(doctype); - const newForeignKeys = []; - const meta = getMeta(doctype); - for (const field of meta.getValidFields({ withChildren: false })) { - if ( - field.fieldtype === 'Link' && - !foreignKeys.includes(field.fieldname) - ) { - newForeignKeys.push(field); - } - } - return newForeignKeys; - } - - async get(doctype, name = null, fields = '*') { - const meta = getMeta(doctype); - let doc; - if (meta.isSingle) { - doc = await this.getSingle(doctype); - doc.name = doctype; - } else { - if (!name) { - throw new ValueError('name is mandatory'); - } - doc = await this.getOne(doctype, name, fields); - } - if (!doc) { - return; - } - await this.loadChildren(doc, meta); - return doc; - } - - async loadChildren(doc, meta) { - // load children - const tableFields = meta.getTableFields(); - for (const field of tableFields) { - doc[field.fieldname] = await this.getAll({ - doctype: field.childtype, - fields: ['*'], - filters: { parent: doc.name }, - orderBy: 'idx', - order: 'asc', - }); - } - } - - async getSingle(doctype) { - const values = await this.getAll({ - doctype: 'SingleValue', - fields: ['fieldname', 'value'], - filters: { parent: doctype }, - orderBy: 'fieldname', - order: 'asc', - }); - const doc = {}; - for (const row of values) { - doc[row.fieldname] = row.value; - } - return doc; - } - - /** - * Get list of values from the singles table. - * @param {...string | Object} fieldnames list of fieldnames to get the values of - * @returns {Array} array of {parent, value, fieldname}. - * @example - * Database.getSingleValues('internalPrecision'); - * // returns [{ fieldname: 'internalPrecision', parent: 'SystemSettings', value: '12' }] - * @example - * Database.getSingleValues({fieldname:'internalPrecision', parent: 'SystemSettings'}); - * // returns [{ fieldname: 'internalPrecision', parent: 'SystemSettings', value: '12' }] - */ - async getSingleValues(...fieldnames) { - fieldnames = fieldnames.map((fieldname) => { - if (typeof fieldname === 'string') { - return { fieldname }; - } - return fieldname; - }); - - let builder = this.knex('SingleValue'); - builder = builder.where(fieldnames[0]); - - fieldnames.slice(1).forEach(({ fieldname, parent }) => { - if (typeof parent === 'undefined') { - builder = builder.orWhere({ fieldname }); - } else { - builder = builder.orWhere({ fieldname, parent }); - } - }); - - let values = []; - try { - values = await builder.select('fieldname', 'value', 'parent'); - } catch (error) { - if (error.message.includes('no such table')) { - return []; - } - throw error; - } - - return values.map((value) => { - const fields = getMeta(value.parent).fields; - return this.getDocFormattedDoc(fields, values); - }); - } - - async getOne(doctype, name, fields = '*') { - const meta = getMeta(doctype); - const baseDoctype = meta.getBaseDocType(); - - const doc = await this.knex - .select(fields) - .from(baseDoctype) - .where('name', name) - .first(); - - if (!doc) { - return doc; - } - - return this.getDocFormattedDoc(meta.fields, doc); - } - - getDocFormattedDoc(fields, doc) { - // format for usage, not going into the db - const docFields = Object.keys(doc); - const filteredFields = fields.filter(({ fieldname }) => - docFields.includes(fieldname) - ); - - const formattedValues = filteredFields.reduce((d, field) => { - const { fieldname } = field; - d[fieldname] = this.getDocFormattedValues(field, doc[fieldname]); - return d; - }, {}); - - return Object.assign(doc, formattedValues); - } - - getDocFormattedValues(field, value) { - // format for usage, not going into the db - try { - if (field.fieldtype === 'Currency') { - return pesa(value); - } - } catch (err) { - err.message += ` value: '${value}' of type: ${typeof value}, fieldname: '${ - field.fieldname - }', label: '${field.label}'`; - throw err; - } - return value; - } - - triggerChange(doctype, name) { - this.trigger(`change:${doctype}`, { name }, 500); - this.trigger(`change`, { doctype, name }, 500); - // also trigger change for basedOn doctype - const meta = getMeta(doctype); - if (meta.basedOn) { - this.triggerChange(meta.basedOn, name); - } - } - - async insert(doctype, doc) { - const meta = getMeta(doctype); - const baseDoctype = meta.getBaseDocType(); - doc = this.applyBaseDocTypeFilters(doctype, doc); - - // insert parent - if (meta.isSingle) { - await this.updateSingle(doctype, doc); - } else { - await this.insertOne(baseDoctype, doc); - } - - // insert children - await this.insertChildren(meta, doc, baseDoctype); - - this.triggerChange(doctype, doc.name); - - return doc; - } - - async insertChildren(meta, doc, doctype) { - const tableFields = meta.getTableFields(); - for (const field of tableFields) { - let idx = 0; - for (const child of doc[field.fieldname] || []) { - this.prepareChild(doctype, doc.name, child, field, idx); - await this.insertOne(field.childtype, child); - idx++; - } - } - } - - insertOne(doctype, doc) { - const fields = this.getValidFields(doctype); - - if (!doc.name) { - doc.name = getRandomString(); - } - - const formattedDoc = this.getFormattedDoc(fields, doc); - return this.knex(doctype).insert(formattedDoc); - } - - async update(doctype, doc) { - const meta = getMeta(doctype); - const baseDoctype = meta.getBaseDocType(); - doc = this.applyBaseDocTypeFilters(doctype, doc); - - // update parent - if (meta.isSingle) { - await this.updateSingle(doctype, doc); - } else { - await this.updateOne(baseDoctype, doc); - } - - // insert or update children - await this.updateChildren(meta, doc, baseDoctype); - - this.triggerChange(doctype, doc.name); - - return doc; - } - - async updateChildren(meta, doc, doctype) { - const tableFields = meta.getTableFields(); - for (const field of tableFields) { - const added = []; - for (const child of doc[field.fieldname] || []) { - this.prepareChild(doctype, doc.name, child, field, added.length); - if (await this.exists(field.childtype, child.name)) { - await this.updateOne(field.childtype, child); - } else { - await this.insertOne(field.childtype, child); - } - added.push(child.name); - } - await this.runDeleteOtherChildren(field, doc.name, added); - } - } - - updateOne(doctype, doc) { - const validFields = this.getValidFields(doctype); - const fieldsToUpdate = Object.keys(doc).filter((f) => f !== 'name'); - const fields = validFields.filter((df) => - fieldsToUpdate.includes(df.fieldname) - ); - const formattedDoc = this.getFormattedDoc(fields, doc); - - return this.knex(doctype) - .where('name', doc.name) - .update(formattedDoc) - .then(() => { - const cacheKey = `${doctype}:${doc.name}`; - if (this.cache.hexists(cacheKey)) { - for (const fieldname in formattedDoc) { - const value = formattedDoc[fieldname]; - this.cache.hset(cacheKey, fieldname, value); - } - } - }); - } - - runDeleteOtherChildren(field, parent, added) { - // delete other children - return this.knex(field.childtype) - .where('parent', parent) - .andWhere('name', 'not in', added) - .delete(); - } - - async updateSingle(doctype, doc) { - const meta = getMeta(doctype); - await this.deleteSingleValues(doctype); - for (const field of meta.getValidFields({ withChildren: false })) { - const value = doc[field.fieldname]; - if (value != null) { - const singleValue = getNewDoc({ - doctype: 'SingleValue', - parent: doctype, - fieldname: field.fieldname, - value: value, - }); - await singleValue.insert(); - } - } - } - - deleteSingleValues(name) { - return this.knex('SingleValue').where('parent', name).delete(); - } - - async rename(doctype, oldName, newName) { - const meta = getMeta(doctype); - const baseDoctype = meta.getBaseDocType(); - await this.knex(baseDoctype) - .update({ name: newName }) - .where('name', oldName) - .then(() => { - this.clearValueCache(doctype, oldName); - }); - await this.commit(); - - this.triggerChange(doctype, newName); - } - - prepareChild(parenttype, parent, child, field, idx) { - if (!child.name) { - child.name = getRandomString(); - } - child.parent = parent; - child.parenttype = parenttype; - child.parentfield = field.fieldname; - child.idx = idx; - } - - getValidFields(doctype) { - return getMeta(doctype).getValidFields({ withChildren: false }); - } - - getFormattedDoc(fields, doc) { - // format for storage, going into the db - const formattedDoc = {}; - fields.map((field) => { - const value = doc[field.fieldname]; - formattedDoc[field.fieldname] = this.getFormattedValue(field, value); - }); - return formattedDoc; - } - - getFormattedValue(field, value) { - // format for storage, going into the db - const type = typeof value; - if (field.fieldtype === 'Currency') { - let currency = value; - - if (type === 'number' || type === 'string') { - currency = pesa(value); - } - - const currencyValue = currency.store; - if (typeof currencyValue !== 'string') { - throw new Error( - `invalid currencyValue '${currencyValue}' of type '${typeof currencyValue}' on converting from '${value}' of type '${type}'` - ); - } - - return currencyValue; - } - - if (value instanceof Date) { - if (field.fieldtype === 'Date') { - // date - return value.toISOString().substr(0, 10); - } else { - // datetime - return value.toISOString(); - } - } else if (field.fieldtype === 'Link' && !value) { - // empty value must be null to satisfy - // foreign key constraint - return null; - } else { - return value; - } - } - - applyBaseDocTypeFilters(doctype, doc) { - const meta = getMeta(doctype); - if (meta.filters) { - for (const fieldname in meta.filters) { - const value = meta.filters[fieldname]; - if (typeof value !== 'object') { - doc[fieldname] = value; - } - } - } - return doc; - } - - async deleteMany(doctype, names) { - for (const name of names) { - await this.delete(doctype, name); - } - } - - async delete(doctype, name) { - const meta = getMeta(doctype); - const baseDoctype = meta.getBaseDocType(); - await this.deleteOne(baseDoctype, name); - - // delete children - const tableFields = getMeta(doctype).getTableFields(); - for (const field of tableFields) { - await this.deleteChildren(field.childtype, name); - } - - this.triggerChange(doctype, name); - } - - async deleteOne(doctype, name) { - return this.knex(doctype) - .where('name', name) - .delete() - .then(() => { - this.clearValueCache(doctype, name); - }); - } - - deleteChildren(parenttype, parent) { - return this.knex(parenttype).where('parent', parent).delete(); - } - - async exists(doctype, name) { - return (await this.getValue(doctype, name)) ? true : false; - } - - async getValue(doctype, filters, fieldname = 'name') { - const meta = getMeta(doctype); - const baseDoctype = meta.getBaseDocType(); - if (typeof filters === 'string') { - filters = { name: filters }; - } - if (meta.filters) { - Object.assign(filters, meta.filters); - } - - const row = await this.getAll({ - doctype: baseDoctype, - fields: [fieldname], - filters: filters, - start: 0, - limit: 1, - orderBy: 'name', - order: 'asc', - }); - return row.length ? row[0][fieldname] : null; - } - - async setValue(doctype, name, fieldname, value) { - return await this.setValues(doctype, name, { - [fieldname]: value, - }); - } - - async setValues(doctype, name, fieldValuePair) { - const doc = Object.assign({}, fieldValuePair, { name }); - return this.updateOne(doctype, doc); - } - - async getCachedValue(doctype, name, fieldname) { - let value = this.cache.hget(`${doctype}:${name}`, fieldname); - if (value == null) { - value = await this.getValue(doctype, name, fieldname); - } - return value; - } - - async getAll({ - doctype, - fields, - filters, - start, - limit, - groupBy, - orderBy = 'creation', - order = 'desc', - }: GetAllOptions = {}) { - const meta = getMeta(doctype); - const baseDoctype = meta.getBaseDocType(); - if (!fields) { - fields = meta.getKeywordFields(); - fields.push('name'); - } - if (typeof fields === 'string') { - fields = [fields]; - } - if (meta.filters) { - filters = Object.assign({}, filters, meta.filters); - } - - const builder = this.knex.select(fields).from(baseDoctype); - - this.applyFiltersToBuilder(builder, filters); - - if (orderBy) { - builder.orderBy(orderBy, order); - } - - if (groupBy) { - builder.groupBy(groupBy); - } - - if (start) { - builder.offset(start); - } - - if (limit) { - builder.limit(limit); - } - - const docs = await builder; - return docs.map((doc) => this.getDocFormattedDoc(meta.fields, doc)); - } - - applyFiltersToBuilder(builder, filters) { - // {"status": "Open"} => `status = "Open"` - - // {"status": "Open", "name": ["like", "apple%"]} - // => `status="Open" and name like "apple%" - - // {"date": [">=", "2017-09-09", "<=", "2017-11-01"]} - // => `date >= 2017-09-09 and date <= 2017-11-01` - - const filtersArray = []; - - for (const field in filters) { - const value = filters[field]; - let operator = '='; - let comparisonValue = value; - - if (Array.isArray(value)) { - operator = value[0]; - comparisonValue = value[1]; - operator = operator.toLowerCase(); - - if (operator === 'includes') { - operator = 'like'; - } - - if (operator === 'like' && !comparisonValue.includes('%')) { - comparisonValue = `%${comparisonValue}%`; - } - } - - filtersArray.push([field, operator, comparisonValue]); - - if (Array.isArray(value) && value.length > 2) { - // multiple conditions - const operator = value[2]; - const comparisonValue = value[3]; - filtersArray.push([field, operator, comparisonValue]); - } - } - - filtersArray.map((filter) => { - const [field, operator, comparisonValue] = filter; - if (operator === '=') { - builder.where(field, comparisonValue); - } else { - builder.where(field, operator, comparisonValue); - } - }); - } - - sql(query: string, params: Knex.RawBinding[] = []) { - // run raw query - return this.knex.raw(query, params); - } - - async commit() { - try { - await this.sql('commit'); - } catch (e) { - if (e.type !== CannotCommitError) { - throw e; - } - } - } - - clearValueCache(doctype, name) { - const cacheKey = `${doctype}:${name}`; - this.cache.hclear(cacheKey); - } - - getColumnType(field) { - return this.typeMap[field.fieldtype]; - } - - async addForeignKeys(doctype, newForeignKeys) { - await this.sql('PRAGMA foreign_keys=OFF'); - await this.sql('BEGIN TRANSACTION'); - - const tempName = 'TEMP' + doctype; - - // create temp table - await this.createTable(doctype, tempName); - - try { - // copy from old to new table - await this.knex(tempName).insert(this.knex.select().from(doctype)); - } catch (err) { - await this.sql('ROLLBACK'); - await this.sql('PRAGMA foreign_keys=ON'); - - const rows = await this.knex.select().from(doctype); - await this.prestigeTheTable(doctype, rows); - return; - } - - // drop old table - await this.knex.schema.dropTable(doctype); - - // rename new table - await this.knex.schema.renameTable(tempName, doctype); - - await this.sql('COMMIT'); - await this.sql('PRAGMA foreign_keys=ON'); - } - - async getTableColumns(doctype: string) { - return (await this.sql(`PRAGMA table_info(${doctype})`)).map((d) => d.name); - } - - async getForeignKeys(doctype: string) { - return (await this.sql(`PRAGMA foreign_key_list(${doctype})`)).map( - (d) => d.from - ); - } - - getError(err) { - let errorType = DatabaseError; - if (err.message.includes('FOREIGN KEY')) { - errorType = LinkValidationError; - } - if (err.message.includes('SQLITE_ERROR: cannot commit')) { - errorType = CannotCommitError; - } - if (err.message.includes('SQLITE_CONSTRAINT: UNIQUE constraint failed:')) { - errorType = DuplicateEntryError; - } - return errorType; - } - - async prestigeTheTable(tableName, tableRows) { - const max = 200; - - // Alter table hacx for sqlite in case of schema change. - const tempName = `__${tableName}`; - await this.knex.schema.dropTableIfExists(tempName); - - await this.knex.raw('PRAGMA foreign_keys=OFF'); - await this.createTable(tableName, tempName); - - if (tableRows.length > 200) { - const fi = Math.floor(tableRows.length / max); - for (let i = 0; i <= fi; i++) { - const rowSlice = tableRows.slice(i * max, i + 1 * max); - if (rowSlice.length === 0) { - break; - } - await this.knex.batchInsert(tempName, rowSlice); - } - } else { - await this.knex.batchInsert(tempName, tableRows); - } - - await this.knex.schema.dropTable(tableName); - await this.knex.schema.renameTable(tempName, tableName); - await this.knex.raw('PRAGMA foreign_keys=ON'); - } -} diff --git a/backend/database/manager.ts b/backend/database/manager.ts new file mode 100644 index 00000000..ce5ed3af --- /dev/null +++ b/backend/database/manager.ts @@ -0,0 +1,79 @@ +import fs from 'fs/promises'; +import { getSchemas } from '../../schemas'; +import patches from '../patches'; +import DatabaseCore from './core'; +import { runPatches } from './runPatch'; +import { Patch } from './types'; + +export class DatabaseManager { + db: DatabaseCore; + constructor() {} + + async createNewDatabase(dbPath: string, countryCode: string) { + await this.#unlinkIfExists(dbPath); + this.connectToDatabase(dbPath, countryCode); + } + + async connectToDatabase(dbPath: string, countryCode?: string) { + this.db = new DatabaseCore(dbPath); + this.db.connect(); + + countryCode ??= await this.#getCountryCode(); + const schemaMap = getSchemas(countryCode); + this.db.setSchemaMap(schemaMap); + + await this.migrate(); + } + + async migrate() { + const patchesToExecute = await this.#getPatchesToExecute(); + const preMigrationPatches = patchesToExecute.filter( + (p) => p.patch.beforeMigrate + ); + const postMigrationPatches = patchesToExecute.filter( + (p) => !p.patch.beforeMigrate + ); + + await runPatches(preMigrationPatches, this); + await this.db.migrate(); + await runPatches(postMigrationPatches, this); + } + + async #getPatchesToExecute(): Promise { + const query: { name: string }[] = await this.db + .knex('PatchRun') + .select('name'); + const executedPatches = query.map((q) => q.name); + return patches.filter((p) => !executedPatches.includes(p.name)); + } + + async #getCountryCode(): Promise { + if (!this.db) { + return undefined; + } + + const query = await this.db + .knex('SingleValue') + .where({ fieldname: 'countryCode', parent: 'SystemSettings' }); + + if (query.length > 0) { + return query[0].countryCode as string; + } + + return undefined; + } + + async #unlinkIfExists(dbPath: string) { + try { + fs.unlink(dbPath); + } catch (err) { + if (err.code === 'ENOENT') { + return; + } + + throw err; + } + } +} + +export default new DatabaseManager(); diff --git a/backend/database/runPatch.ts b/backend/database/runPatch.ts new file mode 100644 index 00000000..39cf5ece --- /dev/null +++ b/backend/database/runPatch.ts @@ -0,0 +1,31 @@ +import { getDefaultMetaFieldValueMap } from '../common'; +import { DatabaseManager } from './manager'; +import { FieldValueMap, Patch } from './types'; + +export async function runPatches(patches: Patch[], dm: DatabaseManager) { + const list: { name: string; success: boolean }[] = []; + for (const patch of patches) { + const success = await runPatch(patch, dm); + list.push({ name: patch.name, success }); + } + return list; +} + +async function runPatch(patch: Patch, dm: DatabaseManager): Promise { + try { + await patch.patch.execute(dm); + } catch (err) { + console.error('PATCH FAILED: ', patch.name); + console.error(err); + return false; + } + + await makeEntry(patch.name, dm); + return true; +} + +async function makeEntry(patchName: string, dm: DatabaseManager) { + const defaultFieldValueMap = getDefaultMetaFieldValueMap() as FieldValueMap; + defaultFieldValueMap.name = patchName; + await dm.db.insert('PatchRun', defaultFieldValueMap); +} diff --git a/backend/database/types.ts b/backend/database/types.ts index 25f30fbc..45547423 100644 --- a/backend/database/types.ts +++ b/backend/database/types.ts @@ -26,3 +26,12 @@ export type FieldValueMap = Record< string, RawValue | undefined | FieldValueMap[] >; + +export interface Patch { + name: string; + version: string; + patch: { + execute: (DatabaseManager) => Promise; + beforeMigrate?: boolean; + }; +} diff --git a/backend/patches/index.ts b/backend/patches/index.ts new file mode 100644 index 00000000..779c69a8 --- /dev/null +++ b/backend/patches/index.ts @@ -0,0 +1,6 @@ +import { Patch } from '../database/types'; +import testPatch from './testPatch'; + +export default [ + { name: 'testPatch', version: '0.4.2-beta.0', patch: testPatch }, +] as Patch[]; diff --git a/backend/patches/testPatch.ts b/backend/patches/testPatch.ts new file mode 100644 index 00000000..a18f9635 --- /dev/null +++ b/backend/patches/testPatch.ts @@ -0,0 +1,10 @@ +import { DatabaseManager } from '../database/manager'; + +async function execute(dm: DatabaseManager) { + /** + * Execute function will receive the DatabaseManager which is to be used + * to apply database patches. + */ +} + +export default { execute, beforeMigrate: true }; diff --git a/models/doctype/Account/AccountDocument.js b/models/doctype/Account/AccountDocument.js index f2fa2abb..d83a7129 100644 --- a/models/doctype/Account/AccountDocument.js +++ b/models/doctype/Account/AccountDocument.js @@ -4,11 +4,8 @@ import Document from 'frappe/model/document'; export default class Account extends Document { async validate() { if (!this.accountType && this.parentAccount) { - this.accountType = await frappe.db.getValue( - 'Account', - this.parentAccount, - 'accountType' - ); + const account = frappe.db.get('Account', this.parentAccount); + this.accountType = account.accountType; } } } diff --git a/models/doctype/Transaction/TransactionServer.js b/models/doctype/Transaction/TransactionServer.js index 3f0623a0..c2ddb296 100644 --- a/models/doctype/Transaction/TransactionServer.js +++ b/models/doctype/Transaction/TransactionServer.js @@ -30,12 +30,10 @@ export default { await entries.post(); // update outstanding amounts - await frappe.db.setValue( - this.doctype, - this.name, - 'outstandingAmount', - this.baseGrandTotal - ); + await frappe.db.update(this.doctype, { + name: this.name, + outstandingAmount: this.baseGrandTotal, + }); let party = await frappe.getDoc('Party', this.customer || this.supplier); await party.updateOutstandingAmount(); diff --git a/schemas/core/SystemSettings.json b/schemas/core/SystemSettings.json index de638d9d..06e5dbaa 100644 --- a/schemas/core/SystemSettings.json +++ b/schemas/core/SystemSettings.json @@ -73,6 +73,12 @@ "fieldtype": "Check", "default": false, "description": "Hides the Get Started section from the sidebar. Change will be visible on restart or refreshing the app." + }, + { + "fieldname": "countryCode", + "label": "Country Code", + "fieldtype": "Data", + "description": "Country code used to initialize regional settings." } ], "quickEditFields": [ diff --git a/src/initialization.js b/src/initialization.js index a95287e6..9c2213f3 100644 --- a/src/initialization.js +++ b/src/initialization.js @@ -27,9 +27,7 @@ async function runRegionalModelUpdates() { return; } - const { country, setupComplete } = await frappe.db.getSingle( - 'AccountingSettings' - ); + const { country, setupComplete } = await frappe.db.get('AccountingSettings'); if (!parseInt(setupComplete)) return; await regionalModelUpdates({ country }); } diff --git a/src/utils.js b/src/utils.js index 677639c9..af2a1587 100644 --- a/src/utils.js +++ b/src/utils.js @@ -124,7 +124,8 @@ export function partyWithAvatar(party) { Avatar, }, async mounted() { - this.imageURL = await frappe.db.getValue('Party', party, 'image'); + const p = await frappe.db.get('Party', party); + this.imageURL = p.image; this.label = party; }, template: ` From 7514c9588329f84922e44f62e40d7973cf7fa067 Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Fri, 25 Mar 2022 14:39:52 +0530 Subject: [PATCH 017/163] chore: add mocha and deps --- package.json | 8 +- yarn.lock | 369 ++++++++++++++++++++++++++++++++++++++++----------- 2 files changed, 299 insertions(+), 78 deletions(-) diff --git a/package.json b/package.json index 5b9af4ec..f8f3f6ae 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,8 @@ "electron:serve": "vue-cli-service electron:serve", "postinstall": "electron-builder install-app-deps", "postuninstall": "electron-builder install-app-deps", - "script:translate": "node scripts/generateTranslations.js" + "script:translate": "node scripts/generateTranslations.js", + "test": "TS_NODE_COMPILER_OPTIONS='{\"module\":\"commonjs\"}' mocha --reporter nyan --require ts-node/register ./**/tests/**/*.spec.ts" }, "dependencies": { "@popperjs/core": "^2.10.2", @@ -35,8 +36,11 @@ "devDependencies": { "@babel/core": "^7.16.0", "@babel/eslint-parser": "^7.16.0", + "@types/assert": "^1.5.6", "@types/electron-devtools-installer": "^2.2.0", "@types/lodash": "^4.14.179", + "@types/mocha": "^9.1.0", + "@types/node": "^17.0.23", "@typescript-eslint/eslint-plugin": "^4.15.1", "@typescript-eslint/parser": "^4.15.1", "@vue/cli-plugin-babel": "^4.5.0", @@ -56,10 +60,12 @@ "eslint-plugin-prettier": "^4.0.0", "eslint-plugin-vue": "^7.0.0", "lint-staged": "^11.2.6", + "mocha": "^9.2.2", "postcss": "^8", "prettier": "^2.4.1", "raw-loader": "^4.0.2", "tailwindcss": "npm:@tailwindcss/postcss7-compat", + "ts-node": "^10.7.0", "tslib": "^2.3.1", "typescript": "^4.6.2", "vue-cli-plugin-electron-builder": "^2.0.0", diff --git a/yarn.lock b/yarn.lock index b935e531..5f4f31ac 100644 --- a/yarn.lock +++ b/yarn.lock @@ -953,6 +953,18 @@ "@babel/helper-validator-identifier" "^7.15.7" to-fast-properties "^2.0.0" +"@cspotcode/source-map-consumer@0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz#33bf4b7b39c178821606f669bbc447a6a629786b" + integrity sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg== + +"@cspotcode/source-map-support@0.7.0": + version "0.7.0" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz#4789840aa859e46d2f3173727ab707c66bf344f5" + integrity sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA== + dependencies: + "@cspotcode/source-map-consumer" "0.8.0" + "@develar/schema-utils@~2.6.5": version "2.6.5" resolved "https://registry.yarnpkg.com/@develar/schema-utils/-/schema-utils-2.6.5.tgz#3ece22c5838402419a6e0425f85742b961d9b6c6" @@ -1191,6 +1203,31 @@ dependencies: defer-to-connect "^1.0.1" +"@tsconfig/node10@^1.0.7": + version "1.0.8" + resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.8.tgz#c1e4e80d6f964fbecb3359c43bd48b40f7cadad9" + integrity sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg== + +"@tsconfig/node12@^1.0.7": + version "1.0.9" + resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.9.tgz#62c1f6dee2ebd9aead80dc3afa56810e58e1a04c" + integrity sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw== + +"@tsconfig/node14@^1.0.0": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.1.tgz#95f2d167ffb9b8d2068b0b235302fafd4df711f2" + integrity sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg== + +"@tsconfig/node16@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.2.tgz#423c77877d0569db20e1fc80885ac4118314010e" + integrity sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA== + +"@types/assert@^1.5.6": + version "1.5.6" + resolved "https://registry.yarnpkg.com/@types/assert/-/assert-1.5.6.tgz#a8b5a94ce5fb8f4ba65fdc37fc9507609114189e" + integrity sha512-Y7gDJiIqb9qKUHfBQYOWGngUpLORtirAVPuj/CWJrU2C6ZM4/y3XLwuwfGMF8s7QzW746LQZx23m0+1FSgjfug== + "@types/body-parser@*": version "1.19.2" resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.2.tgz#aea2059e28b7658639081347ac4fab3de166e6f0" @@ -1321,6 +1358,11 @@ resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.2.tgz#ee771e2ba4b3dc5b372935d549fd9617bf345b8c" integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ== +"@types/mocha@^9.1.0": + version "9.1.0" + resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-9.1.0.tgz#baf17ab2cca3fcce2d322ebc30454bff487efad5" + integrity sha512-QCWHkbMv4Y5U9oW10Uxbr45qMMSzl4OzijsozynUAgx3kEHUdXB00udx2dWDQ7f2TU2a2uuiFaRZjCe3unPpeg== + "@types/ms@*": version "0.7.31" resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197" @@ -1336,6 +1378,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-14.17.32.tgz#2ca61c9ef8c77f6fa1733be9e623ceb0d372ad96" integrity sha512-JcII3D5/OapPGx+eJ+Ik1SQGyt6WvuqdRfh9jUwL6/iHGjmyOriBDciBUu7lEIBTL2ijxwrR70WUnw5AEDmFvQ== +"@types/node@^17.0.23": + version "17.0.23" + resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.23.tgz#3b41a6e643589ac6442bdbd7a4a3ded62f33f7da" + integrity sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw== + "@types/normalize-package-data@^2.4.0": version "2.4.1" resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301" @@ -1523,6 +1570,11 @@ "@typescript-eslint/types" "4.33.0" eslint-visitor-keys "^2.0.0" +"@ungap/promise-all-settled@1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz#aa58042711d6e3275dd37dc597e5d31e8c290a44" + integrity sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q== + "@vue/babel-helper-vue-jsx-merge-props@^1.2.1": version "1.2.1" resolved "https://registry.yarnpkg.com/@vue/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-1.2.1.tgz#31624a7a505fb14da1d58023725a4c5f270e6a81" @@ -2240,6 +2292,11 @@ acorn-walk@^7.0.0, acorn-walk@^7.1.1: resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc" integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA== +acorn-walk@^8.1.1: + version "8.2.0" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" + integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== + acorn@^6.4.1: version "6.4.2" resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.2.tgz#35866fd710528e92de10cf06016498e47e39e1e6" @@ -2327,16 +2384,16 @@ ansi-align@^3.0.0: dependencies: string-width "^4.1.0" +ansi-colors@4.1.1, ansi-colors@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" + integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== + ansi-colors@^3.0.0: version "3.2.4" resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.4.tgz#e3a3da4bfbae6c86a9c285625de124a234026fbf" integrity sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA== -ansi-colors@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" - integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== - ansi-escapes@^4.2.1, ansi-escapes@^4.3.0: version "4.3.2" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" @@ -2454,6 +2511,11 @@ arch@^2.1.1: resolved "https://registry.yarnpkg.com/arch/-/arch-2.2.0.tgz#1bc47818f305764f23ab3306b0bfc086c5a29d11" integrity sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ== +arg@^4.1.0: + version "4.1.3" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" + integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== + arg@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/arg/-/arg-5.0.1.tgz#eb0c9a8f77786cad2af8ff2b862899842d7b6adb" @@ -2902,6 +2964,11 @@ brorand@^1.0.1, brorand@^1.1.0: resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8= +browser-stdout@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" + integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== + browserify-aes@^1.0.0, browserify-aes@^1.0.4: version "1.2.0" resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.2.0.tgz#326734642f403dabc3003209853bb70ad428ef48" @@ -3325,6 +3392,21 @@ check-types@^8.0.3: resolved "https://registry.yarnpkg.com/check-types/-/check-types-8.0.3.tgz#3356cca19c889544f2d7a95ed49ce508a0ecf552" integrity sha512-YpeKZngUmG65rLudJ4taU7VLkOCTMhNl/u4ctNC56LQS/zJTyNH0Lrtwm1tfTsbLlwvlfsA2d1c8vCf/Kh2KwQ== +chokidar@3.5.3, chokidar@^3.3.0: + version "3.5.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" + integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + chokidar@^2.1.8: version "2.1.8" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917" @@ -3359,21 +3441,6 @@ chokidar@^3.0.2, chokidar@^3.4.1, chokidar@^3.5.2: optionalDependencies: fsevents "~2.3.2" -chokidar@^3.3.0: - version "3.5.3" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" - integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== - dependencies: - anymatch "~3.1.2" - braces "~3.0.2" - glob-parent "~5.1.2" - is-binary-path "~2.1.0" - is-glob "~4.0.1" - normalize-path "~3.0.0" - readdirp "~3.6.0" - optionalDependencies: - fsevents "~2.3.2" - chownr@^1.1.1: version "1.1.4" resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" @@ -3955,6 +4022,11 @@ create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7: safe-buffer "^5.0.1" sha.js "^2.4.8" +create-require@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" + integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== + cross-spawn@^5.0.1: version "5.1.0" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" @@ -4223,6 +4295,13 @@ debug@4.3.2: dependencies: ms "2.1.2" +debug@4.3.3, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2: + version "4.3.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664" + integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q== + dependencies: + ms "2.1.2" + debug@^3.1.1, debug@^3.2.6: version "3.2.7" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" @@ -4230,18 +4309,16 @@ debug@^3.1.1, debug@^3.2.6: dependencies: ms "^2.1.1" -debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2: - version "4.3.3" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664" - integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q== - dependencies: - ms "2.1.2" - decamelize@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= +decamelize@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837" + integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ== + decode-uri-component@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" @@ -4407,6 +4484,11 @@ didyoumean@^1.2.2: resolved "https://registry.yarnpkg.com/didyoumean/-/didyoumean-1.2.2.tgz#989346ffe9e839b4555ecf5666edea0d3e8ad037" integrity sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw== +diff@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b" + integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w== + diff@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" @@ -4915,16 +4997,16 @@ escape-html@~1.0.3: resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= +escape-string-regexp@4.0.0, escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= -escape-string-regexp@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" - integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== - eslint-config-prettier@^8.3.0: version "8.3.0" resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.3.0.tgz#f7471b20b6fe8a9a9254cc684454202886a2dd7a" @@ -5521,6 +5603,14 @@ find-cache-dir@^3.0.0, find-cache-dir@^3.3.1: make-dir "^3.0.2" pkg-dir "^4.1.0" +find-up@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + find-up@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" @@ -5544,6 +5634,11 @@ flat-cache@^3.0.4: flatted "^3.1.0" rimraf "^3.0.2" +flat@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" + integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== + flatted@^3.1.0: version "3.2.2" resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.2.tgz#64bfed5cb68fe3ca78b3eb214ad97b63bedce561" @@ -5838,7 +5933,7 @@ glob-to-regexp@^0.4.1: resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== -glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.1.7: +glob@7.2.0, glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.1.7: version "7.2.0" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== @@ -5991,6 +6086,11 @@ graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725" integrity sha1-TK+tdrxi8C+gObL5Tpo906ORpyU= +growl@1.10.5: + version "1.10.5" + resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e" + integrity sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA== + gzip-size@^5.0.0: version "5.1.1" resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-5.1.1.tgz#cb9bee692f87c0612b232840a873904e4c135274" @@ -6121,7 +6221,7 @@ hash.js@^1.0.0, hash.js@^1.0.3: inherits "^2.0.3" minimalistic-assert "^1.0.1" -he@1.2.x, he@^1.1.0: +he@1.2.0, he@1.2.x, he@^1.1.0: version "1.2.0" resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== @@ -6849,6 +6949,11 @@ is-plain-obj@^1.0.0: resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4= +is-plain-obj@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" + integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== + is-plain-obj@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-3.0.0.tgz#af6f2ea14ac5a646183a5bbdb5baabbc156ad9d7" @@ -7052,6 +7157,13 @@ js-tokens@^4.0.0: resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== +js-yaml@4.1.0, js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + js-yaml@^3.13.1: version "3.14.1" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" @@ -7060,13 +7172,6 @@ js-yaml@^3.13.1: argparse "^1.0.7" esprima "^4.0.0" -js-yaml@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" - integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== - dependencies: - argparse "^2.0.1" - jsbn@~0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" @@ -7380,6 +7485,13 @@ locate-path@^5.0.0: dependencies: p-locate "^4.1.0" +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + lodash.clonedeep@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" @@ -7450,14 +7562,7 @@ lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17 resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== -log-symbols@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-2.2.0.tgz#5740e1c5d6f0dfda4ad9323b5332107ef6b4c40a" - integrity sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg== - dependencies: - chalk "^2.0.1" - -log-symbols@^4.1.0: +log-symbols@4.1.0, log-symbols@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== @@ -7465,6 +7570,13 @@ log-symbols@^4.1.0: chalk "^4.1.0" is-unicode-supported "^0.1.0" +log-symbols@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-2.2.0.tgz#5740e1c5d6f0dfda4ad9323b5332107ef6b4c40a" + integrity sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg== + dependencies: + chalk "^2.0.1" + log-update@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/log-update/-/log-update-4.0.0.tgz#589ecd352471f2a1c0c570287543a64dfd20e0a1" @@ -7544,6 +7656,11 @@ make-dir@^3.0.0, make-dir@^3.0.2, make-dir@^3.1.0: dependencies: semver "^6.0.0" +make-error@^1.1.1: + version "1.3.6" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" + integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== + map-cache@^0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" @@ -7758,6 +7875,13 @@ minimatch@3.0.4, minimatch@^3.0.4: dependencies: brace-expansion "^1.1.7" +minimatch@4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-4.2.1.tgz#40d9d511a46bdc4e563c22c3080cde9c0d8299b4" + integrity sha512-9Uq1ChtSZO+Mxa/CL1eGizn2vRn3MlLgzhT0Iz8zaY8NdvxvB0d5QdPFmCKf7JKA9Lerx5vRrnwO03jsSfGG9g== + dependencies: + brace-expansion "^1.1.7" + minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5: version "1.2.6" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" @@ -7835,6 +7959,36 @@ mkdirp@^1.0.3, mkdirp@^1.0.4: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== +mocha@^9.2.2: + version "9.2.2" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-9.2.2.tgz#d70db46bdb93ca57402c809333e5a84977a88fb9" + integrity sha512-L6XC3EdwT6YrIk0yXpavvLkn8h+EU+Y5UcCHKECyMbdUIxyMuZj4bX4U9e1nvnvUUvQVsV2VHQr5zLdcUkhW/g== + dependencies: + "@ungap/promise-all-settled" "1.1.2" + ansi-colors "4.1.1" + browser-stdout "1.3.1" + chokidar "3.5.3" + debug "4.3.3" + diff "5.0.0" + escape-string-regexp "4.0.0" + find-up "5.0.0" + glob "7.2.0" + growl "1.10.5" + he "1.2.0" + js-yaml "4.1.0" + log-symbols "4.1.0" + minimatch "4.2.1" + ms "2.1.3" + nanoid "3.3.1" + serialize-javascript "6.0.0" + strip-json-comments "3.1.1" + supports-color "8.1.1" + which "2.0.2" + workerpool "6.2.0" + yargs "16.2.0" + yargs-parser "20.2.4" + yargs-unparser "2.0.0" + modern-normalize@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/modern-normalize/-/modern-normalize-1.1.0.tgz#da8e80140d9221426bd4f725c6e11283d34f90b7" @@ -7904,6 +8058,11 @@ nan@^2.12.1: resolved "https://registry.yarnpkg.com/nan/-/nan-2.15.0.tgz#3f34a473ff18e15c1b5626b62903b5ad6e665fee" integrity sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ== +nanoid@3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.1.tgz#6347a18cac88af88f58af0b3594b723d5e99bb35" + integrity sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw== + nanoid@^3.1.22, nanoid@^3.1.30: version "3.1.32" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.32.tgz#8f96069e6239cc0a9ae8c0d3b41a3b4933a88c0a" @@ -8372,6 +8531,13 @@ p-locate@^4.1.0: dependencies: p-limit "^2.2.0" +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + p-map@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175" @@ -9942,6 +10108,13 @@ serialize-error@^7.0.1: dependencies: type-fest "^0.13.1" +serialize-javascript@6.0.0, serialize-javascript@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.0.tgz#efae5d88f45d7924141da8b5c3a7a7e663fefeb8" + integrity sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag== + dependencies: + randombytes "^2.1.0" + serialize-javascript@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz#b525e1238489a5ecfc42afacc3fe99e666f4b1aa" @@ -9949,13 +10122,6 @@ serialize-javascript@^4.0.0: dependencies: randombytes "^2.1.0" -serialize-javascript@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.0.tgz#efae5d88f45d7924141da8b5c3a7a7e663fefeb8" - integrity sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag== - dependencies: - randombytes "^2.1.0" - serve-index@^1.9.1: version "1.9.1" resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.1.tgz#d3768d69b1e7d82e5ce050fff5b453bea12a9239" @@ -10554,7 +10720,7 @@ strip-indent@^2.0.0: resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-2.0.0.tgz#5ef8db295d01e6ed6cbf7aab96998d7822527b68" integrity sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g= -strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: +strip-json-comments@3.1.1, strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== @@ -10987,6 +11153,25 @@ ts-loader@^6.2.2: micromatch "^4.0.0" semver "^6.0.0" +ts-node@^10.7.0: + version "10.7.0" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.7.0.tgz#35d503d0fab3e2baa672a0e94f4b40653c2463f5" + integrity sha512-TbIGS4xgJoX2i3do417KSaep1uRAW/Lu+WAL2doDHC0D6ummjirVOXU5/7aiZotbQ5p1Zp9tP7U6cYhA0O7M8A== + dependencies: + "@cspotcode/source-map-support" "0.7.0" + "@tsconfig/node10" "^1.0.7" + "@tsconfig/node12" "^1.0.7" + "@tsconfig/node14" "^1.0.0" + "@tsconfig/node16" "^1.0.2" + acorn "^8.4.1" + acorn-walk "^8.1.1" + arg "^4.1.0" + create-require "^1.1.0" + diff "^4.0.1" + make-error "^1.1.1" + v8-compile-cache-lib "^3.0.0" + yn "3.1.1" + ts-pnp@^1.1.6: version "1.2.0" resolved "https://registry.yarnpkg.com/ts-pnp/-/ts-pnp-1.2.0.tgz#a500ad084b0798f1c3071af391e65912c86bca92" @@ -11388,6 +11573,11 @@ uuid@^8.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== +v8-compile-cache-lib@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.0.tgz#0582bcb1c74f3a2ee46487ceecf372e46bce53e8" + integrity sha512-mpSYqfsFvASnSn5qMiwrr4VKfumbPyONLCOPmsR3A6pTY/r0+tSaVbgPWSAIuzbk3lCTa+FForeTiO+wBQGkjA== + v8-compile-cache@^2.0.3: version "2.3.0" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" @@ -11818,6 +12008,13 @@ which-module@^2.0.0: resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= +which@2.0.2, which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + which@^1.2.9: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" @@ -11825,13 +12022,6 @@ which@^1.2.9: dependencies: isexe "^2.0.0" -which@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" - integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== - dependencies: - isexe "^2.0.0" - widest-line@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-3.1.0.tgz#8292333bbf66cb45ff0de1603b136b7ae1496eca" @@ -11858,6 +12048,11 @@ worker-rpc@^0.1.0: dependencies: microevent.ts "~0.1.1" +workerpool@6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.0.tgz#827d93c9ba23ee2019c3ffaff5c27fccea289e8b" + integrity sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A== + wrap-ansi@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09" @@ -11962,6 +12157,11 @@ yaml@^1.10.0, yaml@^1.10.2, yaml@^1.7.2: resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== +yargs-parser@20.2.4: + version "20.2.4" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54" + integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA== + yargs-parser@^13.1.2: version "13.1.2" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38" @@ -11983,6 +12183,29 @@ yargs-parser@^20.2.2: resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== +yargs-unparser@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-2.0.0.tgz#f131f9226911ae5d9ad38c432fe809366c2325eb" + integrity sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA== + dependencies: + camelcase "^6.0.0" + decamelize "^4.0.0" + flat "^5.0.2" + is-plain-obj "^2.1.0" + +yargs@16.2.0, yargs@^16.0.0: + version "16.2.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" + integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.0" + y18n "^5.0.5" + yargs-parser "^20.2.2" + yargs@^13.3.2: version "13.3.2" resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd" @@ -12016,19 +12239,6 @@ yargs@^15.3.1: y18n "^4.0.0" yargs-parser "^18.1.2" -yargs@^16.0.0: - version "16.2.0" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" - integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== - dependencies: - cliui "^7.0.2" - escalade "^3.1.1" - get-caller-file "^2.0.5" - require-directory "^2.1.1" - string-width "^4.2.0" - y18n "^5.0.5" - yargs-parser "^20.2.2" - yargs@^17.0.1: version "17.2.1" resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.2.1.tgz#e2c95b9796a0e1f7f3bf4427863b42e0418191ea" @@ -12050,6 +12260,11 @@ yauzl@^2.10.0: buffer-crc32 "~0.2.3" fd-slicer "~1.1.0" +yn@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== + yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" From 74bfe7931cee3c9469d7eb2cc525a3037df5c53c Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Fri, 25 Mar 2022 15:42:39 +0530 Subject: [PATCH 018/163] chore: fix types --- backend/common.ts | 14 ++-- backend/database/core.ts | 138 +++++++++++++++++------------------ backend/database/manager.ts | 27 ++++--- backend/database/runPatch.ts | 2 +- backend/database/types.ts | 27 +++++-- schemas/helpers.ts | 8 +- schemas/index.ts | 44 ++++++----- schemas/regional/index.ts | 3 +- tsconfig.json | 5 +- 9 files changed, 149 insertions(+), 119 deletions(-) diff --git a/backend/common.ts b/backend/common.ts index a5832ebe..79a885cb 100644 --- a/backend/common.ts +++ b/backend/common.ts @@ -1,21 +1,23 @@ -export const sqliteTypeMap = { +import { KnexColumnType } from './database/types'; + +export const sqliteTypeMap: Record = { AutoComplete: 'text', Currency: 'text', Int: 'integer', Float: 'float', Percent: 'float', - Check: 'integer', + Check: 'boolean', Code: 'text', - Date: 'text', - Datetime: 'text', - Time: 'text', + Date: 'date', + Datetime: 'datetime', + Time: 'time', Text: 'text', Data: 'text', Link: 'text', DynamicLink: 'text', Password: 'text', Select: 'text', - File: 'text', + File: 'binary', Attach: 'text', AttachImage: 'text', Color: 'text', diff --git a/backend/database/core.ts b/backend/database/core.ts index 063bb5e6..65fe7415 100644 --- a/backend/database/core.ts +++ b/backend/database/core.ts @@ -6,14 +6,14 @@ import { DuplicateEntryError, LinkValidationError, NotFoundError, - ValueError, + ValueError } from '../../frappe/utils/errors'; import { Field, FieldTypeEnum, RawValue, SchemaMap, - TargetField, + TargetField } from '../../schemas/types'; import { sqliteTypeMap } from '../common'; import { @@ -21,11 +21,11 @@ import { FieldValueMap, GetAllOptions, GetQueryBuilderOptions, - QueryFilter, + QueryFilter } from './types'; export default class DatabaseCore { - knex: Knex; + knex?: Knex; typeMap = sqliteTypeMap; dbPath: string; schemaMap: SchemaMap = {}; @@ -39,7 +39,7 @@ export default class DatabaseCore { filename: this.dbPath, }, pool: { - afterCreate(conn, done) { + afterCreate(conn: { run: (query: string) => void }, done: () => void) { conn.run('PRAGMA foreign_keys=ON'); done(); }, @@ -61,21 +61,22 @@ export default class DatabaseCore { } close() { - this.knex.destroy(); + this.knex!.destroy(); } async commit() { try { await this.raw('commit'); } catch (err) { - if (err.type !== CannotCommitError) { + const type = this.#getError(err as Error); + if (type !== CannotCommitError) { throw err; } } } async raw(query: string, params: Knex.RawBinding[] = []) { - return await this.knex.raw(query, params); + return await this.knex!.raw(query, params); } async migrate() { @@ -104,7 +105,7 @@ export default class DatabaseCore { let row = []; try { - const qb = this.knex(schemaName); + const qb = this.knex!(schemaName); if (name !== undefined) { qb.where({ name }); } @@ -197,7 +198,7 @@ export default class DatabaseCore { groupBy, orderBy = 'creation', order = 'desc', - }: GetAllOptions = {}): Promise { + }: GetAllOptions): Promise { const schema = this.schemaMap[schemaName]; if (!fields) { fields = ['name', ...(schema.keywordFields ?? [])]; @@ -207,7 +208,7 @@ export default class DatabaseCore { fields = [fields]; } - return (await this.#getQueryBuilder(schemaName, fields, filters, { + return (await this.#getQueryBuilder(schemaName, fields, filters ?? {}, { offset: start, limit, groupBy, @@ -226,7 +227,7 @@ export default class DatabaseCore { return fieldname; }); - let builder = this.knex('SingleValue'); + let builder = this.knex!('SingleValue'); builder = builder.where(fieldnames[0]); fieldnames.slice(1).forEach(({ fieldname, parent }) => { @@ -252,7 +253,7 @@ export default class DatabaseCore { } async rename(schemaName: string, oldName: string, newName: string) { - await this.knex(schemaName) + await this.knex!(schemaName) .update({ name: newName }) .where('name', oldName); await this.commit(); @@ -282,15 +283,15 @@ export default class DatabaseCore { } async #tableExists(schemaName: string) { - return await this.knex.schema.hasTable(schemaName); + return await this.knex!.schema.hasTable(schemaName); } async #singleExists(singleSchemaName: string) { - const res = await this.knex('SingleValue') + const res = await this.knex!('SingleValue') .count('parent as count') .where('parent', singleSchemaName) .first(); - return res.count > 0; + return (res?.count ?? 0) > 0; } async #removeColumns(schemaName: string, targetColumns: string[]) { @@ -298,7 +299,7 @@ export default class DatabaseCore { } async #deleteSingleValues(singleSchemaName: string) { - return await this.knex('SingleValue') + return await this.knex!('SingleValue') .where('parent', singleSchemaName) .delete(); } @@ -325,9 +326,9 @@ export default class DatabaseCore { // Alter table hacx for sqlite in case of schema change. const tempName = `__${schemaName}`; - await this.knex.schema.dropTableIfExists(tempName); + await this.knex!.schema.dropTableIfExists(tempName); - await this.knex.raw('PRAGMA foreign_keys=OFF'); + await this.knex!.raw('PRAGMA foreign_keys=OFF'); await this.#createTable(schemaName, tempName); if (tableRows.length > 200) { @@ -337,27 +338,29 @@ export default class DatabaseCore { if (rowSlice.length === 0) { break; } - await this.knex.batchInsert(tempName, rowSlice); + await this.knex!.batchInsert(tempName, rowSlice); } } else { - await this.knex.batchInsert(tempName, tableRows); + await this.knex!.batchInsert(tempName, tableRows); } - await this.knex.schema.dropTable(schemaName); - await this.knex.schema.renameTable(tempName, schemaName); - await this.knex.raw('PRAGMA foreign_keys=ON'); + await this.knex!.schema.dropTable(schemaName); + await this.knex!.schema.renameTable(tempName, schemaName); + await this.knex!.raw('PRAGMA foreign_keys=ON'); } async #getTableColumns(schemaName: string): Promise { - const info = await this.raw(`PRAGMA table_info(${schemaName})`); - return info.map((d) => d.name); + const info: FieldValueMap[] = await this.raw( + `PRAGMA table_info(${schemaName})` + ); + return info.map((d) => d.name as string); } async #getForeignKeys(schemaName: string): Promise { - const foreignKeyList = await this.raw( + const foreignKeyList: FieldValueMap[] = await this.raw( `PRAGMA foreign_key_list(${schemaName})` ); - return foreignKeyList.map((d) => d.from); + return foreignKeyList.map((d) => d.from as string); } #getQueryBuilder( @@ -366,7 +369,7 @@ export default class DatabaseCore { filters: QueryFilter, options: GetQueryBuilderOptions ): Knex.QueryBuilder { - const builder = this.knex.select(fields).from(schemaName); + const builder = this.knex!.select(fields).from(schemaName); this.#applyFiltersToBuilder(builder, filters); @@ -432,9 +435,9 @@ export default class DatabaseCore { filtersArray.map((filter) => { const [field, operator, comparisonValue] = filter; if (operator === '=') { - builder.where(field, comparisonValue); + builder.where(field as string, comparisonValue); } else { - builder.where(field, operator, comparisonValue); + builder.where(field as string, operator as string, comparisonValue); } }); } @@ -445,10 +448,8 @@ export default class DatabaseCore { const diff: ColumnDiff = { added: [], removed: [] }; for (const field of validFields) { - if ( - !tableColumns.includes(field.fieldname) && - this.typeMap[field.fieldtype] - ) { + const hasDbType = this.typeMap.hasOwnProperty(field.fieldtype); + if (!tableColumns.includes(field.fieldname) && hasDbType) { diff.added.push(field); } } @@ -463,7 +464,7 @@ export default class DatabaseCore { return diff; } - async #getNewForeignKeys(schemaName): Promise { + async #getNewForeignKeys(schemaName: string): Promise { const foreignKeys = await this.#getForeignKeys(schemaName); const newForeignKeys: Field[] = []; const schema = this.schemaMap[schemaName]; @@ -490,7 +491,9 @@ export default class DatabaseCore { return; } - const column = table[columnType](field.fieldname); + const column = table[columnType]( + field.fieldname + ) as Knex.SqlLiteColumnBuilder; // primary key if (field.fieldname === 'name') { @@ -525,23 +528,21 @@ export default class DatabaseCore { const diff: ColumnDiff = await this.#getColumnDiff(schemaName); const newForeignKeys: Field[] = await this.#getNewForeignKeys(schemaName); - return this.knex.schema - .table(schemaName, (table) => { - if (diff.added.length) { - for (const field of diff.added) { - this.#buildColumnForTable(table, field); - } + return this.knex!.schema.table(schemaName, (table) => { + if (diff.added.length) { + for (const field of diff.added) { + this.#buildColumnForTable(table, field); } + } - if (diff.removed.length) { - this.#removeColumns(schemaName, diff.removed); - } - }) - .then(() => { - if (newForeignKeys.length) { - return this.#addForeignKeys(schemaName, newForeignKeys); - } - }); + if (diff.removed.length) { + this.#removeColumns(schemaName, diff.removed); + } + }).then(() => { + if (newForeignKeys.length) { + return this.#addForeignKeys(schemaName, newForeignKeys); + } + }); } async #createTable(schemaName: string, tableName?: string) { @@ -551,7 +552,7 @@ export default class DatabaseCore { } #runCreateTableQuery(schemaName: string, fields: Field[]) { - return this.knex.schema.createTable(schemaName, (table) => { + return this.knex!.schema.createTable(schemaName, (table) => { for (const field of fields) { this.#buildColumnForTable(table, field); } @@ -560,7 +561,7 @@ export default class DatabaseCore { async #getNonExtantSingleValues(singleSchemaName: string) { const existingFields = ( - await this.knex('SingleValue') + await this.knex!('SingleValue') .where({ parent: singleSchemaName }) .select('fieldname') ).map(({ fieldname }) => fieldname); @@ -577,11 +578,11 @@ export default class DatabaseCore { } async #deleteOne(schemaName: string, name: string) { - return this.knex(schemaName).where('name', name).delete(); + return this.knex!(schemaName).where('name', name).delete(); } #deleteChildren(schemaName: string, parentName: string) { - return this.knex(schemaName).where('parent', parentName).delete(); + return this.knex!(schemaName).where('parent', parentName).delete(); } #runDeleteOtherChildren( @@ -590,7 +591,7 @@ export default class DatabaseCore { added: string[] ) { // delete other children - return this.knex(field.target) + return this.knex!(field.target) .where('parent', parentName) .andWhere('name', 'not in', added) .delete(); @@ -623,21 +624,21 @@ export default class DatabaseCore { try { // copy from old to new table - await this.knex(tempName).insert(this.knex.select().from(schemaName)); + await this.knex!(tempName).insert(this.knex!.select().from(schemaName)); } catch (err) { await this.raw('ROLLBACK'); await this.raw('PRAGMA foreign_keys=ON'); - const rows = await this.knex.select().from(schemaName); + const rows = await this.knex!.select().from(schemaName); await this.prestigeTheTable(schemaName, rows); return; } // drop old table - await this.knex.schema.dropTable(schemaName); + await this.knex!.schema.dropTable(schemaName); // rename new table - await this.knex.schema.renameTable(tempName, schemaName); + await this.knex!.schema.renameTable(tempName, schemaName); await this.raw('COMMIT'); await this.raw('PRAGMA foreign_keys=ON'); @@ -663,8 +664,7 @@ export default class DatabaseCore { name: string, fields: string | string[] = '*' ) { - const fieldValueMap: FieldValueMap = await this.knex - .select(fields) + const fieldValueMap: FieldValueMap = await this.knex!.select(fields) .from(schemaName) .where('name', name) .first(); @@ -694,7 +694,7 @@ export default class DatabaseCore { fieldValueMap.name = getRandomString(); } - const validMap = {}; + const validMap: FieldValueMap = {}; for (const { fieldname, fieldtype } of fields) { if (fieldtype === FieldTypeEnum.Table) { continue; @@ -703,7 +703,7 @@ export default class DatabaseCore { validMap[fieldname] = fieldValueMap[fieldname]; } - return this.knex(schemaName).insert(fieldValueMap); + return this.knex!(schemaName).insert(fieldValueMap); } async #updateSingleValues( @@ -728,7 +728,7 @@ export default class DatabaseCore { fieldname: string, value: RawValue ) { - return await this.knex('SingleValue') + return await this.knex!('SingleValue') .where({ parent: singleSchemaName, fieldname, @@ -758,7 +758,7 @@ export default class DatabaseCore { } return acc; - }, {}); + }, {} as FieldValueMap); await this.#updateSingleValues(schemaName, defaultValues); } @@ -767,7 +767,7 @@ export default class DatabaseCore { async #updateNonExtantSingleValues(schemaName: string) { const singleValues = await this.#getNonExtantSingleValues(schemaName); for (const sv of singleValues) { - await this.#updateSingleValue(schemaName, sv.fieldname, sv.value); + await this.#updateSingleValue(schemaName, sv.fieldname, sv.value!); } } @@ -775,7 +775,7 @@ export default class DatabaseCore { const updateMap = { ...fieldValueMap }; delete updateMap.name; - return await this.knex(schemaName) + return await this.knex!(schemaName) .where('name', fieldValueMap.name as string) .update(updateMap); } diff --git a/backend/database/manager.ts b/backend/database/manager.ts index ce5ed3af..51d142b8 100644 --- a/backend/database/manager.ts +++ b/backend/database/manager.ts @@ -6,7 +6,7 @@ import { runPatches } from './runPatch'; import { Patch } from './types'; export class DatabaseManager { - db: DatabaseCore; + db?: DatabaseCore; constructor() {} async createNewDatabase(dbPath: string, countryCode: string) { @@ -26,6 +26,10 @@ export class DatabaseManager { } async migrate() { + if (this.db === undefined) { + return; + } + const patchesToExecute = await this.#getPatchesToExecute(); const preMigrationPatches = patchesToExecute.filter( (p) => p.patch.beforeMigrate @@ -40,21 +44,26 @@ export class DatabaseManager { } async #getPatchesToExecute(): Promise { - const query: { name: string }[] = await this.db - .knex('PatchRun') - .select('name'); + if (this.db === undefined) { + return []; + } + + const query: { name: string }[] = await this.db.knex!('PatchRun').select( + 'name' + ); const executedPatches = query.map((q) => q.name); return patches.filter((p) => !executedPatches.includes(p.name)); } async #getCountryCode(): Promise { - if (!this.db) { + if (this.db === undefined) { return undefined; } - const query = await this.db - .knex('SingleValue') - .where({ fieldname: 'countryCode', parent: 'SystemSettings' }); + const query = await this.db.knex!('SingleValue').where({ + fieldname: 'countryCode', + parent: 'SystemSettings', + }); if (query.length > 0) { return query[0].countryCode as string; @@ -67,7 +76,7 @@ export class DatabaseManager { try { fs.unlink(dbPath); } catch (err) { - if (err.code === 'ENOENT') { + if ((err as NodeJS.ErrnoException).code === 'ENOENT') { return; } diff --git a/backend/database/runPatch.ts b/backend/database/runPatch.ts index 39cf5ece..8c521700 100644 --- a/backend/database/runPatch.ts +++ b/backend/database/runPatch.ts @@ -27,5 +27,5 @@ async function runPatch(patch: Patch, dm: DatabaseManager): Promise { async function makeEntry(patchName: string, dm: DatabaseManager) { const defaultFieldValueMap = getDefaultMetaFieldValueMap() as FieldValueMap; defaultFieldValueMap.name = patchName; - await dm.db.insert('PatchRun', defaultFieldValueMap); + await dm.db!.insert('PatchRun', defaultFieldValueMap); } diff --git a/backend/database/types.ts b/backend/database/types.ts index 45547423..11f50ee0 100644 --- a/backend/database/types.ts +++ b/backend/database/types.ts @@ -1,19 +1,20 @@ import { Field, RawValue } from '../../schemas/types'; +import { DatabaseManager } from './manager'; export type QueryFilter = Record; export interface GetQueryBuilderOptions { - offset: number; - limit: number; - groupBy: string; - orderBy: string; - order: 'desc' | 'asc'; + offset?: number; + limit?: number; + groupBy?: string; + orderBy?: string; + order?: 'desc' | 'asc'; } export interface GetAllOptions { - schemaName?: string; + schemaName: string; fields?: string[]; - filters?: Record; + filters?: QueryFilter; start?: number; limit?: number; groupBy?: string; @@ -31,7 +32,17 @@ export interface Patch { name: string; version: string; patch: { - execute: (DatabaseManager) => Promise; + execute: (dm: DatabaseManager) => Promise; beforeMigrate?: boolean; }; } + +export type KnexColumnType = + | 'text' + | 'integer' + | 'float' + | 'boolean' + | 'date' + | 'datetime' + | 'time' + | 'binary'; diff --git a/schemas/helpers.ts b/schemas/helpers.ts index 65e7a570..93fce200 100644 --- a/schemas/helpers.ts +++ b/schemas/helpers.ts @@ -1,15 +1,15 @@ -export function getMapFromList( +export function getMapFromList( list: T[], - name: string = 'name' + name: K ): Record { const acc: Record = {}; for (const t of list) { - const key = t[name] as string | undefined; + const key = t[name]; if (key === undefined) { continue; } - acc[key] = t; + acc[String(key)] = t; } return acc; } diff --git a/schemas/index.ts b/schemas/index.ts index a71611cc..59f2a443 100644 --- a/schemas/index.ts +++ b/schemas/index.ts @@ -13,8 +13,8 @@ export function getSchemas(countryCode: string = '-'): SchemaMap { return schemaMap; } -function addMetaFields(schemaMap: SchemaMap): SchemaMap { - const metaSchemaMap = getMapFromList(metaSchemas); +export function addMetaFields(schemaMap: SchemaMap): SchemaMap { + const metaSchemaMap = getMapFromList(metaSchemas, 'name'); const base = metaSchemaMap.base; const tree = getCombined(metaSchemaMap.tree, base); @@ -29,15 +29,15 @@ function addMetaFields(schemaMap: SchemaMap): SchemaMap { } if (schema.isTree && schema.isSubmittable) { - schema.fields = [...schema.fields, ...submittableTree.fields]; + schema.fields = [...schema.fields, ...submittableTree.fields!]; } else if (schema.isTree) { - schema.fields = [...schema.fields, ...tree.fields]; + schema.fields = [...schema.fields, ...tree.fields!]; } else if (schema.isSubmittable) { - schema.fields = [...schema.fields, ...submittable.fields]; + schema.fields = [...schema.fields, ...submittable.fields!]; } else if (schema.isChild) { - schema.fields = [...schema.fields, ...child.fields]; + schema.fields = [...schema.fields, ...child.fields!]; } else { - schema.fields = [...schema.fields, ...base.fields]; + schema.fields = [...schema.fields, ...base.fields!]; } } @@ -45,18 +45,23 @@ function addMetaFields(schemaMap: SchemaMap): SchemaMap { } function getCoreSchemas(): SchemaMap { - const rawSchemaMap = getMapFromList(coreSchemas); + const rawSchemaMap = getMapFromList(coreSchemas, 'name'); const coreSchemaMap = getAbstractCombinedSchemas(rawSchemaMap); return cleanSchemas(coreSchemaMap); } function getAppSchemas(countryCode: string): SchemaMap { - const combinedSchemas = getRegionalCombinedSchemas(countryCode); + const appSchemaMap = getMapFromList(appSchemas, 'name'); + const regionalSchemaMap = getRegionalSchemaMap(countryCode); + const combinedSchemas = getRegionalCombinedSchemas( + appSchemaMap, + regionalSchemaMap + ); const schemaMap = getAbstractCombinedSchemas(combinedSchemas); return cleanSchemas(schemaMap); } -function cleanSchemas(schemaMap: SchemaMap): SchemaMap { +export function cleanSchemas(schemaMap: SchemaMap): SchemaMap { for (const name in schemaMap) { const schema = schemaMap[name]; if (schema.isAbstract && !schema.extends) { @@ -97,13 +102,13 @@ function getCombined( return combined; } -function getAbstractCombinedSchemas(schemas: SchemaStubMap): SchemaMap { +export function getAbstractCombinedSchemas(schemas: SchemaStubMap): SchemaMap { const abstractSchemaNames: string[] = Object.keys(schemas).filter( (n) => schemas[n].isAbstract ); const extendingSchemaNames: string[] = Object.keys(schemas).filter((n) => - abstractSchemaNames.includes(schemas[n].extends) + abstractSchemaNames.includes(schemas[n].extends ?? '') ); const completeSchemas: Schema[] = Object.keys(schemas) @@ -113,11 +118,11 @@ function getAbstractCombinedSchemas(schemas: SchemaStubMap): SchemaMap { ) .map((n) => schemas[n] as Schema); - const schemaMap = getMapFromList(completeSchemas) as SchemaMap; + const schemaMap = getMapFromList(completeSchemas, 'name') as SchemaMap; for (const name of extendingSchemaNames) { const extendingSchema = schemas[name] as Schema; - const abstractSchema = schemas[extendingSchema.extends] as SchemaStub; + const abstractSchema = schemas[extendingSchema.extends!] as SchemaStub; schemaMap[name] = getCombined(extendingSchema, abstractSchema) as Schema; } @@ -129,9 +134,10 @@ function getAbstractCombinedSchemas(schemas: SchemaStubMap): SchemaMap { return schemaMap; } -function getRegionalCombinedSchemas(countryCode: string): SchemaStubMap { - const regionalSchemaMap = getRegionalSchema(countryCode); - const appSchemaMap = getMapFromList(appSchemas); +export function getRegionalCombinedSchemas( + appSchemaMap: SchemaStubMap, + regionalSchemaMap: SchemaStubMap +): SchemaStubMap { const combined = { ...appSchemaMap }; for (const name in regionalSchemaMap) { @@ -148,7 +154,7 @@ function getRegionalCombinedSchemas(countryCode: string): SchemaStubMap { return combined; } -function getRegionalSchema(countryCode: string): SchemaStubMap { +function getRegionalSchemaMap(countryCode: string): SchemaStubMap { const countrySchemas = regionalSchemas[countryCode] as | SchemaStub[] | undefined; @@ -156,5 +162,5 @@ function getRegionalSchema(countryCode: string): SchemaStubMap { return {}; } - return getMapFromList(countrySchemas); + return getMapFromList(countrySchemas, 'name'); } diff --git a/schemas/regional/index.ts b/schemas/regional/index.ts index 3616cca1..de55493e 100644 --- a/schemas/regional/index.ts +++ b/schemas/regional/index.ts @@ -1,6 +1,7 @@ +import { SchemaStub } from 'schemas/types'; import IndianSchemas from './in'; /** * Regional Schemas are exported by country code. */ -export default { in: IndianSchemas }; +export default { in: IndianSchemas } as Record; diff --git a/tsconfig.json b/tsconfig.json index 560cf708..e06233d2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,6 +10,7 @@ "skipLibCheck": true, "esModuleInterop": true, "allowSyntheticDefaultImports": true, + "resolveJsonModule": true, "sourceMap": true, "baseUrl": ".", "types": ["webpack-env"], @@ -23,8 +24,8 @@ "src/**/*.tsx", "src/**/*.vue", - "tests/**/*.ts", - "tests/**/*.tsx", + "schemas/**/*.ts", + "backend/**/*.ts", "frappe/**/*.ts", "models/**/*.ts", From 4800112ff408c54feeaf4d307076823fdd492dcd Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Fri, 25 Mar 2022 18:02:37 +0530 Subject: [PATCH 019/163] test: schema builder --- schemas/tests/helpers.ts | 63 +++++++ schemas/tests/testSchemaBuilder.spec.ts | 213 ++++++++++++++++++++++++ schemas/types.ts | 5 +- 3 files changed, 279 insertions(+), 2 deletions(-) create mode 100644 schemas/tests/helpers.ts create mode 100644 schemas/tests/testSchemaBuilder.spec.ts diff --git a/schemas/tests/helpers.ts b/schemas/tests/helpers.ts new file mode 100644 index 00000000..bf512333 --- /dev/null +++ b/schemas/tests/helpers.ts @@ -0,0 +1,63 @@ +import Account from '../app/Account.json'; +import Customer from '../app/Customer.json'; +import JournalEntry from '../app/JournalEntry.json'; +import JournalEntryAccount from '../app/JournalEntryAccount.json'; +import Party from '../app/Party.json'; +import PartyRegional from '../regional/in/Party.json'; +import { Schema, SchemaStub, SchemaStubMap } from '../types'; + +interface AppSchemaMap extends SchemaStubMap { + Account: SchemaStub; + JournalEntry: SchemaStub; + JournalEntryAccount: SchemaStub; + Party: SchemaStub; + Customer: SchemaStub; +} + +interface RegionalSchemaMap extends SchemaStubMap { + Party: SchemaStub; +} + +export function getTestSchemaMap(): { + appSchemaMap: AppSchemaMap; + regionalSchemaMap: RegionalSchemaMap; +} { + const appSchemaMap = { + Account, + JournalEntry, + JournalEntryAccount, + Party, + Customer, + } as AppSchemaMap; + const regionalSchemaMap = { Party: PartyRegional } as RegionalSchemaMap; + + return { + appSchemaMap, + regionalSchemaMap, + }; +} + +export function everyFieldExists(fieldList: string[], schema: Schema): boolean { + return fieldsExist(fieldList, schema, 'every'); +} + +export function someFieldExists(fieldList: string[], schema: Schema): boolean { + return fieldsExist(fieldList, schema, 'some'); +} + +function fieldsExist( + fieldList: string[], + schema: Schema, + type: 'every' | 'some' +): boolean { + const schemaFieldNames = schema.fields.map((f) => f.fieldname); + return fieldList.map((f) => schemaFieldNames.includes(f))[type](Boolean); +} + +export function subtract( + targetList: string[], + ...removalLists: string[][] +): string[] { + const removalList = removalLists.flat(); + return targetList.filter((f) => !removalList.includes(f)); +} diff --git a/schemas/tests/testSchemaBuilder.spec.ts b/schemas/tests/testSchemaBuilder.spec.ts new file mode 100644 index 00000000..eeabafb9 --- /dev/null +++ b/schemas/tests/testSchemaBuilder.spec.ts @@ -0,0 +1,213 @@ +import * as assert from 'assert'; +import { cloneDeep, isEqual } from 'lodash'; +import { describe } from 'mocha'; +import { getMapFromList } from '../helpers'; +import { + addMetaFields, + cleanSchemas, + getAbstractCombinedSchemas, + getRegionalCombinedSchemas, +} from '../index'; +import { metaSchemas } from '../schemas'; +import { + everyFieldExists, + getTestSchemaMap, + someFieldExists, + subtract, +} from './helpers'; + +describe('Schema Builder', function () { + const { appSchemaMap, regionalSchemaMap } = getTestSchemaMap(); + describe('Raw Schemas', function () { + specify('Meta Properties', function () { + assert.strictEqual(appSchemaMap.Party.isAbstract, true); + assert.strictEqual(appSchemaMap.Customer.extends, 'Party'); + assert.strictEqual(appSchemaMap.Account.isTree, true); + assert.strictEqual(appSchemaMap.JournalEntryAccount.isChild, true); + }); + + specify('Field Counts', function () { + assert.strictEqual(appSchemaMap.Account.fields?.length, 6); + assert.strictEqual(appSchemaMap.JournalEntry.fields?.length, 8); + assert.strictEqual(appSchemaMap.JournalEntryAccount.fields?.length, 3); + assert.strictEqual(appSchemaMap.Party.fields?.length, 8); + assert.strictEqual(appSchemaMap.Customer.fields?.length, undefined); + assert.strictEqual(regionalSchemaMap.Party.fields?.length, 2); + }); + + specify('Quick Edit Field Counts', function () { + assert.strictEqual(appSchemaMap.Party.quickEditFields?.length, 5); + assert.strictEqual(regionalSchemaMap.Party.quickEditFields?.length, 7); + }); + }); + + const regionalCombined = getRegionalCombinedSchemas( + appSchemaMap, + regionalSchemaMap + ); + describe('Regional Combined Schemas', function () { + specify('Field Counts', function () { + assert.strictEqual(regionalCombined.Party.fields?.length, 10); + }); + + specify('Quick Edit Field Counts', function () { + assert.strictEqual(regionalSchemaMap.Party.quickEditFields?.length, 7); + }); + + specify('Schema Equality with App Schemas', function () { + assert.strictEqual( + isEqual(regionalCombined.Account, appSchemaMap.Account), + true + ); + assert.strictEqual( + isEqual(regionalCombined.JournalEntry, appSchemaMap.JournalEntry), + true + ); + assert.strictEqual( + isEqual( + regionalCombined.JournalEntryAccount, + appSchemaMap.JournalEntryAccount + ), + true + ); + assert.strictEqual( + isEqual(regionalCombined.Customer, appSchemaMap.Customer), + true + ); + assert.strictEqual( + isEqual(regionalCombined.Party, appSchemaMap.Party), + false + ); + }); + }); + + const abstractCombined = cleanSchemas( + getAbstractCombinedSchemas(regionalCombined) + ); + describe('Abstract Combined Schemas', function () { + specify('Meta Properties', function () { + assert.strictEqual(abstractCombined.Customer.extends, undefined); + }); + + specify('Abstract Schema Existance', function () { + assert.strictEqual(abstractCombined.Party, undefined); + }); + + specify('Field Counts', function () { + assert.strictEqual(abstractCombined.Customer.fields?.length, 10); + }); + + specify('Quick Edit Field Counts', function () { + assert.strictEqual(abstractCombined.Customer.quickEditFields?.length, 7); + }); + + specify('Schema Equality with App Schemas', function () { + assert.strictEqual( + isEqual(abstractCombined.Account, appSchemaMap.Account), + true + ); + assert.strictEqual( + isEqual(abstractCombined.JournalEntry, appSchemaMap.JournalEntry), + true + ); + assert.strictEqual( + isEqual( + abstractCombined.JournalEntryAccount, + appSchemaMap.JournalEntryAccount + ), + true + ); + assert.strictEqual( + isEqual(abstractCombined.Customer, appSchemaMap.Customer), + false + ); + }); + + specify('Schema Field Existance', function () { + assert.strictEqual( + everyFieldExists( + regionalSchemaMap.Party.quickEditFields ?? [], + abstractCombined.Customer + ), + true + ); + }); + }); + + const finalSchemas = addMetaFields(cloneDeep(abstractCombined)); + const metaSchemaMap = getMapFromList(metaSchemas, 'name'); + const baseFieldNames = metaSchemaMap.base.fields!.map((f) => f.fieldname); + const childFieldNames = metaSchemaMap.child.fields!.map((f) => f.fieldname); + const treeFieldNames = metaSchemaMap.tree.fields!.map((f) => f.fieldname); + const submittableFieldNames = metaSchemaMap.submittable.fields!.map( + (f) => f.fieldname + ); + const allFieldNames = [ + ...baseFieldNames, + ...childFieldNames, + ...treeFieldNames, + ...submittableFieldNames, + ]; + + describe('Final Schemas', function () { + specify('Schema Field Existance', function () { + assert.strictEqual( + everyFieldExists(baseFieldNames, finalSchemas.Customer), + true + ); + + assert.strictEqual( + someFieldExists( + subtract(allFieldNames, baseFieldNames), + finalSchemas.Customer + ), + false + ); + + assert.strictEqual( + everyFieldExists( + [...baseFieldNames, ...submittableFieldNames], + finalSchemas.JournalEntry + ), + true + ); + + assert.strictEqual( + someFieldExists( + subtract(allFieldNames, baseFieldNames, submittableFieldNames), + finalSchemas.JournalEntry + ), + false + ); + + assert.strictEqual( + everyFieldExists(childFieldNames, finalSchemas.JournalEntryAccount), + true + ); + + assert.strictEqual( + someFieldExists( + subtract(allFieldNames, childFieldNames), + finalSchemas.JournalEntryAccount + ), + false + ); + + assert.strictEqual( + everyFieldExists( + [...treeFieldNames, ...baseFieldNames], + finalSchemas.Account + ), + true + ); + + assert.strictEqual( + someFieldExists( + subtract(allFieldNames, treeFieldNames, baseFieldNames), + finalSchemas.Account + ), + false + ); + }); + }); +}); diff --git a/schemas/types.ts b/schemas/types.ts index 1e531ac7..269e3f44 100644 --- a/schemas/types.ts +++ b/schemas/types.ts @@ -107,7 +107,7 @@ export type Field = export type TreeSettings = { parentField: string }; -// prettier-ignore +// @formattoer:off export interface Schema { name: string; // Table PK label: string; // Translateable UI facing name @@ -118,7 +118,8 @@ export interface Schema { isSingle?: boolean; // Fields will be values in SingleValue, i.e. an Entity Attr. Value isAbstract?: boolean; // Not entered into db, used to extend a Subclass schema isSubmittable?: boolean; // For transactional types, values considered only after submit - keywordFields?: string[]; // Used for 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 treeSettings?: TreeSettings; // Used to determine root nodes } From f7a2dd8f2b342135ff413bac2e2e24eb1bedf750 Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Mon, 28 Mar 2022 15:31:29 +0530 Subject: [PATCH 020/163] test: start with dbCore tests --- backend/database/core.ts | 48 ++++--- backend/database/tests/helpers.ts | 164 ++++++++++++++++++++++++ backend/database/tests/testCore.spec.ts | 126 ++++++++++++++++++ backend/database/types.ts | 10 ++ package.json | 3 +- schemas/app/Payment.json | 1 - schemas/app/SalesInvoice.json | 1 - schemas/index.ts | 27 +++- schemas/schemas.ts | 3 +- schemas/tests/helpers.ts | 5 +- schemas/types.ts | 11 +- src/main.js | 2 + tsconfig.json | 5 +- vue.config.js | 3 + yarn.lock | 25 ++++ 15 files changed, 403 insertions(+), 31 deletions(-) create mode 100644 backend/database/tests/helpers.ts create mode 100644 backend/database/tests/testCore.spec.ts diff --git a/backend/database/core.ts b/backend/database/core.ts index 65fe7415..11077980 100644 --- a/backend/database/core.ts +++ b/backend/database/core.ts @@ -24,6 +24,17 @@ import { QueryFilter } from './types'; +/** + * Db Core Call Sequence + * + * 1. Init core: `const db = new DatabaseCore(dbPath)`. + * 2. Connect db: `db.connect()`. This will allow for raw queries to be executed. + * 3. Set schemas: `bb.setSchemaMap(schemaMap)`. This will allow for ORM functions to be executed. + * 4. Migrate: `await db.migrate()`. This will create absent tables and update the tables' shape. + * 5. ORM function execution: `db.get(...)`, `db.insert(...)`, etc. + * 6. Close connection: `await db.close()`. + */ + export default class DatabaseCore { knex?: Knex; typeMap = sqliteTypeMap; @@ -60,13 +71,13 @@ export default class DatabaseCore { }); } - close() { - this.knex!.destroy(); + async close() { + await this.knex!.destroy(); } async commit() { try { - await this.raw('commit'); + await this.knex!.raw('commit'); } catch (err) { const type = this.#getError(err as Error); if (type !== CannotCommitError) { @@ -75,10 +86,6 @@ export default class DatabaseCore { } } - async raw(query: string, params: Knex.RawBinding[] = []) { - return await this.knex!.raw(query, params); - } - async migrate() { for (const schemaName in this.schemaMap) { const schema = this.schemaMap[schemaName]; @@ -350,14 +357,14 @@ export default class DatabaseCore { } async #getTableColumns(schemaName: string): Promise { - const info: FieldValueMap[] = await this.raw( + const info: FieldValueMap[] = await this.knex!.raw( `PRAGMA table_info(${schemaName})` ); return info.map((d) => d.name as string); } async #getForeignKeys(schemaName: string): Promise { - const foreignKeyList: FieldValueMap[] = await this.raw( + const foreignKeyList: FieldValueMap[] = await this.knex!.raw( `PRAGMA foreign_key_list(${schemaName})` ); return foreignKeyList.map((d) => d.from as string); @@ -506,14 +513,17 @@ export default class DatabaseCore { } // required - if (field.required || field.fieldtype === 'Currency') { + if (field.required) { column.notNullable(); } // link - if (field.fieldtype === 'Link' && (field as TargetField).target) { - const targetschemaName = (field as TargetField).target as string; - const schema = this.schemaMap[targetschemaName]; + if ( + field.fieldtype === FieldTypeEnum.Link && + (field as TargetField).target + ) { + const targetSchemaName = (field as TargetField).target as string; + const schema = this.schemaMap[targetSchemaName]; table .foreign(field.fieldname) .references('name') @@ -614,8 +624,8 @@ export default class DatabaseCore { } async #addForeignKeys(schemaName: string, newForeignKeys: Field[]) { - await this.raw('PRAGMA foreign_keys=OFF'); - await this.raw('BEGIN TRANSACTION'); + await this.knex!.raw('PRAGMA foreign_keys=OFF'); + await this.knex!.raw('BEGIN TRANSACTION'); const tempName = 'TEMP' + schemaName; @@ -626,8 +636,8 @@ export default class DatabaseCore { // copy from old to new table await this.knex!(tempName).insert(this.knex!.select().from(schemaName)); } catch (err) { - await this.raw('ROLLBACK'); - await this.raw('PRAGMA foreign_keys=ON'); + await this.knex!.raw('ROLLBACK'); + await this.knex!.raw('PRAGMA foreign_keys=ON'); const rows = await this.knex!.select().from(schemaName); await this.prestigeTheTable(schemaName, rows); @@ -640,8 +650,8 @@ export default class DatabaseCore { // rename new table await this.knex!.schema.renameTable(tempName, schemaName); - await this.raw('COMMIT'); - await this.raw('PRAGMA foreign_keys=ON'); + await this.knex!.raw('COMMIT'); + await this.knex!.raw('PRAGMA foreign_keys=ON'); } async #loadChildren( diff --git a/backend/database/tests/helpers.ts b/backend/database/tests/helpers.ts new file mode 100644 index 00000000..f9c86206 --- /dev/null +++ b/backend/database/tests/helpers.ts @@ -0,0 +1,164 @@ +import { cloneDeep } from 'lodash'; +import { SchemaMap, SchemaStub, SchemaStubMap } from 'schemas/types'; +import { + addMetaFields, + cleanSchemas, + getAbstractCombinedSchemas, +} from '../../../schemas'; +import SingleValue from '../../../schemas/core/SingleValue.json'; + +const Customer = { + name: 'Customer', + label: 'Customer', + fields: [ + { + fieldname: 'name', + label: 'Name', + fieldtype: 'Data', + default: 'John Thoe', + required: true, + }, + { + fieldname: 'email', + label: 'Email', + fieldtype: 'Data', + placeholder: 'john@thoe.com', + }, + ], + quickEditFields: ['email'], + keywordFields: ['name'], +}; + +const SalesInvoiceItem = { + name: 'SalesInvoiceItem', + label: 'Sales Invoice Item', + isChild: true, + fields: [ + { + fieldname: 'item', + label: 'Item', + fieldtype: 'Data', + required: true, + }, + { + fieldname: 'quantity', + label: 'Quantity', + fieldtype: 'Float', + required: true, + default: 1, + }, + { + fieldname: 'rate', + label: 'Rate', + fieldtype: 'Currency', + required: true, + }, + { + fieldname: 'amount', + label: 'Amount', + fieldtype: 'Currency', + computed: true, + readOnly: true, + }, + ], + tableFields: ['item', 'quantity', 'rate', 'amount'], +}; + +const SalesInvoice = { + name: 'SalesInvoice', + label: 'Sales Invoice', + isSingle: false, + isChild: false, + isSubmittable: true, + keywordFields: ['name', 'customer'], + fields: [ + { + label: 'Invoice No', + fieldname: 'name', + fieldtype: 'Data', + required: true, + readOnly: true, + }, + { + fieldname: 'date', + label: 'Date', + fieldtype: 'Date', + }, + { + fieldname: 'customer', + label: 'Customer', + fieldtype: 'Link', + target: 'Customer', + required: true, + }, + { + fieldname: 'account', + label: 'Account', + fieldtype: 'Data', + required: true, + }, + { + fieldname: 'items', + label: 'Items', + fieldtype: 'Table', + target: 'SalesInvoiceItem', + required: true, + }, + { + fieldname: 'grandTotal', + label: 'Grand Total', + fieldtype: 'Currency', + computed: true, + readOnly: true, + }, + ], +}; + +const SystemSettings = { + name: 'SystemSettings', + label: 'System Settings', + isSingle: true, + isChild: false, + fields: [ + { + fieldname: 'dateFormat', + label: 'Date Format', + fieldtype: 'Select', + options: [ + { + label: '23/03/2022', + value: 'dd/MM/yyyy', + }, + { + label: '03/23/2022', + value: 'MM/dd/yyyy', + }, + ], + default: 'dd/MM/yyyy', + required: true, + }, + { + fieldname: 'locale', + label: 'Locale', + fieldtype: 'Data', + default: 'en-IN', + }, + ], + quickEditFields: ['locale', 'dateFormat'], + keywordFields: [], +}; + +export function getBuiltTestSchemaMap(): SchemaMap { + const testSchemaMap: SchemaStubMap = { + SingleValue: SingleValue as SchemaStub, + Customer: Customer as SchemaStub, + SalesInvoice: SalesInvoice as SchemaStub, + SalesInvoiceItem: SalesInvoiceItem as SchemaStub, + SystemSettings: SystemSettings as SchemaStub, + }; + + const schemaMapClone = cloneDeep(testSchemaMap); + const abstractCombined = getAbstractCombinedSchemas(schemaMapClone); + const cleanedSchemas = cleanSchemas(abstractCombined); + return addMetaFields(cleanedSchemas); +} diff --git a/backend/database/tests/testCore.spec.ts b/backend/database/tests/testCore.spec.ts new file mode 100644 index 00000000..810a526e --- /dev/null +++ b/backend/database/tests/testCore.spec.ts @@ -0,0 +1,126 @@ +import * as assert from 'assert'; +import 'mocha'; +import { getMapFromList } from 'schemas/helpers'; +import { FieldTypeEnum } from 'schemas/types'; +import { sqliteTypeMap } from '../../common'; +import DatabaseCore from '../core'; +import { SqliteTableInfo } from '../types'; +import { getBuiltTestSchemaMap } from './helpers'; + +describe('DatabaseCore: Connect Migrate Close', async function () { + const db = new DatabaseCore(); + specify('dbPath', function () { + assert.strictEqual(db.dbPath, ':memory:'); + }); + + const schemaMap = getBuiltTestSchemaMap(); + db.setSchemaMap(schemaMap); + specify('schemaMap', function () { + assert.strictEqual(schemaMap, db.schemaMap); + }); + + specify('connect', function () { + assert.doesNotThrow(() => db.connect()); + assert.notStrictEqual(db.knex, undefined); + }); + + specify('migrate and close', async function () { + // Does not throw + await db.migrate(); + // Does not throw + await db.close(); + }); +}); + +describe('DatabaseCore: Migrate and Check Db', function () { + let db: DatabaseCore; + const schemaMap = getBuiltTestSchemaMap(); + + this.beforeEach(async function () { + db = new DatabaseCore(); + db.connect(); + db.setSchemaMap(schemaMap); + }); + + this.afterEach(async function () { + await db.close(); + }); + + specify(`Pre Migrate TableInfo`, async function () { + for (const schemaName in schemaMap) { + const columns = await db.knex?.raw('pragma table_info(??)', schemaName); + assert.strictEqual(columns.length, 0, `column count ${schemaName}`); + } + }); + + specify('Post Migrate TableInfo', async function () { + await db.migrate(); + for (const schemaName in schemaMap) { + const schema = schemaMap[schemaName]; + const fieldMap = getMapFromList(schema.fields, 'fieldname'); + const columns: SqliteTableInfo[] = await db.knex!.raw( + 'pragma table_info(??)', + schemaName + ); + + let columnCount = schema.fields.filter( + (f) => f.fieldtype !== FieldTypeEnum.Table + ).length; + + if (schema.isSingle) { + columnCount = 0; + } + + assert.strictEqual( + columns.length, + columnCount, + `${schemaName}:: column count: ${columns.length}, ${columnCount}` + ); + + for (const column of columns) { + const field = fieldMap[column.name]; + const dbColType = sqliteTypeMap[field.fieldtype]; + + assert.strictEqual( + column.name, + field.fieldname, + `${schemaName}.${column.name}:: name check: ${column.name}, ${field.fieldname}` + ); + + assert.strictEqual( + column.type, + dbColType, + `${schemaName}.${column.name}:: type check: ${column.type}, ${dbColType}` + ); + + if (field.required !== undefined) { + assert.strictEqual( + !!column.notnull, + field.required, + `${schemaName}.${column.name}:: notnull check: ${column.notnull}, ${field.required}` + ); + } else { + assert.strictEqual( + column.notnull, + 0, + `${schemaName}.${column.name}:: notnull check: ${column.notnull}, ${field.required}` + ); + } + + if (column.dflt_value === null) { + assert.strictEqual( + field.default, + undefined, + `${schemaName}.${column.name}:: dflt_value check: ${column.dflt_value}, ${field.default}` + ); + } else { + assert.strictEqual( + column.dflt_value.slice(1, -1), + String(field.default), + `${schemaName}.${column.name}:: dflt_value check: ${column.type}, ${dbColType}` + ); + } + } + } + }); +}); diff --git a/backend/database/types.ts b/backend/database/types.ts index 11f50ee0..b7aba7f0 100644 --- a/backend/database/types.ts +++ b/backend/database/types.ts @@ -46,3 +46,13 @@ export type KnexColumnType = | 'datetime' | 'time' | 'binary'; + +// Returned by pragma table_info +export interface SqliteTableInfo { + pk: number; + cid: number; + name: string; + type: string; + notnull: number; // 0 | 1 + dflt_value: string | null; +} diff --git a/package.json b/package.json index f8f3f6ae..25e5b7e7 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "postinstall": "electron-builder install-app-deps", "postuninstall": "electron-builder install-app-deps", "script:translate": "node scripts/generateTranslations.js", - "test": "TS_NODE_COMPILER_OPTIONS='{\"module\":\"commonjs\"}' mocha --reporter nyan --require ts-node/register ./**/tests/**/*.spec.ts" + "test": "TS_NODE_COMPILER_OPTIONS='{\"module\":\"commonjs\"}' mocha --reporter nyan --require ts-node/register --require tsconfig-paths/register ./**/tests/**/*.spec.ts" }, "dependencies": { "@popperjs/core": "^2.10.2", @@ -66,6 +66,7 @@ "raw-loader": "^4.0.2", "tailwindcss": "npm:@tailwindcss/postcss7-compat", "ts-node": "^10.7.0", + "tsconfig-paths": "^3.14.1", "tslib": "^2.3.1", "typescript": "^4.6.2", "vue-cli-plugin-electron-builder": "^2.0.0", diff --git a/schemas/app/Payment.json b/schemas/app/Payment.json index 7d614385..7aa1463e 100644 --- a/schemas/app/Payment.json +++ b/schemas/app/Payment.json @@ -4,7 +4,6 @@ "isSingle": false, "isChild": false, "isSubmittable": true, - "settings": "PaymentSettings", "fields": [ { "label": "Payment No", diff --git a/schemas/app/SalesInvoice.json b/schemas/app/SalesInvoice.json index 76a74b62..11b93d6a 100644 --- a/schemas/app/SalesInvoice.json +++ b/schemas/app/SalesInvoice.json @@ -5,7 +5,6 @@ "isChild": false, "isSubmittable": true, "keywordFields": ["name", "customer"], - "settings": "SalesInvoiceSettings", "fields": [ { "label": "Invoice No", diff --git a/schemas/index.ts b/schemas/index.ts index 59f2a443..2ce1e138 100644 --- a/schemas/index.ts +++ b/schemas/index.ts @@ -2,7 +2,15 @@ import { cloneDeep } from 'lodash'; import { getListFromMap, getMapFromList } from './helpers'; import regionalSchemas from './regional'; import { appSchemas, coreSchemas, metaSchemas } from './schemas'; -import { Schema, SchemaMap, SchemaStub, SchemaStubMap } from './types'; +import { Field, Schema, SchemaMap, SchemaStub, SchemaStubMap } from './types'; + +const NAME_FIELD = { + fieldname: 'name', + label: `ID`, + fieldtype: 'Data', + required: true, + readOnly: true, +}; export function getSchemas(countryCode: string = '-'): SchemaMap { const builtCoreSchemas = getCoreSchemas(); @@ -41,9 +49,26 @@ export function addMetaFields(schemaMap: SchemaMap): SchemaMap { } } + addNameField(schemaMap); return schemaMap; } +function addNameField(schemaMap: SchemaMap) { + for (const name in schemaMap) { + const schema = schemaMap[name]; + if (schema.isSingle) { + continue; + } + + const pkField = schema.fields.find((f) => f.fieldname === 'name'); + if (pkField !== undefined) { + continue; + } + + schema.fields.push(NAME_FIELD as Field); + } +} + function getCoreSchemas(): SchemaMap { const rawSchemaMap = getMapFromList(coreSchemas, 'name'); const coreSchemaMap = getAbstractCombinedSchemas(rawSchemaMap); diff --git a/schemas/schemas.ts b/schemas/schemas.ts index 189d012d..1eeae100 100644 --- a/schemas/schemas.ts +++ b/schemas/schemas.ts @@ -29,9 +29,8 @@ import PatchRun from './core/PatchRun.json'; import SingleValue from './core/SingleValue.json'; import SystemSettings from './core/SystemSettings.json'; import base from './meta/base.json'; -import submittable from './meta/submittable.json'; -//asdf import child from './meta/child.json'; +import submittable from './meta/submittable.json'; import tree from './meta/tree.json'; import { Schema, SchemaStub } from './types'; diff --git a/schemas/tests/helpers.ts b/schemas/tests/helpers.ts index bf512333..0d9cd837 100644 --- a/schemas/tests/helpers.ts +++ b/schemas/tests/helpers.ts @@ -1,3 +1,4 @@ +import { cloneDeep } from 'lodash'; import Account from '../app/Account.json'; import Customer from '../app/Customer.json'; import JournalEntry from '../app/JournalEntry.json'; @@ -31,10 +32,10 @@ export function getTestSchemaMap(): { } as AppSchemaMap; const regionalSchemaMap = { Party: PartyRegional } as RegionalSchemaMap; - return { + return cloneDeep({ appSchemaMap, regionalSchemaMap, - }; + }); } export function everyFieldExists(fieldList: string[], schema: Schema): boolean { diff --git a/schemas/types.ts b/schemas/types.ts index 269e3f44..2dfbf271 100644 --- a/schemas/types.ts +++ b/schemas/types.ts @@ -9,7 +9,6 @@ * If any field has to have a dynamic value, it should be added to the controller * file by the same name. * - * * There are a few types of schemas: * - _Regional_: Schemas that are in the '../regional' subdirectories * these can be of any of the below types. @@ -31,6 +30,11 @@ * * Note: if a Regional schema is not present as a non regional variant it's used * as it is. + * + * ## Additional Notes + * + * In all the schemas, the 'name' field/column is the primary key. If it isn't + * explicitly added, the schema builder will add it in. */ export enum FieldTypeEnum { @@ -107,9 +111,9 @@ export type Field = export type TreeSettings = { parentField: string }; -// @formattoer:off +// @formatter:off export interface Schema { - name: string; // Table PK + name: string; // Table name label: string; // Translateable UI facing name fields: Field[]; // Maps to database columns isTree?: boolean; // Used for nested set, eg for Chart of Accounts @@ -117,6 +121,7 @@ export interface Schema { isChild?: boolean; // Indicates a child table, i.e table with "parent" FK column isSingle?: boolean; // Fields will be values in SingleValue, i.e. an Entity Attr. Value isAbstract?: boolean; // Not entered into db, used to extend a Subclass schema + tableFields?: string[] // Used for displaying childTableFields 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 diff --git a/src/main.js b/src/main.js index a0264169..33bb2f59 100644 --- a/src/main.js +++ b/src/main.js @@ -1,6 +1,7 @@ import { ipcRenderer } from 'electron'; import frappe from 'frappe'; import { createApp } from 'vue'; +import { getBuiltTestSchemaMap } from '../backend/database/tests/helpers'; import { getSchemas } from '../schemas'; import App from './App'; import FeatherIcon from './components/FeatherIcon'; @@ -104,3 +105,4 @@ import { setLanguageMap, stringifyCircular } from './utils'; })(); window.gs = getSchemas; +window.gst = getBuiltTestSchemaMap; diff --git a/tsconfig.json b/tsconfig.json index e06233d2..c890372a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,7 +15,10 @@ "baseUrl": ".", "types": ["webpack-env"], "paths": { - "@/*": ["src/*"] + "@/*": ["src/*"], + "schemas/*": ["schemas/*"], + "backend/*": ["backend/*"], + "common/*": ["common/*"] }, "lib": ["esnext", "dom", "dom.iterable", "scripthost"] }, diff --git a/vue.config.js b/vue.config.js index 890a3e37..04f17139 100644 --- a/vue.config.js +++ b/vue.config.js @@ -40,6 +40,9 @@ module.exports = { Object.assign(config.resolve.alias, { frappe: path.resolve(__dirname, './frappe'), '~': path.resolve('.'), + schemas: path.resolve(__dirname, './schemas'), + backend: path.resolve(__dirname, './backend'), + common: path.resolve(__dirname, './common'), }); config.plugins.push( diff --git a/yarn.lock b/yarn.lock index 5f4f31ac..f591ad28 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1338,6 +1338,11 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d" integrity sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ== +"@types/json5@^0.0.29": + version "0.0.29" + resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" + integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= + "@types/lodash@^4.14.179": version "4.14.179" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.179.tgz#490ec3288088c91295780237d2497a3aa9dfb5c5" @@ -7887,6 +7892,11 @@ minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== +minimist@^1.2.6: + version "1.2.6" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" + integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== + minipass-collect@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/minipass-collect/-/minipass-collect-1.0.2.tgz#22b813bf745dc6edba2576b940022ad6edc8c617" @@ -10705,6 +10715,11 @@ strip-ansi@^6.0.0, strip-ansi@^6.0.1: dependencies: ansi-regex "^5.0.1" +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= + strip-eof@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" @@ -11177,6 +11192,16 @@ ts-pnp@^1.1.6: resolved "https://registry.yarnpkg.com/ts-pnp/-/ts-pnp-1.2.0.tgz#a500ad084b0798f1c3071af391e65912c86bca92" integrity sha512-csd+vJOb/gkzvcCHgTGSChYpy5f1/XKNsmvBGO4JXS+z1v2HobugDz4s1IeFXM3wZB44uczs+eazB5Q/ccdhQw== +tsconfig-paths@^3.14.1: + version "3.14.1" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz#ba0734599e8ea36c862798e920bcf163277b137a" + integrity sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ== + dependencies: + "@types/json5" "^0.0.29" + json5 "^1.0.1" + minimist "^1.2.6" + strip-bom "^3.0.0" + tslib@^1.8.0, tslib@^1.8.1, tslib@^1.9.0: version "1.14.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" From f5d795d95b88f8ba61b116af2675ed469df20415 Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Tue, 29 Mar 2022 13:48:39 +0530 Subject: [PATCH 021/163] test: add tests for singlevalue CRUD - add utils --- backend/common.ts | 10 +- backend/database/core.ts | 78 ++++++++---- backend/database/tests/helpers.ts | 9 ++ backend/database/tests/testCore.spec.ts | 156 +++++++++++++++++++++++- tsconfig.json | 3 +- utils/index.ts | 29 +++++ vue.config.js | 1 + 7 files changed, 253 insertions(+), 33 deletions(-) create mode 100644 utils/index.ts diff --git a/backend/common.ts b/backend/common.ts index 79a885cb..9eb9cb60 100644 --- a/backend/common.ts +++ b/backend/common.ts @@ -23,12 +23,14 @@ export const sqliteTypeMap: Record = { Color: 'text', }; +export const SYSTEM = '__SYSTEM__'; export const validTypes = Object.keys(sqliteTypeMap); export function getDefaultMetaFieldValueMap() { + const now = new Date().toISOString(); return { - createdBy: '__SYSTEM__', - modifiedBy: '__SYSTEM__', - created: new Date().toISOString(), - modified: new Date().toISOString(), + createdBy: SYSTEM, + modifiedBy: SYSTEM, + created: now, + modified: now, }; } diff --git a/backend/database/core.ts b/backend/database/core.ts index 11077980..c371e07b 100644 --- a/backend/database/core.ts +++ b/backend/database/core.ts @@ -1,5 +1,5 @@ import { knex, Knex } from 'knex'; -import { getRandomString } from '../../frappe/utils'; +import { getRandomString, getValueMapFromList } from 'utils'; import { CannotCommitError, DatabaseError, @@ -15,7 +15,7 @@ import { SchemaMap, TargetField } from '../../schemas/types'; -import { sqliteTypeMap } from '../common'; +import { getDefaultMetaFieldValueMap, sqliteTypeMap, SYSTEM } from '../common'; import { ColumnDiff, FieldValueMap, @@ -155,9 +155,7 @@ export default class DatabaseCore { */ let fieldValueMap: FieldValueMap = {}; if (isSingle) { - fieldValueMap = await this.#getSingle(schemaName); - fieldValueMap.name = schemaName; - return fieldValueMap; + return await this.#getSingle(schemaName); } if (fields !== '*' && typeof fields === 'string') { @@ -203,7 +201,7 @@ export default class DatabaseCore { start, limit, groupBy, - orderBy = 'creation', + orderBy = 'created', order = 'desc', }: GetAllOptions): Promise { const schema = this.schemaMap[schemaName]; @@ -225,9 +223,9 @@ export default class DatabaseCore { } async getSingleValues( - ...fieldnames: { fieldname: string; parent?: string }[] + ...fieldnames: ({ fieldname: string; parent?: string } | string)[] ): Promise<{ fieldname: string; parent: string; value: RawValue }[]> { - fieldnames = fieldnames.map((fieldname) => { + const fieldnameList = fieldnames.map((fieldname) => { if (typeof fieldname === 'string') { return { fieldname }; } @@ -235,9 +233,9 @@ export default class DatabaseCore { }); let builder = this.knex!('SingleValue'); - builder = builder.where(fieldnames[0]); + builder = builder.where(fieldnameList[0]); - fieldnames.slice(1).forEach(({ fieldname, parent }) => { + fieldnameList.slice(1).forEach(({ fieldname, parent }) => { if (typeof parent === 'undefined') { builder = builder.orWhere({ fieldname }); } else { @@ -279,6 +277,12 @@ export default class DatabaseCore { } async delete(schemaName: string, name: string) { + const schema = this.schemaMap[schemaName]; + if (schema.isSingle) { + await this.#deleteSingle(schemaName, name); + return; + } + await this.#deleteOne(schemaName, name); // delete children @@ -305,12 +309,6 @@ export default class DatabaseCore { // TODO: Implement this for sqlite } - async #deleteSingleValues(singleSchemaName: string) { - return await this.knex!('SingleValue') - .where('parent', singleSchemaName) - .delete(); - } - #getError(err: Error) { let errorType = DatabaseError; if (err.message.includes('SQLITE_ERROR: no such table')) { @@ -588,7 +586,13 @@ export default class DatabaseCore { } async #deleteOne(schemaName: string, name: string) { - return this.knex!(schemaName).where('name', name).delete(); + return await this.knex!(schemaName).where('name', name).delete(); + } + + async #deleteSingle(schemaName: string, fieldname: string) { + return await this.knex!('SingleValue') + .where({ parent: schemaName, fieldname }) + .delete(); } #deleteChildren(schemaName: string, parentName: string) { @@ -690,12 +694,7 @@ export default class DatabaseCore { order: 'asc', }); - const fieldValueMap: FieldValueMap = {}; - for (const row of values) { - fieldValueMap[row.fieldname as string] = row.value as RawValue; - } - - return fieldValueMap; + return getValueMapFromList(values, 'fieldname', 'value') as FieldValueMap; } #insertOne(schemaName: string, fieldValueMap: FieldValueMap) { @@ -721,7 +720,6 @@ export default class DatabaseCore { fieldValueMap: FieldValueMap ) { const fields = this.schemaMap[singleSchemaName].fields; - await this.#deleteSingleValues(singleSchemaName); for (const field of fields) { const value = fieldValueMap[field.fieldname] as RawValue | undefined; @@ -738,12 +736,38 @@ export default class DatabaseCore { fieldname: string, value: RawValue ) { - return await this.knex!('SingleValue') + const names: { name: string }[] = await this.knex!('SingleValue') + .select('name') .where({ parent: singleSchemaName, fieldname, - }) - .update({ value }); + }); + const name = names?.[0]?.name as string | undefined; + + if (name === undefined) { + this.#insertSingleValue(singleSchemaName, fieldname, value); + } else { + return await this.knex!('SingleValue').where({ name }).update({ + value, + modifiedBy: SYSTEM, + modified: new Date().toISOString(), + }); + } + } + + async #insertSingleValue( + singleSchemaName: string, + fieldname: string, + value: RawValue + ) { + const updateMap = getDefaultMetaFieldValueMap(); + const fieldValueMap: FieldValueMap = Object.assign({}, updateMap, { + parent: singleSchemaName, + fieldname, + value, + name: getRandomString(), + }); + return await this.knex!('SingleValue').insert(fieldValueMap); } async #initializeSingles() { diff --git a/backend/database/tests/helpers.ts b/backend/database/tests/helpers.ts index f9c86206..1852a4e2 100644 --- a/backend/database/tests/helpers.ts +++ b/backend/database/tests/helpers.ts @@ -162,3 +162,12 @@ export function getBuiltTestSchemaMap(): SchemaMap { const cleanedSchemas = cleanSchemas(abstractCombined); return addMetaFields(cleanedSchemas); } + +export function getBaseMeta() { + return { + createdBy: 'Administrator', + modifiedBy: 'Administrator', + created: new Date().toISOString(), + modified: new Date().toISOString(), + }; +} \ No newline at end of file diff --git a/backend/database/tests/testCore.spec.ts b/backend/database/tests/testCore.spec.ts index 810a526e..68c4c0f9 100644 --- a/backend/database/tests/testCore.spec.ts +++ b/backend/database/tests/testCore.spec.ts @@ -1,7 +1,8 @@ import * as assert from 'assert'; import 'mocha'; import { getMapFromList } from 'schemas/helpers'; -import { FieldTypeEnum } from 'schemas/types'; +import { FieldTypeEnum, RawValue } from 'schemas/types'; +import { getValueMapFromList } from 'utils'; import { sqliteTypeMap } from '../../common'; import DatabaseCore from '../core'; import { SqliteTableInfo } from '../types'; @@ -124,3 +125,156 @@ describe('DatabaseCore: Migrate and Check Db', function () { } }); }); + +describe('DatabaseCore: CRUD', function () { + let db: DatabaseCore; + const schemaMap = getBuiltTestSchemaMap(); + + this.beforeEach(async function () { + db = new DatabaseCore(); + db.connect(); + db.setSchemaMap(schemaMap); + await db.migrate(); + }); + + this.afterEach(async function () { + await db.close(); + }); + + specify('exists() before insertion', async function () { + for (const schemaName in schemaMap) { + const doesExist = await db.exists(schemaName); + if (['SingleValue', 'SystemSettings'].includes(schemaName)) { + assert.strictEqual(doesExist, true, `${schemaName} exists`); + } else { + assert.strictEqual(doesExist, false, `${schemaName} exists`); + } + } + }); + + specify('CRUD single values', async function () { + /** + * Checking default values which are created when db.migrate + * takes place. + */ + let rows: Record[] = await db.knex!.raw( + 'select * from SingleValue' + ); + const defaultMap = getValueMapFromList( + schemaMap.SystemSettings.fields, + 'fieldname', + 'default' + ); + for (const row of rows) { + assert.strictEqual( + row.value, + defaultMap[row.fieldname as string], + `${row.fieldname} default values equality` + ); + } + + /** + * Insertion and updation for single values call the same function. + * + * Insert + */ + + let localeRow = rows.find((r) => r.fieldname === 'locale'); + const localeEntryName = localeRow?.name as string; + const localeEntryCreated = localeRow?.created as string; + + let locale = 'hi-IN'; + await db.insert('SystemSettings', { locale }); + rows = await db.knex!.raw('select * from SingleValue'); + localeRow = rows.find((r) => r.fieldname === 'locale'); + + assert.notStrictEqual(localeEntryName, undefined, 'localeEntryName'); + assert.strictEqual(rows.length, 2, 'row length'); + assert.strictEqual( + localeRow?.name as string, + localeEntryName, + `localeEntryName ${localeRow?.name}, ${localeEntryName}` + ); + assert.strictEqual( + localeRow?.value, + locale, + `locale ${localeRow?.value}, ${locale}` + ); + assert.strictEqual( + localeRow?.created, + localeEntryCreated, + `locale ${localeRow?.value}, ${locale}` + ); + + /** + * Update + */ + locale = 'ca-ES'; + await db.update('SystemSettings', { locale }); + rows = await db.knex!.raw('select * from SingleValue'); + localeRow = rows.find((r) => r.fieldname === 'locale'); + + assert.notStrictEqual(localeEntryName, undefined, 'localeEntryName'); + assert.strictEqual(rows.length, 2, 'row length'); + assert.strictEqual( + localeRow?.name as string, + localeEntryName, + `localeEntryName ${localeRow?.name}, ${localeEntryName}` + ); + assert.strictEqual( + localeRow?.value, + locale, + `locale ${localeRow?.value}, ${locale}` + ); + assert.strictEqual( + localeRow?.created, + localeEntryCreated, + `locale ${localeRow?.value}, ${locale}` + ); + + /** + * Delete + */ + await db.delete('SystemSettings', 'locale'); + rows = await db.knex!.raw('select * from SingleValue'); + assert.strictEqual(rows.length, 1, 'delete one'); + await db.delete('SystemSettings', 'dateFormat'); + rows = await db.knex!.raw('select * from SingleValue'); + assert.strictEqual(rows.length, 0, 'delete two'); + + const dateFormat = 'dd/mm/yy'; + await db.insert('SystemSettings', { locale, dateFormat }); + rows = await db.knex!.raw('select * from SingleValue'); + assert.strictEqual(rows.length, 2, 'delete two'); + + /** + * Read + * + * getSingleValues + */ + const svl = await db.getSingleValues('locale', 'dateFormat'); + assert.strictEqual(svl.length, 2, 'getSingleValues length'); + for (const sv of svl) { + assert.strictEqual( + sv.parent, + 'SystemSettings', + `singleValue parent ${sv.parent}` + ); + assert.strictEqual( + sv.value, + { locale, dateFormat }[sv.fieldname], + `singleValue value ${sv.value}` + ); + + /** + * get + */ + const svlMap = await db.get('SystemSettings'); + assert.strictEqual(Object.keys(svlMap).length, 2, 'get key length'); + assert.strictEqual(svlMap.locale, locale, 'get locale'); + assert.strictEqual(svlMap.dateFormat, dateFormat, 'get locale'); + } + }); + + specify('CRUD simple nondependent schema', async function () {}); +}); diff --git a/tsconfig.json b/tsconfig.json index c890372a..6330a185 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,7 +18,8 @@ "@/*": ["src/*"], "schemas/*": ["schemas/*"], "backend/*": ["backend/*"], - "common/*": ["common/*"] + "common/*": ["common/*"], + "utils/*": ["utils/*"] }, "lib": ["esnext", "dom", "dom.iterable", "scripthost"] }, diff --git a/utils/index.ts b/utils/index.ts new file mode 100644 index 00000000..dcac32b4 --- /dev/null +++ b/utils/index.ts @@ -0,0 +1,29 @@ +/** + * Functions in utils/*.ts can be used by the frontend or the backends + * And so should not contain and platforma specific imports. + */ +export function getValueMapFromList( + list: T[], + key: K, + valueKey: V, + filterUndefined: boolean = true +): Record { + if (filterUndefined) { + list = list.filter( + (f) => + (f[valueKey] as unknown) !== undefined && + (f[key] as unknown) !== undefined + ); + } + + return list.reduce((acc, f) => { + const keyValue = String(f[key]); + const value = f[valueKey]; + acc[keyValue] = value; + return acc; + }, {} as Record); +} + +export function getRandomString(): string { + return Math.random().toString(36).slice(2, 8); +} diff --git a/vue.config.js b/vue.config.js index 04f17139..bc2e0e95 100644 --- a/vue.config.js +++ b/vue.config.js @@ -43,6 +43,7 @@ module.exports = { schemas: path.resolve(__dirname, './schemas'), backend: path.resolve(__dirname, './backend'), common: path.resolve(__dirname, './common'), + utils: path.resolve(__dirname, './utils'), }); config.plugins.push( From 18dd3f8518d8d0a6521889a606c3d0b2d2e3dd11 Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Wed, 30 Mar 2022 12:46:01 +0530 Subject: [PATCH 022/163] test: add more dbcore tests - fix bugs in core.ts --- backend/database/core.ts | 148 ++++++----- backend/database/tests/helpers.ts | 43 ++- backend/database/tests/testCore.spec.ts | 332 +++++++++++++++++++++++- backend/database/types.ts | 3 +- utils/index.ts | 4 + 5 files changed, 451 insertions(+), 79 deletions(-) diff --git a/backend/database/core.ts b/backend/database/core.ts index c371e07b..4468715f 100644 --- a/backend/database/core.ts +++ b/backend/database/core.ts @@ -6,14 +6,14 @@ import { DuplicateEntryError, LinkValidationError, NotFoundError, - ValueError + ValueError, } from '../../frappe/utils/errors'; import { Field, FieldTypeEnum, RawValue, SchemaMap, - TargetField + TargetField, } from '../../schemas/types'; import { getDefaultMetaFieldValueMap, sqliteTypeMap, SYSTEM } from '../common'; import { @@ -21,11 +21,15 @@ import { FieldValueMap, GetAllOptions, GetQueryBuilderOptions, - QueryFilter + QueryFilter, } from './types'; /** - * Db Core Call Sequence + * # DatabaseCore + * This is the ORM, the DatabaseCore interface (function signatures) should be + * replicated by the frontend demuxes and all the backend muxes. + * + * ## Db Core Call Sequence * * 1. Init core: `const db = new DatabaseCore(dbPath)`. * 2. Connect db: `db.connect()`. This will allow for raw queries to be executed. @@ -33,6 +37,10 @@ import { * 4. Migrate: `await db.migrate()`. This will create absent tables and update the tables' shape. * 5. ORM function execution: `db.get(...)`, `db.insert(...)`, etc. * 6. Close connection: `await db.close()`. + * + * Note: Meta values: created, modified, createdBy, modifiedBy are set by DatabaseCore + * only for schemas that are SingleValue. Else they have to be passed by the caller in + * the `fieldValueMap`. */ export default class DatabaseCore { @@ -141,10 +149,10 @@ export default class DatabaseCore { async get( schemaName: string, name: string = '', - fields: string | string[] = '*' + fields?: string | string[] ): Promise { - const isSingle = this.schemaMap[schemaName].isSingle; - if (!isSingle && !name) { + const schema = this.schemaMap[schemaName]; + if (!schema.isSingle && !name) { throw new ValueError('name is mandatory'); } @@ -154,38 +162,37 @@ export default class DatabaseCore { * is ignored. */ let fieldValueMap: FieldValueMap = {}; - if (isSingle) { + if (schema.isSingle) { return await this.#getSingle(schemaName); } - if (fields !== '*' && typeof fields === 'string') { + if (typeof fields === 'string') { fields = [fields]; } + if (fields === undefined) { + fields = schema.fields.map((f) => f.fieldname); + } + /** * Separate table fields and non table fields */ - const allTableFields = this.#getTableFields(schemaName); - const allTableFieldNames = allTableFields.map((f) => f.fieldname); - - let tableFields: TargetField[] = []; - let nonTableFieldNames: string[] = []; - - if (Array.isArray(fields)) { - tableFields = tableFields.filter((f) => fields.includes(f.fieldname)); - nonTableFieldNames = fields.filter( - (f) => !allTableFieldNames.includes(f) - ); - } else if (fields === '*') { - tableFields = allTableFields; - } + const allTableFields: TargetField[] = this.#getTableFields(schemaName); + const allTableFieldNames: string[] = allTableFields.map((f) => f.fieldname); + const tableFields: TargetField[] = allTableFields.filter((f) => + fields!.includes(f.fieldname) + ); + const nonTableFieldNames: string[] = fields.filter( + (f) => !allTableFieldNames.includes(f) + ); /** * If schema is not single then return specific fields * if child fields are selected, all child fields are returned. */ if (nonTableFieldNames.length) { - fieldValueMap = (await this.#getOne(schemaName, name, fields)) ?? {}; + fieldValueMap = + (await this.#getOne(schemaName, name, nonTableFieldNames)) ?? {}; } if (tableFields.length) { @@ -194,32 +201,34 @@ export default class DatabaseCore { return fieldValueMap; } - async getAll({ - schemaName, - fields, - filters, - start, - limit, - groupBy, - orderBy = 'created', - order = 'desc', - }: GetAllOptions): Promise { + async getAll( + schemaName: string, + options: GetAllOptions = {} + ): Promise { const schema = this.schemaMap[schemaName]; - if (!fields) { - fields = ['name', ...(schema.keywordFields ?? [])]; - } - - if (typeof fields === 'string') { - fields = [fields]; - } - - return (await this.#getQueryBuilder(schemaName, fields, filters ?? {}, { - offset: start, + const hasCreated = !!schema.fields.find((f) => f.fieldname === 'created'); + const { + fields = ['name', ...(schema.keywordFields ?? [])], + filters, + offset, limit, groupBy, - orderBy, - order, - })) as FieldValueMap[]; + orderBy = hasCreated ? 'created' : undefined, + order = 'desc', + } = options; + + return (await this.#getQueryBuilder( + schemaName, + typeof fields === 'string' ? [fields] : fields, + filters ?? {}, + { + offset, + limit, + groupBy, + orderBy, + order, + } + )) as FieldValueMap[]; } async getSingleValues( @@ -258,6 +267,11 @@ export default class DatabaseCore { } async rename(schemaName: string, oldName: string, newName: string) { + /** + * Rename is expensive mostly won't allow it. + * TODO: rename all links + * TODO: rename in childtables + */ await this.knex!(schemaName) .update({ name: newName }) .where('name', oldName); @@ -621,7 +635,7 @@ export default class DatabaseCore { if (!child.name) { child.name = getRandomString(); } - child.parentName = parentName; + child.parent = parentName; child.parentSchemaName = parentSchemaName; child.parentFieldname = field.fieldname; child.idx = idx; @@ -663,8 +677,7 @@ export default class DatabaseCore { tableFields: TargetField[] ) { for (const field of tableFields) { - fieldValueMap[field.fieldname] = await this.getAll({ - schemaName: field.target, + fieldValueMap[field.fieldname] = await this.getAll(field.target, { fields: ['*'], filters: { parent: fieldValueMap.name as string }, orderBy: 'idx', @@ -673,11 +686,7 @@ export default class DatabaseCore { } } - async #getOne( - schemaName: string, - name: string, - fields: string | string[] = '*' - ) { + async #getOne(schemaName: string, name: string, fields: string[]) { const fieldValueMap: FieldValueMap = await this.knex!.select(fields) .from(schemaName) .where('name', name) @@ -686,8 +695,7 @@ export default class DatabaseCore { } async #getSingle(schemaName: string): Promise { - const values = await this.getAll({ - schemaName: 'SingleValue', + const values = await this.getAll('SingleValue', { fields: ['fieldname', 'value'], filters: { parent: schemaName }, orderBy: 'fieldname', @@ -698,21 +706,21 @@ export default class DatabaseCore { } #insertOne(schemaName: string, fieldValueMap: FieldValueMap) { - const fields = this.schemaMap[schemaName].fields; if (!fieldValueMap.name) { fieldValueMap.name = getRandomString(); } - const validMap: FieldValueMap = {}; - for (const { fieldname, fieldtype } of fields) { - if (fieldtype === FieldTypeEnum.Table) { - continue; - } + // Non Table Fields + const fields = this.schemaMap[schemaName].fields.filter( + (f) => f.fieldtype !== FieldTypeEnum.Table + ); + const validMap: FieldValueMap = {}; + for (const { fieldname } of fields) { validMap[fieldname] = fieldValueMap[fieldname]; } - return this.knex!(schemaName).insert(fieldValueMap); + return this.knex!(schemaName).insert(validMap); } async #updateSingleValues( @@ -808,6 +816,18 @@ export default class DatabaseCore { async #updateOne(schemaName: string, fieldValueMap: FieldValueMap) { const updateMap = { ...fieldValueMap }; delete updateMap.name; + const schema = this.schemaMap[schemaName]; + for (const { fieldname, fieldtype } of schema.fields) { + if (fieldtype !== FieldTypeEnum.Table) { + continue; + } + + delete updateMap[fieldname]; + } + + if (Object.keys(updateMap).length === 0) { + return; + } return await this.knex!(schemaName) .where('name', fieldValueMap.name as string) diff --git a/backend/database/tests/helpers.ts b/backend/database/tests/helpers.ts index 1852a4e2..448816e1 100644 --- a/backend/database/tests/helpers.ts +++ b/backend/database/tests/helpers.ts @@ -1,3 +1,4 @@ +import assert from 'assert'; import { cloneDeep } from 'lodash'; import { SchemaMap, SchemaStub, SchemaStubMap } from 'schemas/types'; import { @@ -15,7 +16,6 @@ const Customer = { fieldname: 'name', label: 'Name', fieldtype: 'Data', - default: 'John Thoe', required: true, }, { @@ -50,13 +50,13 @@ const SalesInvoiceItem = { { fieldname: 'rate', label: 'Rate', - fieldtype: 'Currency', + fieldtype: 'Float', required: true, }, { fieldname: 'amount', label: 'Amount', - fieldtype: 'Currency', + fieldtype: 'Float', computed: true, readOnly: true, }, @@ -170,4 +170,39 @@ export function getBaseMeta() { created: new Date().toISOString(), modified: new Date().toISOString(), }; -} \ No newline at end of file +} + +export async function assertThrows( + func: () => Promise, + message?: string +) { + let threw = true; + try { + await func(); + threw = false; + } catch { + } finally { + if (!threw) { + throw new assert.AssertionError({ + message: `Missing expected exception: ${message}`, + }); + } + } +} + +export async function assertDoesNotThrow( + func: () => Promise, + message?: string +) { + try { + await func(); + } catch (err) { + throw new assert.AssertionError({ + message: `Missing expected exception: ${message} Error: ${ + (err as Error).message + }`, + }); + } +} + +export type BaseMetaKey = 'created' | 'modified' | 'createdBy' | 'modifiedBy'; diff --git a/backend/database/tests/testCore.spec.ts b/backend/database/tests/testCore.spec.ts index 68c4c0f9..9f0d926b 100644 --- a/backend/database/tests/testCore.spec.ts +++ b/backend/database/tests/testCore.spec.ts @@ -2,13 +2,29 @@ import * as assert from 'assert'; import 'mocha'; import { getMapFromList } from 'schemas/helpers'; import { FieldTypeEnum, RawValue } from 'schemas/types'; -import { getValueMapFromList } from 'utils'; -import { sqliteTypeMap } from '../../common'; +import { getValueMapFromList, sleep } from 'utils'; +import { getDefaultMetaFieldValueMap, sqliteTypeMap } from '../../common'; import DatabaseCore from '../core'; -import { SqliteTableInfo } from '../types'; -import { getBuiltTestSchemaMap } from './helpers'; +import { FieldValueMap, SqliteTableInfo } from '../types'; +import { + assertDoesNotThrow, + assertThrows, + BaseMetaKey, + getBuiltTestSchemaMap, +} from './helpers'; -describe('DatabaseCore: Connect Migrate Close', async function () { +/** + * Note: these tests have a strange structure where multiple tests are + * inside a `specify`, this is cause `describe` doesn't support `async` or waiting + * on promises. + * + * Due to this `async` db operations need to be handled in `specify`. And `specify` + * can't be nested in the `describe` can, hence the strange structure. + * + * This also implies that assert calls should have discriptive + */ + +describe('DatabaseCore: Connect Migrate Close', function () { const db = new DatabaseCore(); specify('dbPath', function () { assert.strictEqual(db.dbPath, ':memory:'); @@ -98,7 +114,7 @@ describe('DatabaseCore: Migrate and Check Db', function () { assert.strictEqual( !!column.notnull, field.required, - `${schemaName}.${column.name}:: notnull check: ${column.notnull}, ${field.required}` + `${schemaName}.${column.name}:: iotnull iheck: ${column.notnull}, ${field.required}` ); } else { assert.strictEqual( @@ -189,7 +205,7 @@ describe('DatabaseCore: CRUD', function () { localeRow = rows.find((r) => r.fieldname === 'locale'); assert.notStrictEqual(localeEntryName, undefined, 'localeEntryName'); - assert.strictEqual(rows.length, 2, 'row length'); + assert.strictEqual(rows.length, 2, 'rows length insert'); assert.strictEqual( localeRow?.name as string, localeEntryName, @@ -215,7 +231,7 @@ describe('DatabaseCore: CRUD', function () { localeRow = rows.find((r) => r.fieldname === 'locale'); assert.notStrictEqual(localeEntryName, undefined, 'localeEntryName'); - assert.strictEqual(rows.length, 2, 'row length'); + assert.strictEqual(rows.length, 2, 'rows length update'); assert.strictEqual( localeRow?.name as string, localeEntryName, @@ -276,5 +292,303 @@ describe('DatabaseCore: CRUD', function () { } }); - specify('CRUD simple nondependent schema', async function () {}); + specify('CRUD nondependent schema', async function () { + const schemaName = 'Customer'; + let rows = await db.knex!(schemaName); + assert.strictEqual(rows.length, 0, 'rows length before insertion'); + + /** + * Insert + */ + const metaValues = getDefaultMetaFieldValueMap(); + const name = 'John Thoe'; + + await assertThrows( + async () => await db.insert(schemaName, { name }), + 'insert() did not throw without meta values' + ); + + const updateMap = Object.assign({}, metaValues, { name }); + await db.insert(schemaName, updateMap); + rows = await db.knex!(schemaName); + let firstRow = rows?.[0]; + assert.strictEqual(rows.length, 1, `rows length insert ${rows.length}`); + assert.strictEqual( + firstRow.name, + name, + `name check ${firstRow.name}, ${name}` + ); + assert.strictEqual(firstRow.email, null, `email check ${firstRow.email}`); + + for (const key in metaValues) { + assert.strictEqual( + firstRow[key], + metaValues[key as BaseMetaKey], + `${key} check` + ); + } + + /** + * Update + */ + const email = 'john@thoe.com'; + await sleep(1); // required for modified to change + await db.update(schemaName, { + name, + email, + modified: new Date().toISOString(), + }); + rows = await db.knex!(schemaName); + firstRow = rows?.[0]; + assert.strictEqual(rows.length, 1, `rows length update ${rows.length}`); + assert.strictEqual( + firstRow.name, + name, + `name check update ${firstRow.name}, ${name}` + ); + assert.strictEqual( + firstRow.email, + email, + `email check update ${firstRow.email}` + ); + + for (const key in metaValues) { + const val = firstRow[key]; + const expected = metaValues[key as BaseMetaKey]; + if (key !== 'modified') { + assert.strictEqual(val, expected, `${key} check ${val}, ${expected}`); + } else { + assert.notStrictEqual( + val, + expected, + `${key} check ${val}, ${expected}` + ); + } + } + + /** + * Delete + */ + await db.delete(schemaName, name); + rows = await db.knex!(schemaName); + assert.strictEqual(rows.length, 0, `rows length delete ${rows.length}`); + + /** + * Get + */ + let fvMap = await db.get(schemaName, name); + assert.strictEqual( + Object.keys(fvMap).length, + 0, + `key count get ${JSON.stringify(fvMap)}` + ); + + /** + * > 1 entries + */ + + const cOne = { name: 'John Whoe', ...getDefaultMetaFieldValueMap() }; + const cTwo = { name: 'Jane Whoe', ...getDefaultMetaFieldValueMap() }; + + // Insert + await db.insert(schemaName, cOne); + assert.strictEqual( + (await db.knex!(schemaName)).length, + 1, + `rows length minsert` + ); + await db.insert(schemaName, cTwo); + rows = await db.knex!(schemaName); + assert.strictEqual(rows.length, 2, `rows length minsert`); + + const cs = [cOne, cTwo]; + for (const i in cs) { + for (const k in cs[i]) { + const val = cs[i][k as BaseMetaKey]; + assert.strictEqual( + rows?.[i]?.[k], + val, + `equality check ${i} ${k} ${val} ${rows?.[i]?.[k]}` + ); + } + } + + // Update + await db.update(schemaName, { name: cOne.name, email }); + const cOneEmail = await db.get(schemaName, cOne.name, 'email'); + assert.strictEqual( + cOneEmail.email, + email, + `mi update check one ${cOneEmail}` + ); + const cTwoEmail = await db.get(schemaName, cTwo.name, 'email'); + assert.strictEqual( + cOneEmail.email, + email, + `mi update check two ${cTwoEmail}` + ); + + // Rename + const newName = 'Johnny Whoe'; + await db.rename(schemaName, cOne.name, newName); + + fvMap = await db.get(schemaName, cOne.name); + assert.strictEqual( + Object.keys(fvMap).length, + 0, + `mi rename check old ${JSON.stringify(fvMap)}` + ); + + fvMap = await db.get(schemaName, newName); + assert.strictEqual( + fvMap.email, + email, + `mi rename check new ${JSON.stringify(fvMap)}` + ); + + // Delete + await db.delete(schemaName, newName); + rows = await db.knex!(schemaName); + assert.strictEqual(rows.length, 1, `mi delete length ${rows.length}`); + assert.strictEqual( + rows[0].name, + cTwo.name, + `mi delete name ${rows[0].name}` + ); + }); + + specify('CRUD dependent schema', async function () { + const Customer = 'Customer'; + const SalesInvoice = 'SalesInvoice'; + const SalesInvoiceItem = 'SalesInvoiceItem'; + + const customer: FieldValueMap = { + name: 'John Whoe', + email: 'john@whoe.com', + ...getDefaultMetaFieldValueMap(), + }; + + const invoice: FieldValueMap = { + name: 'SINV-1001', + date: '2022-01-21', + customer: customer.name, + account: 'Debtors', + submitted: false, + cancelled: false, + ...getDefaultMetaFieldValueMap(), + }; + + await assertThrows( + async () => await db.insert(SalesInvoice, invoice), + 'foreign key constraint fail failed' + ); + + await assertDoesNotThrow(async () => { + await db.insert(Customer, customer); + await db.insert(SalesInvoice, invoice); + }, 'insertion failed'); + + await assertThrows( + async () => await db.delete(Customer, customer.name as string), + 'foreign key constraint fail failed' + ); + + await assertDoesNotThrow(async () => { + await db.delete(SalesInvoice, invoice.name as string); + await db.delete(Customer, customer.name as string); + }, 'deletion failed'); + + await db.insert(Customer, customer); + await db.insert(SalesInvoice, invoice); + + let fvMap = await db.get(SalesInvoice, invoice.name as string); + for (const key in invoice) { + let expected = invoice[key]; + if (typeof expected === 'boolean') { + expected = +expected; + } + + assert.strictEqual( + fvMap[key], + expected, + `equality check ${key}: ${fvMap[key]}, ${invoice[key]}` + ); + } + + assert.strictEqual( + (fvMap.items as unknown[])?.length, + 0, + 'empty items check' + ); + + const items: FieldValueMap[] = [ + { + item: 'Bottle Caps', + quantity: 2, + rate: 100, + amount: 200, + }, + ]; + + await assertThrows( + async () => await db.insert(SalesInvoice, { name: invoice.name, items }), + 'invoice insertion with ct did not fail' + ); + await assertDoesNotThrow( + async () => await db.update(SalesInvoice, { name: invoice.name, items }), + 'ct insertion failed' + ); + + fvMap = await db.get(SalesInvoice, invoice.name as string); + const ct = fvMap.items as FieldValueMap[]; + assert.strictEqual(ct.length, 1, `ct length ${ct.length}`); + assert.strictEqual(ct[0].parent, invoice.name, `ct parent ${ct[0].parent}`); + assert.strictEqual( + ct[0].parentFieldname, + 'items', + `ct parentFieldname ${ct[0].parentFieldname}` + ); + assert.strictEqual( + ct[0].parentSchemaName, + SalesInvoice, + `ct parentSchemaName ${ct[0].parentSchemaName}` + ); + for (const key in items[0]) { + assert.strictEqual( + ct[0][key], + items[0][key], + `ct values ${key}: ${ct[0][key]}, ${items[0][key]}` + ); + } + + items.push({ + item: 'Mentats', + quantity: 4, + rate: 200, + amount: 800, + }); + await assertDoesNotThrow( + async () => await db.update(SalesInvoice, { name: invoice.name, items }), + 'ct updation failed' + ); + + let rows = await db.getAll(SalesInvoiceItem, { + fields: ['item', 'quantity', 'rate', 'amount'], + }); + assert.strictEqual(rows.length, 2, `ct length update ${rows.length}`); + + for (const i in rows) { + for (const key in rows[i]) { + assert.strictEqual( + rows[i][key], + items[i][key], + `ct values ${i},${key}: ${rows[i][key]}` + ); + } + } + + await db.delete(SalesInvoice, invoice.name as string); + rows = await db.getAll(SalesInvoiceItem); + assert.strictEqual(rows.length, 0, `ct length delete ${rows.length}`); + }); }); diff --git a/backend/database/types.ts b/backend/database/types.ts index b7aba7f0..e91d7694 100644 --- a/backend/database/types.ts +++ b/backend/database/types.ts @@ -12,10 +12,9 @@ export interface GetQueryBuilderOptions { } export interface GetAllOptions { - schemaName: string; fields?: string[]; filters?: QueryFilter; - start?: number; + offset?: number; limit?: number; groupBy?: string; orderBy?: string; diff --git a/utils/index.ts b/utils/index.ts index dcac32b4..2bef4b69 100644 --- a/utils/index.ts +++ b/utils/index.ts @@ -27,3 +27,7 @@ export function getValueMapFromList( export function getRandomString(): string { return Math.random().toString(36).slice(2, 8); } + +export async function sleep(durationMilliseconds: number = 1000) { + return new Promise((r) => setTimeout(() => r(null), durationMilliseconds)); +} From 9765707d21b8381affc80f388b2e8ef50e46d840 Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Thu, 31 Mar 2022 12:48:32 +0530 Subject: [PATCH 023/163] incr: add DatabaseManager.call --- backend/database/core.ts | 19 ++++--- backend/database/manager.ts | 66 ++++++++++++++++++++----- backend/database/runPatch.ts | 2 +- backend/database/tests/testCore.spec.ts | 2 +- backend/database/types.ts | 12 ----- backend/{common.ts => helpers.ts} | 13 +++++ common/model.ts | 6 --- tsconfig.json | 1 - utils/db/types.ts | 63 +++++++++++++++++++++++ vue.config.js | 1 - 10 files changed, 142 insertions(+), 43 deletions(-) rename backend/{common.ts => helpers.ts} (77%) delete mode 100644 common/model.ts create mode 100644 utils/db/types.ts diff --git a/backend/database/core.ts b/backend/database/core.ts index 4468715f..26a95a84 100644 --- a/backend/database/core.ts +++ b/backend/database/core.ts @@ -1,5 +1,6 @@ import { knex, Knex } from 'knex'; import { getRandomString, getValueMapFromList } from 'utils'; +import { DatabaseBase, GetAllOptions, QueryFilter } from 'utils/db/types'; import { CannotCommitError, DatabaseError, @@ -15,14 +16,8 @@ import { SchemaMap, TargetField, } from '../../schemas/types'; -import { getDefaultMetaFieldValueMap, sqliteTypeMap, SYSTEM } from '../common'; -import { - ColumnDiff, - FieldValueMap, - GetAllOptions, - GetQueryBuilderOptions, - QueryFilter, -} from './types'; +import { getDefaultMetaFieldValueMap, sqliteTypeMap, SYSTEM } from '../helpers'; +import { ColumnDiff, FieldValueMap, GetQueryBuilderOptions } from './types'; /** * # DatabaseCore @@ -43,7 +38,7 @@ import { * the `fieldValueMap`. */ -export default class DatabaseCore { +export default class DatabaseCore extends DatabaseBase { knex?: Knex; typeMap = sqliteTypeMap; dbPath: string; @@ -51,6 +46,7 @@ export default class DatabaseCore { connectionParams: Knex.Config; constructor(dbPath?: string) { + super(); this.dbPath = dbPath ?? ':memory:'; this.connectionParams = { client: 'sqlite3', @@ -133,7 +129,10 @@ export default class DatabaseCore { return row.length > 0; } - async insert(schemaName: string, fieldValueMap: FieldValueMap) { + async insert( + schemaName: string, + fieldValueMap: FieldValueMap + ): Promise { // insert parent if (this.schemaMap[schemaName].isSingle) { await this.#updateSingleValues(schemaName, fieldValueMap); diff --git a/backend/database/manager.ts b/backend/database/manager.ts index 51d142b8..8a9590c6 100644 --- a/backend/database/manager.ts +++ b/backend/database/manager.ts @@ -1,4 +1,6 @@ +import { databaseMethodSet } from 'backend/helpers'; import fs from 'fs/promises'; +import { DatabaseMethod } from 'utils/db/types'; import { getSchemas } from '../../schemas'; import patches from '../patches'; import DatabaseCore from './core'; @@ -7,11 +9,14 @@ import { Patch } from './types'; export class DatabaseManager { db?: DatabaseCore; - constructor() {} - async createNewDatabase(dbPath: string, countryCode: string) { + get #isInitialized(): boolean { + return this.db !== undefined && this.db.knex !== undefined; + } + + async createNewDatabase(dbPath: string, countryCode?: string) { await this.#unlinkIfExists(dbPath); - this.connectToDatabase(dbPath, countryCode); + await this.connectToDatabase(dbPath, countryCode); } async connectToDatabase(dbPath: string, countryCode?: string) { @@ -22,14 +27,37 @@ export class DatabaseManager { const schemaMap = getSchemas(countryCode); this.db.setSchemaMap(schemaMap); - await this.migrate(); + await this.#migrate(); } - async migrate() { - if (this.db === undefined) { + async call(method: DatabaseMethod, ...args: unknown[]) { + if (!this.#isInitialized) { return; } + if (!databaseMethodSet.has(method)) { + return; + } + + // @ts-ignore + const response = await this.db[method](...args); + if (method === 'close') { + delete this.db; + } + + return response; + } + + async #migrate(): Promise { + if (!this.#isInitialized) { + return; + } + + const isFirstRun = await this.#getIsFirstRun(); + if (isFirstRun) { + await this.db!.migrate(); + } + const patchesToExecute = await this.#getPatchesToExecute(); const preMigrationPatches = patchesToExecute.filter( (p) => p.patch.beforeMigrate @@ -39,7 +67,7 @@ export class DatabaseManager { ); await runPatches(preMigrationPatches, this); - await this.db.migrate(); + await this.db!.migrate(); await runPatches(postMigrationPatches, this); } @@ -60,10 +88,15 @@ export class DatabaseManager { return undefined; } - const query = await this.db.knex!('SingleValue').where({ - fieldname: 'countryCode', - parent: 'SystemSettings', - }); + let query: { countryCode: string }[] = []; + try { + query = await this.db.knex!('SingleValue').where({ + fieldname: 'countryCode', + parent: 'SystemSettings', + }); + } catch { + // Database not inialized and no countryCode passed + } if (query.length > 0) { return query[0].countryCode as string; @@ -83,6 +116,17 @@ export class DatabaseManager { throw err; } } + + async #getIsFirstRun(): Promise { + if (!this.#isInitialized) { + return true; + } + + const tableList: unknown[] = await this.db!.knex!.raw( + "SELECT name FROM sqlite_master WHERE type='table'" + ); + return tableList.length === 0; + } } export default new DatabaseManager(); diff --git a/backend/database/runPatch.ts b/backend/database/runPatch.ts index 8c521700..f93704fd 100644 --- a/backend/database/runPatch.ts +++ b/backend/database/runPatch.ts @@ -1,4 +1,4 @@ -import { getDefaultMetaFieldValueMap } from '../common'; +import { getDefaultMetaFieldValueMap } from '../helpers'; import { DatabaseManager } from './manager'; import { FieldValueMap, Patch } from './types'; diff --git a/backend/database/tests/testCore.spec.ts b/backend/database/tests/testCore.spec.ts index 9f0d926b..2b1ddd80 100644 --- a/backend/database/tests/testCore.spec.ts +++ b/backend/database/tests/testCore.spec.ts @@ -3,7 +3,7 @@ import 'mocha'; import { getMapFromList } from 'schemas/helpers'; import { FieldTypeEnum, RawValue } from 'schemas/types'; import { getValueMapFromList, sleep } from 'utils'; -import { getDefaultMetaFieldValueMap, sqliteTypeMap } from '../../common'; +import { getDefaultMetaFieldValueMap, sqliteTypeMap } from '../../helpers'; import DatabaseCore from '../core'; import { FieldValueMap, SqliteTableInfo } from '../types'; import { diff --git a/backend/database/types.ts b/backend/database/types.ts index e91d7694..ff521c53 100644 --- a/backend/database/types.ts +++ b/backend/database/types.ts @@ -1,8 +1,6 @@ import { Field, RawValue } from '../../schemas/types'; import { DatabaseManager } from './manager'; -export type QueryFilter = Record; - export interface GetQueryBuilderOptions { offset?: number; limit?: number; @@ -11,16 +9,6 @@ export interface GetQueryBuilderOptions { order?: 'desc' | 'asc'; } -export interface GetAllOptions { - fields?: string[]; - filters?: QueryFilter; - offset?: number; - limit?: number; - groupBy?: string; - orderBy?: string; - order?: 'asc' | 'desc'; -} - export type ColumnDiff = { added: Field[]; removed: string[] }; export type FieldValueMap = Record< string, diff --git a/backend/common.ts b/backend/helpers.ts similarity index 77% rename from backend/common.ts rename to backend/helpers.ts index 9eb9cb60..4e480160 100644 --- a/backend/common.ts +++ b/backend/helpers.ts @@ -1,3 +1,4 @@ +import { DatabaseMethod } from 'utils/db/types'; import { KnexColumnType } from './database/types'; export const sqliteTypeMap: Record = { @@ -34,3 +35,15 @@ export function getDefaultMetaFieldValueMap() { modified: now, }; } + +export const databaseMethodSet: Set = new Set([ + 'insert', + 'get', + 'getAll', + 'getSingleValues', + 'rename', + 'update', + 'delete', + 'close', + 'exists', +]); diff --git a/common/model.ts b/common/model.ts deleted file mode 100644 index 278a6696..00000000 --- a/common/model.ts +++ /dev/null @@ -1,6 +0,0 @@ -import models from '../models'; -import coreModels from '../frappe/models'; - -export function getModel() { - console.log(models, coreModels); -} diff --git a/tsconfig.json b/tsconfig.json index 6330a185..462722a4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,7 +18,6 @@ "@/*": ["src/*"], "schemas/*": ["schemas/*"], "backend/*": ["backend/*"], - "common/*": ["common/*"], "utils/*": ["utils/*"] }, "lib": ["esnext", "dom", "dom.iterable", "scripthost"] diff --git a/utils/db/types.ts b/utils/db/types.ts new file mode 100644 index 00000000..f25170bb --- /dev/null +++ b/utils/db/types.ts @@ -0,0 +1,63 @@ +/** + * The types in this file will be used by the main db class (core.ts) in the + * backend process and the the frontend db class (dbHandler.ts). + * + * DatabaseBase is an abstract class so that the function signatures + * match on both ends. + */ + +type UnknownMap = Record; +export abstract class DatabaseBase { + // Create + abstract insert( + schemaName: string, + fieldValueMap: UnknownMap + ): Promise; + + // Read + abstract get( + schemaName: string, + name: string, + fields?: string | string[] + ): Promise; + + abstract getAll( + schemaName: string, + options: GetAllOptions + ): Promise; + + abstract getSingleValues( + ...fieldnames: ({ fieldname: string; parent?: string } | string)[] + ): Promise<{ fieldname: string; parent: string; value: unknown }[]>; + + // Update + abstract rename( + schemaName: string, + oldName: string, + newName: string + ): Promise; + + abstract update(schemaName: string, fieldValueMap: UnknownMap): Promise; + + // Delete + abstract delete(schemaName: string, name: string): Promise; + + // Other + abstract close(): Promise; + + abstract exists(schemaName: string, name?: string): Promise; +} + +export type DatabaseMethod = keyof DatabaseBase; + +export interface GetAllOptions { + fields?: string[]; + filters?: QueryFilter; + offset?: number; + limit?: number; + groupBy?: string; + orderBy?: string; + order?: 'asc' | 'desc'; +} + +export type QueryFilter = Record; diff --git a/vue.config.js b/vue.config.js index bc2e0e95..80a8bfd2 100644 --- a/vue.config.js +++ b/vue.config.js @@ -42,7 +42,6 @@ module.exports = { '~': path.resolve('.'), schemas: path.resolve(__dirname, './schemas'), backend: path.resolve(__dirname, './backend'), - common: path.resolve(__dirname, './common'), utils: path.resolve(__dirname, './utils'), }); From 804d8414661a5d20e06f65746b3e6f9273649fed Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Thu, 31 Mar 2022 13:03:58 +0530 Subject: [PATCH 024/163] incr: move messages, add DB_ actions --- backend/database/core.ts | 4 +- backend/database/manager.ts | 2 +- main.ts | 2 +- main/registerAutoUpdaterListeners.ts | 2 +- main/registerIpcMainActionListeners.ts | 29 ++++++++++- main/registerIpcMainMessageListeners.ts | 2 +- src/App.vue | 2 +- src/components/Controls/AttachImage.vue | 2 +- src/components/HowTo.vue | 2 +- src/components/WindowsTitleBar.vue | 2 +- src/errorHandling.ts | 2 +- src/initialization.js | 2 +- src/main.js | 4 -- src/messages.js | 47 ------------------ src/pages/DataImport.vue | 2 +- src/pages/DatabaseSelector.vue | 2 +- src/pages/GetStarted.vue | 2 +- src/pages/PrintView/PrintView.vue | 2 +- src/pages/Settings/Settings.vue | 2 +- src/pages/Settings/TabInvoice.vue | 2 +- src/pages/SetupWizard/SetupWizard.vue | 2 +- src/renderer/registerIpcRendererListeners.ts | 2 +- src/telemetry/helpers.ts | 2 +- src/utils.js | 2 +- utils/messages.ts | 51 ++++++++++++++++++++ vue.config.js | 1 + 26 files changed, 102 insertions(+), 74 deletions(-) delete mode 100644 src/messages.js create mode 100644 utils/messages.ts diff --git a/backend/database/core.ts b/backend/database/core.ts index 26a95a84..874fa826 100644 --- a/backend/database/core.ts +++ b/backend/database/core.ts @@ -1,6 +1,4 @@ import { knex, Knex } from 'knex'; -import { getRandomString, getValueMapFromList } from 'utils'; -import { DatabaseBase, GetAllOptions, QueryFilter } from 'utils/db/types'; import { CannotCommitError, DatabaseError, @@ -16,6 +14,8 @@ import { SchemaMap, TargetField, } from '../../schemas/types'; +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'; diff --git a/backend/database/manager.ts b/backend/database/manager.ts index 8a9590c6..80519c79 100644 --- a/backend/database/manager.ts +++ b/backend/database/manager.ts @@ -1,7 +1,7 @@ -import { databaseMethodSet } from 'backend/helpers'; import fs from 'fs/promises'; import { DatabaseMethod } from 'utils/db/types'; import { getSchemas } from '../../schemas'; +import { databaseMethodSet } from '../helpers'; import patches from '../patches'; import DatabaseCore from './core'; import { runPatches } from './runPatch'; diff --git a/main.ts b/main.ts index 3c666bfa..39ee7b9a 100644 --- a/main.ts +++ b/main.ts @@ -16,7 +16,7 @@ import registerAutoUpdaterListeners from './main/registerAutoUpdaterListeners'; import registerIpcMainActionListeners from './main/registerIpcMainActionListeners'; import registerIpcMainMessageListeners from './main/registerIpcMainMessageListeners'; import registerProcessListeners from './main/registerProcessListeners'; -import { IPC_CHANNELS } from './src/messages'; +import { IPC_CHANNELS } from './utils/messages'; export class Main { title: string = 'Frappe Books'; diff --git a/main/registerAutoUpdaterListeners.ts b/main/registerAutoUpdaterListeners.ts index a334656f..4137c970 100644 --- a/main/registerAutoUpdaterListeners.ts +++ b/main/registerAutoUpdaterListeners.ts @@ -1,6 +1,6 @@ import { autoUpdater, UpdateInfo } from 'electron-updater'; import { Main } from '../main'; -import { IPC_CHANNELS } from '../src/messages'; +import { IPC_CHANNELS } from '../utils/messages'; export default function registerAutoUpdaterListeners(main: Main) { autoUpdater.autoDownload = false; diff --git a/main/registerIpcMainActionListeners.ts b/main/registerIpcMainActionListeners.ts index 5260800d..4b06857c 100644 --- a/main/registerIpcMainActionListeners.ts +++ b/main/registerIpcMainActionListeners.ts @@ -2,11 +2,13 @@ import { app, dialog, ipcMain } from 'electron'; import { autoUpdater } from 'electron-updater'; import fs from 'fs/promises'; import path from 'path'; +import databaseManager from '../backend/database/manager'; import { Main } from '../main'; import { getUrlAndTokenString, sendError } from '../src/contactMothership'; import { getLanguageMap } from '../src/getLanguageMap'; -import { IPC_ACTIONS } from '../src/messages'; import saveHtmlAsPdf from '../src/saveHtmlAsPdf'; +import { DatabaseMethod } from '../utils/db/types'; +import { IPC_ACTIONS } from '../utils/messages'; import { getMainWindowSize } from './helpers'; export default function registerIpcMainActionListeners(main: Main) { @@ -116,4 +118,29 @@ export default function registerIpcMainActionListeners(main: Main) { ipcMain.handle(IPC_ACTIONS.GET_VERSION, (_) => { return app.getVersion(); }); + + /** + * Database Related Actions + */ + + ipcMain.handle( + IPC_ACTIONS.DB_CREATE, + async (_, dbPath: string, countryCode?: string) => { + return databaseManager.createNewDatabase(dbPath, countryCode); + } + ); + + ipcMain.handle( + IPC_ACTIONS.DB_CONNECT, + async (_, dbPath: string, countryCode?: string) => { + return databaseManager.createNewDatabase(dbPath, countryCode); + } + ); + + ipcMain.handle( + IPC_ACTIONS.DB_CALL, + async (_, method: DatabaseMethod, ...args: unknown[]) => { + return databaseManager.call(method, ...args); + } + ); } diff --git a/main/registerIpcMainMessageListeners.ts b/main/registerIpcMainMessageListeners.ts index b178edb7..aa5da057 100644 --- a/main/registerIpcMainMessageListeners.ts +++ b/main/registerIpcMainMessageListeners.ts @@ -1,7 +1,7 @@ import { ipcMain, Menu, shell } from 'electron'; import { autoUpdater } from 'electron-updater'; import { Main } from '../main'; -import { IPC_MESSAGES } from '../src/messages'; +import { IPC_MESSAGES } from '../utils/messages'; export default function registerIpcMainMessageListeners(main: Main) { ipcMain.on(IPC_MESSAGES.OPEN_MENU, (event) => { diff --git a/src/App.vue b/src/App.vue index c8350880..285cbbf2 100644 --- a/src/App.vue +++ b/src/App.vue @@ -37,7 +37,7 @@ import { postSetup, purgeCache, } from '@/initialization'; -import { IPC_ACTIONS, IPC_MESSAGES } from '@/messages'; +import { IPC_ACTIONS, IPC_MESSAGES } from 'utils/messages'; import { ipcRenderer } from 'electron'; import frappe from 'frappe'; import fs from 'fs/promises'; diff --git a/src/components/Controls/AttachImage.vue b/src/components/Controls/AttachImage.vue index f25210cb..d3b14237 100644 --- a/src/components/Controls/AttachImage.vue +++ b/src/components/Controls/AttachImage.vue @@ -59,7 +59,7 @@ import frappe from 'frappe'; import { ipcRenderer } from 'electron'; import Base from './Base'; -import { IPC_ACTIONS } from '@/messages'; +import { IPC_ACTIONS } from 'utils/messages'; import fs from 'fs'; import path from 'path'; diff --git a/src/components/HowTo.vue b/src/components/HowTo.vue index 749a3d55..131767c8 100644 --- a/src/components/HowTo.vue +++ b/src/components/HowTo.vue @@ -9,7 +9,7 @@ diff --git a/src/components/FilterDropdown.vue b/src/components/FilterDropdown.vue index fb0a7f40..d9a3e9de 100644 --- a/src/components/FilterDropdown.vue +++ b/src/components/FilterDropdown.vue @@ -116,8 +116,8 @@ diff --git a/src/components/ReconciliationValidation.vue b/src/components/ReconciliationValidation.vue index a07c31e1..b0b5454c 100644 --- a/src/components/ReconciliationValidation.vue +++ b/src/components/ReconciliationValidation.vue @@ -36,6 +36,7 @@ diff --git a/src/components/SalesInvoice/Templates/Base.vue b/src/components/SalesInvoice/Templates/Base.vue index 48d519f4..2558c910 100644 --- a/src/components/SalesInvoice/Templates/Base.vue +++ b/src/components/SalesInvoice/Templates/Base.vue @@ -1,5 +1,5 @@ diff --git a/src/pages/ListView/ListView.vue b/src/pages/ListView/ListView.vue index c7a76353..95d9c581 100644 --- a/src/pages/ListView/ListView.vue +++ b/src/pages/ListView/ListView.vue @@ -30,11 +30,11 @@ diff --git a/src/components/Controls/LanguageSelector.vue b/src/components/Controls/LanguageSelector.vue index 373723b9..bdabf64c 100644 --- a/src/components/Controls/LanguageSelector.vue +++ b/src/components/Controls/LanguageSelector.vue @@ -9,9 +9,8 @@ diff --git a/src/components/once/TelemetryModal.vue b/src/components/once/TelemetryModal.vue index ef2460b7..66f612a8 100644 --- a/src/components/once/TelemetryModal.vue +++ b/src/components/once/TelemetryModal.vue @@ -54,7 +54,7 @@ import { getTelemetryOptions } from 'fyo/telemetry/helpers'; import { TelemetrySetting } from 'fyo/telemetry/types'; import { fyo } from 'src/initFyo'; import Button from '../Button.vue'; -import FormControl from '../Controls/FormControl'; +import FormControl from '../Controls/FormControl.vue'; import FeatherIcon from '../FeatherIcon.vue'; import HowTo from '../HowTo.vue'; import Modal from '../Modal.vue'; diff --git a/src/initFyo.ts b/src/initFyo.ts index f099adfb..cc0c0ae7 100644 --- a/src/initFyo.ts +++ b/src/initFyo.ts @@ -1,15 +1,49 @@ import { Fyo } from 'fyo'; import { getRegionalModels, models } from 'models'; +import { getValueMapFromList } from 'utils'; export const fyo = new Fyo({ isTest: false, isElectron: true }); -export async function initializeModels(dbPath: string, countryCode?: string) { - if (countryCode) { +async function closeDbIfConnected() { + if (!fyo.db.isConnected) { + return; + } + + await fyo.db.close(); +} + +export async function initializeInstance( + dbPath: string, + isNew: boolean, + countryCode: string +) { + if (isNew) { + await closeDbIfConnected(); countryCode = await fyo.db.createNewDatabase(dbPath, countryCode); - } else { + } else if (!fyo.db.isConnected) { countryCode = await fyo.db.connectToDatabase(dbPath); } const regionalModels = await getRegionalModels(countryCode); await fyo.initializeAndRegister(models, regionalModels); + + await setSingles(); + await setCurrencySymbols(); +} + +async function setSingles() { + await fyo.doc.getSingle('AccountingSettings'); + await fyo.doc.getSingle('GetStarted'); +} + +async function setCurrencySymbols() { + const currencies = (await fyo.db.getAll('Currency', { + fields: ['name', 'symbol'], + })) as { name: string; symbol: string }[]; + + fyo.currencySymbols = getValueMapFromList( + currencies, + 'name', + 'symbol' + ) as Record; } diff --git a/src/pages/DataImport.vue b/src/pages/DataImport.vue index be5cb580..5bf47e26 100644 --- a/src/pages/DataImport.vue +++ b/src/pages/DataImport.vue @@ -338,7 +338,7 @@ diff --git a/src/README.md b/src/README.md index 9fd6cab7..9c399a07 100644 --- a/src/README.md +++ b/src/README.md @@ -2,12 +2,18 @@ This is where all the frontend code lives -## Initialization -New Instance -1. Run _Setup Wizard_ for initialization values (eg: `countryCode`). -2. +## Fyo Initialization -Existing Instance -1. Connect to db +The initialization flows are different when the instance is new or is existing. +All of them are triggered from `src/App.vue`. + +**New Instance** + +1. Run _Setup Wizard_ for init values (eg: `country`). +2. Call `setupInstance.ts/setupInstance` using init values. + +**Existing Instance** + +1. Connect to db. 2. Check if _Setup Wizard_ has been completed, if not, jump to **New Instance** -3. Call `initFyo/initializeInstance` with `dbPath` and `countryCode` \ No newline at end of file +3. Call `initFyo/initializeInstance` with `dbPath` and `countryCode` diff --git a/src/components/Controls/Base.vue b/src/components/Controls/Base.vue index 9e693e91..705ca30e 100644 --- a/src/components/Controls/Base.vue +++ b/src/components/Controls/Base.vue @@ -22,19 +22,19 @@ diff --git a/src/components/Controls/FormControl.vue b/src/components/Controls/FormControl.vue index c334fb19..e1268cb6 100644 --- a/src/components/Controls/FormControl.vue +++ b/src/components/Controls/FormControl.vue @@ -1,19 +1,19 @@ diff --git a/src/renderer.ts b/src/renderer.ts index 32e07d71..8ce8aca2 100644 --- a/src/renderer.ts +++ b/src/renderer.ts @@ -7,7 +7,7 @@ import Badge from './components/Badge.vue'; import FeatherIcon from './components/FeatherIcon.vue'; import { getErrorHandled, handleError } from './errorHandling'; import { fyo } from './initFyo'; -import { incrementOpenCount, outsideClickDirective } from './renderer/helpers'; +import { outsideClickDirective } from './renderer/helpers'; import registerIpcRendererListeners from './renderer/registerIpcRendererListeners'; import router from './router'; import { stringifyCircular } from './utils'; @@ -33,7 +33,7 @@ import { setLanguageMap } from './utils/language'; app.use(router); app.component('App', App); - app.component('feather-icon', FeatherIcon); + app.component('FeatherIcon', FeatherIcon); app.component('Badge', Badge); app.directive('on-outside-click', outsideClickDirective); @@ -62,8 +62,6 @@ import { setLanguageMap } from './utils/language'; }); fyo.store.appVersion = await ipcRenderer.invoke(IPC_ACTIONS.GET_VERSION); - - incrementOpenCount(); app.mount('body'); })(); diff --git a/src/renderer/helpers.ts b/src/renderer/helpers.ts index 148966cf..71806b39 100644 --- a/src/renderer/helpers.ts +++ b/src/renderer/helpers.ts @@ -1,5 +1,3 @@ -import { ConfigKeys } from 'fyo/core/types'; -import { fyo } from 'src/initFyo'; import { Directive } from 'vue'; const instances: OutsideClickCallback[] = []; @@ -35,14 +33,3 @@ function onDocumentClick(e: Event, el: HTMLElement, fn: OutsideClickCallback) { fn(e); } } - -export function incrementOpenCount() { - let openCount = fyo.config.get(ConfigKeys.OpenCount); - if (typeof openCount !== 'number') { - openCount = 1; - } else { - openCount += 1; - } - - fyo.config.set(ConfigKeys.OpenCount, openCount); -} diff --git a/src/renderer/registerIpcRendererListeners.ts b/src/renderer/registerIpcRendererListeners.ts index 1c5c5ed7..732225f1 100644 --- a/src/renderer/registerIpcRendererListeners.ts +++ b/src/renderer/registerIpcRendererListeners.ts @@ -1,6 +1,7 @@ import { ipcRenderer } from 'electron'; import { handleError } from 'src/errorHandling'; import { fyo } from 'src/initFyo'; +import { startTelemetry } from 'src/utils/misc'; import { showToast } from 'src/utils/ui'; import { IPC_CHANNELS, IPC_MESSAGES } from 'utils/messages'; @@ -56,7 +57,7 @@ export default function registerIpcRendererListeners() { document.addEventListener('visibilitychange', function () { const { visibilityState } = document; if (visibilityState === 'visible' && !fyo.telemetry.started) { - fyo.telemetry.start(); + startTelemetry(); } if (visibilityState !== 'hidden') { diff --git a/src/setup/setupInstance.ts b/src/setup/setupInstance.ts index f48ad646..1becabe6 100644 --- a/src/setup/setupInstance.ts +++ b/src/setup/setupInstance.ts @@ -1,4 +1,3 @@ -import countryInfo from 'fixtures/countryInfo.json'; import { ConfigFile, DocValueMap } from 'fyo/core/types'; import Doc from 'fyo/model/doc'; import { createNumberSeries } from 'fyo/model/naming'; @@ -9,16 +8,21 @@ import { DEFAULT_SERIES_START, } from 'fyo/utils/consts'; import { AccountingSettings } from 'models/baseModels/AccountingSettings/AccountingSettings'; -import { fyo } from 'src/initFyo'; +import { fyo, initializeInstance } from 'src/initFyo'; import { createRegionalRecords } from 'src/regional'; +import { getCountryCodeFromCountry, getCountryInfo } from 'utils/misc'; +import { CountryInfo } from 'utils/types'; import { createCOA } from './createCOA'; -import { CountrySettings, SetupWizardOptions } from './types'; +import { SetupWizardOptions } from './types'; export default async function setupInstance( + dbPath: string, setupWizardOptions: SetupWizardOptions ) { const { companyName, country, bankName, chartOfAccounts } = setupWizardOptions; + + await initializeDatabase(dbPath, country); await updateSystemSettings(setupWizardOptions); await updateAccountingSettings(setupWizardOptions); await updatePrintSettings(setupWizardOptions); @@ -31,10 +35,15 @@ export default async function setupInstance( await completeSetup(companyName); } +async function initializeDatabase(dbPath: string, country: string) { + const countryCode = getCountryCodeFromCountry(country); + await initializeInstance(dbPath, true, countryCode); +} + async function updateAccountingSettings({ companyName, country, - name, + fullname, email, bankName, fiscalYearStart, @@ -46,7 +55,7 @@ async function updateAccountingSettings({ await accountingSettings.setAndUpdate({ companyName, country, - fullname: name, + fullname, email, bankName, fiscalYearStart, @@ -73,8 +82,8 @@ async function updateSystemSettings({ country, currency: companyCurrency, }: SetupWizardOptions) { - // @ts-ignore - const countryOptions = countryInfo[country] as CountrySettings; + const countryInfo = getCountryInfo(); + const countryOptions = countryInfo[country] as CountryInfo; const currency = companyCurrency ?? countryOptions.currency ?? DEFAULT_CURRENCY; const locale = countryOptions.locale ?? DEFAULT_LOCALE; @@ -88,10 +97,7 @@ async function updateSystemSettings({ async function createCurrencyRecords() { const promises: Promise[] = []; const queue: string[] = []; - const countrySettings: CountrySettings[] = Object.values( - // @ts-ignore - countryInfo as Record - ); + const countrySettings = Object.values(getCountryInfo()) as CountryInfo[]; for (const country of countrySettings) { const { diff --git a/src/setup/types.ts b/src/setup/types.ts index e2db66f6..2abf8adf 100644 --- a/src/setup/types.ts +++ b/src/setup/types.ts @@ -3,7 +3,6 @@ export interface SetupWizardOptions { companyName: string; country: string; fullname: string; - name: string; email: string; bankName: string; currency: string; @@ -11,15 +10,3 @@ export interface SetupWizardOptions { fiscalYearEnd: string; chartOfAccounts: string; } - -export interface CountrySettings { - code: string; - currency: string; - fiscal_year_start: string; - fiscal_year_end: string; - locale: string; - currency_fraction?: string; - currency_fraction_units?: number; - smallest_currency_fraction_value?: number; - currency_symbol?: string; -} \ No newline at end of file diff --git a/src/utils/misc.ts b/src/utils/misc.ts index cc32b0e1..2a62dec3 100644 --- a/src/utils/misc.ts +++ b/src/utils/misc.ts @@ -1,3 +1,4 @@ +import { ConfigKeys } from 'fyo/core/types'; import { getSingleValue } from 'fyo/utils'; import { DateTime } from 'luxon'; import { SetupWizard } from 'models/baseModels/SetupWizard/SetupWizard'; @@ -51,3 +52,31 @@ export async function getSetupComplete(): Promise { fyo )); } + +export function incrementOpenCount() { + let openCount = fyo.config.get(ConfigKeys.OpenCount); + if (typeof openCount !== 'number') { + openCount = 1; + } else { + openCount += 1; + } + + fyo.config.set(ConfigKeys.OpenCount, openCount); +} + +export async function startTelemetry() { + fyo.telemetry.interestingDocs = [ + ModelNameEnum.Payment, + ModelNameEnum.PaymentFor, + ModelNameEnum.SalesInvoice, + ModelNameEnum.SalesInvoiceItem, + ModelNameEnum.PurchaseInvoice, + ModelNameEnum.PurchaseInvoiceItem, + ModelNameEnum.JournalEntry, + ModelNameEnum.JournalEntryAccount, + ModelNameEnum.Party, + ModelNameEnum.Account, + ModelNameEnum.Tax, + ]; + await fyo.telemetry.start(); +} diff --git a/utils/misc.ts b/utils/misc.ts new file mode 100644 index 00000000..82cff3a4 --- /dev/null +++ b/utils/misc.ts @@ -0,0 +1,17 @@ +import countryInfo from 'fixtures/countryInfo.json'; +import { CountryInfoMap } from './types'; + +export function getCountryInfo(): CountryInfoMap { + // @ts-ignore + return countryInfo as CountryInfoMap; +} + +export function getCountryCodeFromCountry(countryName: string): string { + const countryInfoMap = getCountryInfo(); + const countryInfo = countryInfoMap[countryName]; + if (countryInfo === undefined) { + return ''; + } + + return countryInfo.code; +} diff --git a/utils/types.ts b/utils/types.ts index 254518a6..359e26ee 100644 --- a/utils/types.ts +++ b/utils/types.ts @@ -1,2 +1,16 @@ export type Translation = { translation: string; context?: string }; export type LanguageMap = Record; + +export type CountryInfoMap = Record; +export interface CountryInfo { + code: string; + currency: string; + currency_fraction?: string; + currency_fraction_units?: number; + smallest_currency_fraction_value?: number; + currency_symbol?: string; + timezones?: string[]; + fiscal_year_start: string; + fiscal_year_end: string; + locale: string; +} From 9877bf4fd385f07d73c13cd022698bb873008618 Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Sun, 24 Apr 2022 12:18:44 +0530 Subject: [PATCH 065/163] incr: simplify doc a bit - allow nulls in converter else not not null'll fail --- fyo/core/converter.ts | 20 +-- fyo/core/docHandler.ts | 25 ++-- fyo/core/types.ts | 2 +- fyo/demux/config.ts | 14 +-- fyo/index.ts | 4 +- fyo/model/doc.ts | 96 +++++++-------- fyo/model/helpers.ts | 2 +- fyo/model/naming.ts | 4 +- fyo/model/types.ts | 2 +- fyo/models/NumberSeries.ts | 4 +- fyo/models/SystemSettings.ts | 2 +- fyo/tests/testFyo.spec.ts | 4 +- fyo/utils/format.ts | 2 +- fyo/utils/index.ts | 2 +- models/baseModels/Account/Account.ts | 2 +- .../AccountingLedgerEntry.ts | 2 +- .../AccountingSettings/AccountingSettings.ts | 2 +- models/baseModels/Address/Address.ts | 2 +- models/baseModels/Invoice/Invoice.ts | 2 +- models/baseModels/InvoiceItem/InvoiceItem.ts | 2 +- models/baseModels/Item/Item.ts | 10 +- .../baseModels/JournalEntry/JournalEntry.ts | 2 +- .../JournalEntryAccount.ts | 2 +- models/baseModels/Party/Party.ts | 8 +- models/baseModels/Payment/Payment.ts | 8 +- models/baseModels/PaymentFor/PaymentFor.ts | 2 +- models/baseModels/SetupWizard/SetupWizard.ts | 31 ++--- models/baseModels/Tax/Tax.ts | 2 +- models/baseModels/TaxSummary/TaxSummary.ts | 2 +- models/helpers.ts | 4 +- models/ledgerPosting/ledgerPosting.ts | 8 +- models/ledgerPosting/types.ts | 2 +- src/App.vue | 4 +- src/README.md | 5 + src/components/Controls/Link.vue | 2 +- src/components/ReconciliationValidation.vue | 2 +- src/components/TwoColumnForm.vue | 15 +-- src/dataImport.ts | 6 +- src/errorHandling.ts | 2 +- src/initFyo.ts | 18 +-- src/pages/ChartOfAccounts.vue | 4 +- src/pages/Dashboard/PeriodSelector.vue | 1 - src/pages/Dashboard/UnpaidInvoices.vue | 2 +- src/pages/GetStarted.vue | 4 +- src/pages/InvoiceForm.vue | 2 +- src/pages/JournalEntryForm.vue | 2 +- src/pages/ListView/ListView.vue | 2 +- src/pages/QuickEditForm.vue | 6 +- src/pages/Settings/TabInvoice.vue | 4 +- src/regional/in/in.ts | 32 +++-- src/regional/index.ts | 5 +- src/setup/createCOA.ts | 105 +++++++++------- src/setup/setupInstance.ts | 115 ++++++++++-------- src/utils/doc.ts | 2 +- src/utils/index.ts | 3 +- src/utils/misc.ts | 1 + src/utils/ui.ts | 4 +- tests/helpers.ts | 17 +++ tests/testSetupInstance.spec.ts | 25 ++++ tsconfig.json | 1 + utils/misc.ts | 19 +++ 61 files changed, 371 insertions(+), 315 deletions(-) create mode 100644 tests/helpers.ts create mode 100644 tests/testSetupInstance.spec.ts diff --git a/fyo/core/converter.ts b/fyo/core/converter.ts index 0fcf96c6..b06a8abf 100644 --- a/fyo/core/converter.ts +++ b/fyo/core/converter.ts @@ -1,5 +1,5 @@ import { Fyo } from 'fyo'; -import Doc from 'fyo/model/doc'; +import { Doc } from 'fyo/model/doc'; import { isPesa } from 'fyo/utils'; import { ValueError } from 'fyo/utils/errors'; import { DateTime } from 'luxon'; @@ -267,14 +267,18 @@ function toRawFloat(value: DocValue, field: Field): number { throwError(value, field, 'raw'); } -function toRawDate(value: DocValue, field: Field): string { +function toRawDate(value: DocValue, field: Field): string | null { const dateTime = toRawDateTime(value, field); + if (dateTime === null) { + return null; + } + return dateTime.split('T')[0]; } -function toRawDateTime(value: DocValue, field: Field): string { - if (getIsNullOrUndef(value)) { - return ''; +function toRawDateTime(value: DocValue, field: Field): string | null { + if (value === null) { + return null; } if (typeof value === 'string') { @@ -304,9 +308,9 @@ function toRawCheck(value: DocValue, field: Field): number { throwError(value, field, 'raw'); } -function toRawString(value: DocValue, field: Field): string { - if (getIsNullOrUndef(value)) { - return ''; +function toRawString(value: DocValue, field: Field): string | null { + if (value === null) { + return null; } if (typeof value === 'string') { diff --git a/fyo/core/docHandler.ts b/fyo/core/docHandler.ts index 17a6fae9..c873af2e 100644 --- a/fyo/core/docHandler.ts +++ b/fyo/core/docHandler.ts @@ -1,4 +1,4 @@ -import Doc from 'fyo/model/doc'; +import { Doc } from 'fyo/model/doc'; import { DocMap, ModelMap, SinglesMap } from 'fyo/model/types'; import { coreModels } from 'fyo/models'; import Observable from 'fyo/utils/observable'; @@ -143,20 +143,10 @@ export class DocHandler { return newDoc; } - getEmptyDoc(schemaName: string, cacheDoc: boolean = true): Doc { - const doc = this.getNewDoc(schemaName); - doc.name = getRandomString(); - - if (cacheDoc) { - this.addToCache(doc); - } - - return doc; - } - getNewDoc( schemaName: string, data: DocValueMap = {}, + cacheDoc: boolean = true, schema?: Schema, Model?: typeof Doc ): Doc { @@ -166,12 +156,17 @@ export class DocHandler { Model ??= this.models[schemaName]; schema ??= this.fyo.schemaMap[schemaName]; + if (schema === undefined) { throw new Error(`Schema not found for ${schemaName}`); } const doc = new Model!(schema, data, this.fyo); - doc.setDefaults(); + doc.name ??= getRandomString(); + if (cacheDoc) { + this.addToCache(doc); + } + return doc; } @@ -184,12 +179,12 @@ export class DocHandler { const docExists = await this.fyo.db.exists(schemaName, name); if (!docExists) { const doc = this.getNewDoc(schemaName, data); - await doc.insert; + await doc.sync(); return; } const doc = await this.getDoc(schemaName, name); await doc.setMultiple(data); - await doc.update(); + await doc.sync(); } } diff --git a/fyo/core/types.ts b/fyo/core/types.ts index c4af5d15..98d11fc6 100644 --- a/fyo/core/types.ts +++ b/fyo/core/types.ts @@ -1,4 +1,4 @@ -import Doc from 'fyo/model/doc'; +import { Doc } from 'fyo/model/doc'; import Money from 'pesa/dist/types/src/money'; import { RawValue } from 'schemas/types'; import { AuthDemuxBase } from 'utils/auth/types'; diff --git a/fyo/demux/config.ts b/fyo/demux/config.ts index 2126c631..252810c1 100644 --- a/fyo/demux/config.ts +++ b/fyo/demux/config.ts @@ -1,14 +1,14 @@ import config from 'utils/config'; export class Config { - #isElectron: boolean; + #useElectronConfig: boolean; fallback: Map = new Map(); constructor(isElectron: boolean) { - this.#isElectron = isElectron; + this.#useElectronConfig = isElectron; } get store(): Record { - if (this.#isElectron) { + if (this.#useElectronConfig) { return config.store; } else { const store: Record = {}; @@ -22,7 +22,7 @@ export class Config { } get(key: string, defaultValue?: unknown): unknown { - if (this.#isElectron) { + if (this.#useElectronConfig) { return config.get(key, defaultValue); } else { return this.fallback.get(key) ?? defaultValue; @@ -30,7 +30,7 @@ export class Config { } set(key: string, value: unknown) { - if (this.#isElectron) { + if (this.#useElectronConfig) { config.set(key, value); } else { this.fallback.set(key, value); @@ -38,7 +38,7 @@ export class Config { } delete(key: string) { - if (this.#isElectron) { + if (this.#useElectronConfig) { config.delete(key); } else { this.fallback.delete(key); @@ -46,7 +46,7 @@ export class Config { } clear() { - if (this.#isElectron) { + if (this.#useElectronConfig) { config.clear(); } else { this.fallback.clear(); diff --git a/fyo/index.ts b/fyo/index.ts index f281e246..650d11b1 100644 --- a/fyo/index.ts +++ b/fyo/index.ts @@ -6,7 +6,7 @@ import { DatabaseHandler } from './core/dbHandler'; import { DocHandler } from './core/docHandler'; import { DocValue, FyoConfig } from './core/types'; import { Config } from './demux/config'; -import Doc from './model/doc'; +import { Doc } from './model/doc'; import { ModelMap } from './model/types'; import { TelemetryManager } from './telemetry/telemetry'; import { @@ -60,7 +60,7 @@ export class Fyo { }); this.telemetry = new TelemetryManager(this); - this.config = new Config(this.isElectron); + this.config = new Config(this.isElectron && !this.isTest); } get initialized() { diff --git a/fyo/model/doc.ts b/fyo/model/doc.ts index 43f259db..54fa7500 100644 --- a/fyo/model/doc.ts +++ b/fyo/model/doc.ts @@ -45,7 +45,7 @@ import { } from './types'; import { validateSelect } from './validationFunction'; -export default class Doc extends Observable { +export class Doc extends Observable { name?: string; schema: Readonly; fyo: Fyo; @@ -72,12 +72,14 @@ export default class Doc extends Observable { super(); this.fyo = fyo; this.schema = schema; - this._setInitialValues(data); this.fieldMap = getMapFromList(schema.fields, 'fieldname'); if (this.schema.isSingle) { this.name = this.schemaName; } + + this._setDefaults(); + this._setValuesWithoutChecks(data); } get schemaName(): string { @@ -103,23 +105,17 @@ export default class Doc extends Observable { return fieldnames.map((f) => this.fieldMap[f]); } - _setInitialValues(data: DocValueMap) { - for (const fieldname in data) { - const value = data[fieldname]; + _setValuesWithoutChecks(data: DocValueMap) { + for (const field of this.schema.fields) { + const fieldname = field.fieldname; + const value = data[field.fieldname]; if (Array.isArray(value)) { for (const row of value) { this.push(fieldname, row); } } else { - this[fieldname] = value; - } - } - - // set unset fields as null - for (const field of this.schema.fields) { - if (this[field.fieldname] === undefined) { - this[field.fieldname] = null; + this[fieldname] = value ?? null; } } } @@ -198,7 +194,7 @@ export default class Doc extends Observable { }); } - setDefaults() { + _setDefaults() { for (const field of this.schema.fields) { if (!getIsNullOrUndef(this[field.fieldname])) { continue; @@ -224,21 +220,21 @@ export default class Doc extends Observable { } } - append(fieldname: string, docValueMap: Doc | DocValueMap = {}) { + async append(fieldname: string, docValueMap: Doc | DocValueMap = {}) { // push child row and trigger change this.push(fieldname, docValueMap); this._dirty = true; - this._applyChange(fieldname); + await this._applyChange(fieldname); } push(fieldname: string, docValueMap: Doc | DocValueMap = {}) { // push child row without triggering change this[fieldname] ??= []; - const childDoc = this._initChild(docValueMap, fieldname); + const childDoc = this._getChildDoc(docValueMap, fieldname); (this[fieldname] as Doc[]).push(childDoc); } - _initChild(docValueMap: Doc | DocValueMap, fieldname: string): Doc { + _getChildDoc(docValueMap: Doc | DocValueMap, fieldname: string): Doc { if (docValueMap instanceof Doc) { return docValueMap; } @@ -258,14 +254,13 @@ export default class Doc extends Observable { data.name = getRandomString(); } - const childSchemaName = this.fieldMap[fieldname] as TargetField; - const schema = this.fyo.db.schemaMap[childSchemaName.target] as Schema; + const targetField = this.fieldMap[fieldname] as TargetField; + const schema = this.fyo.db.schemaMap[targetField.target] as Schema; const childDoc = new Doc(schema, data as DocValueMap, this.fyo); - childDoc.setDefaults(); return childDoc; } - async validateInsert() { + async _validateInsert() { this._validateMandatory(); await this._validateFields(); } @@ -373,10 +368,6 @@ export default class Doc extends Observable { if (data && data.name) { this.syncValues(data); - if (this.schema.isSingle) { - this.setDefaults(); - } - await this.loadLinks(); } else { throw new NotFoundError(`Not Found: ${this.schemaName} ${this.name}`); @@ -420,7 +411,7 @@ export default class Doc extends Observable { syncValues(data: DocValueMap) { this.clearValues(); - this._setInitialValues(data); + this._setValuesWithoutChecks(data); this._dirty = false; this.trigger('change', { doc: this, @@ -436,7 +427,7 @@ export default class Doc extends Observable { this._notInserted = true; } - setChildIdx() { + _setChildIdx() { const childFields = this.schema.fields.filter( (f) => f.fieldtype === FieldTypeEnum.Table ) as TargetField[]; @@ -450,7 +441,7 @@ export default class Doc extends Observable { } } - async compareWithCurrentDoc() { + async _compareWithCurrentDoc() { if (this.isNew || !this.name) { return; } @@ -556,24 +547,24 @@ export default class Doc extends Observable { return; } if (Array.isArray(value) && field.fieldtype === FieldTypeEnum.Table) { - value = value.map((row) => this._initChild(row, field.fieldname)); + value = value.map((row) => this._getChildDoc(row, field.fieldname)); } return value; } - async commit() { + async _commit() { // re-run triggers - this.setChildIdx(); + this._setChildIdx(); await this._applyFormula(); await this.trigger('validate', null); } - async insert() { + async _insert() { await setName(this, this.fyo); this._setBaseMetaValues(); - await this.commit(); - await this.validateInsert(); + await this._commit(); + await this._validateInsert(); await this.trigger('beforeInsert', null); const oldName = this.name!; @@ -591,9 +582,9 @@ export default class Doc extends Observable { return this; } - async update() { - await this.compareWithCurrentDoc(); - await this.commit(); + async _update() { + await this._compareWithCurrentDoc(); + await this._commit(); await this.trigger('beforeUpdate'); // before submit @@ -617,11 +608,11 @@ export default class Doc extends Observable { return this; } - async insertOrUpdate() { + async sync() { if (this._notInserted) { - return await this.insert(); + return await this._insert(); } else { - return await this.update(); + return await this._update(); } } @@ -633,11 +624,11 @@ export default class Doc extends Observable { this.fyo.telemetry.log(Verb.Deleted, this.schemaName); } - async submitOrRevert(isSubmit: boolean) { + async _submitOrRevert(isSubmit: boolean) { const wasSubmitted = this.submitted; this.submitted = isSubmit; try { - await this.update(); + await this.sync(); } catch (e) { this.submitted = wasSubmitted; throw e; @@ -646,11 +637,11 @@ export default class Doc extends Observable { async submit() { this.cancelled = false; - await this.submitOrRevert(true); + await this._submitOrRevert(true); } async revert() { - await this.submitOrRevert(false); + await this._submitOrRevert(false); } async rename(newName: string) { @@ -701,15 +692,12 @@ export default class Doc extends Observable { return this.fyo.doc.getCachedValue(schemaName, name, fieldname); } - async setAndUpdate( - fieldname: string | DocValueMap, - value?: DocValue | Doc[] - ) { + async setAndSync(fieldname: string | DocValueMap, value?: DocValue | Doc[]) { await this.set(fieldname, value); - return await this.update(); + return await this.sync(); } - async duplicate(shouldInsert: boolean = true): Promise { + async duplicate(shouldSync: boolean = true): Promise { const updateMap: DocValueMap = {}; const docValueMap = this.getValidDict(); const fieldnames = this.schema.fields.map((f) => f.fieldname); @@ -736,11 +724,11 @@ export default class Doc extends Observable { updateMap.name = updateMap.name + ' CPY'; } - const doc = this.fyo.doc.getEmptyDoc(this.schemaName, false); + const doc = this.fyo.doc.getNewDoc(this.schemaName, {}, false); await doc.setMultiple(updateMap); - if (shouldInsert) { - await doc.insert(); + if (shouldSync) { + await doc.sync(); } return doc; diff --git a/fyo/model/helpers.ts b/fyo/model/helpers.ts index 9d9a16ed..6b0678a9 100644 --- a/fyo/model/helpers.ts +++ b/fyo/model/helpers.ts @@ -5,7 +5,7 @@ import { isEqual } from 'lodash'; import Money from 'pesa/dist/types/src/money'; import { Field, FieldType, FieldTypeEnum } from 'schemas/types'; import { getIsNullOrUndef } from 'utils'; -import Doc from './doc'; +import { Doc } from './doc'; export function areDocValuesEqual( dvOne: DocValue | Doc[], diff --git a/fyo/model/naming.ts b/fyo/model/naming.ts index e8c7a59e..e34b3ab0 100644 --- a/fyo/model/naming.ts +++ b/fyo/model/naming.ts @@ -4,7 +4,7 @@ import { DEFAULT_SERIES_START } from 'fyo/utils/consts'; import { BaseError } from 'fyo/utils/errors'; import { Field, Schema } from 'schemas/types'; import { getRandomString } from 'utils'; -import Doc from './doc'; +import { Doc } from './doc'; export function getNumberSeries(schema: Schema): Field | undefined { const numberSeries = schema.fields.find( @@ -119,5 +119,5 @@ export async function createNumberSeries( referenceType, }); - await series.insert(); + await series.sync(); } diff --git a/fyo/model/types.ts b/fyo/model/types.ts index 8150f7af..4e7b6bc4 100644 --- a/fyo/model/types.ts +++ b/fyo/model/types.ts @@ -4,7 +4,7 @@ import SystemSettings from 'fyo/models/SystemSettings'; import { FieldType } from 'schemas/types'; import { QueryFilter } from 'utils/db/types'; import { Router } from 'vue-router'; -import Doc from './doc'; +import { Doc } from './doc'; /** * The functions below are used for dynamic evaluation diff --git a/fyo/models/NumberSeries.ts b/fyo/models/NumberSeries.ts index 60088c92..f8759f4a 100644 --- a/fyo/models/NumberSeries.ts +++ b/fyo/models/NumberSeries.ts @@ -1,4 +1,4 @@ -import Doc from 'fyo/model/doc'; +import { Doc } from 'fyo/model/doc'; function getPaddedName(prefix: string, next: number, padZeros: number): string { return prefix + next.toString().padStart(padZeros ?? 4, '0'); @@ -21,7 +21,7 @@ export default class NumberSeries extends Doc { } this.current = (this.current as number) + 1; - await this.update(); + await this.sync(); return this.getPaddedName(this.current as number); } diff --git a/fyo/models/SystemSettings.ts b/fyo/models/SystemSettings.ts index 0f7f9dfa..ad4a5b10 100644 --- a/fyo/models/SystemSettings.ts +++ b/fyo/models/SystemSettings.ts @@ -1,5 +1,5 @@ import { DocValue } from 'fyo/core/types'; -import Doc from 'fyo/model/doc'; +import { Doc } from 'fyo/model/doc'; import { ValidationMap } from 'fyo/model/types'; import { ValidationError } from 'fyo/utils/errors'; import { t } from 'fyo/utils/translation'; diff --git a/fyo/tests/testFyo.spec.ts b/fyo/tests/testFyo.spec.ts index ed2c266b..16134be2 100644 --- a/fyo/tests/testFyo.spec.ts +++ b/fyo/tests/testFyo.spec.ts @@ -58,14 +58,14 @@ describe('Fyo Docs', function () { await fyo.close(); }); - specify('getEmptyDoc', async function () { + specify('getNewDoc', async function () { for (const schemaName in schemaMap) { const schema = schemaMap[schemaName]; if (schema?.isSingle) { continue; } - const doc = fyo.doc.getEmptyDoc(schemaName); + const doc = fyo.doc.getNewDoc(schemaName); } }); }); diff --git a/fyo/utils/format.ts b/fyo/utils/format.ts index bc7c2cb1..534c7138 100644 --- a/fyo/utils/format.ts +++ b/fyo/utils/format.ts @@ -1,6 +1,6 @@ import { Fyo } from 'fyo'; import { DocValue } from 'fyo/core/types'; -import Doc from 'fyo/model/doc'; +import { Doc } from 'fyo/model/doc'; import { DateTime } from 'luxon'; import Money from 'pesa/dist/types/src/money'; import { Field, FieldType, FieldTypeEnum } from 'schemas/types'; diff --git a/fyo/utils/index.ts b/fyo/utils/index.ts index f07b2962..9ba16f4a 100644 --- a/fyo/utils/index.ts +++ b/fyo/utils/index.ts @@ -1,5 +1,5 @@ import { Fyo } from 'fyo'; -import Doc from 'fyo/model/doc'; +import { Doc } from 'fyo/model/doc'; import { Action } from 'fyo/model/types'; import { pesa } from 'pesa'; diff --git a/models/baseModels/Account/Account.ts b/models/baseModels/Account/Account.ts index 5a64b6ab..88728328 100644 --- a/models/baseModels/Account/Account.ts +++ b/models/baseModels/Account/Account.ts @@ -1,5 +1,5 @@ import { Fyo } from 'fyo'; -import Doc from 'fyo/model/doc'; +import { Doc } from 'fyo/model/doc'; import { FiltersMap, ListViewSettings, diff --git a/models/baseModels/AccountingLedgerEntry/AccountingLedgerEntry.ts b/models/baseModels/AccountingLedgerEntry/AccountingLedgerEntry.ts index 2bd32b1c..6441b9f3 100644 --- a/models/baseModels/AccountingLedgerEntry/AccountingLedgerEntry.ts +++ b/models/baseModels/AccountingLedgerEntry/AccountingLedgerEntry.ts @@ -1,4 +1,4 @@ -import Doc from 'fyo/model/doc'; +import { Doc } from 'fyo/model/doc'; import { ListViewSettings } from 'fyo/model/types'; export class AccountingLedgerEntry extends Doc { diff --git a/models/baseModels/AccountingSettings/AccountingSettings.ts b/models/baseModels/AccountingSettings/AccountingSettings.ts index ac8cd45c..3ea13a82 100644 --- a/models/baseModels/AccountingSettings/AccountingSettings.ts +++ b/models/baseModels/AccountingSettings/AccountingSettings.ts @@ -1,4 +1,4 @@ -import Doc from 'fyo/model/doc'; +import { Doc } from 'fyo/model/doc'; import { FiltersMap, ListsMap, ValidationMap } from 'fyo/model/types'; import { validateEmail } from 'fyo/model/validationFunction'; import { getCountryInfo } from 'utils/misc'; diff --git a/models/baseModels/Address/Address.ts b/models/baseModels/Address/Address.ts index af9abaa6..a0f04804 100644 --- a/models/baseModels/Address/Address.ts +++ b/models/baseModels/Address/Address.ts @@ -1,5 +1,5 @@ import { Fyo } from 'fyo'; -import Doc from 'fyo/model/doc'; +import { Doc } from 'fyo/model/doc'; import { EmptyMessageMap, FormulaMap, ListsMap } from 'fyo/model/types'; import { stateCodeMap } from 'regional/in'; import { titleCase } from 'utils'; diff --git a/models/baseModels/Invoice/Invoice.ts b/models/baseModels/Invoice/Invoice.ts index 4d0311aa..74095ec2 100644 --- a/models/baseModels/Invoice/Invoice.ts +++ b/models/baseModels/Invoice/Invoice.ts @@ -1,5 +1,5 @@ import { DocValue } from 'fyo/core/types'; -import Doc from 'fyo/model/doc'; +import { Doc } from 'fyo/model/doc'; import { DefaultMap, FiltersMap, FormulaMap } from 'fyo/model/types'; import { getExchangeRate } from 'models/helpers'; import { LedgerPosting } from 'models/ledgerPosting/ledgerPosting'; diff --git a/models/baseModels/InvoiceItem/InvoiceItem.ts b/models/baseModels/InvoiceItem/InvoiceItem.ts index 5b6aa581..2147d0e5 100644 --- a/models/baseModels/InvoiceItem/InvoiceItem.ts +++ b/models/baseModels/InvoiceItem/InvoiceItem.ts @@ -1,5 +1,5 @@ import { DocValue } from 'fyo/core/types'; -import Doc from 'fyo/model/doc'; +import { Doc } from 'fyo/model/doc'; import { DependsOnMap, FiltersMap, diff --git a/models/baseModels/Item/Item.ts b/models/baseModels/Item/Item.ts index 891d8205..6b984bfa 100644 --- a/models/baseModels/Item/Item.ts +++ b/models/baseModels/Item/Item.ts @@ -1,6 +1,6 @@ import { Fyo } from 'fyo'; import { DocValue } from 'fyo/core/types'; -import Doc from 'fyo/model/doc'; +import { Doc } from 'fyo/model/doc'; import { Action, DependsOnMap, @@ -68,8 +68,8 @@ export class Item extends Doc { label: fyo.t`New Invoice`, condition: (doc) => !doc.isNew, action: async (doc, router) => { - const invoice = await fyo.doc.getEmptyDoc('SalesInvoice'); - invoice.append('items', { + const invoice = await fyo.doc.getNewDoc('SalesInvoice'); + await invoice.append('items', { item: doc.name as string, rate: doc.rate as Money, tax: doc.tax as string, @@ -81,8 +81,8 @@ export class Item extends Doc { label: fyo.t`New Bill`, condition: (doc) => !doc.isNew, action: async (doc, router) => { - const invoice = await fyo.doc.getEmptyDoc('PurchaseInvoice'); - invoice.append('items', { + const invoice = await fyo.doc.getNewDoc('PurchaseInvoice'); + await invoice.append('items', { item: doc.name as string, rate: doc.rate as Money, tax: doc.tax as string, diff --git a/models/baseModels/JournalEntry/JournalEntry.ts b/models/baseModels/JournalEntry/JournalEntry.ts index 2f11b019..64ab48a1 100644 --- a/models/baseModels/JournalEntry/JournalEntry.ts +++ b/models/baseModels/JournalEntry/JournalEntry.ts @@ -1,5 +1,5 @@ import { Fyo } from 'fyo'; -import Doc from 'fyo/model/doc'; +import { Doc } from 'fyo/model/doc'; import { Action, DefaultMap, diff --git a/models/baseModels/JournalEntryAccount/JournalEntryAccount.ts b/models/baseModels/JournalEntryAccount/JournalEntryAccount.ts index 0768a0ba..3374b744 100644 --- a/models/baseModels/JournalEntryAccount/JournalEntryAccount.ts +++ b/models/baseModels/JournalEntryAccount/JournalEntryAccount.ts @@ -1,4 +1,4 @@ -import Doc from 'fyo/model/doc'; +import { Doc } from 'fyo/model/doc'; import { FiltersMap, FormulaMap } from 'fyo/model/types'; import Money from 'pesa/dist/types/src/money'; diff --git a/models/baseModels/Party/Party.ts b/models/baseModels/Party/Party.ts index eea0eb26..2509b39c 100644 --- a/models/baseModels/Party/Party.ts +++ b/models/baseModels/Party/Party.ts @@ -1,5 +1,5 @@ import { Fyo } from 'fyo'; -import Doc from 'fyo/model/doc'; +import { Doc } from 'fyo/model/doc'; import { Action, FiltersMap, @@ -47,7 +47,7 @@ export class Party extends Doc { .reduce((a, b) => a.add(b), this.fyo.pesa(0)); await this.set('outstandingAmount', totalOutstanding); - await this.update(); + await this.sync(); } formulas: FormulaMap = { @@ -111,7 +111,7 @@ export class Party extends Doc { condition: (doc: Doc) => !doc.isNew && (doc.role as PartyRole) !== 'Customer', action: async (partyDoc, router) => { - const doc = await fyo.doc.getEmptyDoc('PurchaseInvoice'); + const doc = await fyo.doc.getNewDoc('PurchaseInvoice'); router.push({ path: `/edit/PurchaseInvoice/${doc.name}`, query: { @@ -146,7 +146,7 @@ export class Party extends Doc { condition: (doc: Doc) => !doc.isNew && (doc.role as PartyRole) !== 'Supplier', action: async (partyDoc, router) => { - const doc = await fyo.doc.getEmptyDoc('SalesInvoice'); + const doc = await fyo.doc.getNewDoc('SalesInvoice'); router.push({ path: `/edit/SalesInvoice/${doc.name}`, query: { diff --git a/models/baseModels/Payment/Payment.ts b/models/baseModels/Payment/Payment.ts index ef3788b6..9eb86ec5 100644 --- a/models/baseModels/Payment/Payment.ts +++ b/models/baseModels/Payment/Payment.ts @@ -1,7 +1,6 @@ -import { LedgerPosting } from 'models/ledgerPosting/ledgerPosting'; import { Fyo } from 'fyo'; import { DocValue } from 'fyo/core/types'; -import Doc from 'fyo/model/doc'; +import { Doc } from 'fyo/model/doc'; import { Action, DefaultMap, @@ -14,6 +13,7 @@ import { } from 'fyo/model/types'; import { ValidationError } from 'fyo/utils/errors'; import { getLedgerLinkAction } from 'models/helpers'; +import { LedgerPosting } from 'models/ledgerPosting/ledgerPosting'; import Money from 'pesa/dist/types/src/money'; import { getIsNullOrUndef } from 'utils'; import { Party } from '../Party/Party'; @@ -237,7 +237,7 @@ export class Payment extends Doc { // update outstanding amounts in invoice and party const newOutstanding = outstandingAmount.sub(amount); await referenceDoc.set('outstandingAmount', newOutstanding); - await referenceDoc.update(); + await referenceDoc.sync(); const party = (await this.fyo.doc.getDoc( 'Party', this.party! @@ -276,7 +276,7 @@ export class Payment extends Doc { amount as Money ), }); - refDoc.update(); + refDoc.sync(); } ); } diff --git a/models/baseModels/PaymentFor/PaymentFor.ts b/models/baseModels/PaymentFor/PaymentFor.ts index eb0730ee..ab3b144e 100644 --- a/models/baseModels/PaymentFor/PaymentFor.ts +++ b/models/baseModels/PaymentFor/PaymentFor.ts @@ -1,4 +1,4 @@ -import Doc from 'fyo/model/doc'; +import { Doc } from 'fyo/model/doc'; import { FiltersMap, FormulaMap } from 'fyo/model/types'; import Money from 'pesa/dist/types/src/money'; diff --git a/models/baseModels/SetupWizard/SetupWizard.ts b/models/baseModels/SetupWizard/SetupWizard.ts index 2081e37c..093d27c1 100644 --- a/models/baseModels/SetupWizard/SetupWizard.ts +++ b/models/baseModels/SetupWizard/SetupWizard.ts @@ -1,5 +1,5 @@ import { t } from 'fyo'; -import Doc from 'fyo/model/doc'; +import { Doc } from 'fyo/model/doc'; import { DependsOnMap, FormulaMap, @@ -7,8 +7,7 @@ import { ValidationMap, } from 'fyo/model/types'; import { validateEmail } from 'fyo/model/validationFunction'; -import { DateTime } from 'luxon'; -import { getCountryInfo } from 'utils/misc'; +import { getCountryInfo, getFiscalYear } from 'utils/misc'; export function getCOAList() { return [ @@ -42,35 +41,19 @@ export class SetupWizard extends Doc { fiscalYearStart: async () => { if (!this.country) return; - const today = DateTime.local(); - const countryInfo = getCountryInfo(); - const fyStart = countryInfo[this.country as string]?.fiscal_year_start as - | string - | undefined; - - if (fyStart) { - return DateTime.fromFormat(fyStart, 'MM-dd') - .plus({ year: [1, 2, 3].includes(today.month) ? -1 : 0 }) - .toISODate(); - } + const fyStart = + countryInfo[this.country as string]?.fiscal_year_start ?? ''; + return getFiscalYear(fyStart, true); }, fiscalYearEnd: async () => { if (!this.country) { return; } - const today = DateTime.local(); - const countryInfo = getCountryInfo(); - const fyEnd = countryInfo[this.country as string]?.fiscal_year_end as - | string - | undefined; - if (fyEnd) { - return DateTime.fromFormat(fyEnd, 'MM-dd') - .plus({ year: [1, 2, 3].includes(today.month) ? 0 : 1 }) - .toISODate(); - } + const fyEnd = countryInfo[this.country as string]?.fiscal_year_end ?? ''; + return getFiscalYear(fyEnd, false); }, currency: async () => { if (!this.country) { diff --git a/models/baseModels/Tax/Tax.ts b/models/baseModels/Tax/Tax.ts index 4f125a2f..2c102124 100644 --- a/models/baseModels/Tax/Tax.ts +++ b/models/baseModels/Tax/Tax.ts @@ -1,4 +1,4 @@ -import Doc from 'fyo/model/doc'; +import { Doc } from 'fyo/model/doc'; import { ListViewSettings } from 'fyo/model/types'; export class Tax extends Doc { diff --git a/models/baseModels/TaxSummary/TaxSummary.ts b/models/baseModels/TaxSummary/TaxSummary.ts index 86e9dc4a..aa297b1f 100644 --- a/models/baseModels/TaxSummary/TaxSummary.ts +++ b/models/baseModels/TaxSummary/TaxSummary.ts @@ -1,4 +1,4 @@ -import Doc from 'fyo/model/doc'; +import { Doc } from 'fyo/model/doc'; import { FormulaMap } from 'fyo/model/types'; import Money from 'pesa/dist/types/src/money'; diff --git a/models/helpers.ts b/models/helpers.ts index 72fa8a79..b258be73 100644 --- a/models/helpers.ts +++ b/models/helpers.ts @@ -1,5 +1,5 @@ import { Fyo } from 'fyo'; -import Doc from 'fyo/model/doc'; +import { Doc } from 'fyo/model/doc'; import { Action, ColumnConfig } from 'fyo/model/types'; import { NotFoundError } from 'fyo/utils/errors'; import { DateTime } from 'luxon'; @@ -34,7 +34,7 @@ export function getTransactionActions(schemaName: string, fyo: Fyo): Action[] { condition: (doc: Doc) => (doc.submitted as boolean) && (doc.outstandingAmount as Money).gt(0), action: async function makePayment(doc: Doc) { - const payment = await fyo.doc.getEmptyDoc('Payment'); + const payment = await fyo.doc.getNewDoc('Payment'); payment.once('afterInsert', async () => { await payment.submit(); }); diff --git a/models/ledgerPosting/ledgerPosting.ts b/models/ledgerPosting/ledgerPosting.ts index 4e2d6f38..08d64494 100644 --- a/models/ledgerPosting/ledgerPosting.ts +++ b/models/ledgerPosting/ledgerPosting.ts @@ -1,5 +1,5 @@ import { Fyo } from 'fyo'; -import Doc from 'fyo/model/doc'; +import { Doc } from 'fyo/model/doc'; import { ValidationError } from 'fyo/utils/errors'; import Money from 'pesa/dist/types/src/money'; import { @@ -127,7 +127,7 @@ export class LedgerPosting { entry.name as string ); entryDoc.reverted = true; - await entryDoc.update(); + await entryDoc.sync(); } let temp; @@ -193,13 +193,13 @@ export class LedgerPosting { for (const entry of this.entries) { const entryDoc = this.fyo.doc.getNewDoc('AccountingLedgerEntry'); Object.assign(entryDoc, entry); - await entryDoc.insert(); + await entryDoc.sync(); } for (const entry of this.accountEntries) { const entryDoc = await this.fyo.doc.getDoc('Account', entry.name); const balance = entryDoc.get('balance') as Money; entryDoc.balance = balance.add(entry.balanceChange); - await entryDoc.update(); + await entryDoc.sync(); } } diff --git a/models/ledgerPosting/types.ts b/models/ledgerPosting/types.ts index 9423ef0a..4461cd86 100644 --- a/models/ledgerPosting/types.ts +++ b/models/ledgerPosting/types.ts @@ -1,4 +1,4 @@ -import Doc from 'fyo/model/doc'; +import { Doc } from 'fyo/model/doc'; import Money from 'pesa/dist/types/src/money'; export interface LedgerPostingOptions { diff --git a/src/App.vue b/src/App.vue index f991c0aa..37728e27 100644 --- a/src/App.vue +++ b/src/App.vue @@ -101,7 +101,7 @@ export default { }, async setupComplete(setupWizardOptions) { const filePath = fyo.config.get(ConfigKeys.LastSelectedFilePath); - await setupInstance(filePath, setupWizardOptions); + await setupInstance(filePath, setupWizardOptions, fyo); await this.setDesk(); }, async showSetupWizardOrDesk(filePath) { @@ -113,7 +113,7 @@ export default { return; } - await initializeInstance(filePath, false, countryCode); + await initializeInstance(filePath, false, countryCode, fyo); await this.setDesk(); }, async setDeskRoute() { diff --git a/src/README.md b/src/README.md index 9c399a07..c81f01dc 100644 --- a/src/README.md +++ b/src/README.md @@ -17,3 +17,8 @@ All of them are triggered from `src/App.vue`. 1. Connect to db. 2. Check if _Setup Wizard_ has been completed, if not, jump to **New Instance** 3. Call `initFyo/initializeInstance` with `dbPath` and `countryCode` + +## Global Fyo + +Global fyo is exported from `initFyo.ts`. Only code that isn't going to be unit +tested should use this. diff --git a/src/components/Controls/Link.vue b/src/components/Controls/Link.vue index 94a5d0fd..3656a38b 100644 --- a/src/components/Controls/Link.vue +++ b/src/components/Controls/Link.vue @@ -104,7 +104,7 @@ export default { }, async openNewDoc() { let doctype = this.df.target; - let doc = await fyo.getEmptyDoc(doctype); + let doc = await fyo.doc.getNewDoc(doctype); let filters = await this.getFilters(); openQuickEdit({ doctype, diff --git a/src/components/ReconciliationValidation.vue b/src/components/ReconciliationValidation.vue index b0b5454c..098573f8 100644 --- a/src/components/ReconciliationValidation.vue +++ b/src/components/ReconciliationValidation.vue @@ -78,7 +78,7 @@ export default { 'dd/MM/yyyy' ).toISO(); payment.set({ clearanceDate }); - await payment.update(); + await payment.sync(); } this.close(); await this.afterReconcile(); diff --git a/src/components/TwoColumnForm.vue b/src/components/TwoColumnForm.vue index f6fc99f9..a723005a 100644 --- a/src/components/TwoColumnForm.vue +++ b/src/components/TwoColumnForm.vue @@ -87,7 +87,7 @@ diff --git a/src/components/Controls/Table.vue b/src/components/Controls/Table.vue index c34b4fd1..9e832c7c 100644 --- a/src/components/Controls/Table.vue +++ b/src/components/Controls/Table.vue @@ -61,10 +61,10 @@ diff --git a/src/components/BackLink.vue b/src/components/BackLink.vue index aa305a88..9b896474 100644 --- a/src/components/BackLink.vue +++ b/src/components/BackLink.vue @@ -1,4 +1,4 @@ - diff --git a/src/components/Controls/Link.vue b/src/components/Controls/Link.vue index fffc27dd..7ad65d9c 100644 --- a/src/components/Controls/Link.vue +++ b/src/components/Controls/Link.vue @@ -27,8 +27,15 @@ export default { }, }, methods: { + getTargetSchemaName() { + return this.df.target; + }, async getSuggestions(keyword = '') { - const schemaName = this.df.target; + const schemaName = this.getTargetSchemaName(); + if (!schemaName) { + return []; + } + const schema = fyo.schemaMap[schemaName]; const filters = await this.getFilters(keyword); diff --git a/src/components/Widgets/PartyWidget.vue b/src/components/Widgets/PartyWidget.vue index 3115505b..d981e9e1 100644 --- a/src/components/Widgets/PartyWidget.vue +++ b/src/components/Widgets/PartyWidget.vue @@ -101,6 +101,7 @@ export default { ], filters: { party: this.doc.name, + cancelled: false, }, limit: 3, orderBy: 'created', diff --git a/src/pages/InvoiceForm.vue b/src/pages/InvoiceForm.vue index 0e6021ff..b9ebebf9 100644 --- a/src/pages/InvoiceForm.vue +++ b/src/pages/InvoiceForm.vue @@ -11,7 +11,7 @@ > {{ t`Print` }} - + @@ -78,7 +78,7 @@ const keys = useKeys(); {{ si.label }}

{{ groupLabelMap[si.group] }} @@ -90,25 +90,30 @@ const keys = useKeys();
-
+
-
+

↑↓ {{ t`Navigate` }}

↩ {{ t`Select` }}

-

esc {{ t`Close` }}

+

esc {{ t`Close` }}

@@ -165,6 +170,8 @@ export default { if (!getIsNullOrUndef(input) && document.activeElement !== input) { input.focus(); } + + this.setFilter(keys); }); this.openModal = false; }, @@ -172,6 +179,37 @@ export default { this.openModal = false; }, methods: { + setFilter(keys) { + if (!keys.has('MetaLeft') && !keys.has('ControlLeft')) { + return; + } + + if (!keys.size === 2) { + return; + } + + const matches = [...keys].join(',').match(/Digit(\d+)/); + if (!matches) { + return; + } + + const digit = matches[1]; + const index = parseInt(digit) - 1; + const group = searchGroups[index]; + if (!group || this.groupFilters[group] === undefined) { + return; + } + + this.groupFilters[group] = !this.groupFilters[group]; + }, + modKey(key) { + key = key.toUpperCase(); + if (this.platform === 'Mac') { + return `⌘ ${key}`; + } + + return `Ctrl ${key}`; + }, open() { this.openModal = true; nextTick(() => { From 104a0fc43f9d8c2b01b7d7553550f584d18317d2 Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Tue, 3 May 2022 23:18:50 +0530 Subject: [PATCH 091/163] fix: add a few event listeners on cached docs --- fyo/core/docHandler.ts | 130 +++++++++++++++++---------------------- fyo/model/doc.ts | 13 ++-- fyo/utils/observable.ts | 2 +- schemas/app/Account.json | 1 - 4 files changed, 61 insertions(+), 85 deletions(-) diff --git a/fyo/core/docHandler.ts b/fyo/core/docHandler.ts index 991b8b22..495b3816 100644 --- a/fyo/core/docHandler.ts +++ b/fyo/core/docHandler.ts @@ -5,13 +5,13 @@ import Observable from 'fyo/utils/observable'; import { Schema } from 'schemas/types'; import { getRandomString } from 'utils'; import { Fyo } from '..'; -import { DocValue, DocValueMap } from './types'; +import { DocValueMap } from './types'; export class DocHandler { fyo: Fyo; models: ModelMap = {}; singles: SinglesMap = {}; - docs: Observable = new Observable(); + docs: Observable = new Observable(); observer: Observable = new Observable(); constructor(fyo: Fyo) { @@ -43,59 +43,6 @@ export class DocHandler { } } - /** - * Cache operations - */ - - addToCache(doc: Doc) { - if (!this.docs) { - return; - } - - // add to `docs` cache - const name = doc.name; - const schemaName = doc.schemaName; - - if (!name) { - return; - } - - if (!this.docs[schemaName]) { - this.docs[schemaName] = {}; - } - - (this.docs[schemaName] as DocMap)[name] = doc; - - // singles available as first level objects too - if (schemaName === doc.name) { - this.singles[name] = doc; - } - - // propagate change to `docs` - doc.on('change', (params: unknown) => { - this.docs!.trigger('change', params); - }); - } - - removeFromCache(schemaName: string, name: string) { - const docMap = this.docs[schemaName] as DocMap | undefined; - delete docMap?.[name]; - } - - getFromCache(schemaName: string, name: string): Doc | undefined { - const docMap = this.docs[schemaName] as DocMap | undefined; - return docMap?.[name]; - } - - isDirty(schemaName: string, name: string): boolean { - const doc = (this.docs?.[schemaName] as DocMap)?.[name]; - if (doc === undefined) { - return false; - } - - return doc.dirty; - } - /** * Doc Operations */ @@ -107,7 +54,7 @@ export class DocHandler { ) { let doc: Doc | undefined; if (!options?.skipDocumentCache) { - doc = this.getFromCache(schemaName, name); + doc = this.#getFromCache(schemaName, name); } if (doc) { @@ -116,7 +63,7 @@ export class DocHandler { doc = this.getNewDoc(schemaName, { name }); await doc.load(); - this.addToCache(doc); + this.#addToCache(doc); return doc; } @@ -125,12 +72,6 @@ export class DocHandler { return await this.getDoc(schemaName, schemaName); } - async getDuplicate(doc: Doc): Promise { - const newDoc = await doc.duplicate(false); - delete newDoc.name; - return newDoc; - } - getNewDoc( schemaName: string, data: DocValueMap = {}, @@ -152,27 +93,66 @@ export class DocHandler { const doc = new Model!(schema, data, this.fyo); doc.name ??= getRandomString(); if (cacheDoc) { - this.addToCache(doc); + this.#addToCache(doc); } return doc; } - async syncDoc(schemaName: string, data: DocValueMap) { - const name = data.name as string | undefined; - if (name === undefined) { + /** + * Cache operations + */ + + #addToCache(doc: Doc) { + if (!doc.name) { return; } - const docExists = await this.fyo.db.exists(schemaName, name); - if (!docExists) { - const doc = this.getNewDoc(schemaName, data); - await doc.sync(); - return; + const name = doc.name; + const schemaName = doc.schemaName; + + if (!this.docs[schemaName]) { + this.docs.set(schemaName, {}); } - const doc = await this.getDoc(schemaName, name); - await doc.setMultiple(data); - await doc.sync(); + this.docs.get(schemaName)![name] = doc; + + // singles available as first level objects too + if (schemaName === doc.name) { + this.singles[name] = doc; + } + + // propagate change to `docs` + doc.on('change', (params: unknown) => { + this.docs!.trigger('change', params); + }); + + doc.on('afterRename', (names: { oldName: string }) => { + this.#removeFromCache(doc.schemaName, names.oldName); + this.#addToCache(doc); + }); + + doc.on('afterSync', () => { + if (doc.name === name) { + return; + } + + this.#removeFromCache(doc.schemaName, name); + this.#addToCache(doc); + }); + + doc.once('afterDelete', () => { + this.#removeFromCache(doc.schemaName, doc.name!); + }); + } + + #removeFromCache(schemaName: string, name: string) { + const docMap = this.docs.get(schemaName); + delete docMap?.[name]; + } + + #getFromCache(schemaName: string, name: string): Doc | undefined { + const docMap = this.docs.get(schemaName); + return docMap?.[name]; } } diff --git a/fyo/model/doc.ts b/fyo/model/doc.ts index c2bfb27e..600e79bf 100644 --- a/fyo/model/doc.ts +++ b/fyo/model/doc.ts @@ -10,7 +10,7 @@ import { FieldTypeEnum, OptionField, Schema, - TargetField + TargetField, } from 'schemas/types'; import { getIsNullOrUndef, getMapFromList, getRandomString } from 'utils'; import { markRaw } from 'vue'; @@ -20,7 +20,7 @@ import { getMissingMandatoryMessage, getPreDefaultValues, setChildDocIdx, - shouldApplyFormula + shouldApplyFormula, } from './helpers'; import { setName } from './naming'; import { @@ -582,14 +582,10 @@ export class Doc extends Observable { this._setBaseMetaValues(); await this._preSync(); - const oldName = this.name!; const validDict = this.getValidDict(); const data = await this.fyo.db.insert(this.schemaName, validDict); this._syncValues(data); - if (oldName !== this.name) { - this.fyo.doc.removeFromCache(this.schemaName, oldName); - } this.fyo.telemetry.log(Verb.Created, this.schemaName); return this; } @@ -662,10 +658,11 @@ export class Doc extends Observable { return; } - await this.trigger('beforeRename'); + const oldName = this.name; + await this.trigger('beforeRename', { oldName, newName }); await this.fyo.db.rename(this.schemaName, this.name!, newName); this.name = newName; - await this.trigger('afterRename'); + await this.trigger('afterRename', { oldName, newName }); this.fyo.doc.observer.trigger(`rename:${this.schemaName}`, this.name); } diff --git a/fyo/utils/observable.ts b/fyo/utils/observable.ts index 8f896d66..5847c888 100644 --- a/fyo/utils/observable.ts +++ b/fyo/utils/observable.ts @@ -39,7 +39,7 @@ export default class Observable { this[key] = value; this.trigger('change', { doc: this, - fieldname: key, + changed: key, }); } diff --git a/schemas/app/Account.json b/schemas/app/Account.json index 6a3b5baa..4e61ae7a 100644 --- a/schemas/app/Account.json +++ b/schemas/app/Account.json @@ -1,7 +1,6 @@ { "name": "Account", "label": "Account", - "doctype": "DocType", "isSingle": false, "isTree": true, "fields": [ From fefc79024d9f26da7a323c868bd9388be916365b Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Wed, 4 May 2022 13:49:40 +0530 Subject: [PATCH 092/163] fix: async issue, transactional deletion cascade - add db observers --- backend/database/bespoke.ts | 1 + fyo/README.md | 15 +- fyo/core/dbHandler.ts | 32 +++- fyo/core/docHandler.ts | 28 +++- fyo/model/doc.ts | 9 +- models/Transactional/LedgerPosting.ts | 3 +- models/Transactional/Transactional.ts | 15 +- .../AccountingLedgerEntry.ts | 10 +- models/baseModels/Invoice/Invoice.ts | 15 +- models/baseModels/Payment/Payment.ts | 8 +- schemas/app/Account.json | 17 ++- src/components/Dropdown.vue | 12 +- src/components/TwoColumnForm.vue | 58 ++++---- src/errorHandling.ts | 4 +- src/pages/ChartOfAccounts.vue | 4 +- src/pages/DataImport.vue | 16 +- src/pages/InvoiceForm.vue | 25 +++- src/pages/JournalEntryForm.vue | 23 ++- src/pages/ListView/List.vue | 4 +- src/pages/SetupWizard/SetupWizard.vue | 5 +- src/utils/types.ts | 2 +- src/utils/ui.ts | 137 +++++++++--------- 22 files changed, 283 insertions(+), 160 deletions(-) diff --git a/backend/database/bespoke.ts b/backend/database/bespoke.ts index f6b18181..1c17d85a 100644 --- a/backend/database/bespoke.ts +++ b/backend/database/bespoke.ts @@ -41,6 +41,7 @@ export class BespokeQueries { .sum({ total: 'baseGrandTotal' }) .sum({ outstanding: 'outstandingAmount' }) .where('submitted', true) + .where('cancelled', false) .whereBetween('date', [fromDate, toDate]) .first(); } diff --git a/fyo/README.md b/fyo/README.md index 5271c738..ec315a15 100644 --- a/fyo/README.md +++ b/fyo/README.md @@ -25,7 +25,7 @@ located in `model`, all classes exported from `books/models` extend this. ### Terminology -- **Schema**: object that defines shape of the data in the database +- **Schema**: object that defines shape of the data in the database. - **Model**: the controller class that extends the `Doc` class or the `Doc` class itself (if a controller doesn't exist). - **Doc**: instance of a Model, i.e. what has the data. @@ -95,3 +95,16 @@ This can be done using `fyo/utils/translation.ts/setLanguageMapOnTranslationStri 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. + +## Observers + +The doc and db handlers have observers (instances of `Observable`) as +properties, these can be accessed using +- `fyo.db.observer` +- `fyo.doc.observer` + +The purpose of the observer is to trigger registered callbacks when some `doc` +operation or `db` operation takes place. + +These are schema level observers i.e. they are registered like so: +`method:schemaName`. The callbacks receive args passed to the functions. \ No newline at end of file diff --git a/fyo/core/dbHandler.ts b/fyo/core/dbHandler.ts index b5ee7750..09158cee 100644 --- a/fyo/core/dbHandler.ts +++ b/fyo/core/dbHandler.ts @@ -1,6 +1,7 @@ import { SingleValue } from 'backend/database/types'; import { Fyo } from 'fyo'; import { DatabaseDemux } from 'fyo/demux/db'; +import Observable from 'fyo/utils/observable'; import { Field, RawValue, SchemaMap } from 'schemas/types'; import { getMapFromList } from 'utils'; import { DatabaseBase, DatabaseDemuxBase, GetAllOptions } from 'utils/db/types'; @@ -23,6 +24,7 @@ export class DatabaseHandler extends DatabaseBase { #demux: DatabaseDemuxBase; dbPath?: string; schemaMap: Readonly = {}; + observer: Observable = new Observable(); fieldValueMap: Record> = {}; constructor(fyo: Fyo, Demux?: DatabaseDemuxConstructor) { @@ -62,6 +64,7 @@ export class DatabaseHandler extends DatabaseBase { const fields = this.schemaMap[schemaName]!.fields!; this.fieldValueMap[schemaName] = getMapFromList(fields, 'fieldname'); } + this.observer = new Observable(); } purgeCache() { @@ -83,6 +86,7 @@ export class DatabaseHandler extends DatabaseBase { schemaName, rawValueMap )) as RawValueMap; + this.observer.trigger(`insert:${schemaName}`, docValueMap); return this.converter.toDocValueMap(schemaName, rawValueMap) as DocValueMap; } @@ -98,6 +102,7 @@ export class DatabaseHandler extends DatabaseBase { name, fields )) as RawValueMap; + this.observer.trigger(`get:${schemaName}`, { name, fields }); return this.converter.toDocValueMap(schemaName, rawValueMap) as DocValueMap; } @@ -106,6 +111,7 @@ export class DatabaseHandler extends DatabaseBase { options: GetAllOptions = {} ): Promise { const rawValueMap = await this.#getAll(schemaName, options); + this.observer.trigger(`getAll:${schemaName}`, options); return this.converter.toDocValueMap( schemaName, rawValueMap @@ -116,7 +122,9 @@ export class DatabaseHandler extends DatabaseBase { schemaName: string, options: GetAllOptions = {} ): Promise { - return await this.#getAll(schemaName, options); + const all = await this.#getAll(schemaName, options); + this.observer.trigger(`getAllRaw:${schemaName}`, options); + return all; } async getSingleValues( @@ -139,6 +147,7 @@ export class DatabaseHandler extends DatabaseBase { }); } + this.observer.trigger(`getSingleValues`, fieldnames); return docSingleValue; } @@ -147,7 +156,9 @@ export class DatabaseHandler extends DatabaseBase { options: GetAllOptions = {} ): Promise { const rawValueMap = await this.#getAll(schemaName, options); - return rawValueMap.length; + const count = rawValueMap.length; + this.observer.trigger(`count:${schemaName}`, options); + return count; } // Update @@ -157,28 +168,37 @@ export class DatabaseHandler extends DatabaseBase { newName: string ): Promise { await this.#demux.call('rename', schemaName, oldName, newName); + this.observer.trigger(`rename:${schemaName}`, { oldName, newName }); } async update(schemaName: string, docValueMap: DocValueMap): Promise { const rawValueMap = this.converter.toRawValueMap(schemaName, docValueMap); await this.#demux.call('update', schemaName, rawValueMap); + this.observer.trigger(`update:${schemaName}`, docValueMap); } // Delete async delete(schemaName: string, name: string): Promise { await this.#demux.call('delete', schemaName, name); + this.observer.trigger(`delete:${schemaName}`, name); } // Other + async exists(schemaName: string, name?: string): Promise { + const doesExist = (await this.#demux.call( + 'exists', + schemaName, + name + )) as boolean; + this.observer.trigger(`exists:${schemaName}`, name); + return doesExist; + } + async close(): Promise { await this.#demux.call('close'); this.purgeCache(); } - async exists(schemaName: string, name?: string): Promise { - return (await this.#demux.call('exists', schemaName, name)) as boolean; - } - /** * Bespoke function * diff --git a/fyo/core/docHandler.ts b/fyo/core/docHandler.ts index 495b3816..76bba70c 100644 --- a/fyo/core/docHandler.ts +++ b/fyo/core/docHandler.ts @@ -1,6 +1,7 @@ import { Doc } from 'fyo/model/doc'; import { DocMap, ModelMap, SinglesMap } from 'fyo/model/types'; import { coreModels } from 'fyo/models'; +import { NotFoundError } from 'fyo/utils/errors'; import Observable from 'fyo/utils/observable'; import { Schema } from 'schemas/types'; import { getRandomString } from 'utils'; @@ -87,7 +88,7 @@ export class DocHandler { schema ??= this.fyo.schemaMap[schemaName]; if (schema === undefined) { - throw new Error(`Schema not found for ${schemaName}`); + throw new NotFoundError(`Schema not found for ${schemaName}`); } const doc = new Model!(schema, data, this.fyo); @@ -113,6 +114,7 @@ export class DocHandler { if (!this.docs[schemaName]) { this.docs.set(schemaName, {}); + this.#setCacheUpdationListeners(schemaName); } this.docs.get(schemaName)![name] = doc; @@ -127,11 +129,6 @@ export class DocHandler { this.docs!.trigger('change', params); }); - doc.on('afterRename', (names: { oldName: string }) => { - this.#removeFromCache(doc.schemaName, names.oldName); - this.#addToCache(doc); - }); - doc.on('afterSync', () => { if (doc.name === name) { return; @@ -140,10 +137,25 @@ export class DocHandler { this.#removeFromCache(doc.schemaName, name); this.#addToCache(doc); }); + } - doc.once('afterDelete', () => { - this.#removeFromCache(doc.schemaName, doc.name!); + #setCacheUpdationListeners(schemaName: string) { + this.fyo.db.observer.on(`delete:${schemaName}`, (name: string) => { + this.#removeFromCache(schemaName, name); }); + + this.fyo.db.observer.on( + `rename:${schemaName}`, + (names: { oldName: string; newName: string }) => { + const doc = this.#getFromCache(schemaName, names.oldName); + if (doc === undefined) { + return; + } + + this.#removeFromCache(schemaName, names.oldName); + this.#addToCache(doc); + } + ); } #removeFromCache(schemaName: string, name: string) { diff --git a/fyo/model/doc.ts b/fyo/model/doc.ts index 600e79bf..ee610310 100644 --- a/fyo/model/doc.ts +++ b/fyo/model/doc.ts @@ -374,7 +374,7 @@ export class Doc extends Observable { } _setBaseMetaValues() { - if (this.schema.isSubmittable && typeof this.submitted !== 'boolean') { + if (this.schema.isSubmittable) { this.submitted = false; this.cancelled = false; } @@ -602,7 +602,7 @@ export class Doc extends Observable { return this; } - async sync() { + async sync(): Promise { await this.trigger('beforeSync'); let doc; if (this.notInserted) { @@ -744,6 +744,11 @@ export class Doc extends Observable { * * Abstractish methods that are called using `this.trigger`. * These are to be overridden if required when subclassing. + * + * Refrain from running methods that call `this.sync` + * in the `beforeLifecycle` methods. + * + * This may cause the lifecycle function to execute incorrectly. */ async change(ch: ChangeArg) {} async validate() {} diff --git a/models/Transactional/LedgerPosting.ts b/models/Transactional/LedgerPosting.ts index 11274d94..008bcf3e 100644 --- a/models/Transactional/LedgerPosting.ts +++ b/models/Transactional/LedgerPosting.ts @@ -117,7 +117,8 @@ export class LedgerPosting { reverted: this.reverted, debit: this.fyo.pesa(0), credit: this.fyo.pesa(0), - } + }, + false ) as AccountingLedgerEntry; this.entries.push(ledgerEntry); diff --git a/models/Transactional/Transactional.ts b/models/Transactional/Transactional.ts index d304d7b6..fc0f2dc5 100644 --- a/models/Transactional/Transactional.ts +++ b/models/Transactional/Transactional.ts @@ -20,25 +20,32 @@ import { LedgerPosting } from './LedgerPosting'; */ export abstract class Transactional extends Doc { - isTransactional = true; + get isTransactional() { + return true; + } + abstract getPosting(): Promise; async validate() { + super.validate(); const posting = await this.getPosting(); posting.validate(); } async afterSubmit(): Promise { + super.afterSubmit(); const posting = await this.getPosting(); await posting.post(); } async afterCancel(): Promise { + super.afterCancel(); const posting = await this.getPosting(); await posting.postReverse(); } async afterDelete(): Promise { + super.afterDelete(); const ledgerEntryIds = (await this.fyo.db.getAll( ModelNameEnum.AccountingLedgerEntry, { @@ -51,7 +58,11 @@ export abstract class Transactional extends Doc { )) as { name: string }[]; for (const { name } of ledgerEntryIds) { - await this.fyo.db.delete(ModelNameEnum.AccountingLedgerEntry, name); + const ledgerEntryDoc = await this.fyo.doc.getDoc( + ModelNameEnum.AccountingLedgerEntry, + name + ); + await ledgerEntryDoc.delete(); } } } diff --git a/models/baseModels/AccountingLedgerEntry/AccountingLedgerEntry.ts b/models/baseModels/AccountingLedgerEntry/AccountingLedgerEntry.ts index 0c0b91c6..91318f94 100644 --- a/models/baseModels/AccountingLedgerEntry/AccountingLedgerEntry.ts +++ b/models/baseModels/AccountingLedgerEntry/AccountingLedgerEntry.ts @@ -41,7 +41,15 @@ export class AccountingLedgerEntry extends Doc { static getListViewSettings(): ListViewSettings { return { - columns: ['account', 'party', 'debit', 'credit', 'balance'], + columns: [ + 'account', + 'party', + 'debit', + 'credit', + 'balance', + 'referenceName', + 'reverted', + ], }; } } diff --git a/models/baseModels/Invoice/Invoice.ts b/models/baseModels/Invoice/Invoice.ts index affaf3a4..540ab938 100644 --- a/models/baseModels/Invoice/Invoice.ts +++ b/models/baseModels/Invoice/Invoice.ts @@ -43,8 +43,13 @@ export abstract class Invoice extends Transactional { await party.updateOutstandingAmount(); } - async beforeCancel() { - await super.beforeCancel(); + async afterCancel() { + await super.afterCancel(); + await this._cancelPayments(); + await this._updatePartyOutStanding(); + } + + async _cancelPayments() { const paymentIds = await this.getPaymentIds(); for (const paymentId of paymentIds) { const paymentDoc = (await this.fyo.doc.getDoc( @@ -55,8 +60,7 @@ export abstract class Invoice extends Transactional { } } - async afterCancel() { - await super.afterCancel(); + async _updatePartyOutStanding() { const partyDoc = (await this.fyo.doc.getDoc( ModelNameEnum.Party, this.party! @@ -69,7 +73,8 @@ export abstract class Invoice extends Transactional { await super.afterDelete(); const paymentIds = await this.getPaymentIds(); for (const name of paymentIds) { - await this.fyo.db.delete(ModelNameEnum.AccountingLedgerEntry, name); + const paymentDoc = await this.fyo.doc.getDoc(ModelNameEnum.Payment, name); + await paymentDoc.delete(); } } diff --git a/models/baseModels/Payment/Payment.ts b/models/baseModels/Payment/Payment.ts index 4419107f..88345efd 100644 --- a/models/baseModels/Payment/Payment.ts +++ b/models/baseModels/Payment/Payment.ts @@ -91,6 +91,10 @@ export class Payment extends Transactional { async validate() { await super.validate(); + if (this.submitted) { + return; + } + this.validateAccounts(); this.validateTotalReferenceAmount(); this.validateWriteOffAccount(); @@ -243,7 +247,8 @@ export class Payment extends Transactional { throw new ValidationError(message); } - async beforeSubmit() { + async afterSubmit() { + await super.afterSubmit(); await this.updateReferenceDocOutstanding(); await this.updatePartyOutstanding(); } @@ -281,6 +286,7 @@ export class Payment extends Transactional { const outstandingAmount = (refDoc.outstandingAmount as Money).add( ref.amount! ); + await refDoc.setAndSync({ outstandingAmount }); } } diff --git a/schemas/app/Account.json b/schemas/app/Account.json index 4e61ae7a..651a57bb 100644 --- a/schemas/app/Account.json +++ b/schemas/app/Account.json @@ -37,13 +37,14 @@ "label": "Expense" } ], - "required": true + "readOnly": true }, { "fieldname": "parentAccount", "label": "Parent Account", "fieldtype": "Link", - "target": "Account" + "target": "Account", + "readOnly": true }, { "fieldname": "accountType", @@ -138,9 +139,17 @@ { "fieldname": "isGroup", "label": "Is Group", - "fieldtype": "Check" + "fieldtype": "Check", + "readOnly": true } ], - "quickEditFields": ["name", "rootType", "parentAccount", "accountType"], + "quickEditFields": [ + "name", + "rootType", + "parentAccount", + "accountType", + "isGroup", + "balance" + ], "keywordFields": ["name", "rootType", "accountType"] } diff --git a/src/components/Dropdown.vue b/src/components/Dropdown.vue index 10e1469f..56d593de 100644 --- a/src/components/Dropdown.vue +++ b/src/components/Dropdown.vue @@ -183,16 +183,16 @@ export default { return emptyMessage; }, - selectItem(d) { + async selectItem(d) { if (!d.action) { return; } if (this.doc) { - return d.action(this.doc, this.$router); + return await d.action(this.doc, this.$router); } - - d.action() + + await d.action(); }, toggleDropdown(flag) { if (flag == null) { @@ -201,11 +201,11 @@ export default { this.isShown = Boolean(flag); } }, - selectHighlightedItem() { + async selectHighlightedItem() { if (![-1, this.items.length].includes(this.highlightedIndex)) { // valid selection let item = this.items[this.highlightedIndex]; - this.selectItem(item); + await this.selectItem(item); } }, highlightItemUp() { diff --git a/src/components/TwoColumnForm.vue b/src/components/TwoColumnForm.vue index db5c25c5..13e62801 100644 --- a/src/components/TwoColumnForm.vue +++ b/src/components/TwoColumnForm.vue @@ -10,7 +10,7 @@ :df="df" :value="doc[df.fieldname]" :read-only="evaluateReadOnly(df)" - @change="(value) => onChange(df, value)" + @change="async (value) => await onChange(df, value)" /> @@ -71,9 +71,9 @@ :value="getRegularValue(df)" :class="{ 'p-2': df.fieldtype === 'Check' }" :read-only="evaluateReadOnly(df)" - @change="(value) => onChange(df, value)" + @change="async (value) => await onChange(df, value)" @focus="activateInlineEditing(df)" - @new-doc="(newdoc) => onChange(df, newdoc.name)" + @new-doc="async (newdoc) => await onChange(df, newdoc.name)" />
{ - this.errors[df.fieldname] = getErrorMessage(e, this.doc); - }) - .then((success) => { - if (!success) { - return; - } + async onChangeCommon(df, value, oldValue) { + let isSet = false; + try { + isSet = this.doc.set(df.fieldname, value); + } catch (err) { + this.errors[df.fieldname] = getErrorMessage(err, this.doc); + } - this.handlePostSet(df, value, oldValue); - }); + if (!isSet) { + return; + } + + await this.handlePostSet(df, value, oldValue); }, - handlePostSet(df, value, oldValue) { + async handlePostSet(df, value, oldValue) { this.setFormFields(); if (this.emitChange) { this.$emit('change', df, value, oldValue); @@ -212,14 +212,22 @@ export default { return; } - this.doc.sync(); + await this.doc.sync(); } }, - sync() { - return this.doc.sync().catch((e) => handleErrorWithDialog(e, this.doc)); + async sync() { + try { + await this.doc.sync(); + } catch (err) { + await handleErrorWithDialog(err, this.doc); + } }, - submit() { - return this.doc.submit().catch((e) => handleErrorWithDialog(e, this.doc)); + async submit() { + try { + await this.doc.submit(); + } catch (err) { + await handleErrorWithDialog(err, this.doc); + } }, async activateInlineEditing(df) { if (!df.inline) { @@ -229,8 +237,8 @@ export default { this.inlineEditField = df; if (!this.doc[df.fieldname]) { this.inlineEditDoc = await fyo.doc.getNewDoc(df.target); - this.inlineEditDoc.once('afterSync', () => { - this.onChangeCommon(df, this.inlineEditDoc.name); + this.inlineEditDoc.once('afterSync', async () => { + await this.onChangeCommon(df, this.inlineEditDoc.name); }); } else { this.inlineEditDoc = this.doc.getLink(df.fieldname); diff --git a/src/errorHandling.ts b/src/errorHandling.ts index ec6ca811..4979816a 100644 --- a/src/errorHandling.ts +++ b/src/errorHandling.ts @@ -102,12 +102,12 @@ export function handleError( } } -export function handleErrorWithDialog(error: Error, doc?: Doc) { +export async function handleErrorWithDialog(error: Error, doc?: Doc) { const errorMessage = getErrorMessage(error, doc); handleError(false, error, { errorMessage, doc }); const name = (error as BaseError).label ?? error.name; - showMessageDialog({ message: name, detail: errorMessage }); + await showMessageDialog({ message: name, detail: errorMessage }); throw error; } diff --git a/src/pages/ChartOfAccounts.vue b/src/pages/ChartOfAccounts.vue index 092933a4..0a32ae76 100644 --- a/src/pages/ChartOfAccounts.vue +++ b/src/pages/ChartOfAccounts.vue @@ -276,7 +276,7 @@ export default { await this.fetchChildren(parentAccount, true); // open quick edit - openQuickEdit({ + await openQuickEdit({ schemaName: 'Account', name: account.name, }); @@ -285,7 +285,7 @@ export default { } catch (e) { // unfreeze input this.insertingAccount = false; - handleErrorWithDialog(e, account); + await handleErrorWithDialog(e, account); } }, isQuickEditOpen(account) { diff --git a/src/pages/DataImport.vue b/src/pages/DataImport.vue index 6a9becc5..f5a7e08d 100644 --- a/src/pages/DataImport.vue +++ b/src/pages/DataImport.vue @@ -533,33 +533,30 @@ export default { } if (this.isRequiredUnassigned) { - showMessageDialog({ + return await showMessageDialog({ message: this.t`Required Fields not Assigned`, detail: this .t`Please assign the following fields ${this.requiredUnassigned.join( ', ' )}`, }); - return; } if (this.importer.assignedMatrix.length === 0) { - showMessageDialog({ + return await showMessageDialog({ message: this.t`No Data to Import`, detail: this.t`Please select a file with data to import.`, }); - return; } const { success, names, message } = await this.importer.importData( this.setLoadingStatus ); if (!success) { - showMessageDialog({ + return await showMessageDialog({ message: this.t`Import Failed`, detail: message, }); - return; } this.names = names; @@ -593,7 +590,9 @@ export default { await ipcRenderer.invoke(IPC_ACTIONS.GET_FILE, options); if (!success && !canceled) { - showMessageDialog({ message: this.t`File selection failed.` }); + return await showMessageDialog({ + message: this.t`File selection failed.`, + }); } if (!success || canceled) { @@ -603,11 +602,10 @@ export default { const text = new TextDecoder().decode(data); const isValid = this.importer.selectFile(text); if (!isValid) { - showMessageDialog({ + return await showMessageDialog({ message: this.t`Bad import data.`, detail: this.t`Could not select file.`, }); - return; } this.file = { diff --git a/src/pages/InvoiceForm.vue b/src/pages/InvoiceForm.vue index b9ebebf9..0f11d010 100644 --- a/src/pages/InvoiceForm.vue +++ b/src/pages/InvoiceForm.vue @@ -276,7 +276,7 @@ export default { routeTo(`/list/${this.schemaName}`); return; } - this.handleError(error); + await this.handleError(error); } this.printSettings = await fyo.doc.getSingle('PrintSettings'); this.companyName = ( @@ -305,20 +305,29 @@ export default { return fyo.getField(this.schemaName, fieldname); }, async sync() { - return this.doc.sync().catch(this.handleError); + try { + await this.doc.sync(); + } catch (err) { + await this.handleError(err); + } }, - submit() { + async submit() { const message = this.schemaName === ModelNameEnum.SalesInvoice ? this.t`Submit Sales Invoice?` : this.t`Submit Purchase Invoice?`; - showMessageDialog({ + const ref = this + await showMessageDialog({ message, buttons: [ { label: this.t`Yes`, - action: () => { - this.doc.submit().catch(this.handleError); + async action() { + try { + await ref.doc.submit(); + } catch (err) { + await ref.handleError(err); + } }, }, { @@ -328,8 +337,8 @@ export default { ], }); }, - handleError(e) { - handleErrorWithDialog(e, this.doc); + async handleError(e) { + await handleErrorWithDialog(e, this.doc); }, openInvoiceSettings() { openSettings('Invoice'); diff --git a/src/pages/JournalEntryForm.vue b/src/pages/JournalEntryForm.vue index 4b62ef1c..d751cb86 100644 --- a/src/pages/JournalEntryForm.vue +++ b/src/pages/JournalEntryForm.vue @@ -200,7 +200,7 @@ export default { return; } - this.handleError(error); + await this.handleError(error); } if (fyo.store.isDevelopment) { @@ -238,16 +238,25 @@ export default { return fyo.getField(ModelNameEnum.JournalEntry, fieldname); }, async sync() { - return this.doc.sync().catch(this.handleError); + try { + await this.doc.sync(); + } catch (err) { + this.handleError(err); + } }, async submit() { - showMessageDialog({ + const ref = this; + await showMessageDialog({ message: this.t`Submit Journal Entry?`, buttons: [ { label: this.t`Yes`, - action: () => { - this.doc.submit().catch(this.handleError); + async action() { + try { + await ref.doc.submit(); + } catch (err) { + await ref.handleError(err); + } }, }, { @@ -257,8 +266,8 @@ export default { ], }); }, - handleError(e) { - handleErrorWithDialog(e, this.doc); + async handleError(e) { + await handleErrorWithDialog(e, this.doc); }, }, }; diff --git a/src/pages/ListView/List.vue b/src/pages/ListView/List.vue index 9868260d..d17baf94 100644 --- a/src/pages/ListView/List.vue +++ b/src/pages/ListView/List.vue @@ -120,7 +120,7 @@ export default { }, methods: { setUpdateListeners() { - const listener = () => { + const listener = (name) => { this.updateData(); }; @@ -130,7 +130,7 @@ export default { } fyo.doc.observer.on(`sync:${this.schemaName}`, listener); - fyo.doc.observer.on(`delete:${this.schemaName}`, listener); + fyo.db.observer.on(`delete:${this.schemaName}`, listener); fyo.doc.observer.on(`rename:${this.schemaName}`, listener); }, openForm(doc) { diff --git a/src/pages/SetupWizard/SetupWizard.vue b/src/pages/SetupWizard/SetupWizard.vue index bc3af5df..64cf4977 100644 --- a/src/pages/SetupWizard/SetupWizard.vue +++ b/src/pages/SetupWizard/SetupWizard.vue @@ -180,8 +180,9 @@ export default { }, async submit() { if (!this.allValuesFilled()) { - showMessageDialog({ message: this.t`Please fill all values` }); - return; + return await showMessageDialog({ + message: this.t`Please fill all values`, + }); } this.loading = true; diff --git a/src/utils/types.ts b/src/utils/types.ts index 4aa2b153..d9809bb1 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -1,6 +1,6 @@ export interface MessageDialogButton { label: string; - action: () => void; + action: () => Promise | unknown; } export interface MessageDialogOptions { diff --git a/src/utils/ui.ts b/src/utils/ui.ts index 731912d7..52ade811 100644 --- a/src/utils/ui.ts +++ b/src/utils/ui.ts @@ -87,9 +87,11 @@ export async function showMessageDialog({ )) as { response: number }; const button = buttons[response]; - if (button && button.action) { - button.action(); + if (!button?.action) { + return null; } + + return await button.action(); } export async function showToast(options: ToastOptions) { @@ -138,37 +140,36 @@ export async function routeTo(route: string | RouteLocationRaw) { await router.push(routeOptions); } -export function deleteDocWithPrompt(doc: Doc) { +export async function deleteDocWithPrompt(doc: Doc) { const schemaLabel = fyo.schemaMap[doc.schemaName]!.label; let detail = t`This action is permanent.`; if (doc.isTransactional) { - detail = t`This action is permanent and will delete all ledger entries.`; + detail = t`This action is permanent and will delete associated ledger entries.`; } - return new Promise((resolve) => { - showMessageDialog({ - message: t`Delete ${schemaLabel} ${doc.name!}?`, - detail, - buttons: [ - { - label: t`Delete`, - action: () => { - doc - .delete() - .then(() => resolve(true)) - .catch((e: Error) => { - handleErrorWithDialog(e, doc); - }); - }, + return await showMessageDialog({ + message: t`Delete ${schemaLabel} ${doc.name!}?`, + detail, + buttons: [ + { + label: t`Delete`, + async action() { + try { + await doc.delete(); + return true; + } catch (err) { + handleErrorWithDialog(err as Error, doc); + return false; + } }, - { - label: t`Cancel`, - action() { - resolve(false); - }, + }, + { + label: t`Cancel`, + action() { + return false; }, - ], - }); + }, + ], }); } @@ -204,30 +205,30 @@ export async function cancelDocWithPrompt(doc: Doc) { } } - return new Promise((resolve) => { - const schemaLabel = fyo.schemaMap[doc.schemaName]!.label; - showMessageDialog({ - message: t`Cancel ${schemaLabel} ${doc.name!}?`, - detail, - buttons: [ - { - label: t`Yes`, - async action() { - const entryDoc = await fyo.doc.getDoc(doc.schemaName, doc.name!); - entryDoc - .cancel() - .then(() => resolve(true)) - .catch((e) => handleErrorWithDialog(e, doc)); - }, + const schemaLabel = fyo.schemaMap[doc.schemaName]!.label; + return await showMessageDialog({ + message: t`Cancel ${schemaLabel} ${doc.name!}?`, + detail, + buttons: [ + { + label: t`Yes`, + async action() { + try { + await doc.cancel(); + return true; + } catch (err) { + handleErrorWithDialog(err as Error, doc); + return false; + } }, - { - label: t`No`, - action() { - resolve(false); - }, + }, + { + label: t`No`, + action() { + return false; }, - ], - }); + }, + ], }); } @@ -258,13 +259,13 @@ function getCancelAction(doc: Doc): Action { component: { template: '{{ t`Cancel` }}', }, - condition: (doc: Doc) => !!doc.submitted && !doc.cancelled, - action: () => { - cancelDocWithPrompt(doc).then((res) => { - if (res) { - router.push(`/list/${doc.schemaName}`); - } - }); + condition: (doc: Doc) => doc.isSubmitted, + async action() { + const res = await cancelDocWithPrompt(doc); + + if (res) { + router.push(`/list/${doc.schemaName}`); + } }, }; } @@ -278,12 +279,12 @@ function getDeleteAction(doc: Doc): Action { condition: (doc: Doc) => (!doc.notInserted && !doc.schema.isSubmittable && !doc.schema.isSingle) || doc.isCancelled, - action: () => - deleteDocWithPrompt(doc).then((res) => { - if (res) { - routeTo(`/list/${doc.schemaName}`); - } - }), + async action() { + const res = await deleteDocWithPrompt(doc); + if (res) { + routeTo(`/list/${doc.schemaName}`); + } + }, }; } @@ -297,20 +298,26 @@ function getDuplicateAction(doc: Doc): Action { !doc.notInserted && !(doc.cancelled || false) ), - action: () => { - showMessageDialog({ + async action() { + await showMessageDialog({ message: t`Duplicate ${doc.schemaName} ${doc.name!}?`, buttons: [ { label: t`Yes`, async action() { - doc.duplicate(); + try { + doc.duplicate(); + return true; + } catch (err) { + handleErrorWithDialog(err as Error, doc); + return false; + } }, }, { label: t`No`, action() { - // no-op + return false; }, }, ], From eedb4415ce15416c22361caee8ca834a8eac39b0 Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Wed, 4 May 2022 18:17:45 +0530 Subject: [PATCH 093/163] incr: redo duplication (don't sync) - fix journal entry --- backend/database/core.ts | 13 ++-- fyo/model/doc.ts | 52 ++++++++------- models/Transactional/LedgerPosting.ts | 28 +++++--- .../baseModels/JournalEntry/JournalEntry.ts | 4 +- models/baseModels/Party/Party.ts | 2 + models/baseModels/Payment/Payment.ts | 64 +++++++++++++++---- schemas/app/Payment.json | 2 +- src/components/Controls/Table.vue | 10 ++- src/utils/ui.ts | 23 +++++-- 9 files changed, 141 insertions(+), 57 deletions(-) diff --git a/backend/database/core.ts b/backend/database/core.ts index 3590d16e..7af66a76 100644 --- a/backend/database/core.ts +++ b/backend/database/core.ts @@ -15,7 +15,11 @@ import { SchemaMap, TargetField, } from '../../schemas/types'; -import { getRandomString, getValueMapFromList } from '../../utils'; +import { + getIsNullOrUndef, + getRandomString, + getValueMapFromList, +} from '../../utils'; import { DatabaseBase, GetAllOptions, QueryFilter } from '../../utils/db/types'; import { getDefaultMetaFieldValueMap, sqliteTypeMap, SYSTEM } from '../helpers'; import { @@ -889,12 +893,13 @@ export default class DatabaseCore extends DatabaseBase { const tableFieldValue = fieldValueMap[field.fieldname] as | FieldValueMap[] - | undefined; - if (tableFieldValue === undefined) { + | undefined + | null; + if (getIsNullOrUndef(tableFieldValue)) { continue; } - for (const child of tableFieldValue) { + for (const child of tableFieldValue!) { this.#prepareChild(schemaName, parentName, child, field, added.length); if ( diff --git a/fyo/model/doc.ts b/fyo/model/doc.ts index ee610310..d32e6e14 100644 --- a/fyo/model/doc.ts +++ b/fyo/model/doc.ts @@ -127,6 +127,10 @@ export class Doc extends Observable { } else { this[fieldname] = value ?? this[fieldname] ?? null; } + + if (field.fieldtype === FieldTypeEnum.Table && !this[fieldname]) { + this[fieldname] = []; + } } } @@ -269,6 +273,10 @@ export class Doc extends Observable { } _getChildDoc(docValueMap: Doc | DocValueMap, fieldname: string): Doc { + if (!this.name) { + this.name = getRandomString(); + } + docValueMap.name ??= getRandomString(); // Child Meta Fields @@ -304,6 +312,10 @@ export class Doc extends Observable { for (const field of tableFields) { const childDocs = this.get(field.fieldname) as Doc[]; + if (!childDocs) { + continue; + } + checkForMandatory.push(...childDocs); } @@ -351,13 +363,18 @@ export class Doc extends Observable { await validator(value); } - getValidDict(): DocValueMap { + getValidDict(filterMeta: boolean = false): DocValueMap { + let fields = this.schema.fields; + if (filterMeta) { + fields = this.schema.fields.filter((f) => !f.meta); + } + const data: DocValueMap = {}; - for (const field of this.schema.fields) { + for (const field of fields) { let value = this[field.fieldname] as DocValue | DocValueMap[]; if (Array.isArray(value)) { - value = value.map((doc) => (doc as Doc).getValidDict()); + value = value.map((doc) => (doc as Doc).getValidDict(filterMeta)); } if (isPesa(value)) { @@ -704,25 +721,17 @@ export class Doc extends Observable { return await this.sync(); } - async duplicate(shouldSync: boolean = true): Promise { - const updateMap: DocValueMap = {}; - const docValueMap = this.getValidDict(); - const fieldnames = this.schema.fields.map((f) => f.fieldname); - - for (const fn of fieldnames) { - const value = docValueMap[fn]; - if (getIsNullOrUndef(value)) { + duplicate(): Doc { + const updateMap = this.getValidDict(true); + for (const field in updateMap) { + const value = updateMap[field]; + if (!Array.isArray(value)) { continue; } - if (Array.isArray(value)) { - value.forEach((row) => { - delete row.name; - delete row.parent; - }); + for (const row of value) { + delete row.name; } - - updateMap[fn] = value; } if (this.numberSeries) { @@ -731,12 +740,7 @@ export class Doc extends Observable { updateMap.name = updateMap.name + ' CPY'; } - const doc = this.fyo.doc.getNewDoc(this.schemaName, updateMap, false); - if (shouldSync) { - await doc.sync(); - } - - return doc; + return this.fyo.doc.getNewDoc(this.schemaName, updateMap); } /** diff --git a/models/Transactional/LedgerPosting.ts b/models/Transactional/LedgerPosting.ts index 008bcf3e..dfd34d00 100644 --- a/models/Transactional/LedgerPosting.ts +++ b/models/Transactional/LedgerPosting.ts @@ -26,7 +26,8 @@ export class LedgerPosting { fyo: Fyo; refDoc: Transactional; entries: AccountingLedgerEntry[]; - entryMap: Record; + creditMap: Record; + debitMap: Record; reverted: boolean; accountBalanceChanges: AccountBalanceChange[]; @@ -34,19 +35,20 @@ export class LedgerPosting { this.fyo = fyo; this.refDoc = refDoc; this.entries = []; - this.entryMap = {}; + this.creditMap = {}; + this.debitMap = {}; this.reverted = false; this.accountBalanceChanges = []; } async debit(account: string, amount: Money) { - const ledgerEntry = this._getLedgerEntry(account); + const ledgerEntry = this._getLedgerEntry(account, 'debit'); await ledgerEntry.set('debit', ledgerEntry.debit!.add(amount)); await this._updateAccountBalanceChange(account, 'debit', amount); } async credit(account: string, amount: Money) { - const ledgerEntry = this._getLedgerEntry(account); + const ledgerEntry = this._getLedgerEntry(account, 'credit'); await ledgerEntry.set('credit', ledgerEntry.credit!.add(amount)); await this._updateAccountBalanceChange(account, 'credit', amount); } @@ -101,9 +103,17 @@ export class LedgerPosting { }); } - _getLedgerEntry(account: string): AccountingLedgerEntry { - if (this.entryMap[account]) { - return this.entryMap[account]; + _getLedgerEntry( + account: string, + type: TransactionType + ): AccountingLedgerEntry { + let map = this.creditMap; + if (type === 'debit') { + map = this.debitMap; + } + + if (map[account]) { + return map[account]; } const ledgerEntry = this.fyo.doc.getNewDoc( @@ -122,9 +132,9 @@ export class LedgerPosting { ) as AccountingLedgerEntry; this.entries.push(ledgerEntry); - this.entryMap[account] = ledgerEntry; + map[account] = ledgerEntry; - return this.entryMap[account]; + return map[account]; } _validateIsEqual() { diff --git a/models/baseModels/JournalEntry/JournalEntry.ts b/models/baseModels/JournalEntry/JournalEntry.ts index 7702d255..7a9e16d1 100644 --- a/models/baseModels/JournalEntry/JournalEntry.ts +++ b/models/baseModels/JournalEntry/JournalEntry.ts @@ -13,12 +13,12 @@ import Money from 'pesa/dist/types/src/money'; import { LedgerPosting } from '../../Transactional/LedgerPosting'; export class JournalEntry extends Transactional { - accounts: Doc[] = []; + accounts?: Doc[]; async getPosting() { const posting: LedgerPosting = new LedgerPosting(this, this.fyo); - for (const row of this.accounts) { + for (const row of this.accounts ?? []) { const debit = row.debit as Money; const credit = row.credit as Money; const account = row.account as string; diff --git a/models/baseModels/Party/Party.ts b/models/baseModels/Party/Party.ts index 9b60ccb2..932df43d 100644 --- a/models/baseModels/Party/Party.ts +++ b/models/baseModels/Party/Party.ts @@ -11,9 +11,11 @@ import { validateEmail, validatePhoneNumber, } from 'fyo/model/validationFunction'; +import Money from 'pesa/dist/types/src/money'; import { PartyRole } from './types'; export class Party extends Doc { + outstandingAmount?: Money; async updateOutstandingAmount() { /** * If Role === "Both" then outstanding Amount diff --git a/models/baseModels/Payment/Payment.ts b/models/baseModels/Payment/Payment.ts index 88345efd..9232f163 100644 --- a/models/baseModels/Payment/Payment.ts +++ b/models/baseModels/Payment/Payment.ts @@ -73,7 +73,7 @@ export class Payment extends Transactional { updateAmountOnReferenceUpdate() { this.amount = this.fyo.pesa(0); - for (const paymentReference of this.for as Doc[]) { + for (const paymentReference of (this.for ?? []) as Doc[]) { this.amount = (this.amount as Money).add( paymentReference.amount as Money ); @@ -163,16 +163,28 @@ export class Payment extends Transactional { } async getPosting() { - const account = this.account as string; - const paymentAccount = this.paymentAccount as string; - const amount = this.amount as Money; - const writeoff = this.writeoff as Money; + /** + * account : From Account + * paymentAccount : To Account + * + * if Receive + * - account : Debtors, etc + * - paymentAccount : Cash, Bank, etc + * + * if Pay + * - account : Cash, Bank, etc + * - paymentAccount : Creditors, etc + */ const posting: LedgerPosting = new LedgerPosting(this, this.fyo); - await posting.debit(paymentAccount as string, amount.sub(writeoff)); - await posting.credit(account as string, amount.sub(writeoff)); + const paymentAccount = this.paymentAccount as string; + const account = this.account as string; + const amount = this.amount as Money; - this.applyWriteOffPosting(posting); + await posting.debit(paymentAccount as string, amount); + await posting.credit(account as string, amount); + + await this.applyWriteOffPosting(posting); return posting; } @@ -183,11 +195,12 @@ export class Payment extends Transactional { } const account = this.account as string; + const paymentAccount = this.paymentAccount as string; const writeOffAccount = this.fyo.singles.AccountingSettings! .writeOffAccount as string; if (this.paymentType === 'Pay') { - await posting.credit(account, writeoff); + await posting.credit(paymentAccount, writeoff); await posting.debit(writeOffAccount, writeoff); } else { await posting.debit(account, writeoff); @@ -277,7 +290,7 @@ export class Payment extends Transactional { } async _revertReferenceOutstanding() { - for (const ref of this.for as PaymentFor[]) { + for (const ref of (this.for ?? []) as PaymentFor[]) { const refDoc = await this.fyo.doc.getDoc( ref.referenceType!, ref.referenceName! @@ -318,6 +331,32 @@ export class Payment extends Transactional { }, dependsOn: ['paymentMethod', 'paymentType'], }, + paymentType: { + formula: async () => { + if (!this.party) { + return; + } + const partyDoc = await this.fyo.doc.getDoc( + ModelNameEnum.Party, + this.party + ); + if (partyDoc.role === 'Supplier') { + return 'Pay'; + } else if (partyDoc.role === 'Customer') { + return 'Receive'; + } + + const outstanding = partyDoc.outstandingAmount as Money; + if (outstanding?.isZero() ?? true) { + return ''; + } + + if (outstanding?.isPositive()) { + return 'Receive'; + } + return 'Pay'; + }, + }, amount: { formula: async () => this.getSum('for', 'amount', false), dependsOn: ['for'], @@ -332,7 +371,10 @@ export class Payment extends Transactional { ); } - if ((this.for as Doc[]).length === 0) return; + if (((this.for ?? []) as Doc[]).length === 0) { + return; + } + const amount = this.getSum('for', 'amount', false); if ((value as Money).gt(amount)) { diff --git a/schemas/app/Payment.json b/schemas/app/Payment.json index 7048d310..e57780e2 100644 --- a/schemas/app/Payment.json +++ b/schemas/app/Payment.json @@ -113,7 +113,7 @@ }, { "fieldname": "writeoff", - "label": "Write Off / Refund", + "label": "Write Off", "fieldtype": "Currency" }, { diff --git a/src/components/Controls/Table.vue b/src/components/Controls/Table.vue index a4b9b6b7..0ecf3f39 100644 --- a/src/components/Controls/Table.vue +++ b/src/components/Controls/Table.vue @@ -21,7 +21,11 @@ -
+
{{ t`${value.length} rows` }}
diff --git a/src/utils/ui.ts b/src/utils/ui.ts index 52ade811..a10ee395 100644 --- a/src/utils/ui.ts +++ b/src/utils/ui.ts @@ -7,6 +7,7 @@ import { t } from 'fyo'; import { Doc } from 'fyo/model/doc'; import { Action } from 'fyo/model/types'; import { getActions } from 'fyo/utils'; +import { ModelNameEnum } from 'models/types'; import { handleErrorWithDialog } from 'src/errorHandling'; import { fyo } from 'src/initFyo'; import router from 'src/router'; @@ -288,15 +289,28 @@ function getDeleteAction(doc: Doc): Action { }; } +async function openEdit(doc: Doc) { + const isFormEdit = [ + ModelNameEnum.SalesInvoice, + ModelNameEnum.PurchaseInvoice, + ModelNameEnum.JournalEntry, + ].includes(doc.schemaName as ModelNameEnum); + + if (isFormEdit) { + return await routeTo(`/edit/${doc.schemaName}/${doc.name!}`); + } + + await openQuickEdit({ schemaName: doc.schemaName, name: doc.name! }); +} + function getDuplicateAction(doc: Doc): Action { const isSubmittable = !!doc.schema.isSubmittable; return { label: t`Duplicate`, condition: (doc: Doc) => !!( - ((isSubmittable && doc && doc.submitted) || !isSubmittable) && - !doc.notInserted && - !(doc.cancelled || false) + ((isSubmittable && doc.submitted) || !isSubmittable) && + !doc.notInserted ), async action() { await showMessageDialog({ @@ -306,7 +320,8 @@ function getDuplicateAction(doc: Doc): Action { label: t`Yes`, async action() { try { - doc.duplicate(); + const dupe = await doc.duplicate(); + await openEdit(dupe); return true; } catch (err) { handleErrorWithDialog(err as Error, doc); From a0f812e1a8ece7397530b3aa3d8985b3b275bd90 Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Wed, 4 May 2022 21:55:08 +0530 Subject: [PATCH 094/163] incr: add keyword fields, start with doc search --- backend/database/core.ts | 3 +- schemas/app/Account.json | 10 +- schemas/app/AccountingLedgerEntry.json | 3 +- schemas/app/Address.json | 8 -- schemas/app/CompanySettings.json | 3 +- schemas/app/Currency.json | 1 - schemas/app/Item.json | 2 +- schemas/app/JournalEntry.json | 7 +- schemas/app/JournalEntryAccount.json | 5 +- schemas/app/NumberSeries.json | 3 +- schemas/app/Party.json | 2 +- schemas/app/PaymentFor.json | 3 +- schemas/app/PurchaseInvoice.json | 2 +- schemas/app/PurchaseInvoiceItem.json | 3 +- schemas/app/SalesInvoice.json | 5 +- schemas/app/SalesInvoiceItem.json | 3 +- schemas/core/SingleValue.json | 5 +- schemas/core/SystemSettings.json | 9 +- src/components/Controls/Link.vue | 1 - src/pages/ListView/ListView.vue | 2 +- src/utils/search.ts | 161 +++++++++++++++++++++++++ 21 files changed, 190 insertions(+), 51 deletions(-) diff --git a/backend/database/core.ts b/backend/database/core.ts index 7af66a76..f5518bb3 100644 --- a/backend/database/core.ts +++ b/backend/database/core.ts @@ -243,8 +243,9 @@ export default class DatabaseCore extends DatabaseBase { } const hasCreated = !!schema.fields.find((f) => f.fieldname === 'created'); + const { - fields = ['name', ...(schema.keywordFields ?? [])], + fields = ['name'], filters, offset, limit, diff --git a/schemas/app/Account.json b/schemas/app/Account.json index 651a57bb..2ad675ca 100644 --- a/schemas/app/Account.json +++ b/schemas/app/Account.json @@ -143,13 +143,5 @@ "readOnly": true } ], - "quickEditFields": [ - "name", - "rootType", - "parentAccount", - "accountType", - "isGroup", - "balance" - ], - "keywordFields": ["name", "rootType", "accountType"] + "quickEditFields": ["name", "rootType", "parentAccount", "accountType", "isGroup", "balance"] } diff --git a/schemas/app/AccountingLedgerEntry.json b/schemas/app/AccountingLedgerEntry.json index 303fc7bf..3ff9e833 100644 --- a/schemas/app/AccountingLedgerEntry.json +++ b/schemas/app/AccountingLedgerEntry.json @@ -83,6 +83,5 @@ "balance", "reverted", "reverts" - ], - "keywordFields": ["account", "party", "referenceName"] + ] } diff --git a/schemas/app/Address.json b/schemas/app/Address.json index f41758f4..c3a40516 100644 --- a/schemas/app/Address.json +++ b/schemas/app/Address.json @@ -76,13 +76,5 @@ "country", "postalCode" ], - "keywordFields": [ - "addressLine1", - "addressLine2", - "city", - "state", - "country", - "postalCode" - ], "inlineEditDisplayField": "addressDisplay" } diff --git a/schemas/app/CompanySettings.json b/schemas/app/CompanySettings.json index db405430..61f5e62b 100644 --- a/schemas/app/CompanySettings.json +++ b/schemas/app/CompanySettings.json @@ -18,6 +18,5 @@ "required": true, "target": "Address" } - ], - "keywordFields": ["companyName"] + ] } diff --git a/schemas/app/Currency.json b/schemas/app/Currency.json index 44c13664..94ab6cad 100644 --- a/schemas/app/Currency.json +++ b/schemas/app/Currency.json @@ -30,6 +30,5 @@ "fieldtype": "Data" } ], - "keywordFields": ["name", "symbol"], "quickEditFields": ["name", "symbol"] } diff --git a/schemas/app/Item.json b/schemas/app/Item.json index 0b9dec05..10456a88 100644 --- a/schemas/app/Item.json +++ b/schemas/app/Item.json @@ -135,5 +135,5 @@ "expenseAccount", "hsnCode" ], - "keywordFields": ["name", "description"] + "keywordFields": ["name", "itemType", "for"] } diff --git a/schemas/app/JournalEntry.json b/schemas/app/JournalEntry.json index ba2f372f..b5c5c603 100644 --- a/schemas/app/JournalEntry.json +++ b/schemas/app/JournalEntry.json @@ -90,7 +90,7 @@ "fieldname": "userRemark", "label": "User Remark", "fieldtype": "Text", - "placeholder": "User Remark" + "placeholder": "Add a remark" }, { "fieldname": "numberSeries", @@ -100,5 +100,6 @@ "required": true, "default": "JV-" } - ] -} \ No newline at end of file + ], + "keywordFields": ["name", "entryType"] +} diff --git a/schemas/app/JournalEntryAccount.json b/schemas/app/JournalEntryAccount.json index 810caadc..5c31e7e1 100644 --- a/schemas/app/JournalEntryAccount.json +++ b/schemas/app/JournalEntryAccount.json @@ -23,5 +23,6 @@ "fieldtype": "Currency" } ], - "tableFields": ["account", "debit", "credit"] -} \ No newline at end of file + "tableFields": ["account", "debit", "credit"], + "keywordFields": ["account"] +} diff --git a/schemas/app/NumberSeries.json b/schemas/app/NumberSeries.json index ac6301b9..0f917a76 100644 --- a/schemas/app/NumberSeries.json +++ b/schemas/app/NumberSeries.json @@ -59,6 +59,5 @@ "readOnly": true } ], - "quickEditFields": ["referenceType", "start", "padZeros"], - "keywordFields": [] + "quickEditFields": ["referenceType", "start", "padZeros"] } diff --git a/schemas/app/Party.json b/schemas/app/Party.json index c6b4c0b1..0803fb6e 100644 --- a/schemas/app/Party.json +++ b/schemas/app/Party.json @@ -90,5 +90,5 @@ "role", "taxId" ], - "keywordFields": ["name"] + "keywordFields": ["name", "email", "role"] } diff --git a/schemas/app/PaymentFor.json b/schemas/app/PaymentFor.json index ff80d68e..0bdf7367 100644 --- a/schemas/app/PaymentFor.json +++ b/schemas/app/PaymentFor.json @@ -36,5 +36,6 @@ "required": true } ], - "tableFields": ["referenceType", "referenceName", "amount"] + "tableFields": ["referenceType", "referenceName", "amount"], + "keywordFields": ["referenceName", "referenceType"] } diff --git a/schemas/app/PurchaseInvoice.json b/schemas/app/PurchaseInvoice.json index 072406af..7ae099f1 100644 --- a/schemas/app/PurchaseInvoice.json +++ b/schemas/app/PurchaseInvoice.json @@ -105,5 +105,5 @@ "default": "PINV-" } ], - "keywordFields": ["name", "party", "numberSeries"] + "keywordFields": ["name", "party"] } diff --git a/schemas/app/PurchaseInvoiceItem.json b/schemas/app/PurchaseInvoiceItem.json index ff0b6283..5bb48e2f 100644 --- a/schemas/app/PurchaseInvoiceItem.json +++ b/schemas/app/PurchaseInvoiceItem.json @@ -68,5 +68,6 @@ "placeholder": "HSN/SAC Code" } ], - "tableFields": ["item", "tax", "quantity", "rate", "amount"] + "tableFields": ["item", "tax", "quantity", "rate", "amount"], + "keywordFields": ["item", "tax"] } diff --git a/schemas/app/SalesInvoice.json b/schemas/app/SalesInvoice.json index 2bf1d585..4bbe22ae 100644 --- a/schemas/app/SalesInvoice.json +++ b/schemas/app/SalesInvoice.json @@ -4,7 +4,6 @@ "isSingle": false, "isChild": false, "isSubmittable": true, - "keywordFields": ["name", "party"], "fields": [ { "label": "Invoice No", @@ -93,6 +92,7 @@ { "fieldname": "terms", "label": "Notes", + "placeholder": "Add invoice terms", "fieldtype": "Text" }, { @@ -103,5 +103,6 @@ "required": true, "default": "SINV-" } - ] + ], + "keywordFields": ["name", "party"] } diff --git a/schemas/app/SalesInvoiceItem.json b/schemas/app/SalesInvoiceItem.json index 31598e87..7407f42a 100644 --- a/schemas/app/SalesInvoiceItem.json +++ b/schemas/app/SalesInvoiceItem.json @@ -69,5 +69,6 @@ "placeholder": "HSN/SAC Code" } ], - "tableFields": ["item", "tax", "quantity", "rate", "amount"] + "tableFields": ["item", "tax", "quantity", "rate", "amount"], + "keywordFields": ["item", "tax"] } diff --git a/schemas/core/SingleValue.json b/schemas/core/SingleValue.json index c34d2570..a36c93cb 100644 --- a/schemas/core/SingleValue.json +++ b/schemas/core/SingleValue.json @@ -22,6 +22,5 @@ "fieldtype": "Data", "required": true } - ], - "keywordFields": [] -} \ No newline at end of file + ] +} diff --git a/schemas/core/SystemSettings.json b/schemas/core/SystemSettings.json index c9683496..4fe68c98 100644 --- a/schemas/core/SystemSettings.json +++ b/schemas/core/SystemSettings.json @@ -90,12 +90,5 @@ "required": true } ], - "quickEditFields": [ - "locale", - "dateFormat", - "currency", - "displayPrecision", - "hideGetStarted" - ], - "keywordFields": [] + "quickEditFields": ["locale", "dateFormat", "currency", "displayPrecision", "hideGetStarted"] } diff --git a/src/components/Controls/Link.vue b/src/components/Controls/Link.vue index 7ad65d9c..d78a4d16 100644 --- a/src/components/Controls/Link.vue +++ b/src/components/Controls/Link.vue @@ -48,7 +48,6 @@ export default { 'name', schema.titleField, this.df.groupBy, - ...schema.keywordFields, ]), ].filter(Boolean); diff --git a/src/pages/ListView/ListView.vue b/src/pages/ListView/ListView.vue index eff3a33f..d5bd4fcf 100644 --- a/src/pages/ListView/ListView.vue +++ b/src/pages/ListView/ListView.vue @@ -108,7 +108,7 @@ function getListConfig(schemaName) { const listConfig = fyo.models[schemaName]?.getListViewSettings?.(fyo); if (listConfig?.columns === undefined) { return { - columns: fyo.schemaMap[schemaName].keywordFields ?? ['name'], + columns: ['name'], }; } return listConfig; diff --git a/src/utils/search.ts b/src/utils/search.ts index f0fb35f9..274e6134 100644 --- a/src/utils/search.ts +++ b/src/utils/search.ts @@ -1,7 +1,10 @@ import { t } from 'fyo'; +import { DocValueMap } from 'fyo/core/types'; import { ModelNameEnum } from 'models/types'; import reports from 'reports/view'; +import { OptionField } from 'schemas/types'; import { fyo } from 'src/initFyo'; +import { GetAllOptions } from 'utils/db/types'; import { routeTo } from './ui'; export const searchGroups = ['Docs', 'List', 'Create', 'Report', 'Page']; @@ -208,3 +211,161 @@ export function getSearchList() { getSetupList(), ].flat(); } + +interface Searchable { + schemaName: string; + fields: string[]; + meta: string[]; + isChild: boolean; + isSubmittable: boolean; +} + +interface Keyword { + values: string[]; + meta: Record; + priority: number; +} + +interface Keywords { + searchable: Searchable; + keywords: Keyword[]; +} + +class Search { + keywords: Record; + priorityMap: Record = { + [ModelNameEnum.SalesInvoice]: 125, + [ModelNameEnum.PurchaseInvoice]: 100, + [ModelNameEnum.Payment]: 75, + [ModelNameEnum.Item]: 50, + [ModelNameEnum.Party]: 50, + [ModelNameEnum.JournalEntry]: 50, + }; + + constructor() { + this.keywords = {}; + } + + getSearchList() { + const keywords = Object.values(this.keywords); + return keywords.map((kw) => kw.keywords).flat(); + } + + async fetchKeywords() { + const searchables = this.#getSearchables(); + for (const searchable of searchables) { + const options: GetAllOptions = { + fields: [searchable.fields, searchable.meta].flat(), + order: 'desc', + }; + + if (!searchable.isChild) { + options.orderBy = 'modified'; + } + + const maps = await fyo.db.getAllRaw(searchable.schemaName, options); + this.addToSearchable(maps, searchable); + } + + this.#setPriority(); + } + + #getSearchables(): Searchable[] { + const searchable: Searchable[] = []; + for (const schemaName of Object.keys(fyo.schemaMap)) { + const schema = fyo.schemaMap[schemaName]; + if (!schema?.keywordFields?.length) { + continue; + } + + const fields = [...schema.keywordFields]; + const meta = []; + if (schema.isChild) { + meta.push('parent', 'parentSchemaName'); + } + + if (schema.isSubmittable) { + meta.push('submitted', 'cancelled'); + } + + searchable.push({ + schemaName, + fields, + meta, + isChild: !!schema.isChild, + isSubmittable: !!schema.isSubmittable, + }); + } + + return searchable; + } + + #setPriority() { + for (const schemaName in this.keywords) { + const kw = this.keywords[schemaName]; + const basePriority = this.priorityMap[schemaName] ?? 0; + + for (const k of kw.keywords) { + k.priority += basePriority; + + if (k.meta.submitted) { + k.priority += 25; + } + + if (k.meta.cancelled) { + k.priority -= 200; + } + + if (kw.searchable.isChild) { + k.priority -= 150; + } + } + } + } + + addToSearchable(maps: DocValueMap[], searchable: Searchable) { + if (!maps.length) { + return; + } + + this.keywords[searchable.schemaName] ??= { searchable, keywords: [] }; + + for (const map of maps) { + const keyword: Keyword = { values: [], meta: {}, priority: 0 }; + this.#setKeywords(map, searchable, keyword); + this.#setMeta(map, searchable, keyword); + this.keywords[searchable.schemaName]!.keywords.push(keyword); + } + } + + #setKeywords(map: DocValueMap, searchable: Searchable, keyword: Keyword) { + // Set individual field values + for (const fn of searchable.fields) { + let value = map[fn] as string | undefined; + const field = fyo.getField(searchable.schemaName, fn); + const { options } = field as OptionField; + if (options) { + value = options.find((o) => o.value === value)?.label ?? value; + } + + keyword.values.push(value ?? ''); + } + } + + #setMeta(map: DocValueMap, searchable: Searchable, keyword: Keyword) { + // Set the meta map + for (const fn of searchable.meta) { + const meta = map[fn]; + if (typeof meta === 'number') { + keyword.meta[fn] = Boolean(meta); + } else if (typeof meta === 'string') { + keyword.meta[fn] = meta; + } + } + + keyword.meta.schemaName = searchable.schemaName; + } +} + +//@ts-ignore +window.sc = new Search(); From 0a3fe9099081a880af53149b36a729fa8b85544f Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Thu, 5 May 2022 13:22:08 +0530 Subject: [PATCH 095/163] incr: add code to generate csv --- utils/csvParser.ts | 83 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 71 insertions(+), 12 deletions(-) diff --git a/utils/csvParser.ts b/utils/csvParser.ts index 3df54758..3ec3b7be 100644 --- a/utils/csvParser.ts +++ b/utils/csvParser.ts @@ -1,11 +1,16 @@ -function unwrapDq(item: string): string { - const s = item.at(0); - const e = item.at(-1); - if (s === '"' && e === '"') { - return item.slice(1, -1); +export function parseCSV(text: string): string[][] { + // Works on RFC 4180 csv + let rows = splitCsvBlock(text); + if (rows.length === 1) { + rows = splitCsvBlock(text, '\n'); } + return rows.map(splitCsvLine); +} - return item; +export function generateCSV(matrix: unknown[][]): string { + // Generates RFC 4180 csv + const formattedRows = getFormattedRows(matrix); + return formattedRows.join('\r\n'); } function splitCsvBlock(text: string, splitter: string = '\r\n'): string[] { @@ -78,11 +83,65 @@ function splitCsvLine(line: string): string[] { return items; } -export function parseCSV(text: string): string[][] { - // Works on RFC 4180 - let rows = splitCsvBlock(text); - if (rows.length === 1) { - rows = splitCsvBlock(text, '\n'); +function unwrapDq(item: string): string { + const s = item.at(0); + const e = item.at(-1); + if (s === '"' && e === '"') { + return item.slice(1, -1); } - return rows.map(splitCsvLine); + + return item; +} + +function getFormattedRows(matrix: unknown[][]): string[] { + const formattedMatrix: string[] = []; + for (const row of matrix) { + const formattedRow: string[] = []; + for (const item of row) { + const formattedItem = getFormattedItem(item); + formattedRow.push(formattedItem); + } + formattedMatrix.push(formattedRow.join(',')); + } + + return formattedMatrix; +} + +function getFormattedItem(item: unknown): string { + if (typeof item === 'string') { + return formatStringToCSV(item); + } + + if (item === null || item === undefined) { + return ''; + } + + if (typeof item === 'object') { + return item.toString(); + } + + return String(item); +} + +function formatStringToCSV(item: string): string { + let shouldDq = false; + if (item.match(/^".*"$/)) { + shouldDq = true; + item = item.slice(1, -1); + } + + if (item.match(/"/)) { + shouldDq = true; + item = item.replaceAll('"', '""'); + } + + if (item.match(/,|\s/)) { + shouldDq = true; + } + + if (shouldDq) { + return '"' + item + '"'; + } + + return item; } From bd6f11055319df322bd1000dbad7dda0e26871ee Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Thu, 5 May 2022 16:14:26 +0530 Subject: [PATCH 096/163] incr: get dataimport to work - need to test on child table docs --- fyo/core/converter.ts | 14 +- fyo/model/naming.ts | 18 +- models/baseModels/InvoiceItem/InvoiceItem.ts | 4 +- models/baseModels/Item/Item.ts | 4 +- schemas/app/Item.json | 9 +- schemas/app/PurchaseInvoiceItem.json | 3 +- schemas/app/SalesInvoiceItem.json | 3 +- src/components/Dropdown.vue | 2 + src/components/DropdownWithActions.vue | 4 +- src/dataImport.ts | 206 ++++++++----------- src/pages/DataImport.vue | 19 +- src/router.ts | 5 +- src/utils/search.ts | 12 +- src/utils/sidebarConfig.ts | 20 +- src/utils/ui.ts | 14 +- utils/index.ts | 27 ++- 16 files changed, 192 insertions(+), 172 deletions(-) diff --git a/fyo/core/converter.ts b/fyo/core/converter.ts index 1c402d51..a9f37bd7 100644 --- a/fyo/core/converter.ts +++ b/fyo/core/converter.ts @@ -164,7 +164,7 @@ function toDocString(value: RawValue, field: Field) { } function toDocDate(value: RawValue, field: Field) { - if (value === null) { + if (value === null || value === '') { return null; } @@ -181,6 +181,10 @@ function toDocDate(value: RawValue, field: Field) { } function toDocCurrency(value: RawValue, field: Field, fyo: Fyo) { + if (value === '') { + return fyo.pesa(0); + } + if (typeof value === 'string') { return fyo.pesa(value); } @@ -201,6 +205,10 @@ function toDocCurrency(value: RawValue, field: Field, fyo: Fyo) { } function toDocInt(value: RawValue, field: Field): number { + if (value === '') { + return 0; + } + if (typeof value === 'string') { value = parseInt(value); } @@ -209,6 +217,10 @@ function toDocInt(value: RawValue, field: Field): number { } function toDocFloat(value: RawValue, field: Field): number { + if (value === '') { + return 0; + } + if (typeof value === 'boolean') { return Number(value); } diff --git a/fyo/model/naming.ts b/fyo/model/naming.ts index f3fb62a6..6eba9b07 100644 --- a/fyo/model/naming.ts +++ b/fyo/model/naming.ts @@ -2,24 +2,24 @@ import { Fyo } from 'fyo'; import NumberSeries from 'fyo/models/NumberSeries'; import { DEFAULT_SERIES_START } from 'fyo/utils/consts'; import { BaseError } from 'fyo/utils/errors'; -import { Field, Schema } from 'schemas/types'; import { getRandomString } from 'utils'; import { Doc } from './doc'; -export function getNumberSeries(schema: Schema): Field | undefined { - const numberSeries = schema.fields.find( - (f) => f.fieldname === 'numberSeries' - ); - return numberSeries; -} - export function isNameAutoSet(schemaName: string, fyo: Fyo): boolean { const schema = fyo.schemaMap[schemaName]!; + if (schema.naming === 'manual') { + return false; + } + if (schema.naming === 'autoincrement') { return true; } - const numberSeries = getNumberSeries(schema); + if (schema.naming === 'random') { + return true; + } + + const numberSeries = fyo.getField(schema.name, 'numberSeries'); if (numberSeries) { return true; } diff --git a/models/baseModels/InvoiceItem/InvoiceItem.ts b/models/baseModels/InvoiceItem/InvoiceItem.ts index 1177af97..d1675083 100644 --- a/models/baseModels/InvoiceItem/InvoiceItem.ts +++ b/models/baseModels/InvoiceItem/InvoiceItem.ts @@ -98,9 +98,9 @@ export abstract class InvoiceItem extends Doc { const itemList = doc.parentdoc!.items as Doc[]; const items = itemList.map((d) => d.item as string).filter(Boolean); - let itemNotFor = 'sales'; + let itemNotFor = 'Sales'; if (doc.isSales) { - itemNotFor = 'purchases'; + itemNotFor = 'Purchases'; } const baseFilter = { for: ['not in', [itemNotFor]] }; diff --git a/models/baseModels/Item/Item.ts b/models/baseModels/Item/Item.ts index 121db713..a7f07549 100644 --- a/models/baseModels/Item/Item.ts +++ b/models/baseModels/Item/Item.ts @@ -67,7 +67,7 @@ export class Item extends Doc { return [ { label: fyo.t`New Sale`, - condition: (doc) => !doc.notInserted && doc.for !== 'purchases', + condition: (doc) => !doc.notInserted && doc.for !== 'Purchases', action: async (doc, router) => { const invoice = await fyo.doc.getNewDoc('SalesInvoice'); await invoice.append('items', { @@ -80,7 +80,7 @@ export class Item extends Doc { }, { label: fyo.t`New Purchase`, - condition: (doc) => !doc.notInserted && doc.for !== 'sales', + condition: (doc) => !doc.notInserted && doc.for !== 'Sales', action: async (doc, router) => { const invoice = await fyo.doc.getNewDoc('PurchaseInvoice'); await invoice.append('items', { diff --git a/schemas/app/Item.json b/schemas/app/Item.json index 10456a88..20dccaa8 100644 --- a/schemas/app/Item.json +++ b/schemas/app/Item.json @@ -74,20 +74,21 @@ "fieldtype": "Select", "options": [ { - "value": "purchases", + "value": "Purchases", "label": "Purchases" }, { - "value": "sales", + "value": "Sales", "label": "Sales" }, { - "value": "both", + "value": "Both", "label": "Both" } ], "readOnly": true, - "default": "both" + "required": true, + "default": "Both" }, { "fieldname": "incomeAccount", diff --git a/schemas/app/PurchaseInvoiceItem.json b/schemas/app/PurchaseInvoiceItem.json index 5bb48e2f..df6eadbc 100644 --- a/schemas/app/PurchaseInvoiceItem.json +++ b/schemas/app/PurchaseInvoiceItem.json @@ -65,7 +65,8 @@ "fieldname": "hsnCode", "label": "HSN/SAC", "fieldtype": "Int", - "placeholder": "HSN/SAC Code" + "placeholder": "HSN/SAC Code", + "hidden": true } ], "tableFields": ["item", "tax", "quantity", "rate", "amount"], diff --git a/schemas/app/SalesInvoiceItem.json b/schemas/app/SalesInvoiceItem.json index 7407f42a..81cec3c6 100644 --- a/schemas/app/SalesInvoiceItem.json +++ b/schemas/app/SalesInvoiceItem.json @@ -66,7 +66,8 @@ "fieldname": "hsnCode", "label": "HSN/SAC", "fieldtype": "Int", - "placeholder": "HSN/SAC Code" + "placeholder": "HSN/SAC Code", + "hidden": true } ], "tableFields": ["item", "tax", "quantity", "rate", "amount"], diff --git a/src/components/Dropdown.vue b/src/components/Dropdown.vue index 56d593de..4bdc1b2b 100644 --- a/src/components/Dropdown.vue +++ b/src/components/Dropdown.vue @@ -206,6 +206,8 @@ export default { // valid selection let item = this.items[this.highlightedIndex]; await this.selectItem(item); + } else if (this.items.length === 1) { + await this.selectItem(this.items[0]) } }, highlightItemUp() { diff --git a/src/components/DropdownWithActions.vue b/src/components/DropdownWithActions.vue index fb631268..613a7a0b 100644 --- a/src/components/DropdownWithActions.vue +++ b/src/components/DropdownWithActions.vue @@ -31,7 +31,9 @@ export default { actions: { default: [] }, type: { type: String, default: 'secondary' }, }, - inject: ['doc'], + inject: { + doc: { default: null }, + }, components: { Dropdown, Button, diff --git a/src/dataImport.ts b/src/dataImport.ts index 79eb46da..91e4bc7c 100644 --- a/src/dataImport.ts +++ b/src/dataImport.ts @@ -1,8 +1,10 @@ import { Fyo, t } from 'fyo'; +import { Converter } from 'fyo/core/converter'; import { DocValueMap } from 'fyo/core/types'; import { Doc } from 'fyo/model/doc'; import { isNameAutoSet } from 'fyo/model/naming'; import { Noun, Verb } from 'fyo/telemetry/types'; +import { ModelNameEnum } from 'models/types'; import { Field, FieldType, @@ -11,16 +13,20 @@ import { SelectOption, TargetField, } from 'schemas/types'; -import { parseCSV } from '../utils/csvParser'; +import { + getDefaultMapFromList, + getMapFromList, + getValueMapFromList, +} from 'utils'; +import { generateCSV, parseCSV } from '../utils/csvParser'; export const importable = [ - 'SalesInvoice', - 'PurchaseInvoice', - 'Payment', - 'JournalEntry', - 'Customer', - 'Supplier', - 'Item', + ModelNameEnum.SalesInvoice, + ModelNameEnum.PurchaseInvoice, + ModelNameEnum.Payment, + ModelNameEnum.Party, + ModelNameEnum.Item, + ModelNameEnum.JournalEntry, ]; type Status = { @@ -29,16 +35,7 @@ type Status = { names: string[]; }; -type Exclusion = { - [key: string]: string[]; -}; - -type Map = Record; -type ObjectMap = Record; - -type LabelTemplateFieldMap = { - [key: string]: TemplateField; -}; +type Exclusion = Record; type LoadingStatusCallback = ( isMakingEntries: boolean, @@ -56,33 +53,9 @@ interface TemplateField { parentField: string; } -function formatValue(value: string, fieldtype: FieldType): unknown { - switch (fieldtype) { - case FieldTypeEnum.Date: - if (value === '') { - return ''; - } - return new Date(value); - case FieldTypeEnum.Currency: - // @ts-ignore - return this.fyo.pesa(value || 0); - case FieldTypeEnum.Int: - case FieldTypeEnum.Float: { - const n = parseFloat(value); - if (!Number.isNaN(n)) { - return n; - } - return 0; - } - default: - return value; - } -} - const exclusion: Exclusion = { Item: ['image'], - Supplier: ['address', 'outstandingAmount', 'supplier', 'image', 'customer'], - Customer: ['address', 'outstandingAmount', 'supplier', 'image', 'customer'], + Party: ['address', 'outstandingAmount', 'image'], }; function getFilteredDocFields( @@ -97,48 +70,68 @@ function getFilteredDocFields( parentField = ''; } - // @ts-ignore - const primaryFields: Field[] = fyo.schemaMap[schemaName]?.fields ?? []; + const primaryFields: Field[] = fyo.schemaMap[schemaName]!.fields; const fields: TemplateField[] = []; const tableTypes: string[][] = []; const exclusionFields: string[] = exclusion[schemaName] ?? []; - primaryFields.forEach((field) => { - const { label, fieldtype, fieldname, readOnly, required, hidden } = field; + for (const field of primaryFields) { + const { label, fieldtype, fieldname, required } = field; - if ( - !(fieldname === 'name' && !parentField) && - (readOnly || - (hidden && typeof hidden === 'number') || - exclusionFields.includes(fieldname)) - ) { - return; + if (shouldSkip(field, exclusionFields, parentField)) { + continue; } - if (fieldtype === FieldTypeEnum.Table && (field as TargetField).target) { - tableTypes.push([(field as TargetField).target, fieldname]); - return; + if (fieldtype === FieldTypeEnum.Table) { + const { target } = field as TargetField; + tableTypes.push([target, fieldname]); + continue; } - let options: SelectOption[] = []; - if ((field as OptionField).options !== undefined) { - options = (field as OptionField).options; - } + const options: SelectOption[] = (field as OptionField).options ?? []; fields.push({ label, fieldname, - schemaName: schemaName, + schemaName, options, fieldtype, parentField, - required: Boolean(required ?? false), + required: required ?? false, }); - }); + } return [fields, tableTypes]; } +function shouldSkip( + field: Field, + exclusionFields: string[], + parentField: string +): boolean { + if (field.meta) { + return true; + } + + if (field.fieldname === 'name' && parentField) { + return true; + } + + if (field.required) { + return false; + } + + if (exclusionFields.includes(field.fieldname)) { + return true; + } + + if (field.hidden || field.readOnly) { + return true; + } + + return false; +} + function getTemplateFields(schemaName: string, fyo: Fyo): TemplateField[] { const fields: TemplateField[] = []; if (!schemaName) { @@ -147,46 +140,28 @@ function getTemplateFields(schemaName: string, fyo: Fyo): TemplateField[] { const schemaNames: string[][] = [[schemaName]]; while (schemaNames.length > 0) { - const dt = schemaNames.pop(); - if (!dt) { + const sn = schemaNames.pop(); + if (!sn) { break; } - const [templateFields, tableTypes] = getFilteredDocFields(dt, fyo); + const [templateFields, tableTypes] = getFilteredDocFields(sn, fyo); fields.push(...templateFields); schemaNames.push(...tableTypes); } return fields; } -function getLabelFieldMap(templateFields: TemplateField[]): Map { - const map: Map = {}; - - templateFields.reduce((acc, tf) => { - const key = tf.label as string; - acc[key] = tf.fieldname; - return acc; - }, map); - - return map; -} - -function getTemplate(templateFields: TemplateField[]): string { - const labels = templateFields.map(({ label }) => `"${label}"`).join(','); - return [labels, ''].join('\n'); -} - export class Importer { schemaName: string; templateFields: TemplateField[]; - map: Map; + labelTemplateFieldMap: Record = {}; template: string; indices: number[] = []; parsedLabels: string[] = []; parsedValues: string[][] = []; - assignedMap: Map = {}; // target: import - requiredMap: Map = {}; - labelTemplateFieldMap: LabelTemplateFieldMap = {}; + assignedMap: Record = {}; // target: import + requiredMap: Record = {}; shouldSubmit: boolean = false; labelIndex: number = -1; csv: string[][] = []; @@ -196,38 +171,30 @@ export class Importer { this.schemaName = schemaName; this.fyo = fyo; this.templateFields = getTemplateFields(schemaName, this.fyo); - this.map = getLabelFieldMap(this.templateFields); - this.template = getTemplate(this.templateFields); - this.assignedMap = this.assignableLabels.reduce((acc: Map, k) => { - acc[k] = ''; - return acc; - }, {}); - this.requiredMap = this.templateFields.reduce((acc: Map, k) => { - acc[k.label] = k.required; - return acc; - }, {}); - this.labelTemplateFieldMap = this.templateFields.reduce( - (acc: LabelTemplateFieldMap, k) => { - acc[k.label] = k; - return acc; - }, - {} - ); + this.template = generateCSV([this.templateFields.map((t) => t.label)]); + this.labelTemplateFieldMap = getMapFromList(this.templateFields, 'label'); + this.assignedMap = getDefaultMapFromList(this.templateFields, '', 'label'); + this.requiredMap = getValueMapFromList( + this.templateFields, + 'label', + 'required' + ) as Record; } get assignableLabels() { const req: string[] = []; const nreq: string[] = []; - Object.keys(this.map).forEach((k) => { - if (this.requiredMap[k]) { - req.push(k); - return; + + for (const label in this.labelTemplateFieldMap) { + if (this.requiredMap[label]) { + req.push(label); + continue; } - nreq.push(k); - }); + nreq.push(label); + } - return [...req, ...nreq]; + return [req, nreq].flat(); } get unassignedLabels() { @@ -332,23 +299,23 @@ export class Importer { }); } - getDocs(): Map[] { + getDocs(): DocValueMap[] { const fields = this.columnLabels.map((k) => this.labelTemplateFieldMap[k]); const nameIndex = fields.findIndex(({ fieldname }) => fieldname === 'name'); - const docMap: ObjectMap = {}; + const docMap: Record = {}; const assignedMatrix = this.assignedMatrix; for (let r = 0; r < assignedMatrix.length; r++) { const row = assignedMatrix[r]; - const cts: ObjectMap = {}; + const cts: Record = {}; const name = row[nameIndex]; docMap[name] ??= {}; for (let f = 0; f < fields.length; f++) { const field = fields[f]; - const value = formatValue(row[f], field.fieldtype); + const value = Converter.toDocValue(row[f], field, this.fyo); if (field.parentField) { cts[field.parentField] ??= {}; @@ -361,7 +328,7 @@ export class Importer { for (const k of Object.keys(cts)) { docMap[name][k] ??= []; - (docMap[name][k] as Map[]).push(cts[k]); + (docMap[name][k] as DocValueMap[]).push(cts[k]); } } @@ -423,9 +390,8 @@ export class Importer { this.parsedValues.push(emptyRow); } - async makeEntry(doc: Doc, docObj: Map) { - await doc.setMultiple(docObj as DocValueMap); - await doc.sync(); + async makeEntry(doc: Doc, docObj: DocValueMap) { + await doc.setAndSync(docObj); if (this.shouldSubmit) { await doc.submit(); } diff --git a/src/pages/DataImport.vue b/src/pages/DataImport.vue index f5a7e08d..986bfcbb 100644 --- a/src/pages/DataImport.vue +++ b/src/pages/DataImport.vue @@ -1,5 +1,6 @@ diff --git a/src/utils/misc.ts b/src/utils/misc.ts index 28cefb0e..b24ab88b 100644 --- a/src/utils/misc.ts +++ b/src/utils/misc.ts @@ -68,16 +68,11 @@ export function incrementOpenCount() { export async function startTelemetry() { fyo.telemetry.interestingDocs = [ ModelNameEnum.Payment, - ModelNameEnum.PaymentFor, ModelNameEnum.SalesInvoice, - ModelNameEnum.SalesInvoiceItem, ModelNameEnum.PurchaseInvoice, - ModelNameEnum.PurchaseInvoiceItem, ModelNameEnum.JournalEntry, - ModelNameEnum.JournalEntryAccount, ModelNameEnum.Party, - ModelNameEnum.Account, - ModelNameEnum.Tax, + ModelNameEnum.Item, ]; await fyo.telemetry.start(); } From 86c48899595680126373f0020516596a5a00f335 Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Thu, 12 May 2022 11:22:25 +0530 Subject: [PATCH 106/163] incr: add list pagination --- .../baseModels/JournalEntry/JournalEntry.ts | 11 +- models/baseModels/Payment/Payment.ts | 4 +- .../PurchaseInvoice/PurchaseInvoice.ts | 2 +- .../baseModels/SalesInvoice/SalesInvoice.ts | 2 +- src/pages/ListView/List.vue | 155 ++++++++++++++---- 5 files changed, 131 insertions(+), 43 deletions(-) diff --git a/models/baseModels/JournalEntry/JournalEntry.ts b/models/baseModels/JournalEntry/JournalEntry.ts index 7a9e16d1..d53f8c57 100644 --- a/models/baseModels/JournalEntry/JournalEntry.ts +++ b/models/baseModels/JournalEntry/JournalEntry.ts @@ -49,7 +49,7 @@ export class JournalEntry extends Transactional { return { formRoute: (name) => `/edit/JournalEntry/${name}`, columns: [ - 'date', + 'name', { label: t`Status`, fieldtype: 'Select', @@ -72,14 +72,7 @@ export class JournalEntry extends Transactional { }; }, }, - { - label: t`Entry ID`, - fieldtype: 'Data', - fieldname: 'name', - getValue(doc) { - return doc.name as string; - }, - }, + 'date', 'entryType', 'referenceNumber', ], diff --git a/models/baseModels/Payment/Payment.ts b/models/baseModels/Payment/Payment.ts index 9232f163..2bceac5d 100644 --- a/models/baseModels/Payment/Payment.ts +++ b/models/baseModels/Payment/Payment.ts @@ -442,7 +442,7 @@ export class Payment extends Transactional { static getListViewSettings(fyo: Fyo): ListViewSettings { return { columns: [ - 'party', + 'name', { label: t`Status`, fieldname: 'status', @@ -465,7 +465,7 @@ export class Payment extends Transactional { }; }, }, - 'paymentType', + 'party', 'date', 'amount', ], diff --git a/models/baseModels/PurchaseInvoice/PurchaseInvoice.ts b/models/baseModels/PurchaseInvoice/PurchaseInvoice.ts index b1d586d4..df76ebd6 100644 --- a/models/baseModels/PurchaseInvoice/PurchaseInvoice.ts +++ b/models/baseModels/PurchaseInvoice/PurchaseInvoice.ts @@ -36,9 +36,9 @@ export class PurchaseInvoice extends Invoice { return { formRoute: (name) => `/edit/PurchaseInvoice/${name}`, columns: [ - 'party', 'name', getTransactionStatusColumn(), + 'party', 'date', 'grandTotal', 'outstandingAmount', diff --git a/models/baseModels/SalesInvoice/SalesInvoice.ts b/models/baseModels/SalesInvoice/SalesInvoice.ts index 180e48cc..c5b27317 100644 --- a/models/baseModels/SalesInvoice/SalesInvoice.ts +++ b/models/baseModels/SalesInvoice/SalesInvoice.ts @@ -35,9 +35,9 @@ export class SalesInvoice extends Invoice { return { formRoute: (name) => `/edit/SalesInvoice/${name}`, columns: [ - 'party', 'name', getTransactionStatusColumn(), + 'party', 'date', 'grandTotal', 'outstandingAmount', diff --git a/src/pages/ListView/List.vue b/src/pages/ListView/List.vue index d17baf94..f48f2ae5 100644 --- a/src/pages/ListView/List.vue +++ b/src/pages/ListView/List.vue @@ -1,10 +1,10 @@ diff --git a/src/renderer.ts b/src/renderer.ts index a7cbcb6c..51cb3e8c 100644 --- a/src/renderer.ts +++ b/src/renderer.ts @@ -1,6 +1,5 @@ import { ipcRenderer } from 'electron'; import { ConfigKeys } from 'fyo/core/types'; -import { GeneralLedger } from 'reports'; import { IPC_ACTIONS } from 'utils/messages'; import { App as VueApp, createApp } from 'vue'; import App from './App.vue'; @@ -106,6 +105,3 @@ function setOnWindow() { window.fyo = fyo; } } - -// @ts-ignore -window.GL = GeneralLedger; diff --git a/src/router.ts b/src/router.ts index c15a3440..9fc48c6a 100644 --- a/src/router.ts +++ b/src/router.ts @@ -8,7 +8,7 @@ import JournalEntryForm from 'src/pages/JournalEntryForm.vue'; import ListView from 'src/pages/ListView/ListView.vue'; import PrintView from 'src/pages/PrintView/PrintView.vue'; import QuickEditForm from 'src/pages/QuickEditForm.vue'; -// import Report from 'src/pages/Report.vue'; +import Report from 'src/pages/Report.vue'; import Settings from 'src/pages/Settings/Settings.vue'; import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'; import { fyo } from './initFyo'; @@ -87,14 +87,12 @@ const routes: RouteRecordRaw[] = [ component: PrintView, props: true, }, - /* { path: '/report/:reportName', name: 'Report', component: Report, props: true, }, - */ { path: '/chart-of-accounts', name: 'Chart Of Accounts', diff --git a/src/utils/sidebarConfig.ts b/src/utils/sidebarConfig.ts index cd8fa1af..6a7b6634 100644 --- a/src/utils/sidebarConfig.ts +++ b/src/utils/sidebarConfig.ts @@ -132,18 +132,18 @@ function getCompleteSidebar(): SidebarConfig { }, ], }, - /* { label: t`Reports`, name: t`reports`, icon: 'reports', - route: '/report/general-ledger', + route: '/report/GeneralLedger', items: [ { label: t`General Ledger`, name: 'general-ledger', - route: '/report/general-ledger', + route: '/report/GeneralLedger', }, + /* { label: t`Profit And Loss`, name: 'profit-and-loss', @@ -171,9 +171,9 @@ function getCompleteSidebar(): SidebarConfig { route: '/report/gstr-2', hidden: () => fyo.singles.AccountingSettings!.country !== 'India', }, + */ ], }, - */ { label: t`Setup`, name: t`setup`, diff --git a/utils/index.ts b/utils/index.ts index 9ee0bbfc..d6cd52a4 100644 --- a/utils/index.ts +++ b/utils/index.ts @@ -104,3 +104,22 @@ export function invertMap(map: Record): Record { return inverted; } + +export function time(func: (...args: K[]) => T, ...args: K[]): T { + const name = func.name; + console.time(name); + const stuff = func(...args); + console.timeEnd(name); + return stuff; +} + +export async function timeAsync( + func: (...args: K[]) => Promise, + ...args: K[] +): Promise { + const name = func.name; + console.time(name); + const stuff = await func(...args); + console.timeEnd(name); + return stuff; +} From 6a2475d9b8244c29b857aaa49bff36f77cff5891 Mon Sep 17 00:00:00 2001 From: 18alantom <2.alan.tom@gmail.com> Date: Fri, 13 May 2022 12:19:08 +0530 Subject: [PATCH 110/163] incr: shift pagination outside - create listreport component - add pagination to listreport --- src/components/Paginator.vue | 135 +++++++++++++++++++++++++++ src/components/Report/ListReport.vue | 119 +++++++++++++++++++++++ src/pages/ListView/List.vue | 118 +++++------------------ src/pages/Report.vue | 98 +------------------ 4 files changed, 281 insertions(+), 189 deletions(-) create mode 100644 src/components/Paginator.vue create mode 100644 src/components/Report/ListReport.vue diff --git a/src/components/Paginator.vue b/src/components/Paginator.vue new file mode 100644 index 00000000..c94b1093 --- /dev/null +++ b/src/components/Paginator.vue @@ -0,0 +1,135 @@ + + diff --git a/src/components/Report/ListReport.vue b/src/components/Report/ListReport.vue new file mode 100644 index 00000000..bc9dcf8c --- /dev/null +++ b/src/components/Report/ListReport.vue @@ -0,0 +1,119 @@ + + diff --git a/src/pages/ListView/List.vue b/src/pages/ListView/List.vue index f48f2ae5..dd03cf83 100644 --- a/src/pages/ListView/List.vue +++ b/src/pages/ListView/List.vue @@ -25,15 +25,12 @@
-
-
+
+

- {{ i + 1 + (pageNo - 1) * count }} + {{ i + pageStart + 1 }}

-
+
-
-
- -
- {{ - `${(pageNo - 1) * count + 1} - ${Math.min( - pageNo * count, - data.length - )}` - }} -
- - -
- -
- -

/

-

- {{ maxPages }} -

-
- -
- - -
- -
+
+
+
@@ -144,12 +74,14 @@ diff --git a/src/pages/Report.vue b/src/pages/Report.vue index 8c620771..d6e05c27 100644 --- a/src/pages/Report.vue +++ b/src/pages/Report.vue @@ -32,74 +32,15 @@ />
- -
-
- -
-

- {{ col.label }} -

-
- - -
- -
- -
- {{ cell.value }} -
-
-
-
-
-
- add pagination here -
+ +
diff --git a/src/components/WithScroll.vue b/src/components/WithScroll.vue index bb4dade1..24ce2e41 100644 --- a/src/components/WithScroll.vue +++ b/src/components/WithScroll.vue @@ -22,19 +22,17 @@ export default { }, }; +