2
0
mirror of https://github.com/frappe/books.git synced 2024-11-10 07:40:55 +00:00

Merge pull request #555 from 18alantom/common-form-view

feat: Common form view
This commit is contained in:
Alan 2023-02-20 22:47:30 -08:00 committed by GitHub
commit 14f832dabf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 939 additions and 965 deletions

View File

@ -14,9 +14,11 @@ export class DocHandler {
singles: SinglesMap = {};
docs: Observable<DocMap | undefined> = new Observable();
observer: Observable<never> = new Observable();
#temporaryNameCounters: Record<string, number>;
constructor(fyo: Fyo) {
this.fyo = fyo;
this.#temporaryNameCounters = {};
}
init() {
@ -97,7 +99,7 @@ export class DocHandler {
}
const doc = new Model!(schema, data, this.fyo, isRawValueMap);
doc.name ??= getRandomString();
doc.name ??= this.getTemporaryName(schema);
if (cacheDoc) {
this.#addToCache(doc);
}
@ -105,6 +107,22 @@ export class DocHandler {
return doc;
}
getTemporaryName(schema: Schema): string {
if (schema.naming === 'random') {
return getRandomString();
}
this.#temporaryNameCounters[schema.name] ??= 1;
const idx = this.#temporaryNameCounters[schema.name];
this.#temporaryNameCounters[schema.name] = idx + 1;
return this.fyo.t`New ${schema.label ?? schema.name} ${String(idx).padStart(
2,
'0'
)}`;
}
/**
* Cache operations
*/

View File

@ -454,7 +454,7 @@ export class Doc extends Observable<DocValue | Doc[]> {
convertToDocValue: boolean = false
): Doc {
if (!this.name && this.schema.naming !== 'manual') {
this.name = getRandomString();
this.name = this.fyo.doc.getTemporaryName(this.schema);
}
docValueMap.name ??= getRandomString();
@ -868,9 +868,9 @@ export class Doc extends Observable<DocValue | Doc[]> {
}
async _insert() {
await setName(this, this.fyo);
this._setBaseMetaValues();
await this._preSync();
await setName(this, this.fyo);
const validDict = this.getValidDict(false, true);
let data: DocValueMap;

View File

@ -111,6 +111,7 @@ export class Item extends Doc {
static getListViewSettings(): ListViewSettings {
return {
formRoute: ({ name }) => `/edit/Item/${name}`,
columns: ['name', 'unit', 'tax', 'rate'],
};
}

View File

@ -4,9 +4,9 @@ import {
Action,
DefaultMap,
FiltersMap,
HiddenMap,
ListViewSettings,
} from 'fyo/model/types';
import { DateTime } from 'luxon';
import {
getDocStatus,
getLedgerLinkAction,
@ -39,6 +39,17 @@ export class JournalEntry extends Transactional {
return posting;
}
hidden: HiddenMap = {
referenceNumber: () =>
!(this.referenceNumber || !(this.isSubmitted || this.isCancelled)),
referenceDate: () =>
!(this.referenceDate || !(this.isSubmitted || this.isCancelled)),
userRemark: () =>
!(this.userRemark || !(this.isSubmitted || this.isCancelled)),
attachment: () =>
!(this.attachment || !(this.isSubmitted || this.isCancelled)),
};
static defaults: DefaultMap = {
numberSeries: (doc) => getNumberSeries(doc.schemaName, doc.fyo),
date: () => new Date(),

View File

@ -617,6 +617,7 @@ export class Payment extends Transactional {
static getListViewSettings(fyo: Fyo): ListViewSettings {
return {
formRoute: ({ name }) => `/edit/Payment/${name}`,
columns: ['name', getDocStatusListColumn(), 'party', 'date', 'amount'],
};
}

View File

@ -75,6 +75,7 @@ export class StockMovement extends Transfer {
static getListViewSettings(fyo: Fyo): ListViewSettings {
return {
formRoute: ({ name }) => `/edit/StockMovement/${name}`,
columns: [
'name',
getDocStatusListColumn(),

View File

@ -1,7 +1,13 @@
import { Fyo, t } from 'fyo';
import { Attachment } from 'fyo/core/types';
import { Doc } from 'fyo/model/doc';
import { Action, DefaultMap, FiltersMap, FormulaMap } from 'fyo/model/types';
import {
Action,
DefaultMap,
FiltersMap,
FormulaMap,
HiddenMap,
} from 'fyo/model/types';
import { ValidationError } from 'fyo/utils/errors';
import { Defaults } from 'models/baseModels/Defaults/Defaults';
import { Invoice } from 'models/baseModels/Invoice/Invoice';
@ -33,6 +39,14 @@ export abstract class StockTransfer extends Transfer {
},
};
hidden: HiddenMap = {
backReference: () =>
!(this.backReference || !(this.isSubmitted || this.isCancelled)),
terms: () => !(this.terms || !(this.isSubmitted || this.isCancelled)),
attachment: () =>
!(this.attachment || !(this.isSubmitted || this.isCancelled)),
};
static defaults: DefaultMap = {
numberSeries: (doc) => getNumberSeries(doc.schemaName, doc.fyo),
terms: (doc) => {

View File

@ -4,50 +4,20 @@
"isSingle": false,
"naming": "manual",
"fields": [
{
"fieldname": "image",
"label": "Image",
"section": "Default",
"fieldtype": "AttachImage"
},
{
"fieldname": "name",
"label": "Item Name",
"fieldtype": "Data",
"placeholder": "Item Name",
"section": "Default",
"required": true
},
{
"fieldname": "image",
"label": "Image",
"fieldtype": "AttachImage"
},
{
"fieldname": "description",
"label": "Description",
"placeholder": "Item Description",
"fieldtype": "Text"
},
{
"fieldname": "unit",
"label": "Unit Type",
"fieldtype": "Link",
"target": "UOM",
"create": true,
"default": "Unit",
"placeholder": "Unit Type"
},
{
"fieldname": "itemType",
"label": "Type",
"placeholder": "Type",
"fieldtype": "Select",
"default": "Product",
"options": [
{
"value": "Product",
"label": "Product"
},
{
"value": "Service",
"label": "Service"
}
]
},
{
"fieldname": "for",
"label": "For",
@ -67,14 +37,57 @@
}
],
"required": true,
"section": "Default",
"default": "Both"
},
{
"fieldname": "itemType",
"label": "Type",
"placeholder": "Type",
"fieldtype": "Select",
"default": "Product",
"section": "Default",
"options": [
{
"value": "Product",
"label": "Product"
},
{
"value": "Service",
"label": "Service"
}
]
},
{
"fieldname": "unit",
"label": "Unit Type",
"fieldtype": "Link",
"target": "UOM",
"create": true,
"default": "Unit",
"section": "Details",
"placeholder": "Unit Type"
},
{
"fieldname": "rate",
"label": "Rate",
"section": "Details",
"fieldtype": "Currency"
},
{
"fieldname": "description",
"label": "Description",
"placeholder": "Item Description",
"section": "Details",
"fieldtype": "Text"
},
{
"fieldname": "incomeAccount",
"label": "Sales Acc.",
"fieldtype": "Link",
"target": "Account",
"placeholder": "Income",
"section": "Accounts",
"create": true,
"required": true
},
@ -84,6 +97,7 @@
"fieldtype": "Link",
"target": "Account",
"placeholder": "Expense",
"section": "Accounts",
"create": true,
"required": true
},
@ -92,31 +106,30 @@
"label": "Tax",
"fieldtype": "Link",
"target": "Tax",
"section": "Accounts",
"create": true,
"placeholder": "Tax"
},
{
"fieldname": "rate",
"label": "Rate",
"fieldtype": "Currency"
},
{
"fieldname": "hsnCode",
"label": "HSN/SAC",
"fieldtype": "Int",
"placeholder": "HSN/SAC Code"
"placeholder": "HSN/SAC Code",
"section": "Inventory"
},
{
"fieldname": "barcode",
"label": "Barcode",
"fieldtype": "Data",
"placeholder": "Barcode"
"placeholder": "Barcode",
"section": "Inventory"
},
{
"fieldname": "trackItem",
"label": "Track Item",
"fieldtype": "Check",
"default": false
"default": false,
"section": "Inventory"
}
],
"quickEditFields": [

View File

@ -4,6 +4,24 @@
"naming": "numberSeries",
"isSubmittable": true,
"fields": [
{
"label": "Entry No",
"fieldname": "name",
"fieldtype": "Data",
"required": true,
"readOnly": true,
"section": "Default"
},
{
"fieldname": "numberSeries",
"label": "Number Series",
"fieldtype": "Link",
"target": "NumberSeries",
"create": true,
"required": true,
"default": "JV-",
"section": "Default"
},
{
"fieldname": "entryType",
"label": "Entry Type",
@ -55,58 +73,49 @@
"label": "Depreciation Entry"
}
],
"required": true
},
{
"label": "Entry No",
"fieldname": "name",
"fieldtype": "Data",
"required": true,
"readOnly": true
"section": "Default"
},
{
"fieldname": "date",
"label": "Date",
"fieldtype": "Date",
"required": true
"required": true,
"section": "Default"
},
{
"fieldname": "accounts",
"label": "Account Entries",
"fieldtype": "Table",
"target": "JournalEntryAccount",
"required": true
"required": true,
"section": "Accounts"
},
{
"fieldname": "referenceNumber",
"label": "Reference Number",
"fieldtype": "Data"
"fieldtype": "Data",
"section": "References"
},
{
"fieldname": "referenceDate",
"label": "Reference Date",
"fieldtype": "Date"
"fieldtype": "Date",
"section": "References"
},
{
"fieldname": "userRemark",
"label": "User Remark",
"fieldtype": "Text",
"placeholder": "Add a remark"
},
{
"fieldname": "numberSeries",
"label": "Number Series",
"fieldtype": "Link",
"target": "NumberSeries",
"create": true,
"required": true,
"default": "JV-"
"placeholder": "Add a remark",
"section": "References"
},
{
"fieldname": "attachment",
"placeholder": "Add attachment",
"label": "Attachment",
"fieldtype": "Attachment"
"fieldtype": "Attachment",
"section": "References"
}
],
"keywordFields": ["name", "entryType"]

View File

@ -46,7 +46,8 @@
{
"fieldname": "outstandingAmount",
"label": "Outstanding Amount",
"fieldtype": "Currency"
"fieldtype": "Currency",
"hidden": true
},
{
"fieldname": "currency",

View File

@ -11,7 +11,18 @@
"fieldname": "name",
"fieldtype": "Data",
"required": true,
"readOnly": true
"readOnly": true,
"section": "Default"
},
{
"fieldname": "numberSeries",
"label": "Number Series",
"fieldtype": "Link",
"target": "NumberSeries",
"create": true,
"required": true,
"default": "PAY-",
"section": "Default"
},
{
"fieldname": "party",
@ -19,13 +30,15 @@
"fieldtype": "Link",
"target": "Party",
"create": true,
"required": true
"required": true,
"section": "Default"
},
{
"fieldname": "date",
"label": "Posting Date",
"fieldtype": "Date",
"required": true
"required": true,
"section": "Default"
},
{
"fieldname": "paymentType",
@ -42,16 +55,27 @@
"label": "Pay"
}
],
"required": true
"required": true,
"section": "Default"
},
{
"fieldname": "numberSeries",
"label": "Number Series",
"fieldname": "account",
"label": "From Account",
"fieldtype": "Link",
"target": "NumberSeries",
"target": "Account",
"create": true,
"required": true,
"default": "PAY-"
"section": "Details"
},
{
"fieldname": "paymentAccount",
"label": "To Account",
"placeholder": "To Account",
"fieldtype": "Link",
"target": "Account",
"create": true,
"required": true,
"section": "Details"
},
{
"fieldname": "paymentMethod",
@ -73,72 +97,64 @@
}
],
"default": "Cash",
"required": true
},
{
"fieldname": "account",
"label": "From Account",
"fieldtype": "Link",
"target": "Account",
"create": true,
"required": true
},
{
"fieldname": "paymentAccount",
"label": "To Account",
"placeholder": "To Account",
"fieldtype": "Link",
"target": "Account",
"create": true,
"required": true
},
{
"fieldname": "referenceId",
"label": "Ref. / Cheque No.",
"placeholder": "Ref. / Cheque No.",
"fieldtype": "Data"
},
{
"fieldname": "referenceDate",
"label": "Ref. Date",
"placeholder": "Ref. Date",
"fieldtype": "Date"
"required": true,
"section": "Details"
},
{
"fieldname": "clearanceDate",
"label": "Clearance Date",
"placeholder": "Clearance Date",
"fieldtype": "Date"
"fieldtype": "Date",
"section": "Details"
},
{
"fieldname": "referenceId",
"label": "Ref. / Cheque No.",
"placeholder": "Ref. / Cheque No.",
"fieldtype": "Data",
"section": "Details"
},
{
"fieldname": "referenceDate",
"label": "Reference Date",
"placeholder": "Ref. Date",
"fieldtype": "Date",
"section": "Details"
},
{
"fieldname": "amount",
"label": "Amount",
"fieldtype": "Currency",
"required": true
"required": true,
"section": "Amounts"
},
{
"fieldname": "writeoff",
"label": "Write Off",
"fieldtype": "Currency"
"fieldtype": "Currency",
"section": "Amounts"
},
{
"fieldname": "amountPaid",
"label": "Amount Paid",
"fieldtype": "Currency",
"computed": true
"computed": true,
"section": "Amounts"
},
{
"fieldname": "for",
"label": "Payment Reference",
"fieldtype": "Table",
"target": "PaymentFor",
"required": false
"required": false,
"section": "References"
},
{
"fieldname": "attachment",
"placeholder": "Add attachment",
"label": "Attachment",
"fieldtype": "Attachment"
"fieldtype": "Attachment",
"section": "References"
}
],
"quickEditFields": [

View File

@ -5,14 +5,6 @@
"naming": "numberSeries",
"showTitle": true,
"fields": [
{
"fieldname": "items",
"label": "Items",
"fieldtype": "Table",
"target": "PurchaseReceiptItem",
"required": true,
"edit": true
},
{
"fieldname": "numberSeries",
"label": "Number Series",
@ -20,14 +12,24 @@
"target": "NumberSeries",
"create": true,
"required": true,
"default": "PREC-"
"default": "PREC-",
"section": "Default"
},
{
"fieldname": "backReference",
"label": "Back Reference",
"fieldtype": "Link",
"target": "PurchaseInvoice",
"readOnly": true
"readOnly": true,
"section": "References"
},
{
"fieldname": "items",
"label": "Items",
"fieldtype": "Table",
"target": "PurchaseReceiptItem",
"required": true,
"edit": true
}
],
"keywordFields": ["name", "party"]

View File

@ -5,14 +5,6 @@
"naming": "numberSeries",
"showTitle": true,
"fields": [
{
"fieldname": "items",
"label": "Items",
"fieldtype": "Table",
"target": "ShipmentItem",
"required": true,
"edit": true
},
{
"fieldname": "numberSeries",
"label": "Number Series",
@ -20,14 +12,25 @@
"target": "NumberSeries",
"create": true,
"required": true,
"default": "SHPM-"
"default": "SHPM-",
"section": "Default"
},
{
"fieldname": "backReference",
"label": "Back Reference",
"fieldtype": "Link",
"target": "SalesInvoice",
"readOnly": true
"readOnly": true,
"section": "References"
},
{
"fieldname": "items",
"label": "Items",
"fieldtype": "Table",
"target": "ShipmentItem",
"required": true,
"edit": true,
"section": "Items"
}
],
"keywordFields": ["name", "party"]

View File

@ -11,13 +11,18 @@
"fieldname": "name",
"fieldtype": "Data",
"required": true,
"readOnly": true
"readOnly": true,
"section": "Default"
},
{
"fieldname": "date",
"label": "Date",
"fieldtype": "Datetime",
"required": true
"fieldname": "numberSeries",
"label": "Number Series",
"fieldtype": "Link",
"target": "NumberSeries",
"create": true,
"required": true,
"default": "SMOV-",
"section": "Default"
},
{
"fieldname": "movementType",
@ -41,29 +46,30 @@
"label": "Manufacture"
}
],
"required": true
},
{
"fieldname": "numberSeries",
"label": "Number Series",
"fieldtype": "Link",
"target": "NumberSeries",
"create": true,
"required": true,
"default": "SMOV-"
"section": "Default"
},
{
"fieldname": "amount",
"label": "Total Amount",
"fieldtype": "Currency",
"readOnly": true
"fieldname": "date",
"label": "Date",
"fieldtype": "Datetime",
"required": true,
"section": "Default"
},
{
"fieldname": "items",
"label": "Items",
"fieldtype": "Table",
"target": "StockMovementItem",
"required": true
"required": true,
"section": "Items"
},
{
"fieldname": "amount",
"label": "Total Amount",
"fieldtype": "Currency",
"readOnly": true,
"section": "Items"
}
],
"quickEditFields": [

View File

@ -11,13 +11,13 @@
"fieldname": "name",
"fieldtype": "Data",
"required": true,
"readOnly": true
"readOnly": true,
"section": "Default"
},
{
"fieldname": "date",
"label": "Date",
"fieldtype": "Datetime",
"required": true
"abstract": true,
"fieldname": "numberSeries",
"section": "Default"
},
{
"fieldname": "party",
@ -25,25 +25,46 @@
"fieldtype": "Link",
"target": "Party",
"create": true,
"required": true
"required": true,
"section": "Default"
},
{
"fieldname": "terms",
"label": "Notes",
"placeholder": "Add transfer terms",
"fieldtype": "Text"
"fieldname": "date",
"label": "Date",
"fieldtype": "Datetime",
"required": true,
"section": "Default"
},
{
"fieldname": "attachment",
"placeholder": "Add attachment",
"label": "Attachment",
"fieldtype": "Attachment"
"abstract": true,
"fieldname": "items",
"section": "Items"
},
{
"fieldname": "grandTotal",
"label": "Grand Total",
"fieldtype": "Currency",
"readOnly": true
"readOnly": true,
"section": "Items"
},
{
"fieldname": "terms",
"label": "Notes",
"placeholder": "Add transfer terms",
"fieldtype": "Text",
"section": "References"
},
{
"fieldname": "attachment",
"placeholder": "Add attachment",
"label": "Attachment",
"fieldtype": "Attachment",
"section": "References"
},
{
"abstract": true,
"fieldname": "backReference",
"section": "References"
}
],
"keywordFields": ["name", "party"]

