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

fix: get Invoice settings to render and work

- remove "computed"
- fix a few formcontrols
- simplify doc.ts a bit more
This commit is contained in:
18alantom 2022-04-26 15:42:33 +05:30
parent 9fbf4fa23c
commit 024687c1b9
46 changed files with 281 additions and 254 deletions

View File

@ -75,7 +75,7 @@ export default class DatabaseCore extends DatabaseBase {
const db = new DatabaseCore(dbPath);
db.connect();
let query: { countryCode: string }[] = [];
let query: { value: string }[] = [];
try {
query = await db.knex!('SingleValue').where({
fieldname: 'countryCode',
@ -86,7 +86,7 @@ export default class DatabaseCore extends DatabaseBase {
}
if (query.length > 0) {
countryCode = query[0].countryCode as string;
countryCode = query[0].value as string;
}
await db.close();

View File

@ -57,7 +57,6 @@ const SalesInvoiceItem = {
fieldname: 'amount',
label: 'Amount',
fieldtype: 'Float',
computed: true,
readOnly: true,
},
],
@ -108,7 +107,6 @@ const SalesInvoice = {
fieldname: 'grandTotal',
label: 'Grand Total',
fieldtype: 'Currency',
computed: true,
readOnly: true,
},
],

View File

@ -18,6 +18,7 @@ import {
TargetField,
} from 'schemas/types';
import { getIsNullOrUndef, getMapFromList, getRandomString } from 'utils';
import { markRaw } from 'vue';
import { isPesa } from '../utils/index';
import {
areDocValuesEqual,
@ -71,7 +72,7 @@ export class Doc extends Observable<DocValue | Doc[]> {
constructor(schema: Schema, data: DocValueMap, fyo: Fyo) {
super();
this.fyo = fyo;
this.fyo = markRaw(fyo);
this.schema = schema;
this.fieldMap = getMapFromList(schema.fields, 'fieldname');
@ -87,7 +88,7 @@ export class Doc extends Observable<DocValue | Doc[]> {
return this.schema.name;
}
get isNew(): boolean {
get notInserted(): boolean {
return this._notInserted;
}
@ -176,7 +177,7 @@ export class Doc extends Observable<DocValue | Doc[]> {
}
_canSet(fieldname: string, value?: DocValue | Doc[]): boolean {
if (fieldname === 'numberSeries' && !this._notInserted) {
if (fieldname === 'numberSeries' && !this.notInserted) {
return false;
}
@ -378,11 +379,13 @@ export class Doc extends Observable<DocValue | Doc[]> {
}
if (data && data.name) {
this.syncValues(data);
this._syncValues(data);
await this.loadLinks();
} else {
throw new NotFoundError(`Not Found: ${this.schemaName} ${this.name}`);
}
this._notInserted = false;
}
async loadLinks() {
@ -420,8 +423,8 @@ export class Doc extends Observable<DocValue | Doc[]> {
return link;
}
syncValues(data: DocValueMap) {
this.clearValues();
_syncValues(data: DocValueMap) {
this._clearValues();
this._setValuesWithoutChecks(data);
this._dirty = false;
this.trigger('change', {
@ -429,7 +432,7 @@ export class Doc extends Observable<DocValue | Doc[]> {
});
}
clearValues() {
_clearValues() {
for (const { fieldname } of this.schema.fields) {
this[fieldname] = null;
}
@ -453,19 +456,19 @@ export class Doc extends Observable<DocValue | Doc[]> {
}
async _compareWithCurrentDoc() {
if (this.isNew || !this.name || this.schema.isSingle) {
if (this.notInserted || !this.name || this.schema.isSingle) {
return;
}
const dbValues = await this.fyo.db.get(this.schemaName, this.name);
const docModified = this.modified as Date;
const dbModified = dbValues.modified as Date;
const docModified = (this.modified as Date)?.toISOString();
const dbModified = (dbValues.modified as Date)?.toISOString();
if (dbValues && docModified !== dbModified) {
throw new ConflictError(
this.fyo
.t`Document ${this.schemaName} ${this.name} has been modified after loading` +
`${docModified?.toISOString()}, ${dbModified?.toISOString()}`
` ${dbModified}, ${docModified}`
);
}
@ -575,20 +578,15 @@ export class Doc extends Observable<DocValue | Doc[]> {
this._setBaseMetaValues();
await this._commit();
await this._validateInsert();
await this.trigger('beforeInsert', null);
const oldName = this.name!;
const validDict = this.getValidDict();
const data = await this.fyo.db.insert(this.schemaName, validDict);
this.syncValues(data);
this._notInserted = false;
this._syncValues(data);
if (oldName !== this.name) {
this.fyo.doc.removeFromCache(this.schemaName, oldName);
}
await this.trigger('afterInsert', null);
this.fyo.telemetry.log(Verb.Created, this.schemaName);
return this;
}
@ -596,7 +594,6 @@ export class Doc extends Observable<DocValue | Doc[]> {
async _update() {
await this._compareWithCurrentDoc();
await this._commit();
await this.trigger('beforeUpdate');
// before submit
if (this.flags.submitAction) await this.trigger('beforeSubmit');
@ -607,9 +604,7 @@ export class Doc extends Observable<DocValue | Doc[]> {
const data = this.getValidDict();
await this.fyo.db.update(this.schemaName, data);
this.syncValues(data);
await this.trigger('afterUpdate');
this._syncValues(data);
// after submit
if (this.flags.submitAction) await this.trigger('afterSubmit');
@ -621,11 +616,12 @@ export class Doc extends Observable<DocValue | Doc[]> {
async sync() {
await this.trigger('beforeSync');
let doc;
if (this._notInserted) {
if (this.notInserted) {
doc = await this._insert();
} else {
doc = await this._update();
}
this._notInserted = false;
await this.trigger('afterSync');
return doc;
}
@ -748,14 +744,12 @@ export class Doc extends Observable<DocValue | Doc[]> {
return doc;
}
async beforeInsert() {}
async afterInsert() {}
async beforeUpdate() {}
async afterUpdate() {}
async beforeSync() {}
async afterSync() {}
async beforeDelete() {}
async afterDelete() {}
async beforeSubmit() {}
async afterSubmit() {}
async beforeRevert() {}
async afterRevert() {}

View File

@ -1,4 +1,3 @@
import { Fyo } from 'fyo';
import { DocValue, DocValueMap } from 'fyo/core/types';
import SystemSettings from 'fyo/models/SystemSettings';
import { FieldType } from 'schemas/types';
@ -50,10 +49,10 @@ export interface SinglesMap {
// Static Config properties
export type FilterFunction = (doc: Doc) => QueryFilter;
export type FilterFunction = (doc: Doc) => QueryFilter | Promise<QueryFilter>;
export type FiltersMap = Record<string, FilterFunction>;
export type EmptyMessageFunction = (doc: Doc, fyo: Fyo) => string;
export type EmptyMessageFunction = (doc: Doc) => string;
export type EmptyMessageMap = Record<string, EmptyMessageFunction>;
export type ListFunction = (doc?: Doc) => string[];

View File

@ -24,7 +24,7 @@ export class Account extends Doc {
rgt: () => 0,
};
async beforeInsert() {
async beforeSync() {
if (this.accountType || !this.parentAccount) {
return;
}

View File

@ -1,6 +1,11 @@
import { Fyo } from 'fyo';
import { t } from 'fyo';
import { Doc } from 'fyo/model/doc';
import { EmptyMessageMap, FormulaMap, ListsMap } from 'fyo/model/types';
import {
DependsOnMap,
EmptyMessageMap,
FormulaMap,
ListsMap,
} from 'fyo/model/types';
import { stateCodeMap } from 'regional/in';
import { titleCase } from 'utils';
import { getCountryInfo } from 'utils/misc';
@ -21,6 +26,17 @@ export class Address extends Doc {
},
};
dependsOn: DependsOnMap = {
addressDisplay: [
'addressLine1',
'addressLine2',
'city',
'state',
'country',
'postalCode',
],
};
static lists: ListsMap = {
state(doc?: Doc) {
const country = doc?.country as string | undefined;
@ -37,12 +53,12 @@ export class Address extends Doc {
};
static emptyMessages: EmptyMessageMap = {
state: (doc: Doc, fyo: Fyo) => {
state: (doc: Doc) => {
if (doc.country) {
return fyo.t`Enter State`;
return t`Enter State`;
}
return fyo.t`Enter Country to load States`;
return t`Enter Country to load States`;
},
};
}

View File

@ -43,12 +43,7 @@ export abstract class Invoice extends Doc {
return [];
}
async beforeUpdate() {
const entries = await this.getPosting();
await entries.validateEntries();
}
async beforeInsert() {
async beforeSync() {
const entries = await this.getPosting();
await entries.validateEntries();
}

View File

@ -66,7 +66,7 @@ export class Item extends Doc {
return [
{
label: fyo.t`New Invoice`,
condition: (doc) => !doc.isNew,
condition: (doc) => !doc.notInserted,
action: async (doc, router) => {
const invoice = await fyo.doc.getNewDoc('SalesInvoice');
await invoice.append('items', {
@ -79,7 +79,7 @@ export class Item extends Doc {
},
{
label: fyo.t`New Bill`,
condition: (doc) => !doc.isNew,
condition: (doc) => !doc.notInserted,
action: async (doc, router) => {
const invoice = await fyo.doc.getNewDoc('PurchaseInvoice');
await invoice.append('items', {

View File

@ -32,11 +32,7 @@ export class JournalEntry extends Doc {
return entries;
}
async beforeUpdate() {
this.getPosting().validateEntries();
}
async beforeInsert() {
async beforeSync() {
this.getPosting().validateEntries();
}

View File

@ -109,7 +109,7 @@ export class Party extends Doc {
{
label: fyo.t`Create Bill`,
condition: (doc: Doc) =>
!doc.isNew && (doc.role as PartyRole) !== 'Customer',
!doc.notInserted && (doc.role as PartyRole) !== 'Customer',
action: async (partyDoc, router) => {
const doc = await fyo.doc.getNewDoc('PurchaseInvoice');
router.push({
@ -127,7 +127,7 @@ export class Party extends Doc {
{
label: fyo.t`View Bills`,
condition: (doc: Doc) =>
!doc.isNew && (doc.role as PartyRole) !== 'Customer',
!doc.notInserted && (doc.role as PartyRole) !== 'Customer',
action: async (partyDoc, router) => {
router.push({
name: 'ListView',
@ -144,7 +144,7 @@ export class Party extends Doc {
{
label: fyo.t`Create Invoice`,
condition: (doc: Doc) =>
!doc.isNew && (doc.role as PartyRole) !== 'Supplier',
!doc.notInserted && (doc.role as PartyRole) !== 'Supplier',
action: async (partyDoc, router) => {
const doc = await fyo.doc.getNewDoc('SalesInvoice');
router.push({
@ -162,7 +162,7 @@ export class Party extends Doc {
{
label: fyo.t`View Invoices`,
condition: (doc: Doc) =>
!doc.isNew && (doc.role as PartyRole) !== 'Supplier',
!doc.notInserted && (doc.role as PartyRole) !== 'Supplier',
action: async (partyDoc, router) => {
router.push({
name: 'ListView',

View File

@ -35,7 +35,7 @@ export function getTransactionActions(schemaName: string, fyo: Fyo): Action[] {
(doc.submitted as boolean) && (doc.outstandingAmount as Money).gt(0),
action: async function makePayment(doc: Doc) {
const payment = await fyo.doc.getNewDoc('Payment');
payment.once('afterInsert', async () => {
payment.once('afterSync', async () => {
await payment.submit();
});
const isSales = schemaName === 'SalesInvoice';

View File

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

View File

@ -55,14 +55,12 @@
"fieldname": "netTotal",
"label": "Net Total",
"fieldtype": "Currency",
"computed": true,
"readOnly": true
},
{
"fieldname": "baseNetTotal",
"label": "Net Total (Company Currency)",
"fieldtype": "Currency",
"computed": true,
"readOnly": true
},
{
@ -76,14 +74,12 @@
"fieldname": "grandTotal",
"label": "Grand Total",
"fieldtype": "Currency",
"computed": true,
"readOnly": true
},
{
"fieldname": "baseGrandTotal",
"label": "Grand Total (Company Currency)",
"fieldtype": "Currency",
"computed": true,
"readOnly": true
},
{

View File

@ -33,7 +33,6 @@
"fieldname": "baseRate",
"label": "Rate (Company Currency)",
"fieldtype": "Currency",
"computed": true,
"readOnly": true
},
{
@ -54,14 +53,12 @@
"fieldname": "amount",
"label": "Amount",
"fieldtype": "Currency",
"computed": true,
"readOnly": true
},
{
"fieldname": "baseAmount",
"label": "Amount (Company Currency)",
"fieldtype": "Currency",
"computed": true,
"readOnly": true
},
{

View File

@ -55,14 +55,12 @@
"fieldname": "netTotal",
"label": "Net Total",
"fieldtype": "Currency",
"computed": true,
"readOnly": true
},
{
"fieldname": "baseNetTotal",
"label": "Net Total (Company Currency)",
"fieldtype": "Currency",
"computed": true,
"readOnly": true
},
{
@ -76,21 +74,18 @@
"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
},
{

View File

@ -33,7 +33,6 @@
"fieldname": "baseRate",
"label": "Rate (Company Currency)",
"fieldtype": "Currency",
"computed": true,
"readOnly": true
},
{
@ -55,14 +54,12 @@
"fieldname": "amount",
"label": "Amount",
"fieldtype": "Currency",
"computed": true,
"readOnly": true
},
{
"fieldname": "baseAmount",
"label": "Amount (Company Currency)",
"fieldtype": "Currency",
"computed": true,
"readOnly": true
},
{

View File

@ -19,11 +19,20 @@ export function getSchemas(countryCode: string = '-'): Readonly<SchemaMap> {
let schemaMap = Object.assign({}, builtAppSchemas, builtCoreSchemas);
schemaMap = addMetaFields(schemaMap);
schemaMap = removeFields(schemaMap);
schemaMap = setSchemaNameOnFields(schemaMap);
deepFreeze(schemaMap);
return schemaMap;
}
export function setSchemaNameOnFields(schemaMap: SchemaMap): SchemaMap {
for (const schemaName in schemaMap) {
const schema = schemaMap[schemaName]!;
schema.fields = schema.fields.map((f) => ({ ...f, schemaName }));
}
return schemaMap;
}
function removeFields(schemaMap: SchemaMap): SchemaMap {
for (const schemaName in schemaMap) {
const schema = schemaMap[schemaName]!;
@ -96,9 +105,16 @@ export function addMetaFields(schemaMap: SchemaMap): SchemaMap {
}
addNameField(schemaMap);
addTitleField(schemaMap);
return schemaMap;
}
function addTitleField(schemaMap: SchemaMap) {
for (const schemaName in schemaMap) {
schemaMap[schemaName]!.titleField ??= 'name';
}
}
function addNameField(schemaMap: SchemaMap) {
for (const name in schemaMap) {
const schema = schemaMap[name] as Schema;

View File

@ -7,6 +7,7 @@ import {
cleanSchemas,
getAbstractCombinedSchemas,
getRegionalCombinedSchemas,
setSchemaNameOnFields,
} from '../index';
import { metaSchemas } from '../schemas';
import {
@ -134,7 +135,9 @@ describe('Schema Builder', function () {
});
});
const finalSchemas = addMetaFields(cloneDeep(abstractCombined));
let almostFinalSchemas = cloneDeep(abstractCombined);
almostFinalSchemas = addMetaFields(almostFinalSchemas);
const finalSchemas = setSchemaNameOnFields(almostFinalSchemas);
const metaSchemaMap = getMapFromList(metaSchemas, 'name');
const baseFieldNames = metaSchemaMap.base.fields!.map((f) => f.fieldname);
const childFieldNames = metaSchemaMap.child.fields!.map((f) => f.fieldname);
@ -150,6 +153,14 @@ describe('Schema Builder', function () {
];
describe('Final Schemas', function () {
specify('Schema Name Existsance', function () {
for (const schemaName in finalSchemas) {
for (const field of finalSchemas[schemaName]?.fields!) {
assert.strictEqual(field.schemaName, schemaName);
}
}
});
specify('Schema Field Existance', function () {
assert.strictEqual(
everyFieldExists(baseFieldNames, finalSchemas.Customer!),

View File

@ -23,6 +23,7 @@ export interface BaseField {
fieldname: string; // Column name in the db
fieldtype: FieldType; // UI Descriptive field types that map to column types
label: string; // Translateable UI facing name
schemaName?: string; // Convenient access to schemaName incase just the field is passed
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
@ -30,7 +31,6 @@ export interface BaseField {
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
meta?: boolean; // Field is a meta field, i.e. only for the db, not UI
inline?: boolean; // UI Facing config, whether to display doc inline.
}
@ -84,6 +84,7 @@ export interface Schema {
quickEditFields?: string[]; // Used to get fields for the quickEditForm
inlineEditDisplayField?:string;// Display field if inline editable
naming?: Naming; // Used for assigning name, default is 'random' else 'numberSeries' if present
titleField?: string; // Main display field
removeFields?: string[]; // Used by the builder to remove fields.
}

View File

@ -117,7 +117,8 @@ export default {
if (hideGetStarted || onboardingComplete) {
routeTo('/');
} else {
routeTo('/get-started');
// routeTo('/get-started');
routeTo('/settings');
}
},
async changeDbFile() {

View File

@ -83,9 +83,9 @@ export default {
this.isLoading = false;
},
async getSuggestions(keyword = '') {
keyword = keyword.toLowerCase();
const options = getOptionList(this.df, this.doc);
keyword = keyword.toLowerCase();
if (!keyword) {
return options;
}

View File

@ -6,7 +6,7 @@
</div>
<div style="width: 14px; height: 14px; overflow: hidden; cursor: pointer">
<svg
v-if="checked === 1"
v-if="checked"
width="14"
height="14"
viewBox="0 0 14 14"
@ -57,7 +57,7 @@
:class="inputClasses"
:checked="value"
:readonly="isReadOnly"
@change="(e) => triggerChange(+e.target.checked)"
@change="(e) => triggerChange(e.target.checked)"
@focus="(e) => $emit('focus', e)"
/>
</div>

View File

@ -78,10 +78,10 @@ export default {
},
computed: {
colors() {
return this.df.colors;
return this.df.options;
},
selectedColorLabel() {
let color = this.colors.find((c) => this.value === c.value);
const color = this.colors.find((c) => this.value === c.value);
return color ? color.label : this.value;
},
},

View File

@ -29,7 +29,7 @@
<script>
import { fyo } from 'src/initFyo';
import Float from './Float';
import Float from './Float.vue';
export default {
name: 'Currency',

View File

@ -1,11 +1,11 @@
<script>
import Link from './Link';
import Link from './Link.vue';
export default {
name: 'DynamicLink',
props: ['target'],
extends: Link,
created() {
let watchKey = `doc.${this.df.references}`;
const watchKey = `doc.${this.df.references}`;
this.targetWatcher = this.$watch(watchKey, function(newTarget, oldTarget) {
if (oldTarget && newTarget !== oldTarget) {
this.triggerChange('');
@ -20,6 +20,7 @@ export default {
if (!this.doc) {
throw new Error('You must provide `doc` for DynamicLink to work.');
}
return this.doc[this.df.references];
}
}

View File

@ -1,5 +1,5 @@
<script>
import Int from './Int';
import Int from './Int.vue';
export default {
name: 'Float',
@ -11,7 +11,7 @@ export default {
},
methods: {
parse(value) {
let parsedValue = parseFloat(value);
const parsedValue = parseFloat(value);
return isNaN(parsedValue) ? 0 : parsedValue;
},
},

View File

@ -15,27 +15,30 @@ import Select from './Select.vue';
import Table from './Table.vue';
import Text from './Text.vue';
const components = {
AttachImage,
Data,
Check,
Color,
Select,
Link,
Date,
Table,
AutoComplete,
DynamicLink,
Int,
Float,
Currency,
Text,
};
export default {
name: 'FormControl',
render() {
let controls = {
Data,
Select,
Link,
Date,
Table,
AutoComplete,
Check,
AttachImage,
DynamicLink,
Int,
Float,
Currency,
Text,
Color,
};
let { df } = this.$attrs;
return h(controls[df.fieldtype] || Data, {
const fieldtype = this.$attrs.df.fieldtype;
const component = components[fieldtype] ?? Data;
return h(component, {
...this.$attrs,
ref: 'control',
});

View File

@ -1,5 +1,5 @@
<script>
import Data from './Data';
import Data from "./Data.vue";
export default {
name: 'Int',
@ -11,7 +11,7 @@ export default {
},
methods: {
parse(value) {
let parsedValue = parseInt(value, 10);
const parsedValue = parseInt(value, 10);
return isNaN(parsedValue) ? 0 : parsedValue;
},
},

View File

@ -1,10 +1,9 @@
<script>
import { t } from 'fyo';
import Badge from 'src/components/Badge';
import Badge from 'src/components/Badge.vue';
import { fyo } from 'src/initFyo';
import { openQuickEdit } from 'src/utils/ui';
import { markRaw } from 'vue';
import AutoComplete from './AutoComplete';
import AutoComplete from './AutoComplete.vue';
export default {
name: 'Link',
@ -12,54 +11,51 @@ export default {
emits: ['new-doc'],
methods: {
async getSuggestions(keyword = '') {
let doctype = this.getTarget();
let meta;
try {
meta = fyo.getMeta(doctype);
} catch (err) {
if (err.message.includes('not a registered doctype')) {
return [];
}
throw err;
}
let filters = await this.getFilters(keyword);
const schemaName = this.df.target;
const schema = fyo.schemaMap[schemaName];
const filters = await this.getFilters(keyword);
if (keyword && !filters.keywords) {
filters.keywords = ['like', keyword];
}
let results = await fyo.db.getAll({
doctype,
const fields = [
...new Set([
'name',
schema.titleField,
this.df.groupBy,
...schema.keywordFields,
]),
].filter(Boolean);
const results = await fyo.db.getAll(schemaName, {
filters,
fields: [
...new Set([
'name',
meta.titleField,
this.df.groupBy,
...meta.getKeywordFields(),
]),
].filter(Boolean),
fields,
});
let createNewOption = this.getCreateNewOption();
let suggestions = results
const suggestions = results
.map((r) => {
let option = { label: r[meta.titleField], value: r.name };
const option = { label: r[schema.titleField], value: r.name };
if (this.df.groupBy) {
option.group = r[this.df.groupBy];
}
return option;
})
.concat(this.df.disableCreation ? null : createNewOption)
.concat(this.getCreateNewOption())
.filter(Boolean);
if (suggestions.length === 0) {
return [
{
component: {
template: '<span class="text-gray-600">No results found</span>',
},
component: markRaw({
template:
'<span class="text-gray-600">{{ t`No results found` }}</span>',
}),
action: () => {},
},
];
}
return suggestions;
},
getCreateNewOption() {
@ -83,14 +79,35 @@ export default {
}),
};
},
async getFilters(keyword) {
const { getFilters } = this.df;
if (!getFilters) {
async openNewDoc() {
const schemaName = this.df.target;
const doc = await fyo.doc.getNewDoc(schemaName);
const filters = await this.getFilters();
const { openQuickEdit } = await import('src/utils/ui');
openQuickEdit({
schemaName,
name: doc.name,
defaults: Object.assign({}, filters, {
name: this.linkValue,
}),
});
doc.once('afterSync', () => {
this.$emit('new-doc', doc);
this.$router.back();
});
},
async getFilters() {
const { schemaName, fieldname } = this.df;
const getFilters = fyo.models[schemaName].filters[fieldname];
if (getFilters === undefined) {
return {};
}
if (this.doc) {
return (await getFilters(keyword, this.doc)) ?? {};
return (await getFilters(this.doc)) ?? {};
}
try {
@ -99,25 +116,6 @@ export default {
return {};
}
},
getTarget() {
return this.df.target;
},
async openNewDoc() {
let doctype = this.df.target;
let doc = await fyo.doc.getNewDoc(doctype);
let filters = await this.getFilters();
openQuickEdit({
doctype,
name: doc.name,
defaults: Object.assign({}, filters, {
name: this.linkValue,
}),
});
doc.once('afterInsert', () => {
this.$emit('new-doc', doc);
this.$router.back();
});
},
},
};
</script>

View File

@ -61,10 +61,10 @@
</template>
<script>
import Row from 'src/components/Row';
import Row from 'src/components/Row.vue';
import { fyo } from 'src/initFyo';
import Base from './Base';
import TableRow from './TableRow';
import Base from './Base.vue';
import TableRow from './TableRow.vue';
export default {
name: 'Table',
@ -122,8 +122,8 @@ export default {
return [0.3].concat(this.tableFields.map(() => 1));
},
tableFields() {
let meta = fyo.getMeta(this.df.childtype);
return meta.tableFields.map((fieldname) => meta.getField(fieldname));
const fields = fyo.schemaMap[this.df.target].tableFields ?? [];
return fields.map((fieldname) => fyo.getField(this.df.target, fieldname));
},
},
};

View File

@ -32,9 +32,9 @@
</Row>
</template>
<script>
import Row from 'src/components/Row';
import { getErrorMessage } from '../../errorHandling';
import FormControl from './FormControl';
import Row from 'src/components/Row.vue';
import { getErrorMessage } from 'src/utils';
import FormControl from './FormControl.vue';
export default {
name: 'TableRow',
@ -42,6 +42,7 @@ export default {
emits: ['remove'],
components: {
Row,
FormControl,
},
data: () => ({ hovering: false, errors: {} }),
beforeCreate() {

View File

@ -17,7 +17,7 @@
</template>
<script>
import Base from './Base';
import Base from './Base.vue';
export default {
name: 'Text',

View File

@ -74,6 +74,7 @@
<script>
import uniq from 'lodash/uniq';
import { fyo } from 'src/initFyo';
import Popover from './Popover.vue';
export default {
@ -166,13 +167,20 @@ export default {
},
methods: {
getEmptyMessage() {
const { emptyMessage } = this.df ?? {};
if (typeof emptyMessage === 'function') {
return emptyMessage(this.doc);
} else if (emptyMessage) {
return emptyMessage;
if (this.df === null) {
return this.t`Empty`;
}
return this.t`Empty`;
const { schemaName, fieldname } = this.df;
const emptyMessage = fyo.models[schemaName]?.emptyMessages[fieldname]?.(
this.doc
);
if (emptyMessage === undefined || emptyMessage.length === 0) {
return this.t`Empty`;
}
return emptyMessage;
},
selectItem(d) {
if (d.action) {

View File

@ -9,9 +9,6 @@ import { statusColor } from 'src/utils/colors';
export default {
name: 'StatusBadge',
props: ['status'],
components: {
Badge,
},
computed: {
color() {
return statusColor[this.status];

View File

@ -22,23 +22,23 @@
<TwoColumnForm
ref="inlineEditForm"
:doc="inlineEditDoc"
:fields="inlineEditFields"
:fields="getInlineEditFields(df)"
:column-ratio="columnRatio"
:no-border="true"
:focus-first-input="true"
:autosave="false"
@error="(msg) => $emit('error', msg)"
/>
<div class="flex px-4 pb-2">
<Button class="w-1/2 text-gray-900" @click="inlineEditField = null">
<div class="flex px-4 pb-2 gap-2">
<Button class="w-1/2 text-gray-900" @click="stopInlineEditing">
{{ t`Cancel` }}
</Button>
<Button
type="primary"
class="ml-2 w-1/2 text-white"
@click="() => saveInlineEditDoc(df)"
class="w-1/2 text-white"
@click="saveInlineEditDoc"
>
{{ df.inlineSaveText || t`Save` }}
{{ t`Save` }}
</Button>
</div>
</div>
@ -90,8 +90,9 @@
import { Doc } from 'fyo/model/doc';
import Button from 'src/components/Button.vue';
import FormControl from 'src/components/Controls/FormControl.vue';
import { getErrorMessage, handleErrorWithDialog } from 'src/errorHandling';
import { handleErrorWithDialog } from 'src/errorHandling';
import { fyo } from 'src/initFyo';
import { getErrorMessage } from 'src/utils';
import { evaluateHidden, evaluateReadOnly } from 'src/utils/doc';
export default {
@ -114,10 +115,8 @@ export default {
},
data() {
return {
inlineEditField: null,
inlineEditDoc: null,
inlineEditFields: null,
inlineEditDisplayField: null,
inlineEditField: null,
errors: {},
};
},
@ -162,7 +161,7 @@ export default {
);
},
evaluateReadOnly(df) {
if (df.fieldname === 'numberSeries' && !this.doc._notInserted) {
if (df.fieldname === 'numberSeries' && !this.doc.notInserted) {
return true;
}
@ -177,7 +176,7 @@ export default {
return;
}
let oldValue = this.doc.get(df.fieldname);
const oldValue = this.doc.get(df.fieldname);
if (oldValue === value) {
return;
}
@ -188,7 +187,7 @@ export default {
}
// handle rename
if (this.autosave && df.fieldname === 'name' && !this.doc.isNew()) {
if (this.autosave && df.fieldname === 'name' && !this.doc.notInserted) {
return this.doc.rename(value);
}
@ -200,21 +199,19 @@ export default {
this.errors[df.fieldname] = getErrorMessage(e, this.doc);
});
if (this.autosave && this.doc._dirty && !this.doc.isNew()) {
if (this.autosave && this.doc.dirty) {
if (df.fieldtype === 'Table') {
return;
}
this.doc.sync();
}
},
sync() {
return this.doc.sync().catch(this.handleError);
return this.doc.sync().catch((e) => handleErrorWithDialog(e, this.doc));
},
submit() {
return this.doc.submit().catch(this.handleError);
},
handleError(e) {
handleErrorWithDialog(e, this.doc);
return this.doc.submit().catch((e) => handleErrorWithDialog(e, this.doc));
},
async activateInlineEditing(df) {
if (!df.inline) {
@ -224,28 +221,39 @@ export default {
this.inlineEditField = df;
if (!this.doc[df.fieldname]) {
this.inlineEditDoc = await fyo.doc.getNewDoc(df.target);
this.inlineEditDoc.once('afterInsert', () => {
this.inlineEditDoc.once('afterSync', () => {
this.onChangeCommon(df, this.inlineEditDoc.name);
});
} else {
this.inlineEditDoc = this.doc.getLink(df.fieldname);
}
this.inlineEditDisplayField =
this.doc.schema.inlineEditDisplayField ?? 'name';
this.inlineEditFields = fyo.schemaMap[df.target].quickEditFields ?? [];
},
async saveInlineEditDoc(df) {
getInlineEditFields(df) {
const inlineEditFieldNames =
fyo.schemaMap[df.target].quickEditFields ?? [];
return inlineEditFieldNames.map((fieldname) =>
fyo.getField(df.target, fieldname)
);
},
async saveInlineEditDoc() {
if (!this.inlineEditDoc) {
return;
}
await this.$refs.inlineEditForm[0].sync();
await this.doc.loadLinks();
if (this.emitChange && df.inline) {
this.$emit('change', df.inlineEditField);
if (this.emitChange) {
this.$emit('change', this.inlineEditField);
}
await this.stopInlineEditing();
},
async stopInlineEditing() {
if (this.inlineEditDoc?.dirty) {
await this.inlineEditDoc.load()
}
this.inlineEditDoc = null;
this.inlineEditField = null;
},
},

View File

@ -3,15 +3,11 @@ 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,
MandatoryError,
ValidationError,
} from 'fyo/utils/errors';
import { MandatoryError, ValidationError } from 'fyo/utils/errors';
import { ErrorLog } from 'fyo/utils/types';
import { IPC_ACTIONS, IPC_MESSAGES } from 'utils/messages';
import { fyo } from './initFyo';
import { getErrorMessage } from './utils';
import { ToastOptions } from './utils/types';
import { showMessageDialog, showToast } from './utils/ui';
@ -106,22 +102,6 @@ export function handleError(
}
}
export function getErrorMessage(e: Error, doc?: Doc): string {
let errorMessage = e.message || t`An error occurred.`;
const { schemaName, name }: { schemaName?: string; name?: string } =
doc ?? {};
const canElaborate = !!(schemaName && name);
if (e instanceof LinkValidationError && canElaborate) {
errorMessage = t`${schemaName} ${name} is linked with existing records.`;
} else if (e instanceof DuplicateEntryError && canElaborate) {
errorMessage = t`${schemaName} ${name} already exists.`;
}
return errorMessage;
}
export function handleErrorWithDialog(error: Error, doc?: Doc) {
const errorMessage = getErrorMessage(error, doc);
handleError(false, error, { errorMessage, doc });

View File

@ -72,7 +72,7 @@ export default {
}
let path = this.getFormPath(doc.name);
routeTo(path);
doc.on('afterInsert', () => {
doc.on('afterSync', () => {
let path = this.getFormPath(doc.name);
this.$router.replace(path);
});

View File

@ -166,7 +166,7 @@ export default {
await this.fetchDoc();
// setup the title field
if (this.doc && this.doc.isNew() && this.doc[this.titleField.fieldname]) {
if (this.doc && this.doc.notInserted && this.doc[this.titleField.fieldname]) {
if (!this.titleField.readOnly) {
this.doc.set(this.titleField.fieldname, '');
setTimeout(() => {
@ -195,10 +195,10 @@ export default {
name: this.doc.name,
});
});
this.doc.on('beforeUpdate', () => {
this.doc.on('beforeSync', () => {
this.statusText = t`Saving...`;
});
this.doc.on('afterUpdate', () => {
this.doc.on('afterSync', () => {
setTimeout(() => {
this.statusText = null;
}, 500);

View File

@ -51,16 +51,15 @@
<script>
import { ipcRenderer } from 'electron';
import { 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 Button from 'src/components/Button.vue';
import Icon from 'src/components/Icon.vue';
import PageHeader from 'src/components/PageHeader.vue';
import Row from 'src/components/Row.vue';
import StatusBadge from 'src/components/StatusBadge.vue';
import WindowControls from 'src/components/WindowControls.vue';
import { showToast } from 'src/utils/ui';
import { IPC_MESSAGES } from 'utils/messages';
import { h, markRaw } from 'vue';
import TabGeneral from './TabGeneral.vue';
import TabInvoice from './TabInvoice.vue';
import TabSystem from './TabSystem.vue';
@ -89,13 +88,15 @@ export default {
key: 'General',
label: t`General`,
icon: 'general',
component: markRaw(TabGeneral),
// component: markRaw(TabGeneral),
component: {template: `<h1>General</h1>`}
},
{
key: 'System',
label: t`System`,
icon: 'system',
component: markRaw(TabSystem),
component: {template: `<h1>System</h1>`}
},
],
};

View File

@ -6,8 +6,7 @@
:value="doc.logo"
@change="
(value) => {
doc.set('logo', value);
doc.sync();
doc.setAndSync('logo', value);
forwardChangeEvent(getField('logo'));
}
"
@ -26,8 +25,7 @@
:show-label="true"
@change="
(value) => {
doc.set('displayLogo', value);
doc.sync();
doc.setAndSync('displayLogo', value);
forwardChangeEvent(getField('displayLogo'));
}
"
@ -47,10 +45,10 @@
</template>
<script>
import { ipcRenderer } from 'electron';
import FormControl from 'src/components/Controls/FormControl.vue';
import TwoColumnForm from 'src/components/TwoColumnForm';
import TwoColumnForm from 'src/components/TwoColumnForm.vue';
import { fyo } from 'src/initFyo';
import { IPC_ACTIONS } from 'utils/messages';
import FormControl from '../../components/Controls/FormControl.vue';
export default {
name: 'TabInvoice',

View File

@ -95,8 +95,8 @@ import { ipcRenderer } from 'electron';
import FormControl from 'src/components/Controls/FormControl.vue';
import LanguageSelector from 'src/components/Controls/LanguageSelector.vue';
import TwoColumnForm from 'src/components/TwoColumnForm';
import { getErrorMessage } from 'src/errorHandling';
import { fyo } from 'src/initFyo';
import { getErrorMessage } from 'src/utils';
import { getSetupWizardDoc } from 'src/utils/misc';
import { showMessageDialog } from 'src/utils/ui';
import { IPC_MESSAGES } from 'utils/messages';

View File

@ -9,7 +9,7 @@ import GetStarted from 'src/pages/GetStarted.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 Settings from 'src/pages/Settings/Settings.vue';
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router';
import { fyo } from './initFyo';
@ -108,13 +108,13 @@ const routes: RouteRecordRaw[] = [
name: 'Data Import',
component: DataImport,
},
*/
{
path: '/settings',
name: 'Settings',
component: Settings,
props: true,
},
*/
];
const router = createRouter({ routes, history: createWebHistory() });

View File

@ -43,7 +43,8 @@ export function getOptionList(field: Field, doc: Doc): SelectOption[] {
}
function _getOptionList(field: Field, doc: Doc) {
if ((field as OptionField).options) {
const options = (field as OptionField).options;
if (options && options.length > 0) {
return (field as OptionField).options;
}

View File

@ -1,8 +1,10 @@
/**
* General purpose utils used by the frontend.
*/
import { t } from 'fyo';
import { Doc } from 'fyo/model/doc';
import { isPesa } from 'fyo/utils';
import { DuplicateEntryError, LinkValidationError } from 'fyo/utils/errors';
import Money from 'pesa/dist/types/src/money';
export function stringifyCircular(
@ -76,3 +78,19 @@ export function convertPesaValuesToFloat(obj: Record<string, unknown>) {
obj[key] = (value as Money).float;
});
}
export function getErrorMessage(e: Error, doc?: Doc): string {
let errorMessage = e.message || t`An error occurred.`;
const { schemaName, name }: { schemaName?: string; name?: string } =
doc ?? {};
const canElaborate = !!(schemaName && name);
if (e instanceof LinkValidationError && canElaborate) {
errorMessage = t`${schemaName} ${name} is linked with existing records.`;
} else if (e instanceof DuplicateEntryError && canElaborate) {
errorMessage = t`${schemaName} ${name} already exists.`;
}
return errorMessage;
}

View File

@ -28,8 +28,6 @@ export async function openQuickEdit({
showFields,
defaults = {},
}: QuickEditOptions) {
const router = (await import('src/router')).default;
const currentRoute = router.currentRoute.value;
const query = currentRoute.query;
let method: 'push' | 'replace' = 'push';
@ -287,7 +285,10 @@ function getDeleteAction(doc: Doc): Action {
template: '<span class="text-red-700">{{ t`Delete` }}</span>',
},
condition: (doc: Doc) =>
!doc.isNew && !doc.submitted && !doc.schema.isSingle && !doc.cancelled,
!doc.notInserted &&
!doc.submitted &&
!doc.schema.isSingle &&
!doc.cancelled,
action: () =>
deleteDocWithPrompt(doc).then((res) => {
if (res) {

View File

@ -23,7 +23,7 @@
"fixtures/*": ["fixtures/*"],
"reports/*": ["reports/*"],
"models/*": ["models/*"],
"utils/*": ["utils/*"],
"utils/*": ["utils/*"]
},
"lib": ["esnext", "dom", "dom.iterable", "scripthost"]
},
@ -46,7 +46,7 @@
"regional/**/*.ts",
"reports/**/*.ts",
"utils/**/*.ts",
"tests/**/*.ts",
"tests/**/*.ts"
],
"exclude": ["node_modules"]
}