2
0
mirror of https://github.com/frappe/books.git synced 2024-11-09 23:30:56 +00:00

incr: refactor and type src/utils

This commit is contained in:
18alantom 2022-04-20 12:08:47 +05:30
parent ae6a5e52f2
commit c56850d08f
69 changed files with 902 additions and 946 deletions

View File

@ -1,6 +1,6 @@
import { showMessageDialog } from '@/utils';
import frappe, { t } from 'fyo';
import { DateTime } from 'luxon';
import { showMessageDialog } from 'src/utils';
import { stateCodeMap } from '../regional/in';
import { exportCsv, saveExportData } from '../reports/commonExporter';
import { getSavePath } from '../src/utils';
@ -50,7 +50,7 @@ export async function generateGstr1Json(getReportData) {
if (!gstin) {
showMessageDialog({
message: t`Export Failed`,
description: t`Please set GSTIN in General Settings.`,
detail: t`Please set GSTIN in General Settings.`,
});
return;
}
@ -232,7 +232,7 @@ export async function generateGstr2Csv(getReportData) {
if (!gstin) {
showMessageDialog({
message: t`Export Failed`,
description: t`Please set GSTIN in General Settings.`,
detail: t`Please set GSTIN in General Settings.`,
});
return;
}
@ -313,7 +313,7 @@ export async function generateGstr1Csv(getReportData) {
if (!gstin) {
showMessageDialog({
message: t`Export Failed`,
description: t`Please set GSTIN in General Settings.`,
detail: t`Please set GSTIN in General Settings.`,
});
return;
}

View File

@ -59,8 +59,11 @@ export type ListsMap = Record<string, ListFunction>;
export interface Action {
label: string;
condition: (doc: Doc) => boolean;
action: (doc: Doc, router: Router) => Promise<void>;
action: (doc: Doc, router: Router) => Promise<void> | void;
condition?: (doc: Doc) => boolean;
component?: {
template?: string;
};
}
export interface ColumnConfig {

View File

@ -41,7 +41,7 @@ describe('Fyo Init', function () {
describe('Fyo Docs', function () {
const countryCode = 'in';
let fyo: Fyo;
const schemas = getSchemas(countryCode);
const schemaMap = getSchemas(countryCode);
this.beforeEach(async function () {
fyo = new Fyo({
DatabaseDemux: DatabaseManager,
@ -58,7 +58,14 @@ describe('Fyo Docs', function () {
await fyo.close();
});
specify('temp', async function () {
fyo.db.schemaMap;
specify('getEmptyDoc', async function () {
for (const schemaName in schemaMap) {
const schema = schemaMap[schemaName];
if (schema?.isSingle) {
continue;
}
const doc = fyo.doc.getEmptyDoc(schemaName);
}
});
});

View File

@ -1,3 +1,6 @@
import { Fyo } from 'fyo';
import Doc from 'fyo/model/doc';
import { Action } from 'fyo/model/types';
import { pesa } from 'pesa';
export function slug(str: string) {
@ -50,3 +53,12 @@ export function getDuplicates(array: unknown[]) {
export function isPesa(value: unknown): boolean {
return value instanceof pesa().constructor;
}
export function getActions(doc: Doc, fyo: Fyo): Action[] {
const Model = fyo.models[doc.schemaName];
if (Model === undefined) {
return [];
}
return Model.getActions(fyo);
}

View File

@ -90,6 +90,8 @@ export function t(...args: TranslationLiteral[]): string {
return new TranslationString(...args).s;
}
export function setLanguageMapOnTranslationString(languageMap: LanguageMap) {
export function setLanguageMapOnTranslationString(
languageMap: LanguageMap | undefined
) {
TranslationString.prototype.languageMap = languageMap;
}

View File

@ -18,8 +18,12 @@ export abstract class Invoice extends Doc {
currency?: string;
netTotal?: Money;
baseGrandTotal?: Money;
outstandingAmount?: Money;
exchangeRate?: number;
submitted?: boolean;
cancelled?: boolean;
abstract getPosting(): Promise<LedgerPosting>;
get isSales() {

View File

@ -43,7 +43,7 @@ export function getTransactionActions(schemaName: string, fyo: Fyo): Action[] {
const paymentType = isSales ? 'Receive' : 'Pay';
const hideAccountField = isSales ? 'account' : 'paymentAccount';
const { openQuickEdit } = await import('../src/utils');
const { openQuickEdit } = await import('src/utils/ui');
await openQuickEdit({
schemaName: 'Payment',
name: payment.name as string,

View File

@ -1,7 +1,33 @@
import { partyWithAvatar } from '@/utils';
import { t } from 'fyo';
import Avatar from 'src/components/Avatar.vue';
import { fyo } from 'src/initFyo';
import getCommonExportActions from '../commonExporter';
export function getPartyWithAvatar(partyName) {
return {
data() {
return {
imageURL: null,
label: null,
};
},
components: {
Avatar,
},
async mounted() {
const p = await fyo.db.get('Party', partyName);
this.imageURL = p.image;
this.label = partyName;
},
template: `
<div class="flex items-center" v-if="label">
<Avatar class="flex-shrink-0" :imageURL="imageURL" :label="label" size="sm" />
<span class="ml-2 truncate">{{ label }}</span>
</div>
`,
};
}
let title = t`General Ledger`;
const viewConfig = {
@ -117,7 +143,7 @@ const viewConfig = {
fieldtype: 'Link',
fieldname: 'party',
component(cellValue) {
return partyWithAvatar(cellValue);
return getPartyWithAvatar(cellValue);
},
},
];

View File

@ -30,16 +30,16 @@
</template>
<script>
import WindowsTitleBar from '@/components/WindowsTitleBar';
import config from '@/config';
import { ipcRenderer } from 'electron';
import fs from 'fs/promises';
import frappe from 'fyo';
import WindowsTitleBar from 'src/components/WindowsTitleBar';
import config from 'src/config';
import {
connectToLocalDatabase,
postSetup,
purgeCache
} from '@/initialization';
import { ipcRenderer } from 'electron';
import fs from 'fs/promises';
import frappe from 'fyo';
} from 'src/initialization';
import { IPC_ACTIONS, IPC_MESSAGES } from 'utils/messages';
import TelemetryModal from './components/once/TelemetryModal.vue';
import { showErrorDialog } from './errorHandling';

View File

@ -32,9 +32,9 @@
</template>
<script>
import Dropdown from 'src/components/Dropdown';
import { fuzzyMatch } from 'src/utils';
import Base from './Base';
import Dropdown from '@/components/Dropdown';
import { fuzzyMatch } from '@/utils';
export default {
name: 'AutoComplete',

View File

@ -55,9 +55,9 @@
</template>
<script>
import Popover from 'src/components/Popover';
import Row from 'src/components/Row';
import Base from './Base';
import Row from '@/components/Row';
import Popover from '@/components/Popover';
export default {
name: 'Color',

View File

@ -7,10 +7,10 @@
/>
</template>
<script>
import config from '@/config';
import { languageCodeMap } from '@/languageCodeMap';
import { setLanguageMap } from '@/utils';
import { DEFAULT_LANGUAGE } from 'frappe/utils/consts';
import config from 'src/config';
import { languageCodeMap } from 'src/languageCodeMap';
import { setLanguageMap } from 'src/utils';
import FormControl from './FormControl';
export default {

View File

@ -1,12 +1,12 @@
import Badge from '@/components/Badge';
import { openQuickEdit } from '@/utils';
import Badge from 'src/components/Badge';
import { openQuickEdit } from 'src/utils';
import frappe, { t } from 'frappe';
import { markRaw } from 'vue';
import AutoComplete from './AutoComplete';
<script>
import Badge from '@/components/Badge';
import { openQuickEdit } from '@/utils';
import frappe, { t } from 'frappe';
import Badge from 'src/components/Badge';
import { openQuickEdit } from 'src/utils';
import { markRaw } from 'vue';
import AutoComplete from './AutoComplete';

View File

@ -62,7 +62,7 @@
<script>
import frappe from 'frappe';
import Row from '@/components/Row';
import Row from 'src/components/Row';
import Base from './Base';
import TableRow from './TableRow';

View File

@ -32,9 +32,9 @@
</Row>
</template>
<script>
import FormControl from './FormControl';
import Row from '@/components/Row';
import Row from 'src/components/Row';
import { getErrorMessage } from '../../errorHandling';
import FormControl from './FormControl';
export default {
name: 'TableRow',

View File

@ -21,8 +21,8 @@
</template>
<script>
import Dropdown from '@/components/Dropdown';
import Button from '@/components/Button';
import Button from 'src/components/Button';
import Dropdown from 'src/components/Dropdown';
export default {
name: 'DropdownWithActions',

View File

@ -47,10 +47,10 @@
</div>
</template>
<script>
import InvoiceTemplate1 from '@/../models/doctype/SalesInvoice/Templates/InvoiceTemplate1';
import InvoiceTemplate2 from '@/../models/doctype/SalesInvoice/Templates/InvoiceTemplate2';
import InvoiceTemplate3 from '@/../models/doctype/SalesInvoice/Templates/InvoiceTemplate3';
import InvoiceCustomizer from '@/components/InvoiceCustomizer';
import InvoiceTemplate1 from 'src/../models/doctype/SalesInvoice/Templates/InvoiceTemplate1';
import InvoiceTemplate2 from 'src/../models/doctype/SalesInvoice/Templates/InvoiceTemplate2';
import InvoiceTemplate3 from 'src/../models/doctype/SalesInvoice/Templates/InvoiceTemplate3';
import InvoiceCustomizer from 'src/components/InvoiceCustomizer';
const invoiceTemplates = {
'Basic I': InvoiceTemplate1,

View File

@ -41,9 +41,9 @@
</div>
</template>
<script>
import Dropdown from '@/components/Dropdown';
import { routeTo } from '@/utils';
import frappe, { t } from 'frappe';
import Dropdown from 'src/components/Dropdown';
import { routeTo } from 'src/utils';
import reports from '../../reports/view';
export default {

View File

@ -96,10 +96,10 @@
</div>
</template>
<script>
import Button from '@/components/Button';
import { reportIssue } from '@/errorHandling';
import { routeTo } from '@/utils';
import path from 'path';
import Button from 'src/components/Button';
import { reportIssue } from 'src/errorHandling';
import { routeTo } from 'src/utils';
import router from '../router';
import sidebarConfig from '../sidebarConfig';
import Icon from './Icon.vue';

View File

@ -4,8 +4,8 @@
}}</Badge>
</template>
<script>
import Badge from 'src/components/Badge';
import { statusColor } from '../colors';
import Badge from '@/components/Badge';
export default {
name: 'StatusBadge',

View File

@ -1,5 +1,5 @@
import Button from '@/components/Button';
import FormControl from '@/components/Controls/FormControl';
import Button from 'src/components/Button';
import FormControl from 'src/components/Controls/FormControl';
import frappe from 'frappe';
import { getErrorMessage, handleErrorWithDialog } from '../errorHandling';
<template>
@ -90,9 +90,9 @@ import { getErrorMessage, handleErrorWithDialog } from '../errorHandling';
</div>
</template>
<script>
import Button from '@/components/Button';
import FormControl from '@/components/Controls/FormControl';
import frappe from 'frappe';
import Button from 'src/components/Button';
import FormControl from 'src/components/Controls/FormControl';
import { getErrorMessage, handleErrorWithDialog } from '../errorHandling';
let TwoColumnForm = {

View File

@ -49,8 +49,8 @@
</template>
<script>
import { routeTo } from '@/utils';
import frappe from 'frappe';
import { routeTo } from 'src/utils';
import { getStatusColumn } from '../Transaction/Transaction';
export default {

View File

@ -19,7 +19,7 @@
</template>
<script>
import { runWindowAction } from '@/utils';
import { runWindowAction } from 'src/utils';
export default {
name: 'WindowControls',

View File

@ -31,7 +31,7 @@
<script>
import { ipcRenderer } from 'electron';
import { runWindowAction } from '@/utils';
import { runWindowAction } from 'src/utils';
import { IPC_MESSAGES } from 'utils/messages';
export default {

View File

@ -49,10 +49,10 @@
</template>
<script>
import config, { ConfigKeys, TelemetrySetting } from '@/config';
import { getTelemetryOptions } from '@/telemetry/helpers';
import telemetry from '@/telemetry/telemetry';
import { NounEnum, Verb } from '@/telemetry/types';
import config, { ConfigKeys, TelemetrySetting } from 'src/config';
import { getTelemetryOptions } from 'src/telemetry/helpers';
import telemetry from 'src/telemetry/telemetry';
import { NounEnum, Verb } from 'src/telemetry/types';
import Button from '../Button.vue';
import FormControl from '../Controls/FormControl';
import FeatherIcon from '../FeatherIcon.vue';

View File

@ -1,11 +1,11 @@
import frappe from 'fyo';
import { t } from 'fyo';
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 { FieldType, FieldTypeEnum } from 'schemas/types';
import telemetry from '../frappe/telemetry/telemetry';
import { Noun, Verb } from '../frappe/telemetry/types';
import { parseCSV } from '../utils/csvParser';
import { fyo } from './initFyo';
export const importable = [
'SalesInvoice',
@ -364,7 +364,7 @@ export class Importer {
async importData(setLoadingStatus: LoadingStatusCallback): Promise<Status> {
const status: Status = { success: false, names: [], message: '' };
const shouldDeleteName = isNameAutoSet(this.doctype);
const shouldDeleteName = isNameAutoSet(this.doctype, fyo);
const docObjs = this.getDocs();
let entriesMade = 0;
@ -383,7 +383,7 @@ export class Importer {
delete docObj[key];
}
const doc: Doc = frappe.doc.getEmptyDoc(this.doctype, false);
const doc: Doc = fyo.doc.getEmptyDoc(this.doctype, false);
try {
await this.makeEntry(doc, docObj);
entriesMade += 1;
@ -391,7 +391,7 @@ export class Importer {
} catch (err) {
setLoadingStatus(false, entriesMade, docObjs.length);
telemetry.log(Verb.Imported, this.doctype as Noun, {
fyo.telemetry.log(Verb.Imported, this.doctype as Noun, {
success: false,
count: entriesMade,
});
@ -405,7 +405,7 @@ export class Importer {
setLoadingStatus(false, entriesMade, docObjs.length);
status.success = true;
telemetry.log(Verb.Imported, this.doctype as Noun, {
fyo.telemetry.log(Verb.Imported, this.doctype as Noun, {
success: true,
count: entriesMade,
});
@ -426,18 +426,18 @@ export class Importer {
}
handleError(doc: Doc, err: Error, status: Status): Status {
const messages = [frappe.t`Could not import ${this.doctype} ${doc.name!}.`];
const messages = [t`Could not import ${this.doctype} ${doc.name!}.`];
const message = err.message;
if (message?.includes('UNIQUE constraint failed')) {
messages.push(frappe.t`${doc.name!} already exists.`);
messages.push(t`${doc.name!} already exists.`);
} else if (message) {
messages.push(message);
}
if (status.names.length) {
messages.push(
frappe.t`The following ${
t`The following ${
status.names.length
} entries were created: ${status.names.join(', ')}`
);

View File

@ -1,6 +1,8 @@
import { ipcRenderer } from 'electron';
import frappe, { t } from 'fyo';
import { t } from 'fyo';
import { ConfigKeys } from 'fyo/core/types';
import Doc from 'fyo/model/doc';
import { TelemetrySetting } from 'fyo/telemetry/types';
import {
DuplicateEntryError,
LinkValidationError,
@ -9,12 +11,12 @@ import {
} from 'fyo/utils/errors';
import { ErrorLog } from 'fyo/utils/types';
import { IPC_ACTIONS, IPC_MESSAGES } from 'utils/messages';
import telemetry from '../frappe/telemetry/telemetry';
import config, { ConfigKeys, TelemetrySetting } from '../utils/config';
import { showMessageDialog, showToast } from './utils.js';
import { fyo } from './initFyo';
import { ToastOptions } from './utils/types';
import { showMessageDialog, showToast } from './utils/ui';
function getCanLog(): boolean {
const telemetrySetting = config.get(ConfigKeys.Telemetry);
const telemetrySetting = fyo.config.get(ConfigKeys.Telemetry);
return telemetrySetting !== TelemetrySetting.dontLogAnything;
}
@ -36,7 +38,7 @@ async function reportError(errorLogObj: ErrorLog, cb?: Function) {
more: JSON.stringify(errorLogObj.more ?? {}),
};
if (frappe.store.isDevelopment) {
if (fyo.store.isDevelopment) {
console.log('errorHandling');
console.log(body);
}
@ -46,7 +48,7 @@ async function reportError(errorLogObj: ErrorLog, cb?: Function) {
}
function getToastProps(errorLogObj: ErrorLog, canLog: boolean, cb?: Function) {
const props = {
const props: ToastOptions = {
message: t`Error: ` + errorLogObj.name,
type: 'error',
};
@ -73,7 +75,7 @@ export function getErrorLogObject(
const errorLogObj = { name, stack, message, more };
// @ts-ignore
frappe.errorLog.push(errorLogObj);
fyo.errorLog.push(errorLogObj);
return errorLogObj;
}
@ -84,7 +86,7 @@ export function handleError(
more?: Record<string, unknown>,
cb?: Function
) {
telemetry.error(error.name);
fyo.telemetry.error(error.name);
if (shouldLog) {
console.error(error);
}
@ -124,7 +126,7 @@ export function handleErrorWithDialog(error: Error, doc?: Doc) {
const errorMessage = getErrorMessage(error, doc);
handleError(false, error, { errorMessage, doc });
showMessageDialog({ message: error.name, description: errorMessage });
showMessageDialog({ message: error.name, detail: errorMessage });
throw error;
}

View File

@ -1,9 +0,0 @@
// Language: Language Code in books/translations
export const languageCodeMap = {
English: 'en',
French: 'fr',
German: 'de',
Portuguese: 'pt',
Arabic: 'ar',
Catalan: 'ca-ES',
};

View File

@ -1,10 +1,10 @@
import { ipcRenderer } from 'electron';
import frappe from 'frappe';
import { createApp } from 'vue';
import App from './App';
import FeatherIcon from './components/FeatherIcon';
import config, { ConfigKeys } from './config';
import { getErrorHandled, handleError } from './errorHandling';
import { fyo } from './initFyo';
import { IPC_ACTIONS } from './messages';
import { incrementOpenCount } from './renderer/helpers';
import registerIpcRendererListeners from './renderer/registerIpcRendererListeners';
@ -13,7 +13,7 @@ import { outsideClickDirective } from './ui';
import { setLanguageMap, stringifyCircular } from './utils';
(async () => {
const language = config.get(ConfigKeys.Language);
const language = fyo.config.get(ConfigKeys.Language);
if (language) {
await setLanguageMap(language);
}
@ -22,15 +22,15 @@ import { setLanguageMap, stringifyCircular } from './utils';
window.config = config;
}
frappe.isElectron = true;
fyo.isElectron = true;
const models = (await import('../models')).default;
await frappe.initializeAndRegister(models);
await fyo.initializeAndRegister(models);
ipcRenderer.send = getErrorHandled(ipcRenderer.send);
ipcRenderer.invoke = getErrorHandled(ipcRenderer.invoke);
window.frappe = frappe;
window.frappe = fyo;
window.onerror = (message, source, lineno, colno, error) => {
error = error ?? new Error('triggered in window.onerror');
@ -50,7 +50,7 @@ import { setLanguageMap, stringifyCircular } from './utils';
app.mixin({
computed: {
frappe() {
return frappe;
return fyo;
},
platform() {
switch (process.platform) {
@ -66,8 +66,8 @@ import { setLanguageMap, stringifyCircular } from './utils';
},
},
methods: {
t: frappe.t,
T: frappe.T,
t: fyo.t,
T: fyo.T,
},
});
@ -88,7 +88,7 @@ import { setLanguageMap, stringifyCircular } from './utils';
console.error(err, vm, info);
};
frappe.store.appVersion = await ipcRenderer.invoke(IPC_ACTIONS.GET_VERSION);
fyo.store.appVersion = await ipcRenderer.invoke(IPC_ACTIONS.GET_VERSION);
incrementOpenCount();
app.mount('body');
@ -100,4 +100,3 @@ import { setLanguageMap, stringifyCircular } from './utils';
handleError(true, error, {}, () => process.exit(1));
});
})();

View File

@ -1,6 +1,6 @@
import PageHeader from '@/components/PageHeader';
import SearchBar from '@/components/SearchBar';
import { openQuickEdit } from '@/utils';
import PageHeader from 'src/components/PageHeader';
import SearchBar from 'src/components/SearchBar';
import { openQuickEdit } from 'src/utils';
import frappe from 'frappe';
import { nextTick } from 'vue';
import { handleErrorWithDialog } from '../errorHandling';
@ -135,10 +135,10 @@ import { handleErrorWithDialog } from '../errorHandling';
</div>
</template>
<script>
import PageHeader from '@/components/PageHeader';
import SearchBar from '@/components/SearchBar';
import { openQuickEdit } from '@/utils';
import frappe from 'frappe';
import PageHeader from 'src/components/PageHeader';
import SearchBar from 'src/components/SearchBar';
import { openQuickEdit } from 'src/utils';
import { nextTick } from 'vue';
import { handleErrorWithDialog } from '../errorHandling';

View File

@ -101,12 +101,12 @@
</template>
<script>
import frappe from 'frappe';
import PeriodSelector from './PeriodSelector';
import { getYMax } from 'src/components/Charts/chartUtils';
import LineChart from 'src/components/Charts/LineChart.vue';
import { formatXLabels } from 'src/utils';
import Cashflow from '../../../reports/Cashflow/Cashflow';
import { getDatesAndPeriodicity } from './getDatesAndPeriodicity';
import LineChart from '@/components/Charts/LineChart.vue';
import { getYMax } from '@/components/Charts/chartUtils';
import { formatXLabels } from '@/utils';
import PeriodSelector from './PeriodSelector';
export default {
name: 'Cashflow',

View File

@ -23,12 +23,12 @@
</template>
<script>
import PageHeader from '@/components/PageHeader';
import SearchBar from '@/components/SearchBar';
import PageHeader from 'src/components/PageHeader';
import SearchBar from 'src/components/SearchBar';
import Cashflow from './Cashflow';
import UnpaidInvoices from './UnpaidInvoices';
import ProfitAndLoss from './ProfitAndLoss';
import Expenses from './Expenses';
import ProfitAndLoss from './ProfitAndLoss';
import UnpaidInvoices from './UnpaidInvoices';
export default {
name: 'Dashboard',

View File

@ -50,8 +50,8 @@
</template>
<script>
import theme from '@/theme';
import frappe from 'frappe';
import theme from 'src/theme';
import DonutChart from '../../components/Charts/DonutChart.vue';
import { getDatesAndPeriodicity } from './getDatesAndPeriodicity';
import PeriodSelector from './PeriodSelector';

View File

@ -36,8 +36,8 @@
</template>
<script>
import Dropdown from '@/components/Dropdown';
import { t } from 'frappe';
import Dropdown from 'src/components/Dropdown';
export default {
name: 'PeriodSelector',

View File

@ -29,13 +29,13 @@
</template>
<script>
import frappe from 'frappe';
import PeriodSelector from './PeriodSelector';
import SectionHeader from './SectionHeader';
import BarChart from 'src/components/Charts/BarChart.vue';
import { getYMax, getYMin } from 'src/components/Charts/chartUtils';
import { formatXLabels } from 'src/utils';
import ProfitAndLoss from '../../../reports/ProfitAndLoss/ProfitAndLoss';
import { getDatesAndPeriodicity } from './getDatesAndPeriodicity';
import BarChart from '@/components/Charts/BarChart.vue';
import { getYMax, getYMin } from '@/components/Charts/chartUtils';
import { formatXLabels } from '@/utils';
import PeriodSelector from './PeriodSelector';
import SectionHeader from './SectionHeader';
export default {
name: 'ProfitAndLoss',

View File

@ -68,9 +68,9 @@
</div>
</template>
<script>
import Button from '@/components/Button';
import { routeTo } from '@/utils';
import frappe, { t } from 'frappe';
import Button from 'src/components/Button';
import { routeTo } from 'src/utils';
import { getDatesAndPeriodicity } from './getDatesAndPeriodicity';
import PeriodSelector from './PeriodSelector';
import SectionHeader from './SectionHeader';

View File

@ -336,17 +336,17 @@
</div>
</template>
<script>
import Button from '@/components/Button.vue';
import FormControl from '@/components/Controls/FormControl';
import DropdownWithActions from '@/components/DropdownWithActions.vue';
import FeatherIcon from '@/components/FeatherIcon.vue';
import HowTo from '@/components/HowTo.vue';
import PageHeader from '@/components/PageHeader.vue';
import { importable, Importer } from '@/dataImport';
import { IPC_ACTIONS } from 'utils/messages';
import { getSavePath, saveData, showMessageDialog } from '@/utils';
import { ipcRenderer } from 'electron';
import frappe from 'frappe';
import Button from 'src/components/Button.vue';
import FormControl from 'src/components/Controls/FormControl';
import DropdownWithActions from 'src/components/DropdownWithActions.vue';
import FeatherIcon from 'src/components/FeatherIcon.vue';
import HowTo from 'src/components/HowTo.vue';
import PageHeader from 'src/components/PageHeader.vue';
import { importable, Importer } from 'src/dataImport';
import { getSavePath, saveData, showMessageDialog } from 'src/utils';
import { IPC_ACTIONS } from 'utils/messages';
import Loading from '../components/Loading.vue';
export default {
components: {
@ -550,7 +550,7 @@ export default {
if (this.isRequiredUnassigned) {
showMessageDialog({
message: this.t`Required Fields not Assigned`,
description: this
detail: this
.t`Please assign the following fields ${this.requiredUnassigned.join(
', '
)}`,
@ -561,7 +561,7 @@ export default {
if (this.importer.assignedMatrix.length === 0) {
showMessageDialog({
message: this.t`No Data to Import`,
description: this.t`Please select a file with data to import.`,
detail: this.t`Please select a file with data to import.`,
});
return;
}
@ -572,7 +572,7 @@ export default {
if (!success) {
showMessageDialog({
message: this.t`Import Failed`,
description: message,
detail: message,
});
return;
}
@ -617,7 +617,7 @@ export default {
if (!isValid) {
showMessageDialog({
message: this.t`Bad import data.`,
description: this.t`Could not select file.`,
detail: this.t`Could not select file.`,
});
return;
}

View File

@ -158,14 +158,14 @@
</div>
</template>
<script>
import LanguageSelector from '@/components/Controls/LanguageSelector.vue';
import config from '@/config';
import { connectToLocalDatabase, createNewDatabase } from '@/initialization';
import { ipcRenderer } from 'electron';
import fs from 'fs';
import { DateTime } from 'luxon';
import { showErrorDialog } from '../errorHandling';
import LanguageSelector from 'src/components/Controls/LanguageSelector.vue';
import config from 'src/config';
import { connectToLocalDatabase, createNewDatabase } from 'src/initialization';
import { DB_CONN_FAILURE, IPC_ACTIONS } from '../../utils/messages';
import { showErrorDialog } from '../errorHandling';
export default {
name: 'DatabaseSelector',

View File

@ -84,12 +84,12 @@
</template>
<script>
import Button from '@/components/Button';
import Icon from '@/components/Icon';
import PageHeader from '@/components/PageHeader';
import { openSettings, routeTo } from '@/utils';
import { ipcRenderer } from 'electron';
import frappe, { t } from 'frappe';
import Button from 'src/components/Button';
import Icon from 'src/components/Icon';
import PageHeader from 'src/components/PageHeader';
import { openSettings, routeTo } from 'src/utils';
import { IPC_MESSAGES } from 'utils/messages';
import { h } from 'vue';

View File

@ -198,20 +198,20 @@
</div>
</template>
<script>
import BackLink from '@/components/BackLink';
import Button from '@/components/Button';
import FormControl from '@/components/Controls/FormControl';
import DropdownWithActions from '@/components/DropdownWithActions';
import PageHeader from '@/components/PageHeader';
import StatusBadge from '@/components/StatusBadge';
import frappe from 'frappe';
import { getInvoiceStatus } from 'models/helpers';
import BackLink from 'src/components/BackLink';
import Button from 'src/components/Button';
import FormControl from 'src/components/Controls/FormControl';
import DropdownWithActions from 'src/components/DropdownWithActions';
import PageHeader from 'src/components/PageHeader';
import StatusBadge from 'src/components/StatusBadge';
import {
getActionsForDocument,
getInvoiceStatus,
openSettings,
routeTo,
showMessageDialog
} from '@/utils';
import frappe from 'frappe';
} from 'src/utils';
import { handleErrorWithDialog } from '../errorHandling';
export default {

View File

@ -128,14 +128,14 @@
</div>
</template>
<script>
import BackLink from '@/components/BackLink';
import Button from '@/components/Button';
import FormControl from '@/components/Controls/FormControl';
import DropdownWithActions from '@/components/DropdownWithActions';
import PageHeader from '@/components/PageHeader';
import StatusBadge from '@/components/StatusBadge';
import { getActionsForDocument, routeTo, showMessageDialog } from '@/utils';
import frappe from 'frappe';
import BackLink from 'src/components/BackLink';
import Button from 'src/components/Button';
import FormControl from 'src/components/Controls/FormControl';
import DropdownWithActions from 'src/components/DropdownWithActions';
import PageHeader from 'src/components/PageHeader';
import StatusBadge from 'src/components/StatusBadge';
import { getActionsForDocument, routeTo, showMessageDialog } from 'src/utils';
import { handleErrorWithDialog } from '../errorHandling';
export default {

View File

@ -56,11 +56,11 @@
</div>
</template>
<script>
import Avatar from '@/components/Avatar';
import Button from '@/components/Button';
import Row from '@/components/Row';
import { openQuickEdit, routeTo } from '@/utils';
import frappe from 'frappe';
import Avatar from 'src/components/Avatar';
import Button from 'src/components/Button';
import Row from 'src/components/Row';
import { openQuickEdit, routeTo } from 'src/utils';
import ListCell from './ListCell';
export default {

View File

@ -30,12 +30,12 @@
</div>
</template>
<script>
import Button from '@/components/Button';
import FilterDropdown from '@/components/FilterDropdown';
import PageHeader from '@/components/PageHeader';
import SearchBar from '@/components/SearchBar';
import { routeTo } from '@/utils';
import frappe from 'fyo';
import Button from 'src/components/Button';
import FilterDropdown from 'src/components/FilterDropdown';
import PageHeader from 'src/components/PageHeader';
import SearchBar from 'src/components/SearchBar';
import { routeTo } from 'src/utils';
import List from './List';
export default {

View File

@ -51,16 +51,16 @@
</div>
</template>
<script>
import BackLink from '@/components/BackLink';
import Button from '@/components/Button';
import PageHeader from '@/components/PageHeader';
import SearchBar from '@/components/SearchBar';
import TwoColumnForm from '@/components/TwoColumnForm';
import telemetry from '@/telemetry/telemetry';
import { Verb } from '@/telemetry/types';
import { makePDF } from '@/utils';
import { ipcRenderer } from 'electron';
import frappe from 'fyo';
import BackLink from 'src/components/BackLink';
import Button from 'src/components/Button';
import PageHeader from 'src/components/PageHeader';
import SearchBar from 'src/components/SearchBar';
import TwoColumnForm from 'src/components/TwoColumnForm';
import telemetry from 'src/telemetry/telemetry';
import { Verb } from 'src/telemetry/types';
import { makePDF } from 'src/utils';
import { IPC_ACTIONS } from 'utils/messages';
export default {

View File

@ -77,13 +77,13 @@
</template>
<script>
import Button from '@/components/Button';
import FormControl from '@/components/Controls/FormControl';
import DropdownWithActions from '@/components/DropdownWithActions';
import StatusBadge from '@/components/StatusBadge';
import TwoColumnForm from '@/components/TwoColumnForm';
import { getActionsForDocument, openQuickEdit } from '@/utils';
import frappe, { t } from 'frappe';
import Button from 'src/components/Button';
import FormControl from 'src/components/Controls/FormControl';
import DropdownWithActions from 'src/components/DropdownWithActions';
import StatusBadge from 'src/components/StatusBadge';
import TwoColumnForm from 'src/components/TwoColumnForm';
import { getActionsForDocument, openQuickEdit } from 'src/utils';
export default {
name: 'QuickEditForm',

View File

@ -136,15 +136,15 @@
</div>
</template>
<script>
import reportViewConfig from '@/../reports/view';
import Button from '@/components/Button';
import FormControl from '@/components/Controls/FormControl';
import FeatherIcon from '@/components/FeatherIcon.vue';
import PageHeader from '@/components/PageHeader';
import Row from '@/components/Row';
import SearchBar from '@/components/SearchBar';
import WithScroll from '@/components/WithScroll';
import frappe from 'frappe';
import reportViewConfig from 'src/../reports/view';
import Button from 'src/components/Button';
import FormControl from 'src/components/Controls/FormControl';
import FeatherIcon from 'src/components/FeatherIcon.vue';
import PageHeader from 'src/components/PageHeader';
import Row from 'src/components/Row';
import SearchBar from 'src/components/SearchBar';
import WithScroll from 'src/components/WithScroll';
import { h, markRaw } from 'vue';
import { getReportData } from '../../reports/index';
import DropdownWithActions from '../components/DropdownWithActions.vue';

View File

@ -51,14 +51,14 @@
</div>
</template>
<script>
import Button from '@/components/Button';
import Icon from '@/components/Icon';
import PageHeader from '@/components/PageHeader';
import Row from '@/components/Row';
import StatusBadge from '@/components/StatusBadge';
import WindowControls from '@/components/WindowControls';
import { ipcRenderer } from 'electron';
import frappe, { t } from 'fyo';
import Button from 'src/components/Button';
import Icon from 'src/components/Icon';
import PageHeader from 'src/components/PageHeader';
import Row from 'src/components/Row';
import StatusBadge from 'src/components/StatusBadge';
import WindowControls from 'src/components/WindowControls';
import { IPC_MESSAGES } from 'utils/messages';
import { h, markRaw } from 'vue';
import { callInitializeMoneyMaker, showToast } from '../../utils';

View File

@ -12,8 +12,8 @@
</template>
<script>
import TwoColumnForm from '@/components/TwoColumnForm';
import frappe from 'fyo';
import TwoColumnForm from 'src/components/TwoColumnForm';
export default {
name: 'TabGeneral',

View File

@ -46,10 +46,10 @@
</div>
</template>
<script>
import FormControl from '@/components/Controls/FormControl';
import TwoColumnForm from '@/components/TwoColumnForm';
import { ipcRenderer } from 'electron';
import frappe from 'fyo';
import FormControl from 'src/components/Controls/FormControl';
import TwoColumnForm from 'src/components/TwoColumnForm';
import { IPC_ACTIONS } from 'utils/messages';
export default {

View File

@ -38,17 +38,17 @@
</template>
<script>
import FormControl from '@/components/Controls/FormControl';
import LanguageSelector from '@/components/Controls/LanguageSelector.vue';
import TwoColumnForm from '@/components/TwoColumnForm';
import frappe from 'fyo';
import FormControl from 'src/components/Controls/FormControl';
import LanguageSelector from 'src/components/Controls/LanguageSelector.vue';
import TwoColumnForm from 'src/components/TwoColumnForm';
import config, {
ConfigKeys,
TelemetrySetting
} from '@/config';
import { getTelemetryOptions } from '@/telemetry/helpers';
import telemetry from '@/telemetry/telemetry';
import { checkForUpdates } from '@/utils';
import frappe from 'fyo';
} from 'src/config';
import { getTelemetryOptions } from 'src/telemetry/helpers';
import telemetry from 'src/telemetry/telemetry';
import { checkForUpdates } from 'src/utils';
export default {
name: 'TabSystem',

View File

@ -89,17 +89,17 @@
</template>
<script>
import FormControl from '@/components/Controls/FormControl';
import LanguageSelector from '@/components/Controls/LanguageSelector.vue';
import Popover from '@/components/Popover';
import TwoColumnForm from '@/components/TwoColumnForm';
import config from '@/config';
import { connectToLocalDatabase, purgeCache } from '@/initialization';
import { setLanguageMap, showMessageDialog } from '@/utils';
import { ipcRenderer } from 'electron';
import fs from 'fs';
import frappe from 'fyo';
import path from 'path';
import FormControl from 'src/components/Controls/FormControl';
import LanguageSelector from 'src/components/Controls/LanguageSelector.vue';
import Popover from 'src/components/Popover';
import TwoColumnForm from 'src/components/TwoColumnForm';
import config from 'src/config';
import { connectToLocalDatabase, purgeCache } from 'src/initialization';
import { setLanguageMap, showMessageDialog } from 'src/utils';
import { IPC_MESSAGES } from 'utils/messages';
import {
getErrorMessage,

View File

@ -32,7 +32,7 @@
</template>
<script>
import Button from '@/components/Button.vue';
import Button from 'src/components/Button.vue';
export default {
emits: ['primary-clicked', 'secondary-clicked'],

View File

@ -1,7 +1,7 @@
import config from '@/config';
import countryList from 'fixtures/countryInfo.json';
import frappe from 'fyo';
import { DEFAULT_LOCALE } from 'fyo/utils/consts';
import countryList from '~/fixtures/countryInfo.json';
import config from 'src/config';
import importCharts from '../../../accounting/importCOA';
import generateTaxes from '../../../models/doctype/Tax/RegionalEntries';
import regionalModelUpdates from '../../../models/regionalModelUpdates';

View File

@ -1,5 +1,5 @@
import { createNumberSeries } from 'frappe/model/naming';
import { DEFAULT_SERIES_START } from 'frappe/utils/consts';
import { createNumberSeries } from 'fyo/model/naming';
import { DEFAULT_SERIES_START } from 'fyo/utils/consts';
import { getValueMapFromList } from 'utils';
import { fyo } from './initFyo';

View File

@ -1,4 +1,4 @@
import { fyo } from '@/initFyo';
import { fyo } from 'src/initFyo';
export type TaxType = 'GST' | 'IGST' | 'Exempt-GST' | 'Exempt-IGST';

View File

@ -1,5 +1,5 @@
import { fyo } from '@/initFyo';
import { ConfigKeys } from 'fyo/core/types';
import { fyo } from 'src/initFyo';
export function incrementOpenCount() {
let openCount = fyo.config.get(ConfigKeys.OpenCount);

View File

@ -1,7 +1,7 @@
import { handleError } from '@/errorHandling';
import { fyo } from '@/initFyo';
import { showToast } from '@/utils';
import { ipcRenderer } from 'electron';
import { handleError } from 'src/errorHandling';
import { fyo } from 'src/initFyo';
import { showToast } from 'src/utils/ui';
import { IPC_CHANNELS, IPC_MESSAGES } from 'utils/messages';
export default function registerIpcRendererListeners() {

View File

@ -1,17 +1,17 @@
import ChartOfAccounts from '@/pages/ChartOfAccounts.vue';
import Dashboard from '@/pages/Dashboard/Dashboard.vue';
import DataImport from '@/pages/DataImport.vue';
import GetStarted from '@/pages/GetStarted.vue';
import InvoiceForm from '@/pages/InvoiceForm.vue';
import JournalEntryForm from '@/pages/JournalEntryForm.vue';
import ListView from '@/pages/ListView/ListView.vue';
import PrintView from '@/pages/PrintView/PrintView.vue';
import QuickEditForm from '@/pages/QuickEditForm.vue';
import Report from '@/pages/Report.vue';
import Settings from '@/pages/Settings/Settings.vue';
import { NounEnum, Verb } from 'fyo/telemetry/types';
import ChartOfAccounts from 'src/pages/ChartOfAccounts.vue';
import Dashboard from 'src/pages/Dashboard/Dashboard.vue';
import DataImport from 'src/pages/DataImport.vue';
import GetStarted from 'src/pages/GetStarted.vue';
import InvoiceForm from 'src/pages/InvoiceForm.vue';
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 Settings from 'src/pages/Settings/Settings.vue';
import { createRouter, createWebHistory } from 'vue-router';
import telemetry from '../frappe/telemetry/telemetry';
import { NounEnum, Verb } from '../frappe/telemetry/types';
import { fyo } from './initFyo';
const routes = [
{
@ -112,7 +112,7 @@ const routes = [
},
];
let router = createRouter({ routes, history: createWebHistory() });
const router = createRouter({ routes, history: createWebHistory() });
function removeDetails(path) {
if (!path) {
@ -133,7 +133,7 @@ router.afterEach((to, from) => {
to: removeDetails(to.fullPath),
};
telemetry.log(Verb.Navigated, NounEnum.Route, more);
fyo.telemetry.log(Verb.Navigated, NounEnum.Route, more);
});
if (process.env.NODE_ENV === 'development') {

View File

@ -1,603 +0,0 @@
import Avatar from '@/components/Avatar.vue';
import Toast from '@/components/Toast.vue';
import router from '@/router';
import { ipcRenderer } from 'electron';
import frappe, { t } from 'frappe';
import { isPesa } from 'frappe/utils';
import { DEFAULT_LANGUAGE } from 'frappe/utils/consts';
import { setLanguageMapOnTranslationString } from 'frappe/utils/translation';
import { IPC_ACTIONS, IPC_MESSAGES } from 'utils/messages';
import { createApp, h } from 'vue';
import config from './config';
import { handleErrorWithDialog } from './errorHandling';
import { languageCodeMap } from './languageCodeMap';
export async function showMessageDialog({
message,
description,
buttons = [],
}) {
const options = {
message,
detail: description,
buttons: buttons.map((a) => a.label),
};
const { response } = await ipcRenderer.invoke(
IPC_ACTIONS.GET_DIALOG_RESPONSE,
options
);
let button = buttons[response];
if (button && button.action) {
button.action();
}
}
export function deleteDocWithPrompt(doc) {
return new Promise((resolve) => {
showMessageDialog({
message: t`Are you sure you want to delete ${doc.doctype} ${doc.name}?`,
description: t`This action is permanent`,
buttons: [
{
label: t`Delete`,
action: () => {
doc
.delete()
.then(() => resolve(true))
.catch((e) => {
handleErrorWithDialog(e, doc);
});
},
},
{
label: t`Cancel`,
action() {
resolve(false);
},
},
],
});
});
}
export async function cancelDocWithPrompt(doc) {
let description = t`This action is permanent`;
if (['SalesInvoice', 'PurchaseInvoice'].includes(doc.doctype)) {
const payments = (
await frappe.db.getAll('Payment', {
fields: ['name'],
filters: { cancelled: false },
})
).map(({ name }) => name);
const query = (
await frappe.db.getAll('PaymentFor', {
fields: ['parent'],
filters: {
referenceName: doc.name,
},
})
).filter(({ parent }) => payments.includes(parent));
const paymentList = [...new Set(query.map(({ parent }) => parent))];
if (paymentList.length === 1) {
description = t`This action is permanent and will cancel the following payment: ${paymentList[0]}`;
} else if (paymentList.length > 1) {
description = t`This action is permanent and will cancel the following payments: ${paymentList.join(
', '
)}`;
}
}
return new Promise((resolve) => {
showMessageDialog({
message: t`Are you sure you want to cancel ${doc.doctype} ${doc.name}?`,
description,
buttons: [
{
label: t`Yes`,
async action() {
const entryDoc = await frappe.doc.getDoc(doc.doctype, doc.name);
entryDoc.cancelled = 1;
await entryDoc.update();
entryDoc
.revert()
.then(() => resolve(true))
.catch((e) => {
handleErrorWithDialog(e, doc);
});
},
},
{
label: t`No`,
action() {
resolve(false);
},
},
],
});
});
}
export function partyWithAvatar(party) {
return {
data() {
return {
imageURL: null,
label: null,
};
},
components: {
Avatar,
},
async mounted() {
const p = await frappe.db.get('Party', party);
this.imageURL = p.image;
this.label = party;
},
template: `
<div class="flex items-center" v-if="label">
<Avatar class="flex-shrink-0" :imageURL="imageURL" :label="label" size="sm" />
<span class="ml-2 truncate">{{ label }}</span>
</div>
`,
};
}
function getShowFields(doctype) {
if (doctype === 'Party') {
return ['customer'];
}
return [];
}
export function openQuickEdit({
doctype,
name,
hideFields,
showFields,
defaults = {},
}) {
let currentRoute = router.currentRoute.value;
let query = currentRoute.query;
let method = 'push';
if (query.edit && query.doctype === doctype) {
// replace the current route if we are
// editing another document of the same doctype
method = 'replace';
}
if (query.name === name) return;
if (defaults?.for?.[0] === 'not in') {
const purpose = defaults.for?.[1]?.[0];
defaults = Object.assign({
for:
purpose === 'sales'
? 'purchases'
: purpose === 'purchases'
? 'sales'
: 'both',
});
}
if (defaults?.for?.[0] === 'not in' && defaults?.for?.[1] === 'sales') {
defaults = Object.assign({ for: 'purchases' });
}
router[method]({
query: {
edit: 1,
doctype,
name,
showFields: showFields ?? getShowFields(doctype),
hideFields,
valueJSON: stringifyCircular(defaults),
lastRoute: currentRoute,
},
});
}
export async function makePDF(html, savePath) {
await ipcRenderer.invoke(IPC_ACTIONS.SAVE_HTML_AS_PDF, html, savePath);
showExportInFolder(frappe.t`Save as PDF Successful`, savePath);
}
export function showExportInFolder(message, filePath) {
showToast({
message,
actionText: frappe.t`Open Folder`,
type: 'success',
action: async () => {
await showItemInFolder(filePath);
},
});
}
export async function saveData(data, savePath) {
await ipcRenderer.invoke(IPC_ACTIONS.SAVE_DATA, data, savePath);
}
export async function showItemInFolder(filePath) {
await ipcRenderer.send(IPC_MESSAGES.SHOW_ITEM_IN_FOLDER, filePath);
}
export function getActionsForDocument(doc) {
if (!doc) return [];
let deleteAction = {
component: {
template: '<span class="text-red-700">{{ t`Delete` }}</span>',
},
condition: (doc) =>
!doc.isNew() && !doc.submitted && !doc.meta.isSingle && !doc.cancelled,
action: () =>
deleteDocWithPrompt(doc).then((res) => {
if (res) {
routeTo(`/list/${doc.doctype}`);
}
}),
};
let cancelAction = {
component: {
template: '<span class="text-red-700">{{ t`Cancel` }}</span>',
},
condition: (doc) => doc.submitted && !doc.cancelled,
action: () => {
cancelDocWithPrompt(doc).then((res) => {
if (res) {
router.push(`/list/${doc.doctype}`);
}
});
},
};
const isSubmittable = !!doc.meta.isSubmittable;
const duplicateAction = {
label: frappe.t`Duplicate`,
condition: (doc) =>
((isSubmittable && doc && doc.submitted) || !isSubmittable) &&
!doc._notInserted &&
!(doc.cancelled || false),
action: () => {
showMessageDialog({
message: t`Duplicate ${doc.doctype} ${doc.name}?`,
buttons: [
{
label: t`Yes`,
async action() {
doc.duplicate();
},
},
{
label: t`No`,
action() {
resolve(false);
},
},
],
});
},
};
let actions = [
...(doc.meta.actions || []),
duplicateAction,
deleteAction,
cancelAction,
]
.filter((d) => (d.condition ? d.condition(doc) : true))
.map((d) => {
return {
label: d.label,
component: d.component,
action: d.action.bind(this, doc, router),
};
});
return actions;
}
export async function runWindowAction(name) {
switch (name) {
case 'close':
ipcRenderer.send(IPC_MESSAGES.CLOSE_CURRENT_WINDOW);
break;
case 'minimize':
ipcRenderer.send(IPC_MESSAGES.MINIMIZE_CURRENT_WINDOW);
break;
case 'maximize':
const maximizing = await ipcRenderer.invoke(
IPC_ACTIONS.TOGGLE_MAXIMIZE_CURRENT_WINDOW
);
name = maximizing ? name : 'unmaximize';
break;
}
return name;
}
export function getInvoiceStatus(doc) {
let status = `Unpaid`;
if (!doc.submitted) {
status = 'Draft';
}
if (doc.submitted === 1 && doc.outstandingAmount.isZero()) {
status = 'Paid';
}
if (doc.cancelled === 1) {
status = 'Cancelled';
}
return status;
}
export function routeTo(route) {
let routeOptions = route;
if (
typeof route === 'string' &&
route === router.currentRoute.value.fullPath
) {
return;
}
if (typeof route === 'string') {
routeOptions = { path: route };
}
router.push(routeOptions);
}
export function fuzzyMatch(keyword, candidate) {
const keywordLetters = [...keyword];
const candidateLetters = [...candidate];
let keywordLetter = keywordLetters.shift();
let candidateLetter = candidateLetters.shift();
let isMatch = true;
let distance = 0;
while (keywordLetter && candidateLetter) {
if (keywordLetter.toLowerCase() === candidateLetter.toLowerCase()) {
keywordLetter = keywordLetters.shift();
} else {
distance += 1;
}
candidateLetter = candidateLetters.shift();
}
if (keywordLetter !== undefined) {
distance = -1;
isMatch = false;
} else {
distance += candidateLetters.length;
}
return { isMatch, distance };
}
export function openSettings(tab) {
routeTo({ path: '/settings', query: { tab } });
}
export async function getSavePath(name, extention) {
let { canceled, filePath } = await ipcRenderer.invoke(
IPC_ACTIONS.GET_SAVE_FILEPATH,
{
title: t`Select Folder`,
defaultPath: `${name}.${extention}`,
}
);
if (filePath && !filePath.endsWith(extention)) {
filePath = filePath + extention;
}
return { canceled, filePath };
}
function replaceAndAppendMount(app, replaceId) {
const fragment = document.createDocumentFragment();
const target = document.getElementById(replaceId);
if (target === null) {
return;
}
const parent = target.parentElement;
const clone = target.cloneNode();
app.mount(fragment);
target.replaceWith(fragment);
parent.append(clone);
}
export function showToast(props) {
const toast = createApp({
render() {
return h(Toast, { ...props });
},
});
replaceAndAppendMount(toast, 'toast-target');
}
export async function getIsSetupComplete() {
try {
const { setupComplete } = await frappe.getSingle('AccountingSettings');
return !!setupComplete;
} catch {
return false;
}
}
export async function getCurrency() {
let currency = frappe?.AccountingSettings?.currency ?? undefined;
if (!currency) {
try {
currency = (
await frappe.db.getSingleValues({
fieldname: 'currency',
parent: 'AccountingSettings',
})
)[0].value;
} catch (err) {
currency = undefined;
}
}
return currency;
}
export async function callInitializeMoneyMaker(currency, force = false) {
currency ??= await getCurrency();
if (!force && !currency && frappe.pesa) {
return;
}
if (!force && currency && frappe.pesa().options.currency === currency) {
return;
}
await frappe.initializeMoneyMaker(currency);
}
export function convertPesaValuesToFloat(obj) {
Object.keys(obj).forEach((key) => {
if (!isPesa(obj[key])) return;
obj[key] = obj[key].float;
});
}
export function formatXLabels(label) {
// Format: Mmm YYYY -> Mm YY
let [month, year] = label.split(' ');
year = year.slice(2);
return `${month} ${year}`;
}
export function stringifyCircular(
obj,
ignoreCircular = false,
convertDocument = false
) {
const cacheKey = [];
const cacheValue = [];
return JSON.stringify(obj, (key, value) => {
if (typeof value !== 'object' || value === null) {
cacheKey.push(key);
cacheValue.push(value);
return value;
}
if (cacheValue.includes(value)) {
const circularKey = cacheKey[cacheValue.indexOf(value)] || '{self}';
return ignoreCircular ? undefined : `[Circular:${circularKey}]`;
}
cacheKey.push(key);
cacheValue.push(value);
if (convertDocument && value instanceof frappe.Document) {
return value.getValidDict();
}
return value;
});
}
export async function checkForUpdates(force = false) {
ipcRenderer.invoke(IPC_ACTIONS.CHECK_FOR_UPDATES, force);
await setLanguageMap();
}
async function fetchAndSetLanguageMap(code) {
const { success, message, languageMap } = await ipcRenderer.invoke(
IPC_ACTIONS.GET_LANGUAGE_MAP,
code
);
if (!success) {
showToast({ type: 'error', message });
} else {
setLanguageMapOnTranslationString(languageMap);
}
return success;
}
export async function setLanguageMap(initLanguage, dontReload = false) {
const oldLanguage = config.get('language');
initLanguage ??= oldLanguage;
const [code, language, usingDefault] = getLanguageCode(
initLanguage,
oldLanguage
);
let success = true;
if (code === 'en') {
setLanguageMapOnTranslationString(undefined);
} else {
success = await fetchAndSetLanguageMap(code);
}
if (success && !usingDefault) {
config.set('language', language);
}
if (!dontReload && success && initLanguage !== oldLanguage) {
await ipcRenderer.send(IPC_MESSAGES.RELOAD_MAIN_WINDOW);
}
return success;
}
function getLanguageCode(initLanguage, oldLanguage) {
let language = initLanguage ?? oldLanguage;
let usingDefault = false;
if (!language) {
language = DEFAULT_LANGUAGE;
usingDefault = true;
}
const code = languageCodeMap[language] ?? 'en';
return [code, language, usingDefault];
}
export function getCOAList() {
if (!frappe.temp.coaList) {
const coaList = [
{ name: t`Standard Chart of Accounts`, countryCode: '' },
{ countryCode: 'ae', name: 'U.A.E - Chart of Accounts' },
{
countryCode: 'ca',
name: 'Canada - Plan comptable pour les provinces francophones',
},
{ countryCode: 'gt', name: 'Guatemala - Cuentas' },
{ countryCode: 'hu', name: 'Hungary - Chart of Accounts' },
{ countryCode: 'id', name: 'Indonesia - Chart of Accounts' },
{ countryCode: 'in', name: 'India - Chart of Accounts' },
{ countryCode: 'mx', name: 'Mexico - Plan de Cuentas' },
{ countryCode: 'ni', name: 'Nicaragua - Catalogo de Cuentas' },
{ countryCode: 'nl', name: 'Netherlands - Grootboekschema' },
{ countryCode: 'sg', name: 'Singapore - Chart of Accounts' },
];
frappe.temp.coaList = coaList;
}
return frappe.temp.coaList;
}
export function invertMap(map) {
const keys = Object.keys(map);
const inverted = {};
for (const key of keys) {
const val = map[key];
inverted[val] = key;
}
return inverted;
}

View File

@ -1,99 +0,0 @@
import Doc from 'fyo/model/doc';
export interface QuickEditOptions {
schemaName: string;
name: string;
hideFields?: string[];
showFields?: string[];
defaults?: Record<string, unknown>;
}
export async function openQuickEdit({
schemaName,
name,
hideFields,
showFields,
defaults = {},
}: QuickEditOptions) {
const router = (await import('./router')).default;
const currentRoute = router.currentRoute.value;
const query = currentRoute.query;
let method: 'push' | 'replace' = 'push';
if (query.edit && query.doctype === schemaName) {
// replace the current route if we are
// editing another document of the same doctype
method = 'replace';
}
if (query.name === name) return;
const forWhat = (defaults?.for ?? []) as string[];
if (forWhat[0] === 'not in') {
const purpose = forWhat[1]?.[0];
defaults = Object.assign({
for:
purpose === 'sales'
? 'purchases'
: purpose === 'purchases'
? 'sales'
: 'both',
});
}
if (forWhat[0] === 'not in' && forWhat[1] === 'sales') {
defaults = Object.assign({ for: 'purchases' });
}
router[method]({
query: {
edit: 1,
doctype: schemaName,
name,
showFields: showFields ?? getShowFields(schemaName),
hideFields,
valueJSON: stringifyCircular(defaults),
// @ts-ignore
lastRoute: currentRoute,
},
});
}
function getShowFields(schemaName: string) {
if (schemaName === 'Party') {
return ['customer'];
}
return [];
}
export function stringifyCircular(
obj: Record<string, unknown>,
ignoreCircular = false,
convertDocument = false
) {
const cacheKey: string[] = [];
const cacheValue: unknown[] = [];
return JSON.stringify(obj, (key, value) => {
if (typeof value !== 'object' || value === null) {
cacheKey.push(key);
cacheValue.push(value);
return value;
}
if (cacheValue.includes(value)) {
const circularKey = cacheKey[cacheValue.indexOf(value)] || '{self}';
return ignoreCircular ? undefined : `[Circular:${circularKey}]`;
}
cacheKey.push(key);
cacheValue.push(value);
if (convertDocument && value instanceof Doc) {
return value.getValidDict();
}
return value;
});
}

View File

@ -17,26 +17,26 @@ export const statusColor = {
Cancelled: 'red',
};
const getValidColor = (color) => {
const getValidColor = (color: string) => {
const isValid = ['gray', 'orange', 'green', 'red', 'yellow', 'blue'].includes(
color
);
return isValid ? color : 'gray';
};
export function getBgColorClass(color) {
export function getBgColorClass(color: string) {
return `bg-${getValidColor(color)}-100`;
}
export function getColorClass(color, type, value = 300) {
export function getColorClass(color: string, type: 'bg' | 'text', value = 300) {
return `${type}-${getValidColor(color)}-${value}`;
}
export function getTextColorClass(color) {
export function getTextColorClass(color: string) {
return `text-${getValidColor(color)}-600`;
}
export function getBgTextColorClass(color) {
export function getBgTextColorClass(color: string) {
const bg = getBgColorClass(color);
const text = getTextColorClass(color);
return [bg, text].join(' ');

97
src/utils/index.ts Normal file
View File

@ -0,0 +1,97 @@
/**
* General purpose utils used by the frontend.
*/
import Doc from 'fyo/model/doc';
import { isPesa } from 'fyo/utils';
import Money from 'pesa/dist/types/src/money';
import { fyo } from 'src/initFyo';
export function stringifyCircular(
obj: unknown,
ignoreCircular: boolean = false,
convertDocument: boolean = false
) {
const cacheKey: string[] = [];
const cacheValue: unknown[] = [];
return JSON.stringify(obj, (key, value) => {
if (typeof value !== 'object' || value === null) {
cacheKey.push(key);
cacheValue.push(value);
return value;
}
if (cacheValue.includes(value)) {
const circularKey = cacheKey[cacheValue.indexOf(value)] || '{self}';
return ignoreCircular ? undefined : `[Circular:${circularKey}]`;
}
cacheKey.push(key);
cacheValue.push(value);
if (convertDocument && value instanceof Doc) {
return value.getValidDict();
}
return value;
});
}
export function fuzzyMatch(keyword: string, candidate: string) {
const keywordLetters = [...keyword];
const candidateLetters = [...candidate];
let keywordLetter = keywordLetters.shift();
let candidateLetter = candidateLetters.shift();
let isMatch = true;
let distance = 0;
while (keywordLetter && candidateLetter) {
if (keywordLetter.toLowerCase() === candidateLetter.toLowerCase()) {
keywordLetter = keywordLetters.shift();
} else {
distance += 1;
}
candidateLetter = candidateLetters.shift();
}
if (keywordLetter !== undefined) {
distance = -1;
isMatch = false;
} else {
distance += candidateLetters.length;
}
return { isMatch, distance };
}
export function formatXLabels(label: string) {
// Format: Mmm YYYY -> Mm YY
const splits = label.split(' ');
const month = splits[0];
const year = splits[1].slice(2);
return `${month} ${year}`;
}
export function convertPesaValuesToFloat(obj: Record<string, unknown>) {
Object.keys(obj).forEach((key) => {
const value = obj[key];
if (!isPesa(value)) {
return;
}
obj[key] = (value as Money).float;
});
}
export async function getIsSetupComplete() {
try {
const { setupComplete } = await fyo.doc.getSingle('AccountingSettings');
return !!setupComplete;
} catch {
return false;
}
}

72
src/utils/ipcCalls.ts Normal file
View File

@ -0,0 +1,72 @@
/**
* Utils that make ipcRenderer calls.
*/
import { ipcRenderer } from 'electron';
import { t } from 'fyo';
import { IPC_ACTIONS, IPC_MESSAGES } from 'utils/messages';
import { setLanguageMap } from './language';
import { WindowAction } from './types';
import { showToast } from './ui';
export async function checkForUpdates(force = false) {
ipcRenderer.invoke(IPC_ACTIONS.CHECK_FOR_UPDATES, force);
await setLanguageMap();
}
export async function saveData(data: string, savePath: string) {
await ipcRenderer.invoke(IPC_ACTIONS.SAVE_DATA, data, savePath);
}
export async function showItemInFolder(filePath: string) {
await ipcRenderer.send(IPC_MESSAGES.SHOW_ITEM_IN_FOLDER, filePath);
}
export async function makePDF(html: string, savePath: string) {
await ipcRenderer.invoke(IPC_ACTIONS.SAVE_HTML_AS_PDF, html, savePath);
showExportInFolder(t`Save as PDF Successful`, savePath);
}
export async function runWindowAction(name: WindowAction) {
switch (name) {
case 'close':
ipcRenderer.send(IPC_MESSAGES.CLOSE_CURRENT_WINDOW);
break;
case 'minimize':
ipcRenderer.send(IPC_MESSAGES.MINIMIZE_CURRENT_WINDOW);
break;
case 'maximize':
const maximizing = await ipcRenderer.invoke(
IPC_ACTIONS.TOGGLE_MAXIMIZE_CURRENT_WINDOW
);
name = maximizing ? name : 'unmaximize';
break;
}
return name;
}
export function showExportInFolder(message: string, filePath: string) {
showToast({
message,
actionText: t`Open Folder`,
type: 'success',
action: async () => {
await showItemInFolder(filePath);
},
});
}
export async function getSavePath(name: string, extention: string) {
const response = (await ipcRenderer.invoke(IPC_ACTIONS.GET_SAVE_FILEPATH, {
title: t`Select Folder`,
defaultPath: `${name}.${extention}`,
})) as { canceled: boolean; filePath?: string };
const canceled = response.canceled;
let filePath = response.filePath;
if (filePath && !filePath.endsWith(extention)) {
filePath = filePath + extention;
}
return { canceled, filePath };
}

71
src/utils/language.ts Normal file
View File

@ -0,0 +1,71 @@
import { ipcRenderer } from 'electron';
import { DEFAULT_LANGUAGE } from 'fyo/utils/consts';
import { setLanguageMapOnTranslationString } from 'fyo/utils/translation';
import { fyo } from 'src/initFyo';
import { IPC_ACTIONS, IPC_MESSAGES } from 'utils/messages';
import { showToast } from './ui';
// Language: Language Code in books/translations
export const languageCodeMap: Record<string, string> = {
English: 'en',
French: 'fr',
German: 'de',
Portuguese: 'pt',
Arabic: 'ar',
Catalan: 'ca-ES',
};
export async function setLanguageMap(
initLanguage?: string,
dontReload: boolean = false
) {
const oldLanguage = fyo.config.get('language') as string;
initLanguage ??= oldLanguage;
const { code, language, usingDefault } = getLanguageCode(
initLanguage,
oldLanguage
);
let success = true;
if (code === 'en') {
setLanguageMapOnTranslationString(undefined);
} else {
success = await fetchAndSetLanguageMap(code);
}
if (success && !usingDefault) {
fyo.config.set('language', language);
}
if (!dontReload && success && initLanguage !== oldLanguage) {
await ipcRenderer.send(IPC_MESSAGES.RELOAD_MAIN_WINDOW);
}
return success;
}
function getLanguageCode(initLanguage: string, oldLanguage: string) {
let language = initLanguage ?? oldLanguage;
let usingDefault = false;
if (!language) {
language = DEFAULT_LANGUAGE;
usingDefault = true;
}
const code = languageCodeMap[language] ?? 'en';
return { code, language, usingDefault };
}
async function fetchAndSetLanguageMap(code: string) {
const { success, message, languageMap } = await ipcRenderer.invoke(
IPC_ACTIONS.GET_LANGUAGE_MAP,
code
);
if (!success) {
showToast({ type: 'error', message });
} else {
setLanguageMapOnTranslationString(languageMap);
}
return success;
}

29
src/utils/types.ts Normal file
View File

@ -0,0 +1,29 @@
export interface MessageDialogButton {
label: string;
action: () => void;
}
export interface MessageDialogOptions {
message: string;
detail?: string;
buttons?: MessageDialogButton[];
}
export interface ToastOptions {
message: string;
type?: 'info' | 'warning' | 'error' | 'success';
duration?: number;
action?: () => void;
actionText?: string;
}
export type WindowAction = 'close' | 'minimize' | 'maximize' | 'unmaximize';
export type SettingsTab = 'Invoice' | 'General' | 'System';
export interface QuickEditOptions {
schemaName: string;
name: string;
hideFields?: string[];
showFields?: string[];
defaults?: Record<string, unknown>;
}

329
src/utils/ui.ts Normal file
View File

@ -0,0 +1,329 @@
/**
* Utils to do UI stuff such as opening dialogs, toasts, etc.
* Basically anything that may directly or indirectly import a Vue file.
*/
import { ipcRenderer } from 'electron';
import { t } from 'fyo';
import Doc from 'fyo/model/doc';
import { Action } from 'fyo/model/types';
import { getActions } from 'fyo/utils';
import { handleErrorWithDialog } from 'src/errorHandling';
import { fyo } from 'src/initFyo';
import router from 'src/router';
import { IPC_ACTIONS } from 'utils/messages';
import { App, createApp, h } from 'vue';
import { RouteLocationRaw } from 'vue-router';
import { stringifyCircular } from './';
import {
MessageDialogOptions,
QuickEditOptions,
SettingsTab,
ToastOptions,
} from './types';
export async function openQuickEdit({
schemaName,
name,
hideFields,
showFields,
defaults = {},
}: QuickEditOptions) {
const router = (await import('src/router')).default;
const currentRoute = router.currentRoute.value;
const query = currentRoute.query;
let method: 'push' | 'replace' = 'push';
if (query.edit && query.doctype === schemaName) {
// replace the current route if we are
// editing another document of the same doctype
method = 'replace';
}
if (query.name === name) return;
const forWhat = (defaults?.for ?? []) as string[];
if (forWhat[0] === 'not in') {
const purpose = forWhat[1]?.[0];
defaults = Object.assign({
for:
purpose === 'sales'
? 'purchases'
: purpose === 'purchases'
? 'sales'
: 'both',
});
}
if (forWhat[0] === 'not in' && forWhat[1] === 'sales') {
defaults = Object.assign({ for: 'purchases' });
}
router[method]({
query: {
edit: 1,
doctype: schemaName,
name,
showFields: showFields ?? getShowFields(schemaName),
hideFields,
valueJSON: stringifyCircular(defaults),
// @ts-ignore
lastRoute: currentRoute,
},
});
}
function getShowFields(schemaName: string) {
if (schemaName === 'Party') {
return ['customer'];
}
return [];
}
export async function showMessageDialog({
message,
detail,
buttons = [],
}: MessageDialogOptions) {
const options = {
message,
detail,
buttons: buttons.map((a) => a.label),
};
const { response } = (await ipcRenderer.invoke(
IPC_ACTIONS.GET_DIALOG_RESPONSE,
options
)) as { response: number };
const button = buttons[response];
if (button && button.action) {
button.action();
}
}
export async function showToast(options: ToastOptions) {
const Toast = (await import('src/components/Toast.vue')).default;
const toast = createApp({
render() {
return h(Toast, { ...options });
},
});
replaceAndAppendMount(toast, 'toast-target');
}
function replaceAndAppendMount(app: App<Element>, replaceId: string) {
const fragment = document.createDocumentFragment();
const target = document.getElementById(replaceId);
if (target === null) {
return;
}
const parent = target.parentElement;
const clone = target.cloneNode();
// @ts-ignore
app.mount(fragment);
target.replaceWith(fragment);
parent!.append(clone);
}
export function openSettings(tab: SettingsTab) {
routeTo({ path: '/settings', query: { tab } });
}
export function routeTo(route: string | RouteLocationRaw) {
let routeOptions = route;
if (
typeof route === 'string' &&
route === router.currentRoute.value.fullPath
) {
return;
}
if (typeof route === 'string') {
routeOptions = { path: route };
}
router.push(routeOptions);
}
export function deleteDocWithPrompt(doc: Doc) {
return new Promise((resolve) => {
showMessageDialog({
message: t`Are you sure you want to delete ${
doc.schemaName
} ${doc.name!}?`,
detail: t`This action is permanent`,
buttons: [
{
label: t`Delete`,
action: () => {
doc
.delete()
.then(() => resolve(true))
.catch((e: Error) => {
handleErrorWithDialog(e, doc);
});
},
},
{
label: t`Cancel`,
action() {
resolve(false);
},
},
],
});
});
}
export async function cancelDocWithPrompt(doc: Doc) {
let detail = t`This action is permanent`;
if (['SalesInvoice', 'PurchaseInvoice'].includes(doc.schemaName)) {
const payments = (
await fyo.db.getAll('Payment', {
fields: ['name'],
filters: { cancelled: false },
})
).map(({ name }) => name);
const query = (
await fyo.db.getAll('PaymentFor', {
fields: ['parent'],
filters: {
referenceName: doc.name!,
},
})
).filter(({ parent }) => payments.includes(parent));
const paymentList = [...new Set(query.map(({ parent }) => parent))];
if (paymentList.length === 1) {
detail = t`This action is permanent and will cancel the following payment: ${
paymentList[0] as string
}`;
} else if (paymentList.length > 1) {
detail = t`This action is permanent and will cancel the following payments: ${paymentList.join(
', '
)}`;
}
}
return new Promise((resolve) => {
showMessageDialog({
message: t`Are you sure you want to cancel ${
doc.schemaName
} ${doc.name!}?`,
detail,
buttons: [
{
label: t`Yes`,
async action() {
const entryDoc = await fyo.doc.getDoc(doc.schemaName, doc.name!);
entryDoc.cancelled = 1;
await entryDoc.update();
entryDoc
.revert()
.then(() => resolve(true))
.catch((e) => {
handleErrorWithDialog(e, doc);
});
},
},
{
label: t`No`,
action() {
resolve(false);
},
},
],
});
});
}
export function getActionsForDocument(doc?: Doc): Action[] {
if (!doc) return [];
const actions: Action[] = [
...getActions(doc, fyo),
getDuplicateAction(doc),
getDeleteAction(doc),
getCancelAction(doc),
];
return actions
.filter((d) => d.condition?.(doc) ?? true)
.map((d) => {
return {
label: d.label,
component: d.component,
action: d.action,
};
});
}
function getCancelAction(doc: Doc): Action {
return {
label: t`Cancel`,
component: {
template: '<span class="text-red-700">{{ t`Cancel` }}</span>',
},
condition: (doc: Doc) => !!(doc.submitted && !doc.cancelled),
action: () => {
cancelDocWithPrompt(doc).then((res) => {
if (res) {
router.push(`/list/${doc.schemaName}`);
}
});
},
};
}
function getDeleteAction(doc: Doc): Action {
return {
label: t`Delete`,
component: {
template: '<span class="text-red-700">{{ t`Delete` }}</span>',
},
condition: (doc: Doc) =>
!doc.isNew && !doc.submitted && !doc.schema.isSingle && !doc.cancelled,
action: () =>
deleteDocWithPrompt(doc).then((res) => {
if (res) {
routeTo(`/list/${doc.schemaName}`);
}
}),
};
}
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)
),
action: () => {
showMessageDialog({
message: t`Duplicate ${doc.schemaName} ${doc.name!}?`,
buttons: [
{
label: t`Yes`,
async action() {
doc.duplicate();
},
},
{
label: t`No`,
action() {
// no-op
},
},
],
});
},
};
}

View File

@ -15,10 +15,11 @@
"baseUrl": ".",
"types": ["webpack-env"],
"paths": {
"@/*": ["src/*"],
"src/*": ["src/*"],
"schemas/*": ["schemas/*"],
"backend/*": ["backend/*"],
"regional/*": ["regional/*"],
"fixtures/*": ["fixtures/*"],
"utils/*": ["utils/*"]
},
"lib": ["esnext", "dom", "dom.iterable", "scripthost"]
@ -43,7 +44,8 @@
"accounting/**/*.ts",
"scripts/**/*.ts",
"utils/csvParser.ts"
, "utils/config.ts" ],
"utils/csvParser.ts",
"utils/config.ts"
],
"exclude": ["node_modules"]
}

View File

@ -68,3 +68,14 @@ export function titleCase(phrase: string): string {
})
.join(' ');
}
export function invertMap(map: Record<string, string>): Record<string, string> {
const keys = Object.keys(map);
const inverted: Record<string, string> = {};
for (const key of keys) {
const val = map[key];
inverted[val] = key;
}
return inverted;
}

View File

@ -40,11 +40,12 @@ module.exports = {
configureWebpack(config) {
Object.assign(config.resolve.alias, {
fyo: path.resolve(__dirname, './fyo'),
'~': path.resolve('.'),
src: path.resolve(__dirname, './src'),
schemas: path.resolve(__dirname, './schemas'),
backend: path.resolve(__dirname, './backend'),
utils: path.resolve(__dirname, './utils'),
regional: path.resolve(__dirname, './regional'),
fixtures: path.resolve(__dirname, './fixtures'),
});
config.plugins.push(