View File

@ -6,28 +6,32 @@
"label": "Created By",
"fieldtype": "Data",
"required": true,
"meta": true
"meta": true,
"section": "System"
},
{
"fieldname": "modifiedBy",
"label": "Modified By",
"fieldtype": "Data",
"required": true,
"meta": true
"meta": true,
"section": "System"
},
{
"fieldname": "created",
"label": "Created",
"fieldtype": "Datetime",
"required": true,
"meta": true
"meta": true,
"section": "System"
},
{
"fieldname": "modified",
"label": "Modified",
"fieldtype": "Datetime",
"required": true,
"meta": true
"meta": true,
"section": "System"
}
]
}

View File

@ -6,14 +6,16 @@
"label": "Submitted",
"fieldtype": "Check",
"required": true,
"meta": true
"meta": true,
"section": "System"
},
{
"fieldname": "cancelled",
"label": "Cancelled",
"fieldtype": "Check",
"required": true,
"meta": true
"meta": true,
"section": "System"
}
]
}

View File

@ -64,6 +64,9 @@ export interface BaseField {
inline?: boolean; // UI Facing config, whether to display doc inline.
filter?: boolean; // UI Facing config, whether to be used to filter the List.
computed?: boolean; // Computed values are not stored in the database.
section?: string; // UI Facing config, for grouping by sections
tab?: string; // UI Facing config, for grouping by tabs
abstract?: string; // Uused to mark the location of a field in an Abstract schema
}
export type SelectOption = { value: string; label: string };

View File

@ -1,41 +1,50 @@
<template>
<div :class="containerClasses" class="flex gap-2 items-center">
<label
for="attachment"
class="block whitespace-nowrap overflow-auto no-scrollbar"
:class="[inputClasses, !value ? 'text-gray-600' : 'cursor-default']"
>{{ label }}</label
>
<input
ref="fileInput"
id="attachment"
type="file"
accept="image/*,.pdf"
class="hidden"
:disabled="!!value"
@input="selectFile"
/>
<!-- Buttons -->
<div class="me-2 flex gap-2">
<!-- Upload Button -->
<button v-if="!value" class="bg-gray-300 p-0.5 rounded" @click="upload">
<FeatherIcon name="upload" class="h-4 w-4 text-gray-600" />
</button>
<!-- Download Button -->
<button v-if="value" class="bg-gray-300 p-0.5 rounded" @click="download">
<FeatherIcon name="download" class="h-4 w-4 text-gray-600" />
</button>
<!-- Clear Button -->
<button
v-if="value && !isReadOnly"
class="bg-gray-300 p-0.5 rounded"
@click="clear"
<div>
<div :class="labelClasses" v-if="showLabel && df">
{{ df.label }}
</div>
<div :class="containerClasses" class="flex gap-2 items-center">
<label
for="attachment"
class="block whitespace-nowrap overflow-auto no-scrollbar"
:class="[inputClasses, !value ? 'text-gray-600' : 'cursor-default']"
>{{ label }}</label
>
<FeatherIcon name="x" class="h-4 w-4 text-gray-600" />
</button>
<input
ref="fileInput"
id="attachment"
type="file"
accept="image/*,.pdf"
class="hidden"
:disabled="!!value"
@input="selectFile"
/>
<!-- Buttons -->
<div class="me-2 flex gap-2">
<!-- Upload Button -->
<button v-if="!value" class="bg-gray-300 p-0.5 rounded" @click="upload">
<FeatherIcon name="upload" class="h-4 w-4 text-gray-600" />
</button>
<!-- Download Button -->
<button
v-if="value"
class="bg-gray-300 p-0.5 rounded"
@click="download"
>
<FeatherIcon name="download" class="h-4 w-4 text-gray-600" />
</button>
<!-- Clear Button -->
<button
v-if="value && !isReadOnly"
class="bg-gray-300 p-0.5 rounded"
@click="clear"
>
<FeatherIcon name="x" class="h-4 w-4 text-gray-600" />
</button>
</div>
</div>
</div>
</template>

View File

@ -28,7 +28,7 @@
<template #content>
<div class="text-sm p-2 text-center">
<div>
<Row class="border-none" :column-count="5" gap="0.5rem">
<Row :column-count="5" gap="0.5rem">
<div
v-for="color in colors"
:key="color.value"

View File

@ -116,7 +116,7 @@ export default {
},
async openNewDoc() {
const schemaName = this.df.target;
const doc = await fyo.doc.getNewDoc(schemaName);
const doc = fyo.doc.getNewDoc(schemaName);
const filters = await this.getCreateFilters();

View File

@ -4,79 +4,85 @@
{{ df.label }}
</div>
<!-- Title Row -->
<Row
:ratio="ratio"
class="border-b px-2 text-gray-600 w-full flex items-center"
>
<div class="flex items-center ps-2">#</div>
<div
class="items-center flex px-2 h-row-mid"
:class="{
'ms-auto': isNumeric(df),
}"
v-for="df in tableFields"
:key="df.fieldname"
:style="{
height: ``,
}"
<div :class="border ? 'border rounded-md' : ''">
<!-- Title Row -->
<Row
:ratio="ratio"
class="border-b px-2 text-gray-600 w-full flex items-center"
>
{{ df.label }}
</div>
</Row>
<!-- Data Rows -->
<div
class="overflow-auto"
:style="{ 'max-height': maxHeight }"
v-if="value"
>
<TableRow
v-for="row in value"
ref="table-row"
:key="row.name"
v-bind="{ row, tableFields, size, ratio, isNumeric }"
:read-only="isReadOnly"
@remove="removeRow(row)"
:can-edit-row="canEditRow"
/>
</div>
<!-- Add Row and Row Count -->
<Row
:ratio="ratio"
class="
text-gray-500
cursor-pointer
border-transparent
px-2
w-full
h-row-mid
flex
items-center
"
v-if="!isReadOnly"
@click="addRow"
>
<div class="flex items-center ps-1">
<feather-icon name="plus" class="w-4 h-4 text-gray-500" />
</div>
<div class="flex justify-between px-2" :style="`grid-column: 2 / ${ratio.length + 1}`">
<p>
{{ t`Add Row` }}
</p>
<p
class="text-end px-2"
v-if="
value &&
maxRowsBeforeOverflow &&
value.length > maxRowsBeforeOverflow
"
<div class="flex items-center ps-2">#</div>
<div
class="items-center flex px-2 h-row-mid"
:class="{
'ms-auto': isNumeric(df),
}"
v-for="df in tableFields"
:key="df.fieldname"
:style="{
height: ``,
}"
>
{{ t`${value.length} rows` }}
</p>
{{ df.label }}
</div>
</Row>
<!-- Data Rows -->
<div
class="overflow-auto"
:style="{ 'max-height': maxHeight }"
v-if="value"
>
<TableRow
v-for="(row, idx) of value"
:class="idx < value.length - 1 ? 'border-b' : ''"
ref="table-row"
:key="row.name"
v-bind="{ row, tableFields, size, ratio, isNumeric }"
:read-only="isReadOnly"
@remove="removeRow(row)"
:can-edit-row="canEditRow"
/>
</div>
</Row>
<!-- Add Row and Row Count -->
<Row
:ratio="ratio"
class="
text-gray-500
cursor-pointer
px-2
w-full
h-row-mid
flex
items-center
"
:class="value.length > 0 ? 'border-t' : ''"
v-if="!isReadOnly"
@click="addRow"
>
<div class="flex items-center ps-1">
<feather-icon name="plus" class="w-4 h-4 text-gray-500" />
</div>
<div
class="flex justify-between px-2"
:style="`grid-column: 2 / ${ratio.length + 1}`"
>
<p>
{{ t`Add Row` }}
</p>
<p
class="text-end px-2"
v-if="
value &&
maxRowsBeforeOverflow &&
value.length > maxRowsBeforeOverflow
"
>
{{ t`${value.length} rows` }}
</p>
</div>
</Row>
</div>
</div>
</template>
@ -101,6 +107,10 @@ export default {
type: Number,
default: 3,
},
border: {
type: Boolean,
default: false,
},
},
components: {
Row,

View File

@ -4,7 +4,6 @@
class="
w-full
px-2
border-b
hover:bg-gray-25
group
flex

View File

@ -8,6 +8,7 @@
justify-between
h-row-large
items-center
flex-shrink-0
"
>
<h1>{{ formTitle }}</h1>

View File

@ -1,5 +1,5 @@
<template>
<div class="inline-grid border-b" :style="style" v-bind="$attrs">
<div class="inline-grid" :style="style" v-bind="$attrs">
<slot></slot>
</div>
</template>

View File

@ -56,7 +56,7 @@
<script lang="ts">
import { Money } from 'pesa';
import { getCreateRoute } from 'src/router';
import { getStatus, routeTo } from 'src/utils/ui';
import { routeTo } from 'src/utils/ui';
import { defineComponent, PropType } from 'vue';
import Button from '../Button.vue';
import StatusBadge from '../StatusBadge.vue';
@ -78,7 +78,17 @@ export default defineComponent({
linked: { type: Object as PropType<Linked>, required: true },
},
methods: {
getStatus,
getStatus(entry: { cancelled?: boolean; submitted?: boolean }) {
if (entry.cancelled) {
return 'Cancelled';
}
if (entry.submitted) {
return 'Submitted';
}
return 'Saved';
},
async openEntry(name: string) {
const route = getCreateRoute(this.linked.schemaName, name);
await routeTo(route);

View File

@ -0,0 +1,317 @@
<template>
<FormContainer>
<template #header v-if="hasDoc">
<StatusBadge :status="status" />
<DropdownWithActions
v-for="group of groupedActions"
:key="group.label"
:type="group.type"
:actions="group.actions"
>
<p v-if="group.group">
{{ group.group }}
</p>
<feather-icon v-else name="more-horizontal" class="w-4 h-4" />
</DropdownWithActions>
<Button v-if="doc?.canSave" type="primary" @click="sync">
{{ t`Save` }}
</Button>
<Button v-else-if="doc?.canSubmit" type="primary" @click="submit">{{
t`Submit`
}}</Button>
</template>
<template #body>
<FormHeader
:form-title="title"
:form-sub-title="schema.label"
class="sticky top-0 bg-white border-b"
>
</FormHeader>
<!-- Section Container -->
<div v-if="hasDoc" class="overflow-auto custom-scroll">
<CommonFormSection
v-for="([name, fields], idx) in activeGroup.entries()"
@editrow="(doc: Doc) => toggleQuickEditDoc(doc)"
:key="name + idx"
ref="section"
class="p-4"
:class="idx !== 0 && activeGroup.size > 1 ? 'border-t' : ''"
:show-title="activeGroup.size > 1 && name !== t`Default`"
:title="name"
:fields="fields"
:doc="doc"
:errors="errors"
@value-change="onValueChange"
/>
</div>
<!-- Tab Bar -->
<div
class="
mt-auto
px-4
pb-4
flex
gap-8
border-t
flex-shrink-0
sticky
bottom-0
bg-white
"
v-if="groupedFields && groupedFields.size > 1"
>
<div
v-for="key of groupedFields.keys()"
:key="key"
@click="activeTab = key"
class="text-sm cursor-pointer"
:class="
key === activeTab
? 'text-blue-500 font-semibold border-t-2 border-blue-500'
: ''
"
:style="{
paddingTop: key === activeTab ? 'calc(1rem - 2px)' : '1rem',
}"
>
{{ key }}
</div>
</div>
</template>
<template #quickedit>
<Transition name="quickedit">
<QuickEditForm
v-if="hasQeDoc"
:name="qeDoc.name"
:show-name="false"
:show-save="false"
:source-doc="qeDoc"
:schema-name="qeDoc.schemaName"
:white="true"
:route-back="false"
:load-on-close="false"
@close="() => toggleQuickEditDoc(null)"
/>
</Transition>
</template>
</FormContainer>
</template>
<script lang="ts">
import { DocValue } from 'fyo/core/types';
import { Doc } from 'fyo/model/doc';
import { ValidationError } from 'fyo/utils/errors';
import { getDocStatus } from 'models/helpers';
import { ModelNameEnum } from 'models/types';
import { Field, Schema } from 'schemas/types';
import Button from 'src/components/Button.vue';
import DropdownWithActions from 'src/components/DropdownWithActions.vue';
import FormContainer from 'src/components/FormContainer.vue';
import FormHeader from 'src/components/FormHeader.vue';
import StatusBadge from 'src/components/StatusBadge.vue';
import { handleErrorWithDialog } from 'src/errorHandling';
import { getErrorMessage } from 'src/utils';
import { ActionGroup, UIGroupedFields } from 'src/utils/types';
import {
getFieldsGroupedByTabAndSection,
getGroupedActionsForDoc,
} from 'src/utils/ui';
import { computed, defineComponent, nextTick } from 'vue';
import QuickEditForm from '../QuickEditForm.vue';
import CommonFormSection from './CommonFormSection.vue';
export default defineComponent({
props: {
name: { type: String, default: '' },
schemaName: { type: String, default: ModelNameEnum.SalesInvoice },
},
provide() {
return {
schemaName: computed(() => this.docOrNull?.schemaName),
name: computed(() => this.docOrNull?.name),
doc: computed(() => this.docOrNull),
};
},
data() {
return {
errors: {},
docOrNull: null,
activeTab: 'Default',
groupedFields: null,
quickEditDoc: null,
} as {
errors: Record<string, string>;
docOrNull: null | Doc;
activeTab: string;
groupedFields: null | UIGroupedFields;
quickEditDoc: null | Doc;
};
},
async mounted() {
if (this.fyo.store.isDevelopment) {
// @ts-ignore
window.cf = this;
}
await this.setDoc();
this.updateGroupedFields();
},
computed: {
hasDoc(): boolean {
return !!this.docOrNull;
},
hasQeDoc(): boolean {
return !!this.quickEditDoc;
},
status(): string {
if (!this.hasDoc) {
return '';
}
return getDocStatus(this.doc);
},
doc(): Doc {
const doc = this.docOrNull as Doc | null;
if (!doc) {
throw new ValidationError(
this.t`Doc ${this.schema.label} ${this.name} not set`
);
}
return doc;
},
qeDoc(): Doc {
const doc = this.quickEditDoc as Doc | null;
if (!doc) {
throw new ValidationError(
this.t`Doc ${this.schema.label} ${this.name} not set`
);
}
return doc;
},
title(): string {
if (this.schema.isSubmittable && this.docOrNull?.notInserted) {
return this.t`New Entry`;
}
return this.docOrNull?.name! ?? this.t`New Entry`;
},
schema(): Schema {
const schema = this.fyo.schemaMap[this.schemaName];
if (!schema) {
throw new ValidationError(`no schema found with ${this.schemaName}`);
}
return schema;
},
activeGroup(): Map<string, Field[]> {
if (!this.groupedFields) {
return new Map();
}
const group = this.groupedFields.get(this.activeTab);
if (!group) {
throw new ValidationError(
`Tab group ${this.activeTab} has no value set`
);
}
return group;
},
groupedActions(): ActionGroup[] {
if (!this.hasDoc) {
return [];
}
return getGroupedActionsForDoc(this.doc);
},
},
methods: {
updateGroupedFields(): void {
if (!this.hasDoc) {
return;
}
this.groupedFields = getFieldsGroupedByTabAndSection(
this.schema,
this.doc
);
},
async sync() {
try {
await this.doc.sync();
this.updateGroupedFields();
} catch (err) {
if (!(err instanceof Error)) {
return;
}
await handleErrorWithDialog(err, this.doc);
}
},
async submit() {
try {
await this.doc.submit();
this.updateGroupedFields();
} catch (err) {
if (!(err instanceof Error)) {
return;
}
await handleErrorWithDialog(err, this.doc);
}
},
async setDoc() {
if (this.hasDoc) {
return;
}
if (this.name) {
await this.setDocFromName(this.name);
} else {
this.docOrNull = this.fyo.doc.getNewDoc(this.schemaName);
}
},
async setDocFromName(name: string) {
try {
this.docOrNull = await this.fyo.doc.getDoc(this.schemaName, name);
} catch (err) {
this.docOrNull = this.fyo.doc.getNewDoc(this.schemaName);
}
},
async toggleQuickEditDoc(doc: Doc | null) {
if (this.quickEditDoc && doc) {
this.quickEditDoc = null;
await nextTick();
}
this.quickEditDoc = doc;
},
async onValueChange(field: Field, value: DocValue) {
const { fieldname } = field;
delete this.errors[fieldname];
try {
await this.doc.set(fieldname, value);
} catch (err) {
if (!(err instanceof Error)) {
return;
}
this.errors[fieldname] = getErrorMessage(err, this.doc);
}
this.updateGroupedFields();
},
},
components: {
FormContainer,
FormHeader,
CommonFormSection,
StatusBadge,
Button,
DropdownWithActions,
QuickEditForm,
},
});
</script>

View File

@ -0,0 +1,81 @@
<template>
<div v-if="(fields ?? []).length > 0">
<div
v-if="showTitle && title"
class="flex justify-between items-center cursor-pointer select-none"
:class="collapsed ? '' : 'mb-4'"
@click="collapsed = !collapsed"
>
<h2 class="text-base text-gray-900 font-semibold">
{{ title }}
</h2>
<feather-icon
:name="collapsed ? 'chevron-up' : 'chevron-down'"
class="w-4 h-4 text-gray-600"
/>
</div>
<div class="grid gap-4 gap-x-8 grid-cols-2" v-if="!collapsed">
<div
v-for="field of fields"
:key="field.fieldname"
:class="field.fieldtype === 'Table' ? 'col-span-2 text-base' : ''"
class="mb-auto"
>
<FormControl
:ref="field.fieldname === 'name' ? 'nameField' : 'fields'"
:show-label="true"
:border="true"
:df="field"
:value="doc[field.fieldname]"
@editrow="(doc: Doc) => $emit('editrow', doc)"
@change="(value: DocValue) => $emit('value-change', field, value)"
/>
<div v-if="errors?.[field.fieldname]" class="text-sm text-red-600 mt-1">
{{ errors[field.fieldname] }}
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import { DocValue } from 'fyo/core/types';
import { Doc } from 'fyo/model/doc';
import { Field } from 'schemas/types';
import FormControl from 'src/components/Controls/FormControl.vue';
import { defineComponent, PropType } from 'vue';
export default defineComponent({
emits: ['editrow', 'value-change'],
props: {
title: String,
errors: Object as PropType<Record<string, string>>,
showTitle: Boolean,
doc: { type: Object as PropType<Doc>, required: true },
fields: Array as PropType<Field[]>,
},
data() {
return { collapsed: false } as {
collapsed: boolean;
};
},
mounted() {
this.focusOnNameField();
},
methods: {
focusOnNameField() {
const naming = this.fyo.schemaMap[this.doc.schemaName]?.naming;
if (naming !== 'manual') {
return;
}
const nameField = (this.$refs.nameField as { focus: Function }[])?.[0];
if (!nameField) {
return;
}
nameField.focus();
},
},
components: { FormControl },
});
</script>

View File

@ -1,320 +0,0 @@
<template>
<FormContainer>
<!-- Page Header (Title, Buttons, etc) -->
<template #header v-if="doc">
<StatusBadge :status="status" />
<Barcode
v-if="showBarcode"
@item-selected="(name) => doc.addItem(name)"
/>
<DropdownWithActions
v-for="group of groupedActions"
:key="group.label"
:type="group.type"
:actions="group.actions"
>
<p v-if="group.group">
{{ group.group }}
</p>
<feather-icon v-else name="more-horizontal" class="w-4 h-4" />
</DropdownWithActions>
<Button v-if="doc?.canSave" type="primary" @click="sync">
{{ t`Save` }}
</Button>
<Button v-else-if="doc?.canSubmit" type="primary" @click="submit">{{
t`Submit`
}}</Button>
</template>
<!-- Form Header -->
<template #body v-if="doc">
<FormHeader
:form-title="doc.notInserted ? t`New Entry` : doc.name"
:form-sub-title="doc.schema?.label ?? ''"
/>
<hr />
<div>
<!-- Form Data Entry -->
<div class="m-4 grid grid-cols-3 gap-4">
<FormControl
input-class="font-semibold"
:border="true"
:df="getField('party')"
:value="doc.party"
@change="(value) => doc.set('party', value, true)"
@new-doc="(party) => doc.set('party', party.name, true)"
:read-only="doc?.submitted"
/>
<FormControl
input-class="text-end"
:border="true"
:df="getField('date')"
:value="doc.date"
@change="(value) => doc.set('date', value)"
:read-only="doc?.submitted"
/>
<FormControl
input-class="text-end"
:border="true"
:df="getField('numberSeries')"
:value="doc.numberSeries"
@change="(value) => doc.set('numberSeries', value)"
:read-only="!doc.notInserted || doc?.submitted"
/>
<FormControl
v-if="doc.backReference"
:border="true"
:df="getField('backReference')"
:value="doc.backReference"
:read-only="true"
/>
<FormControl
v-if="doc.attachment || !(doc.isSubmitted || doc.isCancelled)"
:border="true"
:df="getField('attachment')"
:value="doc.attachment"
@change="(value) => doc.set('attachment', value)"
:read-only="doc?.submitted"
/>
</div>
<hr />
<!-- Items Table -->
<Table
class="text-base"
:df="getField('items')"
:value="doc.items"
:showHeader="true"
:max-rows-before-overflow="4"
@change="(value) => doc.set('items', value)"
@editrow="toggleQuickEditDoc"
:read-only="doc?.submitted"
/>
</div>
<!-- Form Footer -->
<div v-if="doc.items?.length ?? 0" class="mt-auto">
<hr />
<div class="flex justify-between text-base m-4 gap-12">
<div class="w-1/2 flex flex-col justify-between">
<!-- Form Terms-->
<FormControl
:border="true"
v-if="!doc?.submitted || doc.terms"
:df="getField('terms')"
:value="doc.terms"
class="mt-auto"
@change="(value) => doc.set('terms', value)"
:read-only="doc?.submitted"
/>
</div>
<div class="w-1/2" v-if="doc.grandTotal">
<!-- Grand Total -->
<div
class="
flex
justify-between
text-green-600
font-semibold
text-base
"
>
<div>{{ t`Grand Total` }}</div>
<div>{{ formattedValue('grandTotal') }}</div>
</div>
</div>
</div>
</div>
</template>
<template #quickedit v-if="quickEditDoc">
<QuickEditForm
:name="quickEditDoc.name"
:show-name="false"
:show-save="false"
:source-doc="quickEditDoc"
:source-fields="quickEditFields"
:schema-name="quickEditDoc.schemaName"
:white="true"
:route-back="false"
:load-on-close="false"
@close="toggleQuickEditDoc(null)"
/>
</template>
</FormContainer>
</template>
<script>
import { computed } from '@vue/reactivity';
import { t } from 'fyo';
import { getDocStatus } from 'models/helpers';
import { ModelNameEnum } from 'models/types';
import Button from 'src/components/Button.vue';
import Barcode from 'src/components/Controls/Barcode.vue';
import FormControl from 'src/components/Controls/FormControl.vue';
import Table from 'src/components/Controls/Table.vue';
import DropdownWithActions from 'src/components/DropdownWithActions.vue';
import FormContainer from 'src/components/FormContainer.vue';
import FormHeader from 'src/components/FormHeader.vue';
import StatusBadge from 'src/components/StatusBadge.vue';
import { fyo } from 'src/initFyo';
import { docsPathMap } from 'src/utils/misc';
import { docsPathRef, focusedDocsRef } from 'src/utils/refs';
import {
getGroupedActionsForDoc,
routeTo,
showMessageDialog,
} from 'src/utils/ui';
import { nextTick } from 'vue';
import { handleErrorWithDialog } from '../errorHandling';
import QuickEditForm from './QuickEditForm.vue';
export default {
name: 'InvoiceForm',
props: { schemaName: String, name: String },
components: {
StatusBadge,
Button,
FormControl,
DropdownWithActions,
Table,
FormContainer,
QuickEditForm,
FormHeader,
Barcode,
},
provide() {
return {
schemaName: this.schemaName,
name: this.name,
doc: computed(() => this.doc),
};
},
data() {
return {
chstatus: false,
doc: null,
quickEditDoc: null,
quickEditFields: [],
printSettings: null,
};
},
updated() {
this.chstatus = !this.chstatus;
},
computed: {
status() {
this.chstatus;
return getDocStatus(this.doc);
},
groupedActions() {
return getGroupedActionsForDoc(this.doc);
},
showBarcode() {
if (!this.doc) {
return false;
}
if (!this.doc.canEdit) {
return false;
}
if (!fyo.singles.InventorySettings?.enableBarcodes) {
return false;
}
return [
ModelNameEnum.Shipment,
ModelNameEnum.PurchaseReceipt,
ModelNameEnum.StockMovement,
].includes(this.schemaName);
},
},
activated() {
docsPathRef.value = docsPathMap[this.schemaName];
focusedDocsRef.add(this.doc);
},
deactivated() {
docsPathRef.value = '';
focusedDocsRef.delete(this.doc);
},
async mounted() {
try {
this.doc = await fyo.doc.getDoc(this.schemaName, this.name);
focusedDocsRef.add(this.doc);
} catch (error) {
if (error instanceof fyo.errors.NotFoundError) {
routeTo(`/list/${this.schemaName}`);
return;
}
await this.handleError(error);
}
let query = this.$route.query;
if (query.values && query.schemaName === this.schemaName) {
this.doc.set(this.$router.currentRoute.value.query.values);
}
if (fyo.store.isDevelopment) {
window.frm = this;
}
},
methods: {
routeTo,
async toggleQuickEditDoc(doc, fields = []) {
if (this.quickEditDoc && doc) {
this.quickEditDoc = null;
this.quickEditFields = [];
await nextTick();
}
this.quickEditDoc = doc;
this.quickEditFields = fields;
},
getField(fieldname) {
return fyo.getField(this.schemaName, fieldname);
},
async sync() {
try {
await this.doc.sync();
} catch (err) {
await this.handleError(err);
}
},
async submit() {
const message = t`Submit ${this.doc.name}`;
const ref = this;
await showMessageDialog({
message,
buttons: [
{
label: t`Yes`,
async action() {
try {
await ref.doc.submit();
} catch (err) {
await ref.handleError(err);
}
},
},
{
label: t`No`,
action() {},
},
],
});
},
async handleError(e) {
await handleErrorWithDialog(e, this.doc);
},
formattedValue(fieldname, doc) {
if (!doc) {
doc = this.doc;
}
const df = this.getField(fieldname);
return fyo.format(doc[fieldname], df, doc);
},
},
};
</script>

View File

@ -1,269 +0,0 @@
<template>
<FormContainer>
<!-- Page Header (Title, Buttons, etc) -->
<template #header v-if="doc">
<StatusBadge :status="status" />
<DropdownWithActions
v-for="group of groupedActions"
:key="group.label"
:type="group.type"
:actions="group.actions"
>
<p v-if="group.group">
{{ group.group }}
</p>
<feather-icon v-else name="more-horizontal" class="w-4 h-4" />
</DropdownWithActions>
<Button
v-if="doc?.canSave"
type="primary"
class="text-white text-xs"
@click="sync"
>
{{ t`Save` }}
</Button>
<Button
v-else-if="doc.canSubmit"
type="primary"
class="text-white text-xs"
@click="submit"
>
{{ t`Submit` }}
</Button>
</template>
<!-- Journal Entry Form -->
<template #body v-if="doc">
<FormHeader
:form-title="doc.notInserted ? t`New Entry` : doc.name"
:form-sub-title="t`Journal Entry`"
/>
<hr />
<div>
<div class="m-4 grid grid-cols-3 gap-y-4 gap-x-4">
<!-- First Column of Fields -->
<FormControl
:border="true"
:df="getField('numberSeries')"
:value="doc.numberSeries"
@change="(value) => doc.set('numberSeries', value)"
:read-only="!doc.notInserted || doc.submitted"
/>
<FormControl
:border="true"
:df="getField('date')"
:value="doc.date"
:placeholder="'Date'"
@change="(value) => doc.set('date', value)"
:read-only="doc.submitted"
/>
<FormControl
:border="true"
:df="getField('entryType')"
:value="doc.entryType"
placeholder="Entry Type"
@change="(value) => doc.set('entryType', value)"
:read-only="doc.submitted"
/>
<FormControl
:border="true"
:df="getField('referenceNumber')"
:value="doc.referenceNumber"
:placeholder="'Reference Number'"
@change="(value) => doc.set('referenceNumber', value)"
:read-only="doc.submitted"
/>
<FormControl
:border="true"
:df="getField('referenceDate')"
:value="doc.referenceDate"
:placeholder="'Reference Date'"
@change="(value) => doc.set('referenceDate', value)"
:read-only="doc.submitted"
/>
<FormControl
v-if="doc.attachment || !(doc.isSubmitted || doc.isCancelled)"
:border="true"
:df="getField('attachment')"
:value="doc.attachment"
@change="(value) => doc.set('attachment', value)"
:read-only="doc?.submitted"
/>
</div>
<hr />
<!-- Account Entries Table -->
<Table
class="text-base"
:df="getField('accounts')"
:value="doc.accounts"
:showHeader="true"
:max-rows-before-overflow="6"
:read-only="doc.submitted"
/>
</div>
<!-- Footer -->
<div v-if="doc.accounts?.length ?? 0" class="mt-auto">
<hr />
<div class="flex justify-between text-base m-4 gap-12">
<!-- User Remark -->
<FormControl
:border="true"
v-if="!doc.submitted || doc.userRemark"
class="w-1/2 self-end"
:df="getField('userRemark')"
:value="doc.userRemark"
@change="(value) => doc.set('userRemark', value)"
:read-only="doc.submitted"
/>
<!-- Debit and Credit -->
<div class="w-1/2 gap-2 flex flex-col self-end font-semibold ms-auto">
<div class="flex justify-between text-green-600">
<div>{{ t`Total Debit` }}</div>
<div>{{ totalDebit }}</div>
</div>
<hr />
<div class="flex justify-between text-red-600">
<div>{{ t`Total Credit` }}</div>
<div>{{ totalCredit }}</div>
</div>
</div>
</div>
</div>
</template>
</FormContainer>
</template>
<script>
import { computed } from '@vue/reactivity';
import { getDocStatus } from 'models/helpers';
import { ModelNameEnum } from 'models/types';
import Button from 'src/components/Button.vue';
import FormControl from 'src/components/Controls/FormControl.vue';
import Table from 'src/components/Controls/Table.vue';
import DropdownWithActions from 'src/components/DropdownWithActions.vue';
import FormContainer from 'src/components/FormContainer.vue';
import FormHeader from 'src/components/FormHeader.vue';
import StatusBadge from 'src/components/StatusBadge.vue';
import { fyo } from 'src/initFyo';
import { docsPathMap } from 'src/utils/misc';
import { docsPathRef, focusedDocsRef } from 'src/utils/refs';
import {
getGroupedActionsForDoc,
routeTo,
showMessageDialog,
} from 'src/utils/ui';
import { handleErrorWithDialog } from '../errorHandling';
export default {
name: 'JournalEntryForm',
props: ['name'],
components: {
Button,
DropdownWithActions,
StatusBadge,
FormControl,
Table,
FormContainer,
FormHeader,
},
provide() {
return {
schemaName: this.schemaName,
name: this.name,
doc: computed(() => this.doc),
};
},
data() {
return {
schemaName: ModelNameEnum.JournalEntry,
doc: null,
};
},
activated() {
docsPathRef.value = docsPathMap.JournalEntry;
focusedDocsRef.add(this.doc);
},
deactivated() {
docsPathRef.value = '';
focusedDocsRef.delete(this.doc);
},
async mounted() {
try {
this.doc = await fyo.doc.getDoc(this.schemaName, this.name);
focusedDocsRef.add(this.doc);
} catch (error) {
if (error instanceof fyo.errors.NotFoundError) {
routeTo(`/list/${this.schemaName}`);
return;
}
await this.handleError(error);
}
if (fyo.store.isDevelopment) {
window.je = this;
}
},
computed: {
status() {
return getDocStatus(this.doc);
},
totalDebit() {
let value = 0;
if (this.doc.accounts) {
value = this.doc.getSum('accounts', 'debit');
}
return fyo.format(value, 'Currency');
},
totalCredit() {
let value = 0;
if (this.doc.accounts) {
value = this.doc.getSum('accounts', 'credit');
}
return fyo.format(value, 'Currency');
},
groupedActions() {
return getGroupedActionsForDoc(this.doc);
},
},
methods: {
getField(fieldname) {
return fyo.getField(ModelNameEnum.JournalEntry, fieldname);
},
async sync() {
try {
await this.doc.sync();
} catch (err) {
this.handleError(err);
}
},
async submit() {
const ref = this;
await showMessageDialog({
message: this.t`Submit Journal Entry?`,
buttons: [
{
label: this.t`Yes`,
async action() {
try {
await ref.doc.submit();
} catch (err) {
await ref.handleError(err);
}
},
},
{
label: this.t`No`,
action() {},
},
],
});
},
async handleError(e) {
await handleErrorWithDialog(e, this.doc);
},
},
};
</script>

View File

@ -9,7 +9,7 @@
>
<p class="w-8 text-end me-4 text-gray-700">#</p>
<Row
class="flex-1 text-gray-700 border-none h-row-mid"
class="flex-1 text-gray-700 h-row-mid"
:columnCount="columns.length"
gap="1rem"
>
@ -45,7 +45,7 @@
</p>
<Row
gap="1rem"
class="cursor-pointer text-gray-900 flex-1 border-none h-row-mid"
class="cursor-pointer text-gray-900 flex-1 h-row-mid"
@click="$emit('openDoc', doc.name)"
:columnCount="columns.length"
>

View File

@ -1,11 +1,10 @@
import { ModelNameEnum } from 'models/types';
import ChartOfAccounts from 'src/pages/ChartOfAccounts.vue';
import CommonForm from 'src/pages/CommonForm/CommonForm.vue';
import Dashboard from 'src/pages/Dashboard/Dashboard.vue';
import ImportWizard from 'src/pages/ImportWizard.vue';
import GeneralForm from 'src/pages/GeneralForm.vue';
import GetStarted from 'src/pages/GetStarted.vue';
import ImportWizard from 'src/pages/ImportWizard.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';
@ -18,29 +17,34 @@ import {
RouteRecordRaw,
} from 'vue-router';
function getGeneralFormItems(): RouteRecordRaw[] {
return [ModelNameEnum.Shipment, ModelNameEnum.PurchaseReceipt].map(
(schemaName) => {
return {
path: `/edit/${schemaName}/:name`,
name: `${schemaName}Form`,
components: {
default: GeneralForm,
edit: QuickEditForm,
function getCommonFormItems(): RouteRecordRaw[] {
return [
ModelNameEnum.Shipment,
ModelNameEnum.PurchaseReceipt,
ModelNameEnum.JournalEntry,
ModelNameEnum.Payment,
ModelNameEnum.StockMovement,
ModelNameEnum.Item,
].map((schemaName) => {
return {
path: `/edit/${schemaName}/:name`,
name: `${schemaName}Form`,
components: {
default: CommonForm,
edit: QuickEditForm,
},
props: {
default: (route) => {
route.params.schemaName = schemaName;
return {
schemaName,
name: route.params.name,
};
},
props: {
default: (route) => {
route.params.schemaName = schemaName;
return {
schemaName,
name: route.params.name,
};
},
edit: (route) => route.query,
},
};
}
);
edit: (route) => route.query,
},
};
});
}
const routes: RouteRecordRaw[] = [
@ -52,26 +56,7 @@ const routes: RouteRecordRaw[] = [
path: '/get-started',
component: GetStarted,
},
...getGeneralFormItems(),
{
path: '/edit/JournalEntry/:name',
name: 'JournalEntryForm',
components: {
default: JournalEntryForm,
edit: QuickEditForm,
},
props: {
default: (route) => {
// for sidebar item active state
route.params.schemaName = 'JournalEntry';
return {
schemaName: 'JournalEntry',
name: route.params.name,
};
},
edit: (route) => route.query,
},
},
...getCommonFormItems(),
{
path: '/edit/:schemaName/:name',
name: 'InvoiceForm',
@ -167,6 +152,9 @@ export function getCreateRoute(
ModelNameEnum.JournalEntry,
ModelNameEnum.Shipment,
ModelNameEnum.PurchaseReceipt,
ModelNameEnum.StockMovement,
ModelNameEnum.Payment,
ModelNameEnum.Item,
].includes(schemaName as ModelNameEnum)
) {
return `/edit/${schemaName}/${name}`;

View File

@ -93,25 +93,16 @@ async function openFormEditDoc(schemaName: string, fyo: Fyo) {
function getCreateList(fyo: Fyo): SearchItem[] {
const hasInventory = fyo.doc.singles.AccountingSettings?.enableInventory;
const quickEditCreateList = [
...(hasInventory ? [ModelNameEnum.StockMovement] : []),
].map(
(schemaName) =>
({
label: fyo.schemaMap[schemaName]?.label,
group: 'Create',
action() {
openQuickEditDoc(schemaName, fyo);
},
} as SearchItem)
);
const formEditCreateList = [
ModelNameEnum.SalesInvoice,
ModelNameEnum.PurchaseInvoice,
ModelNameEnum.JournalEntry,
...(hasInventory
? [ModelNameEnum.Shipment, ModelNameEnum.PurchaseReceipt]
? [
ModelNameEnum.Shipment,
ModelNameEnum.PurchaseReceipt,
ModelNameEnum.StockMovement,
]
: []),
].map(
(schemaName) =>
@ -197,7 +188,7 @@ function getCreateList(fyo: Fyo): SearchItem[] {
} as SearchItem;
});
return [quickEditCreateList, formEditCreateList, filteredCreateList].flat();
return [formEditCreateList, filteredCreateList].flat();
}
function getReportList(fyo: Fyo): SearchItem[] {

View File

@ -1,6 +1,7 @@
import { Doc } from "fyo/model/doc";
import { FieldType } from "schemas/types";
import { QueryFilter } from "utils/db/types";
import type { Doc } from 'fyo/model/doc';
import type { Action } from 'fyo/model/types';
import type { Field, FieldType } from 'schemas/types';
import type { QueryFilter } from 'utils/db/types';
export interface MessageDialogButton {
label: string;
@ -31,7 +32,7 @@ export interface QuickEditOptions {
hideFields?: string[];
showFields?: string[];
defaults?: Record<string, unknown>;
listFilters?: QueryFilter
listFilters?: QueryFilter;
}
export type SidebarConfig = SidebarRoot[];
@ -44,7 +45,7 @@ export interface SidebarRoot {
iconHeight?: string;
hidden?: () => boolean;
items?: SidebarItem[];
filters?: QueryFilter
filters?: QueryFilter;
}
export interface SidebarItem {
@ -55,7 +56,6 @@ export interface SidebarItem {
hidden?: () => boolean;
}
export interface ExportField {
fieldname: string;
fieldtype: FieldType;
@ -70,5 +70,13 @@ export interface ExportTableField {
fields: ExportField[];
}
export type ActionGroup = {
group: string;
label: string;
type: string;
actions: Action[];
};
export type UIGroupedFields = Map<string, Map<string, Field[]>>;
export type ExportFormat = 'csv' | 'json';
export type PeriodKey = 'This Year' | 'This Quarter' | 'This Month'
export type PeriodKey = 'This Year' | 'This Quarter' | 'This Month';

View File

@ -9,6 +9,7 @@ import { Action } from 'fyo/model/types';
import { getActions } from 'fyo/utils';
import { getDbError, LinkValidationError, ValueError } from 'fyo/utils/errors';
import { ModelNameEnum } from 'models/types';
import { Schema } from 'schemas/types';
import { handleErrorWithDialog } from 'src/errorHandling';
import { fyo } from 'src/initFyo';
import router from 'src/router';
@ -16,11 +17,14 @@ import { IPC_ACTIONS } from 'utils/messages';
import { App, createApp, h, ref } from 'vue';
import { RouteLocationRaw } from 'vue-router';
import { stringifyCircular } from './';
import { evaluateHidden } from './doc';
import {
ActionGroup,
MessageDialogOptions,
QuickEditOptions,
SettingsTab,
ToastOptions,
UIGroupedFields,
} from './types';
export async function openQuickEdit({
@ -70,6 +74,7 @@ export async function openQuickEdit({
if (forWhat[0] === 'not in' && forWhat[1] === 'Sales') {
defaults = Object.assign({ for: 'Purchases' });
}
console.log(method, schemaName, name);
router[method]({
query: {
@ -274,14 +279,7 @@ export function getActionsForDoc(doc?: Doc): Action[] {
});
}
export function getGroupedActionsForDoc(doc?: Doc) {
type Group = {
group: string;
label: string;
type: string;
actions: Action[];
};
export function getGroupedActionsForDoc(doc?: Doc): ActionGroup[] {
const actions = getActionsForDoc(doc);
const actionsMap = actions.reduce((acc, ac) => {
if (!ac.group) {
@ -297,7 +295,7 @@ export function getGroupedActionsForDoc(doc?: Doc) {
acc[ac.group].actions.push(ac);
return acc;
}, {} as Record<string, Group>);
}, {} as Record<string, ActionGroup>);
const grouped = Object.keys(actionsMap)
.filter(Boolean)
@ -393,14 +391,33 @@ function getDuplicateAction(doc: Doc): Action {
};
}
export function getStatus(entry: { cancelled?: boolean; submitted?: boolean }) {
if (entry.cancelled) {
return 'Cancelled';
export function getFieldsGroupedByTabAndSection(
schema: Schema,
doc: Doc
): UIGroupedFields {
const grouped: UIGroupedFields = new Map();
for (const field of schema?.fields ?? []) {
const tab = field.tab ?? 'Default';
const section = field.section ?? 'Default';
if (!grouped.has(tab)) {
grouped.set(tab, new Map());
}
const tabbed = grouped.get(tab)!;
if (!tabbed.has(section)) {
tabbed.set(section, []);
}
if (field.meta) {
continue;
}
if (evaluateHidden(field, doc)) {
continue;
}
tabbed.get(section)!.push(field);
}
if (entry.submitted) {
return 'Submitted';
}
return 'Saved';
return grouped;
}

View File

@ -2,7 +2,13 @@
* Properties of a schema which are to be translated,
* irrespective of nesting.
*/
export const schemaTranslateables = ['label', 'description', 'placeholder'];
export const schemaTranslateables = [
'label',
'description',
'placeholder',
'section',
'tab',
];
export function getIndexFormat(inp: string | string[]) {
/**