diff --git a/electron-builder.yml b/electron-builder.yml
index 95647edb..bed25e95 100644
--- a/electron-builder.yml
+++ b/electron-builder.yml
@@ -5,6 +5,7 @@ extraResources:
[
{ from: 'log_creds.txt', to: '../creds/log_creds.txt' },
{ from: 'translations', to: '../translations' },
+ { from: 'templates', to: '../templates' },
]
mac:
type: distribution
diff --git a/fyo/index.ts b/fyo/index.ts
index 7d8ee855..e85f1ce7 100644
--- a/fyo/index.ts
+++ b/fyo/index.ts
@@ -13,7 +13,7 @@ import { TelemetryManager } from './telemetry/telemetry';
import {
DEFAULT_CURRENCY,
DEFAULT_DISPLAY_PRECISION,
- DEFAULT_INTERNAL_PRECISION
+ DEFAULT_INTERNAL_PRECISION,
} from './utils/consts';
import * as errors from './utils/errors';
import { format } from './utils/format';
@@ -88,7 +88,7 @@ export class Fyo {
return this.db.fieldMap;
}
- format(value: DocValue, field: string | Field, doc?: Doc) {
+ format(value: unknown, field: string | Field, doc?: Doc) {
return format(value, field, doc ?? null, this);
}
diff --git a/fyo/model/types.ts b/fyo/model/types.ts
index 42814452..36135212 100644
--- a/fyo/model/types.ts
+++ b/fyo/model/types.ts
@@ -1,3 +1,4 @@
+import { Fyo } from 'fyo';
import { DocValue, DocValueMap } from 'fyo/core/types';
import SystemSettings from 'fyo/models/SystemSettings';
import { FieldType, Schema, SelectOption } from 'schemas/types';
@@ -76,13 +77,12 @@ export interface RenderData {
[key: string]: DocValue | Schema
}
-export interface ColumnConfig {
+export type ColumnConfig = {
label: string;
fieldtype: FieldType;
- fieldname?: string;
- size?: string;
+ fieldname: string;
render?: (doc: RenderData) => { template: string };
- getValue?: (doc: Doc) => string;
+ display?: (value: unknown, fyo: Fyo) => string;
}
export type ListViewColumn = string | ColumnConfig;
diff --git a/fyo/utils/format.ts b/fyo/utils/format.ts
index 335addb9..5cadb6a9 100644
--- a/fyo/utils/format.ts
+++ b/fyo/utils/format.ts
@@ -1,10 +1,9 @@
import { Fyo } from 'fyo';
-import { DocValue } from 'fyo/core/types';
import { Doc } from 'fyo/model/doc';
import { DateTime } from 'luxon';
import { Money } from 'pesa';
import { Field, FieldType, FieldTypeEnum } from 'schemas/types';
-import { getIsNullOrUndef, safeParseFloat } from 'utils';
+import { getIsNullOrUndef, safeParseFloat, titleCase } from 'utils';
import {
DEFAULT_CURRENCY,
DEFAULT_DATE_FORMAT,
@@ -13,7 +12,7 @@ import {
} from './consts';
export function format(
- value: DocValue,
+ value: unknown,
df: string | Field | null,
doc: Doc | null,
fyo: Fyo
@@ -45,7 +44,7 @@ export function format(
}
if (field.fieldtype === FieldTypeEnum.Check) {
- return Boolean(value).toString();
+ return titleCase(Boolean(value).toString());
}
if (getIsNullOrUndef(value)) {
@@ -55,26 +54,31 @@ export function format(
return String(value);
}
-function toDatetime(value: DocValue) {
+function toDatetime(value: unknown): DateTime | null {
if (typeof value === 'string') {
return DateTime.fromISO(value);
} else if (value instanceof Date) {
return DateTime.fromJSDate(value);
- } else {
+ } else if (typeof value === 'number') {
return DateTime.fromSeconds(value as number);
}
+
+ return null;
}
-function formatDatetime(value: DocValue, fyo: Fyo): string {
+function formatDatetime(value: unknown, fyo: Fyo): string {
if (value == null) {
return '';
}
const dateFormat =
(fyo.singles.SystemSettings?.dateFormat as string) ?? DEFAULT_DATE_FORMAT;
- const formattedDatetime = toDatetime(value).toFormat(
- `${dateFormat} HH:mm:ss`
- );
+ const dateTime = toDatetime(value);
+ if (!dateTime) {
+ return '';
+ }
+
+ const formattedDatetime = dateTime.toFormat(`${dateFormat} HH:mm:ss`);
if (value === 'Invalid DateTime') {
return '';
@@ -83,7 +87,7 @@ function formatDatetime(value: DocValue, fyo: Fyo): string {
return formattedDatetime;
}
-function formatDate(value: DocValue, fyo: Fyo): string {
+function formatDate(value: unknown, fyo: Fyo): string {
if (value == null) {
return '';
}
@@ -91,9 +95,12 @@ function formatDate(value: DocValue, fyo: Fyo): string {
const dateFormat =
(fyo.singles.SystemSettings?.dateFormat as string) ?? DEFAULT_DATE_FORMAT;
- const dateValue: DateTime = toDatetime(value);
+ const dateTime = toDatetime(value);
+ if (!dateTime) {
+ return '';
+ }
- const formattedDate = dateValue.toFormat(dateFormat);
+ const formattedDate = dateTime.toFormat(dateFormat);
if (value === 'Invalid DateTime') {
return '';
}
@@ -102,7 +109,7 @@ function formatDate(value: DocValue, fyo: Fyo): string {
}
function formatCurrency(
- value: DocValue,
+ value: unknown,
field: Field,
doc: Doc | null,
fyo: Fyo
@@ -125,7 +132,7 @@ function formatCurrency(
return valueString;
}
-function formatNumber(value: DocValue, fyo: Fyo): string {
+function formatNumber(value: unknown, fyo: Fyo): string {
const numberFormatter = getNumberFormatter(fyo);
if (typeof value === 'number') {
value = fyo.pesa(value.toFixed(20));
diff --git a/main.ts b/main.ts
index 956b8207..792e6dd7 100644
--- a/main.ts
+++ b/main.ts
@@ -4,7 +4,7 @@ import {
app,
BrowserWindow,
BrowserWindowConstructorOptions,
- protocol
+ protocol,
} from 'electron';
import Store from 'electron-store';
import { autoUpdater } from 'electron-updater';
diff --git a/main/getPrintTemplates.ts b/main/getPrintTemplates.ts
new file mode 100644
index 00000000..d0aa48df
--- /dev/null
+++ b/main/getPrintTemplates.ts
@@ -0,0 +1,41 @@
+import fs from 'fs/promises';
+import path from 'path';
+import { TemplateFile } from 'utils/types';
+
+export async function getTemplates() {
+ const paths = await getPrintTemplatePaths();
+ if (!paths) {
+ return [];
+ }
+
+ const templates: TemplateFile[] = [];
+ for (const file of paths.files) {
+ const filePath = path.join(paths.root, file);
+ const template = await fs.readFile(filePath, 'utf-8');
+ const { mtime } = await fs.stat(filePath);
+ templates.push({ template, file, modified: mtime.toISOString() });
+ }
+
+ return templates;
+}
+
+async function getPrintTemplatePaths(): Promise<{
+ files: string[];
+ root: string;
+} | null> {
+ let root = path.join(process.resourcesPath, `../templates`);
+
+ try {
+ const files = await fs.readdir(root);
+ return { files, root };
+ } catch {
+ root = path.join(__dirname, `../templates`);
+ }
+
+ try {
+ const files = await fs.readdir(root);
+ return { files, root };
+ } catch {
+ return null;
+ }
+}
diff --git a/main/registerIpcMainActionListeners.ts b/main/registerIpcMainActionListeners.ts
index 17561f93..0a41cb74 100644
--- a/main/registerIpcMainActionListeners.ts
+++ b/main/registerIpcMainActionListeners.ts
@@ -10,6 +10,7 @@ import { DatabaseMethod } from '../utils/db/types';
import { IPC_ACTIONS } from '../utils/messages';
import { getUrlAndTokenString, sendError } from './contactMothership';
import { getLanguageMap } from './getLanguageMap';
+import { getTemplates } from './getPrintTemplates';
import {
getConfigFilesWithModified,
getErrorHandledReponse,
@@ -117,7 +118,7 @@ export default function registerIpcMainActionListeners(main: Main) {
);
ipcMain.handle(IPC_ACTIONS.GET_CREDS, async (event) => {
- return await getUrlAndTokenString();
+ return getUrlAndTokenString();
});
ipcMain.handle(IPC_ACTIONS.DELETE_FILE, async (_, filePath) => {
@@ -137,6 +138,10 @@ export default function registerIpcMainActionListeners(main: Main) {
};
});
+ ipcMain.handle(IPC_ACTIONS.GET_TEMPLATES, async () => {
+ return getTemplates();
+ });
+
/**
* Database Related Actions
*/
diff --git a/models/baseModels/Account/Account.ts b/models/baseModels/Account/Account.ts
index 184a6662..55ebfe16 100644
--- a/models/baseModels/Account/Account.ts
+++ b/models/baseModels/Account/Account.ts
@@ -7,7 +7,9 @@ import {
RequiredMap,
TreeViewSettings,
ReadOnlyMap,
+ FormulaMap,
} from 'fyo/model/types';
+import { ModelNameEnum } from 'models/types';
import { QueryFilter } from 'utils/db/types';
import { AccountRootType, AccountRootTypeEnum, AccountType } from './types';
@@ -76,6 +78,22 @@ export class Account extends Doc {
};
}
+ formulas: FormulaMap = {
+ rootType: {
+ formula: async () => {
+ if (!this.parentAccount) {
+ return;
+ }
+
+ return await this.fyo.getValue(
+ ModelNameEnum.Account,
+ this.parentAccount,
+ 'rootType'
+ );
+ },
+ },
+ };
+
static filters: FiltersMap = {
parentAccount: (doc: Doc) => {
const filter: QueryFilter = {
diff --git a/models/baseModels/Defaults/Defaults.ts b/models/baseModels/Defaults/Defaults.ts
index e78c7f28..e6436a2b 100644
--- a/models/baseModels/Defaults/Defaults.ts
+++ b/models/baseModels/Defaults/Defaults.ts
@@ -3,6 +3,7 @@ import { FiltersMap, HiddenMap } from 'fyo/model/types';
import { ModelNameEnum } from 'models/types';
export class Defaults extends Doc {
+ // Number Series
salesInvoiceNumberSeries?: string;
purchaseInvoiceNumberSeries?: string;
journalEntryNumberSeries?: string;
@@ -11,12 +12,23 @@ export class Defaults extends Doc {
shipmentNumberSeries?: string;
purchaseReceiptNumberSeries?: string;
+ // Terms
salesInvoiceTerms?: string;
purchaseInvoiceTerms?: string;
shipmentTerms?: string;
purchaseReceiptTerms?: string;
+ // Print Templates
+ salesInvoicePrintTemplate?: string;
+ purchaseInvoicePrintTemplate?: string;
+ journalEntryPrintTemplate?: string;
+ paymentPrintTemplate?: string;
+ shipmentPrintTemplate?: string;
+ purchaseReceiptPrintTemplate?: string;
+ stockMovementPrintTemplate?: string;
+
static commonFilters = {
+ // Number Series
salesInvoiceNumberSeries: () => ({
referenceType: ModelNameEnum.SalesInvoice,
}),
@@ -38,6 +50,18 @@ export class Defaults extends Doc {
purchaseReceiptNumberSeries: () => ({
referenceType: ModelNameEnum.PurchaseReceipt,
}),
+ // Print Templates
+ salesInvoicePrintTemplate: () => ({ type: ModelNameEnum.SalesInvoice }),
+ purchaseInvoicePrintTemplate: () => ({
+ type: ModelNameEnum.PurchaseInvoice,
+ }),
+ journalEntryPrintTemplate: () => ({ type: ModelNameEnum.JournalEntry }),
+ paymentPrintTemplate: () => ({ type: ModelNameEnum.Payment }),
+ shipmentPrintTemplate: () => ({ type: ModelNameEnum.Shipment }),
+ purchaseReceiptPrintTemplate: () => ({
+ type: ModelNameEnum.PurchaseReceipt,
+ }),
+ stockMovementPrintTemplate: () => ({ type: ModelNameEnum.StockMovement }),
};
static filters: FiltersMap = this.commonFilters;
@@ -53,6 +77,9 @@ export class Defaults extends Doc {
purchaseReceiptNumberSeries: this.getInventoryHidden(),
shipmentTerms: this.getInventoryHidden(),
purchaseReceiptTerms: this.getInventoryHidden(),
+ shipmentPrintTemplate: this.getInventoryHidden(),
+ purchaseReceiptPrintTemplate: this.getInventoryHidden(),
+ stockMovementPrintTemplate: this.getInventoryHidden(),
};
}
diff --git a/models/baseModels/Invoice/Invoice.ts b/models/baseModels/Invoice/Invoice.ts
index b929c117..4037a7ab 100644
--- a/models/baseModels/Invoice/Invoice.ts
+++ b/models/baseModels/Invoice/Invoice.ts
@@ -11,7 +11,6 @@ import {
import { DEFAULT_CURRENCY } from 'fyo/utils/consts';
import { ValidationError } from 'fyo/utils/errors';
import { addItem, getExchangeRate, getNumberSeries } from 'models/helpers';
-import { validateBatch } from 'models/inventory/helpers';
import { InventorySettings } from 'models/inventory/InventorySettings';
import { StockTransfer } from 'models/inventory/StockTransfer';
import { Transactional } from 'models/Transactional/Transactional';
diff --git a/models/baseModels/JournalEntry/JournalEntry.ts b/models/baseModels/JournalEntry/JournalEntry.ts
index 7241d345..828fe9fa 100644
--- a/models/baseModels/JournalEntry/JournalEntry.ts
+++ b/models/baseModels/JournalEntry/JournalEntry.ts
@@ -70,8 +70,8 @@ export class JournalEntry extends Transactional {
'name',
{
label: t`Status`,
+ fieldname: 'status',
fieldtype: 'Select',
- size: 'small',
render(doc) {
const status = getDocStatus(doc);
const color = statusColor[status] ?? 'gray';
diff --git a/models/baseModels/PrintSettings/PrintSettings.ts b/models/baseModels/PrintSettings/PrintSettings.ts
index a8ffea2a..bb0da046 100644
--- a/models/baseModels/PrintSettings/PrintSettings.ts
+++ b/models/baseModels/PrintSettings/PrintSettings.ts
@@ -2,7 +2,5 @@ import { Doc } from 'fyo/model/doc';
import { HiddenMap } from 'fyo/model/types';
export class PrintSettings extends Doc {
- override hidden: HiddenMap = {
- displayBatch: () => !this.fyo.singles.InventorySettings?.enableBatches,
- };
+ override hidden: HiddenMap = {};
}
diff --git a/models/baseModels/PrintTemplate.ts b/models/baseModels/PrintTemplate.ts
new file mode 100644
index 00000000..832aa84b
--- /dev/null
+++ b/models/baseModels/PrintTemplate.ts
@@ -0,0 +1,81 @@
+import { Doc } from 'fyo/model/doc';
+import { SchemaMap } from 'schemas/types';
+import { ListsMap, ListViewSettings, ReadOnlyMap } from 'fyo/model/types';
+import { ModelNameEnum } from 'models/types';
+import { Fyo } from 'fyo';
+
+export class PrintTemplate extends Doc {
+ name?: string;
+ type?: string;
+ template?: string;
+ isCustom?: boolean;
+
+ override get canDelete(): boolean {
+ if (this.isCustom === false) {
+ return false;
+ }
+
+ return super.canDelete;
+ }
+
+ static getListViewSettings(fyo: Fyo): ListViewSettings {
+ return {
+ formRoute: (name) => `/template-builder/${name}`,
+ columns: [
+ 'name',
+ {
+ label: fyo.t`Type`,
+ fieldtype: 'AutoComplete',
+ fieldname: 'type',
+ display(value) {
+ return fyo.schemaMap[value as string]?.label ?? '';
+ },
+ },
+ 'isCustom',
+ ],
+ };
+ }
+
+ readOnly: ReadOnlyMap = {
+ name: () => !this.isCustom,
+ type: () => !this.isCustom,
+ template: () => !this.isCustom,
+ };
+
+ static lists: ListsMap = {
+ type(doc?: Doc) {
+ let enableInventory: boolean = false;
+ let schemaMap: SchemaMap = {};
+ if (doc) {
+ enableInventory = !!doc.fyo.singles.AccountingSettings?.enableInventory;
+ schemaMap = doc.fyo.schemaMap;
+ }
+
+ const models = [
+ ModelNameEnum.SalesInvoice,
+ ModelNameEnum.PurchaseInvoice,
+ ModelNameEnum.JournalEntry,
+ ModelNameEnum.Payment,
+ ];
+
+ if (enableInventory) {
+ models.push(
+ ModelNameEnum.Shipment,
+ ModelNameEnum.PurchaseReceipt,
+ ModelNameEnum.StockMovement
+ );
+ }
+
+ return models.map((value) => ({
+ value,
+ label: schemaMap[value]?.label ?? value,
+ }));
+ },
+ };
+
+ override duplicate(): Doc {
+ const doc = super.duplicate() as PrintTemplate;
+ doc.isCustom = true;
+ return doc;
+ }
+}
diff --git a/models/helpers.ts b/models/helpers.ts
index f66ff961..cb0c50ea 100644
--- a/models/helpers.ts
+++ b/models/helpers.ts
@@ -311,7 +311,6 @@ export function getDocStatusListColumn(): ColumnConfig {
label: t`Status`,
fieldname: 'status',
fieldtype: 'Select',
- size: 'small',
render(doc) {
const status = getDocStatus(doc);
const color = statusColor[status] ?? 'gray';
diff --git a/models/index.ts b/models/index.ts
index 75899c29..14a7ea4d 100644
--- a/models/index.ts
+++ b/models/index.ts
@@ -29,6 +29,7 @@ import { StockLedgerEntry } from './inventory/StockLedgerEntry';
import { StockMovement } from './inventory/StockMovement';
import { StockMovementItem } from './inventory/StockMovementItem';
+import { PrintTemplate } from './baseModels/PrintTemplate';
export const models = {
Account,
AccountingLedgerEntry,
@@ -48,6 +49,7 @@ export const models = {
SalesInvoice,
SalesInvoiceItem,
SetupWizard,
+ PrintTemplate,
Tax,
TaxSummary,
// Inventory Models
diff --git a/models/inventory/StockMovement.ts b/models/inventory/StockMovement.ts
index 9e203d55..68574beb 100644
--- a/models/inventory/StockMovement.ts
+++ b/models/inventory/StockMovement.ts
@@ -80,6 +80,13 @@ export class StockMovement extends Transfer {
};
static getListViewSettings(fyo: Fyo): ListViewSettings {
+ const movementTypeMap = {
+ [MovementType.MaterialIssue]: fyo.t`Material Issue`,
+ [MovementType.MaterialReceipt]: fyo.t`Material Receipt`,
+ [MovementType.MaterialTransfer]: fyo.t`Material Transfer`,
+ [MovementType.Manufacture]: fyo.t`Manufacture`,
+ };
+
return {
formRoute: (name) => `/edit/StockMovement/${name}`,
columns: [
@@ -90,20 +97,8 @@ export class StockMovement extends Transfer {
label: fyo.t`Movement Type`,
fieldname: 'movementType',
fieldtype: 'Select',
- size: 'small',
- render(doc) {
- const movementType = doc.movementType as MovementType;
- const label =
- {
- [MovementType.MaterialIssue]: fyo.t`Material Issue`,
- [MovementType.MaterialReceipt]: fyo.t`Material Receipt`,
- [MovementType.MaterialTransfer]: fyo.t`Material Transfer`,
- [MovementType.Manufacture]: fyo.t`Manufacture`,
- }[movementType] ?? '';
-
- return {
- template: `${label}`,
- };
+ display(value): string {
+ return movementTypeMap[value as MovementType] ?? '';
},
},
],
diff --git a/models/types.ts b/models/types.ts
index d6c4d3c2..e68fea77 100644
--- a/models/types.ts
+++ b/models/types.ts
@@ -6,7 +6,6 @@ export enum ModelNameEnum {
Address = 'Address',
Batch= 'Batch',
Color = 'Color',
- CompanySettings = 'CompanySettings',
Currency = 'Currency',
GetStarted = 'GetStarted',
Defaults = 'Defaults',
@@ -21,6 +20,7 @@ export enum ModelNameEnum {
Payment = 'Payment',
PaymentFor = 'PaymentFor',
PrintSettings = 'PrintSettings',
+ PrintTemplate = 'PrintTemplate',
PurchaseInvoice = 'PurchaseInvoice',
PurchaseInvoiceItem = 'PurchaseInvoiceItem',
SalesInvoice = 'SalesInvoice',
diff --git a/package.json b/package.json
index fd65cea2..0c05a90e 100644
--- a/package.json
+++ b/package.json
@@ -20,8 +20,11 @@
"test": "scripts/test.sh"
},
"dependencies": {
+ "@codemirror/autocomplete": "^6.4.2",
+ "@codemirror/lang-vue": "^0.1.1",
"@popperjs/core": "^2.10.2",
"better-sqlite3": "^7.5.3",
+ "codemirror": "^6.0.1",
"core-js": "^3.19.0",
"electron-store": "^8.0.1",
"feather-icons": "^4.28.0",
diff --git a/reports/GoodsAndServiceTax/gstExporter.ts b/reports/GoodsAndServiceTax/gstExporter.ts
index 01849fdd..f0d55f55 100644
--- a/reports/GoodsAndServiceTax/gstExporter.ts
+++ b/reports/GoodsAndServiceTax/gstExporter.ts
@@ -162,7 +162,7 @@ async function exportReport(extention: ExportExtention, report: BaseGSTR) {
return;
}
- await saveExportData(data, filePath, report.fyo);
+ await saveExportData(data, filePath);
report.fyo.telemetry.log(Verb.Exported, report.reportName, { extention });
}
diff --git a/reports/commonExporter.ts b/reports/commonExporter.ts
index e8001e54..d4cdcdc5 100644
--- a/reports/commonExporter.ts
+++ b/reports/commonExporter.ts
@@ -52,7 +52,7 @@ async function exportReport(extention: ExportExtention, report: Report) {
return;
}
- await saveExportData(data, filePath, report.fyo);
+ await saveExportData(data, filePath);
report.fyo.telemetry.log(Verb.Exported, report.reportName, { extention });
}
@@ -178,7 +178,12 @@ function getValueFromCell(cell: ReportCell, displayPrecision: number) {
return rawValue;
}
-export async function saveExportData(data: string, filePath: string, fyo: Fyo) {
+export async function saveExportData(
+ data: string,
+ filePath: string,
+ message?: string
+) {
await saveData(data, filePath);
- showExportInFolder(fyo.t`Export Successful`, filePath);
+ message ??= t`Export Successful`;
+ showExportInFolder(message, filePath);
}
diff --git a/schemas/app/CompanySettings.json b/schemas/app/CompanySettings.json
deleted file mode 100644
index 61f5e62b..00000000
--- a/schemas/app/CompanySettings.json
+++ /dev/null
@@ -1,22 +0,0 @@
-{
- "name": "CompanySettings",
- "label": "Company Settings",
- "naming": "autoincrement",
- "isSingle": true,
- "isChild": false,
- "fields": [
- {
- "fieldname": "companyName",
- "label": "Company Name",
- "fieldtype": "Data",
- "required": true
- },
- {
- "fieldname": "companyAddress",
- "label": "Company Address",
- "fieldtype": "Link",
- "required": true,
- "target": "Address"
- }
- ]
-}
diff --git a/schemas/app/Defaults.json b/schemas/app/Defaults.json
index d68c6c79..6bd9d1c3 100644
--- a/schemas/app/Defaults.json
+++ b/schemas/app/Defaults.json
@@ -83,6 +83,55 @@
"label": "Purchase Receipt Terms",
"fieldtype": "Text",
"section": "Terms"
+ },
+ {
+ "fieldname": "salesInvoicePrintTemplate",
+ "label": "Sales Invoice Print Template",
+ "fieldtype": "Link",
+ "target": "PrintTemplate",
+ "section": "Print Templates"
+ },
+ {
+ "fieldname": "purchaseInvoicePrintTemplate",
+ "label": "Purchase Invoice Print Template",
+ "fieldtype": "Link",
+ "target": "PrintTemplate",
+ "section": "Print Templates"
+ },
+ {
+ "fieldname": "journalEntryPrintTemplate",
+ "label": "Journal Entry Print Template",
+ "fieldtype": "Link",
+ "target": "PrintTemplate",
+ "section": "Print Templates"
+ },
+ {
+ "fieldname": "paymentPrintTemplate",
+ "label": "Payment Print Template",
+ "fieldtype": "Link",
+ "target": "PrintTemplate",
+ "section": "Print Templates"
+ },
+ {
+ "fieldname": "shipmentPrintTemplate",
+ "label": "Shipment Print Template",
+ "fieldtype": "Link",
+ "target": "PrintTemplate",
+ "section": "Print Templates"
+ },
+ {
+ "fieldname": "purchaseReceiptPrintTemplate",
+ "label": "Purchase Receipt Print Template",
+ "fieldtype": "Link",
+ "target": "PrintTemplate",
+ "section": "Print Templates"
+ },
+ {
+ "fieldname": "stockMovementPrintTemplate",
+ "label": "Stock Movement Print Template",
+ "fieldtype": "Link",
+ "target": "PrintTemplate",
+ "section": "Print Templates"
}
]
}
diff --git a/schemas/app/PrintSettings.json b/schemas/app/PrintSettings.json
index c65cebdc..345c3f46 100644
--- a/schemas/app/PrintSettings.json
+++ b/schemas/app/PrintSettings.json
@@ -37,28 +37,6 @@
"create": true,
"section": "Contacts"
},
- {
- "fieldname": "template",
- "label": "Template",
- "placeholder": "Template",
- "fieldtype": "Select",
- "options": [
- {
- "value": "Basic",
- "label": "Basic"
- },
- {
- "value": "Minimal",
- "label": "Minimal"
- },
- {
- "value": "Business",
- "label": "Business"
- }
- ],
- "default": "Basic",
- "section": "Customizations"
- },
{
"fieldname": "color",
"label": "Color",
@@ -136,30 +114,6 @@
"label": "Display Logo in Invoice",
"fieldtype": "Check",
"section": "Customizations"
- },
- {
- "fieldname": "displayTaxInvoice",
- "label": "Display Tax Invoice",
- "fieldtype": "Check",
- "section": "Customizations"
- },
- {
- "fieldname": "displayBatch",
- "label": "Display Batch",
- "fieldtype": "Check",
- "section": "Customizations"
}
- ],
- "quickEditFields": [
- "logo",
- "displayLogo",
- "displayTaxInvoice",
- "displayBatch",
- "template",
- "color",
- "font",
- "email",
- "phone",
- "address"
]
}
diff --git a/schemas/app/PrintTemplate.json b/schemas/app/PrintTemplate.json
new file mode 100644
index 00000000..ada87c7c
--- /dev/null
+++ b/schemas/app/PrintTemplate.json
@@ -0,0 +1,34 @@
+{
+ "name": "PrintTemplate",
+ "label": "Print Template",
+ "naming": "manual",
+ "isSingle": false,
+ "fields": [
+ {
+ "fieldname": "name",
+ "label": "Template Name",
+ "fieldtype": "Data",
+ "required": true
+ },
+ {
+ "fieldname": "type",
+ "label": "Template Type",
+ "fieldtype": "AutoComplete",
+ "default": "SalesInvoice",
+ "required": true
+ },
+ {
+ "fieldname": "template",
+ "label": "Template",
+ "fieldtype": "Text",
+ "required": true
+ },
+ {
+ "fieldname": "isCustom",
+ "label": "Is Custom",
+ "fieldtype": "Check",
+ "default": true,
+ "readOnly": true
+ }
+ ]
+}
diff --git a/schemas/schemas.ts b/schemas/schemas.ts
index 8d4fdf0f..811049e9 100644
--- a/schemas/schemas.ts
+++ b/schemas/schemas.ts
@@ -2,11 +2,12 @@ import Account from './app/Account.json';
import AccountingLedgerEntry from './app/AccountingLedgerEntry.json';
import AccountingSettings from './app/AccountingSettings.json';
import Address from './app/Address.json';
+import Batch from './app/Batch.json';
import Color from './app/Color.json';
-import CompanySettings from './app/CompanySettings.json';
import Currency from './app/Currency.json';
import Defaults from './app/Defaults.json';
import GetStarted from './app/GetStarted.json';
+import InventorySettings from './app/inventory/InventorySettings.json';
import Location from './app/inventory/Location.json';
import PurchaseReceipt from './app/inventory/PurchaseReceipt.json';
import PurchaseReceiptItem from './app/inventory/PurchaseReceiptItem.json';
@@ -17,6 +18,7 @@ import StockMovement from './app/inventory/StockMovement.json';
import StockMovementItem from './app/inventory/StockMovementItem.json';
import StockTransfer from './app/inventory/StockTransfer.json';
import StockTransferItem from './app/inventory/StockTransferItem.json';
+import UOMConversionItem from './app/inventory/UOMConversionItem.json';
import Invoice from './app/Invoice.json';
import InvoiceItem from './app/InvoiceItem.json';
import Item from './app/Item.json';
@@ -28,6 +30,7 @@ import Party from './app/Party.json';
import Payment from './app/Payment.json';
import PaymentFor from './app/PaymentFor.json';
import PrintSettings from './app/PrintSettings.json';
+import PrintTemplate from './app/PrintTemplate.json';
import PurchaseInvoice from './app/PurchaseInvoice.json';
import PurchaseInvoiceItem from './app/PurchaseInvoiceItem.json';
import SalesInvoice from './app/SalesInvoice.json';
@@ -37,7 +40,6 @@ import Tax from './app/Tax.json';
import TaxDetail from './app/TaxDetail.json';
import TaxSummary from './app/TaxSummary.json';
import UOM from './app/UOM.json';
-import UOMConversionItem from './app/inventory/UOMConversionItem.json';
import PatchRun from './core/PatchRun.json';
import SingleValue from './core/SingleValue.json';
import SystemSettings from './core/SystemSettings.json';
@@ -46,8 +48,6 @@ import child from './meta/child.json';
import submittable from './meta/submittable.json';
import tree from './meta/tree.json';
import { Schema, SchemaStub } from './types';
-import InventorySettings from './app/inventory/InventorySettings.json';
-import Batch from './app/Batch.json'
export const coreSchemas: Schema[] = [
PatchRun as Schema,
@@ -66,6 +66,7 @@ export const appSchemas: Schema[] | SchemaStub[] = [
Misc as Schema,
SetupWizard as Schema,
GetStarted as Schema,
+ PrintTemplate as Schema,
Color as Schema,
Currency as Schema,
@@ -73,7 +74,6 @@ export const appSchemas: Schema[] | SchemaStub[] = [
NumberSeries as Schema,
PrintSettings as Schema,
- CompanySettings as Schema,
Account as Schema,
AccountingSettings as Schema,
@@ -116,5 +116,5 @@ export const appSchemas: Schema[] | SchemaStub[] = [
PurchaseReceipt as Schema,
PurchaseReceiptItem as Schema,
- Batch as Schema
+ Batch as Schema,
];
diff --git a/src/App.vue b/src/App.vue
index f3312327..ff8357f3 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -54,6 +54,7 @@ import './styles/index.css';
import { initializeInstance } from './utils/initialization';
import { checkForUpdates } from './utils/ipcCalls';
import { updateConfigFiles } from './utils/misc';
+import { updatePrintTemplates } from './utils/printTemplates';
import { Search } from './utils/search';
import { setGlobalShortcuts } from './utils/shortcuts';
import { routeTo } from './utils/ui';
@@ -128,7 +129,7 @@ export default {
'companyName'
);
await this.setSearcher();
- await updateConfigFiles(fyo);
+ updateConfigFiles(fyo);
},
async setSearcher() {
this.searcher = new Search(fyo);
@@ -160,6 +161,7 @@ export default {
}
await initializeInstance(filePath, false, countryCode, fyo);
+ await updatePrintTemplates(fyo);
await this.setDesk(filePath);
},
async setDeskRoute() {
diff --git a/src/components/Controls/AutoComplete.vue b/src/components/Controls/AutoComplete.vue
index 3c6ad2a0..8b7ca303 100644
--- a/src/components/Controls/AutoComplete.vue
+++ b/src/components/Controls/AutoComplete.vue
@@ -13,6 +13,7 @@
!isReadOnly && triggerChange(e.target.value)"
@focus="(e) => !isReadOnly && $emit('focus', e)"
@input="(e) => !isReadOnly && $emit('input', e)"
@@ -32,6 +34,7 @@ export default {
name: 'Base',
props: {
df: Object,
+ step: { type: Number, default: 1 },
value: [String, Number, Boolean, Object],
inputClass: [Function, String, Object],
border: { type: Boolean, default: false },
@@ -39,6 +42,7 @@ export default {
size: String,
showLabel: Boolean,
autofocus: Boolean,
+ containerStyles: { type: Object, default: () => ({}) },
textRight: { type: [null, Boolean], default: null },
readOnly: { type: [null, Boolean], default: null },
required: { type: [null, Boolean], default: null },
diff --git a/src/components/Controls/Color.vue b/src/components/Controls/Color.vue
index 88fb579f..9486616d 100644
--- a/src/components/Controls/Color.vue
+++ b/src/components/Controls/Color.vue
@@ -44,6 +44,7 @@
:placeholder="t`Custom Hex`"
:class="[inputClasses, containerClasses]"
:value="value"
+ style="padding: 0"
@change="(e) => setColorValue(e.target.value)"
/>
diff --git a/src/components/Controls/FormControl.vue b/src/components/Controls/FormControl.vue
index 323430ca..3740c9b4 100644
--- a/src/components/Controls/FormControl.vue
+++ b/src/components/Controls/FormControl.vue
@@ -54,6 +54,9 @@ export default {
input.value = '';
}
},
+ select() {
+ this.$refs.control.$refs?.input?.select()
+ },
focus() {
this.$refs.control.focus();
},
diff --git a/src/components/FormContainer.vue b/src/components/FormContainer.vue
index 2ad11f78..5e940c0a 100644
--- a/src/components/FormContainer.vue
+++ b/src/components/FormContainer.vue
@@ -3,6 +3,9 @@
+
+
+
diff --git a/src/components/HorizontalResizer.vue b/src/components/HorizontalResizer.vue
new file mode 100644
index 00000000..9674b5cd
--- /dev/null
+++ b/src/components/HorizontalResizer.vue
@@ -0,0 +1,118 @@
+
+
+
+ {{ value }}
+
+
+
+
diff --git a/src/components/PageHeader.vue b/src/components/PageHeader.vue
index fd436dc2..31076740 100644
--- a/src/components/PageHeader.vue
+++ b/src/components/PageHeader.vue
@@ -8,14 +8,17 @@
>
{{ title }}
+
+
+
diff --git a/src/components/SalesInvoice/Templates/BaseTemplate.vue b/src/components/SalesInvoice/Templates/BaseTemplate.vue
deleted file mode 100644
index e7b90e30..00000000
--- a/src/components/SalesInvoice/Templates/BaseTemplate.vue
+++ /dev/null
@@ -1,109 +0,0 @@
-
diff --git a/src/components/SalesInvoice/Templates/Basic.vue b/src/components/SalesInvoice/Templates/Basic.vue
deleted file mode 100644
index 04fc85fe..00000000
--- a/src/components/SalesInvoice/Templates/Basic.vue
+++ /dev/null
@@ -1,172 +0,0 @@
-
-
-
-
-
- {{ t`Tax Invoice` }}
-
-
-
-
-
-
-
- {{ printObject.companyName }}
-
-
-
-
{{ printObject.email }}
-
{{ printObject.phone }}
-
-
-
- {{ printObject.address }}
-
-
GSTIN: {{ printObject.gstin }}
-
-
-
-
-
-
-
- {{ printObject.invoiceName }}
-
-
- {{ printObject.date }}
-
-
-
-
- {{ printObject.partyName }}
-
-
- {{ printObject.partyAddress }}
-
-
- GSTIN: {{ printObject.partyGSTIN }}
-
-
-
-
-
-
-
-
Item
-
- HSN/SAC
-
-
Quantity
-
- Batch
-
-
Rate
-
Amount
-
-
-
{{ row.item }}
-
- {{ row.hsnCode }}
-
-
{{ row.quantity }}
-
- {{ row.batch }}
-
-
{{ row.rate }}
-
{{ row.amount }}
-
-
-
-
-
-
-
- Notes
-
-
- {{ printObject.terms }}
-
-
-
-
-
{{ t`Subtotal` }}
-
{{ printObject.netTotal }}
-
-
-
{{ t`Discount` }}
-
{{ printObject.totalDiscount }}
-
-
-
{{ tax.account }}
-
{{ tax.amount }}
-
-
-
{{ t`Discount` }}
-
{{ printObject.totalDiscount }}
-
-
-
{{ t`Grand Total` }}
-
{{ printObject.grandTotal }}
-
-
-
-
-
-
-
diff --git a/src/components/SalesInvoice/Templates/Business.vue b/src/components/SalesInvoice/Templates/Business.vue
deleted file mode 100644
index b75c7e36..00000000
--- a/src/components/SalesInvoice/Templates/Business.vue
+++ /dev/null
@@ -1,165 +0,0 @@
-
-
-
-
- {{ t`Tax Invoice` }}
-
-
-
-
-
-
-
-
-
- {{ printObject.companyName }}
-
-
- {{ printObject.address }}
-
-
- GSTIN: {{ printObject.gstin }}
-
-
-
-
-
-
- {{ printObject.isSalesInvoice ? 'Invoice' : 'Bill' }}
-
-
-
- {{ printObject.invoiceName }}
-
-
- {{ printObject.date }}
-
-
-
-
-
- {{ printObject.isSalesInvoice ? 'Customer' : 'Supplier' }}
-
-
-
- {{ printObject.partyName }}
-
-
- {{ printObject.partyAddress }}
-
-
- GSTIN: {{ printObject.partyGSTIN }}
-
-
-
-
-
-
-
-
Item
-
HSN/SAC
-
Quantity
-
- Batch
-
-
Rate
-
Amount
-
-
-
{{ row.item }}
-
- {{ row.hsnCode }}
-
-
{{ row.quantity }}
-
- {{ row.batch }}
-
-
{{ row.rate }}
-
{{ row.amount }}
-
-
-
-
-
-
{{ t`Subtotal` }}
-
- {{ printObject.netTotal }}
-
-
-
-
-
{{ t`Discount` }}
-
- {{ printObject.totalDiscount }}
-
-
-
-
-
- {{ tax.account }}
-
-
- {{ tax.amount }}
-
-
-
-
-
{{ t`Discount` }}
-
- {{ printObject.totalDiscount }}
-
-
-
-
-
-
{{ t`Grand Total` }}
-
- {{ printObject.grandTotal }}
-
-
-
-
-
-
Notes
-
- {{ printObject.terms }}
-
-
-
-
-
-
-
diff --git a/src/components/SalesInvoice/Templates/Minimal.vue b/src/components/SalesInvoice/Templates/Minimal.vue
deleted file mode 100644
index 9d2ec8e8..00000000
--- a/src/components/SalesInvoice/Templates/Minimal.vue
+++ /dev/null
@@ -1,194 +0,0 @@
-
-
-
-
- {{ t`Tax Invoice` }}
-
-
-
-
-
-
-
-
-
-
- {{ printObject.companyName }}
-
-
- {{ printObject.date }}
-
-
-
-
-
- {{
- printObject.isSalesInvoice
- ? t`Sales Invoice`
- : t`Purchase Invoice`
- }}
-
-
- {{ printObject.invoiceName }}
-
-
-
-
-
-
-
- {{ printObject.isSalesInvoice ? 'To' : 'From' }}
-
-
- {{ printObject.partyName }}
- {{ printObject.partyAddress ?? '' }}
-
-
- GSTIN: {{ printObject.partyGSTIN }}
-
-
-
-
- {{ printObject.isSalesInvoice ? 'From' : 'To' }}
-
-
- {{ printObject.address }}
-
-
- GSTIN: {{ printObject.gstin }}
-
-
-
-
-
-
Item
-
HSN/SAC
-
Quantity
-
- Batch
-
-
Rate
-
Amount
-
-
-
{{ row.item }}
-
- {{ row.hsnCode }}
-
-
{{ row.quantity }}
-
- {{ row.batch }}
-
-
{{ row.rate }}
-
{{ row.amount }}
-
-
-
-
-
- Notes
-
-
- {{ printObject.terms }}
-
-
-
-
-
{{ t`Subtotal` }}
-
{{ printObject.netTotal }}
-
-
-
{{ t`Discount` }}
-
{{ printObject.totalDiscount }}
-
-
-
{{ tax.account }}
-
{{ tax.amount }}
-
-
-
{{ t`Discount` }}
-
{{ printObject.totalDiscount }}
-
-
-
{{ t`Grand Total` }}
-
{{ printObject.grandTotal }}
-
-
-
-
-
-
diff --git a/src/components/ShortcutKeys.vue b/src/components/ShortcutKeys.vue
new file mode 100644
index 00000000..3507ceda
--- /dev/null
+++ b/src/components/ShortcutKeys.vue
@@ -0,0 +1,39 @@
+
+
+ {{ keyMap[k] ?? k }}
+
+
+
+
diff --git a/src/components/ShortcutsHelper.vue b/src/components/ShortcutsHelper.vue
index 0c8321e6..a1132041 100644
--- a/src/components/ShortcutsHelper.vue
+++ b/src/components/ShortcutsHelper.vue
@@ -22,29 +22,7 @@
class="grid gap-4 items-start"
style="grid-template-columns: 6rem auto"
>
-
-
- {{ k }}
-
+
{{ s.description }}
@@ -63,8 +41,10 @@
diff --git a/src/components/Sidebar.vue b/src/components/Sidebar.vue
index e4cf08d3..c01dde77 100644
--- a/src/components/Sidebar.vue
+++ b/src/components/Sidebar.vue
@@ -145,7 +145,7 @@
dev mode
@@ -165,7 +165,7 @@
m-4
rtl-rotate-180
"
- @click="$emit('toggle-sidebar')"
+ @click="() => toggleSidebar()"
>
@@ -182,7 +182,7 @@ import { fyo } from 'src/initFyo';
import { openLink } from 'src/utils/ipcCalls';
import { docsPathRef } from 'src/utils/refs';
import { getSidebarConfig } from 'src/utils/sidebarConfig';
-import { routeTo } from 'src/utils/ui';
+import { routeTo, toggleSidebar } from 'src/utils/ui';
import router from '../router';
import Icon from './Icon.vue';
import Modal from './Modal.vue';
@@ -191,7 +191,7 @@ import ShortcutsHelper from './ShortcutsHelper.vue';
export default {
components: [Button],
inject: ['languageDirection', 'shortcuts'],
- emits: ['change-db-file', 'toggle-sidebar'],
+ emits: ['change-db-file'],
data() {
return {
companyName: '',
@@ -222,7 +222,7 @@ export default {
this.shortcuts.shift.set(['KeyH'], () => {
if (document.body === document.activeElement) {
- this.$emit('toggle-sidebar');
+ this.toggleSidebar();
}
});
this.shortcuts.set(['F1'], () => this.openDocumentation());
@@ -234,6 +234,7 @@ export default {
methods: {
routeTo,
reportIssue,
+ toggleSidebar,
openDocumentation() {
openLink('https://docs.frappebooks.com/' + docsPathRef.value);
},
diff --git a/src/errorHandling.ts b/src/errorHandling.ts
index 2d67e83e..784062c1 100644
--- a/src/errorHandling.ts
+++ b/src/errorHandling.ts
@@ -94,11 +94,15 @@ export async function handleError(
}
export async function handleErrorWithDialog(
- error: Error,
+ error: unknown,
doc?: Doc,
reportError?: false,
dontThrow?: false
) {
+ if (!(error instanceof Error)) {
+ return;
+ }
+
const errorMessage = getErrorMessage(error, doc);
await handleError(false, error, { errorMessage, doc });
diff --git a/src/pages/CommonForm/CommonForm.vue b/src/pages/CommonForm/CommonForm.vue
index 124ea130..7e9389a3 100644
--- a/src/pages/CommonForm/CommonForm.vue
+++ b/src/pages/CommonForm/CommonForm.vue
@@ -1,7 +1,16 @@
+
+
+
-
+
;
docOrNull: null | Doc;
activeTab: string;
groupedFields: null | UIGroupedFields;
quickEditDoc: null | Doc;
+ isPrintable: boolean;
};
},
async mounted() {
@@ -159,6 +173,7 @@ export default defineComponent({
await this.setDoc();
focusedDocsRef.add(this.docOrNull);
this.updateGroupedFields();
+ this.isPrintable = await isPrintable(this.schemaName);
},
activated(): void {
docsPathRef.value = docsPathMap[this.schemaName] ?? '';
@@ -166,7 +181,9 @@ export default defineComponent({
},
deactivated(): void {
docsPathRef.value = '';
- focusedDocsRef.add(this.docOrNull);
+ if (this.docOrNull) {
+ focusedDocsRef.delete(this.doc);
+ }
},
computed: {
hasDoc(): boolean {
@@ -238,6 +255,7 @@ export default defineComponent({
},
},
methods: {
+ routeTo,
updateGroupedFields(): void {
if (!this.hasDoc) {
return;
@@ -253,10 +271,6 @@ export default defineComponent({
await this.doc.sync();
this.updateGroupedFields();
} catch (err) {
- if (!(err instanceof Error)) {
- return;
- }
-
await handleErrorWithDialog(err, this.doc);
}
},
@@ -265,10 +279,6 @@ export default defineComponent({
await this.doc.submit();
this.updateGroupedFields();
} catch (err) {
- if (!(err instanceof Error)) {
- return;
- }
-
await handleErrorWithDialog(err, this.doc);
}
},
@@ -277,18 +287,10 @@ export default defineComponent({
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);
- }
+ this.docOrNull = await getDocFromNameIfExistsElseNew(
+ this.schemaName,
+ this.name
+ );
},
async toggleQuickEditDoc(doc: Doc | null) {
if (this.quickEditDoc && doc) {
diff --git a/src/pages/CommonForm/CommonFormSection.vue b/src/pages/CommonForm/CommonFormSection.vue
index 8b7a9cd4..a3385267 100644
--- a/src/pages/CommonForm/CommonFormSection.vue
+++ b/src/pages/CommonForm/CommonFormSection.vue
@@ -44,6 +44,7 @@ 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 { focusOrSelectFormControl } from 'src/utils/ui';
import { defineComponent, PropType } from 'vue';
export default defineComponent({
@@ -61,26 +62,7 @@ export default defineComponent({
};
},
mounted() {
- this.focusOnNameField();
- },
- methods: {
- focusOnNameField() {
- const naming = this.fyo.schemaMap[this.doc.schemaName]?.naming;
- if (naming !== 'manual' || this.doc.inserted) {
- return;
- }
-
- const nameField = (
- this.$refs.nameField as { focus: Function; clear: Function }[]
- )?.[0];
-
- if (!nameField) {
- return;
- }
-
- nameField.clear();
- nameField.focus();
- },
+ focusOrSelectFormControl(this.doc, this.$refs.nameField);
},
components: { FormControl },
});
diff --git a/src/pages/Desk.vue b/src/pages/Desk.vue
index 20fcaebd..a266d800 100644
--- a/src/pages/Desk.vue
+++ b/src/pages/Desk.vue
@@ -1,11 +1,14 @@
+
@@ -32,7 +35,7 @@
-
diff --git a/src/pages/TemplateBuilder/TemplateBuilderHint.vue b/src/pages/TemplateBuilder/TemplateBuilderHint.vue
new file mode 100644
index 00000000..a2dab9c5
--- /dev/null
+++ b/src/pages/TemplateBuilder/TemplateBuilderHint.vue
@@ -0,0 +1,110 @@
+
+
+
+
+
{{ getKey(r) }}
+
+ {{ r.value }}
+
+
+ Array
+
+
+ Object
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/pages/TemplateBuilder/TemplateEditor.vue b/src/pages/TemplateBuilder/TemplateEditor.vue
new file mode 100644
index 00000000..3eb877da
--- /dev/null
+++ b/src/pages/TemplateBuilder/TemplateEditor.vue
@@ -0,0 +1,250 @@
+
+
+
+
+
diff --git a/src/renderer.ts b/src/renderer.ts
index 60e090ad..cd5ed510 100644
--- a/src/renderer.ts
+++ b/src/renderer.ts
@@ -111,6 +111,8 @@ function setOnWindow(isDevelopment: boolean) {
window.fyo = fyo;
// @ts-ignore
window.DateTime = DateTime;
+ // @ts-ignore
+ window.ipcRenderer = ipcRenderer;
}
function getPlatformName(platform: string) {
diff --git a/src/router.ts b/src/router.ts
index f1baf30f..5c5a7a6c 100644
--- a/src/router.ts
+++ b/src/router.ts
@@ -10,12 +10,8 @@ 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,
- RouteLocationRaw,
- RouteRecordRaw,
-} from 'vue-router';
+import TemplateBuilder from 'src/pages/TemplateBuilder/TemplateBuilder.vue';
+import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router';
function getCommonFormItems(): RouteRecordRaw[] {
return [
@@ -127,6 +123,12 @@ const routes: RouteRecordRaw[] = [
name: 'Import Wizard',
component: ImportWizard,
},
+ {
+ path: '/template-builder/:name',
+ name: 'Template Builder',
+ component: TemplateBuilder,
+ props: true,
+ },
{
path: '/settings',
name: 'Settings',
diff --git a/src/styles/index.css b/src/styles/index.css
index 3d1b724a..fad83816 100644
--- a/src/styles/index.css
+++ b/src/styles/index.css
@@ -7,7 +7,7 @@
}
* {
- outline-color: theme('colors.pink.500');
+ outline-color: theme('colors.pink.400');
font-variation-settings: 'slnt' 0deg;
}
.italic {
@@ -133,19 +133,19 @@ input[type='number']::-webkit-inner-spin-button {
}
.custom-scroll::-webkit-scrollbar-track:vertical {
- border-left: solid 1px theme('colors.gray.200');
+ border-left: solid 1px theme('colors.gray.100');
}
.custom-scroll::-webkit-scrollbar-track:horizontal {
- border-top: solid 1px theme('colors.gray.200');
+ border-top: solid 1px theme('colors.gray.100');
}
.custom-scroll::-webkit-scrollbar-thumb {
- background: theme('colors.gray.200');
+ background: theme('colors.gray.100');
}
.custom-scroll::-webkit-scrollbar-thumb:hover {
- background: theme('colors.gray.400');
+ background: theme('colors.gray.200');
}
/*
diff --git a/src/utils/doc.ts b/src/utils/doc.ts
index e8bab8cf..9ce924c9 100644
--- a/src/utils/doc.ts
+++ b/src/utils/doc.ts
@@ -47,9 +47,9 @@ function evaluateFieldMeta(
return value;
}
- const hiddenFunction = doc?.[meta]?.[field.fieldname];
- if (hiddenFunction !== undefined) {
- return hiddenFunction();
+ const evalFunction = doc?.[meta]?.[field.fieldname];
+ if (evalFunction !== undefined) {
+ return evalFunction();
}
return defaultValue;
diff --git a/src/utils/initialization.ts b/src/utils/initialization.ts
index c5184de7..14bd7b2b 100644
--- a/src/utils/initialization.ts
+++ b/src/utils/initialization.ts
@@ -1,6 +1,5 @@
import { Fyo } from 'fyo';
import { ConfigFile, ConfigKeys } from 'fyo/core/types';
-import { Doc } from 'fyo/model/doc';
import { getRegionalModels, models } from 'models/index';
import { ModelNameEnum } from 'models/types';
import { TargetField } from 'schemas/types';
diff --git a/src/utils/ipcCalls.ts b/src/utils/ipcCalls.ts
index 3049c82d..1f1d50ae 100644
--- a/src/utils/ipcCalls.ts
+++ b/src/utils/ipcCalls.ts
@@ -6,7 +6,7 @@ import { t } from 'fyo';
import { BaseError } from 'fyo/utils/errors';
import { BackendResponse } from 'utils/ipc/types';
import { IPC_ACTIONS, IPC_MESSAGES } from 'utils/messages';
-import { SelectFileOptions, SelectFileReturn } from 'utils/types';
+import { SelectFileOptions, SelectFileReturn, TemplateFile } from 'utils/types';
import { setLanguageMap } from './language';
import { showMessageDialog, showToast } from './ui';
@@ -14,6 +14,10 @@ export function reloadWindow() {
return ipcRenderer.send(IPC_MESSAGES.RELOAD_MAIN_WINDOW);
}
+export async function getTemplates(): Promise {
+ return await ipcRenderer.invoke(IPC_ACTIONS.GET_TEMPLATES);
+}
+
export async function selectFile(
options: SelectFileOptions
): Promise {
diff --git a/src/utils/misc.ts b/src/utils/misc.ts
index 251e0b7d..0b3c6e10 100644
--- a/src/utils/misc.ts
+++ b/src/utils/misc.ts
@@ -115,6 +115,7 @@ export const docsPathMap: Record = {
[ModelNameEnum.Party]: 'entries/party',
[ModelNameEnum.Item]: 'entries/items',
[ModelNameEnum.Tax]: 'entries/taxes',
+ [ModelNameEnum.PrintTemplate]: 'miscellaneous/print-templates',
// Miscellaneous
Search: 'miscellaneous/search',
diff --git a/src/utils/printTemplates.ts b/src/utils/printTemplates.ts
new file mode 100644
index 00000000..1df1d1bb
--- /dev/null
+++ b/src/utils/printTemplates.ts
@@ -0,0 +1,370 @@
+import { Fyo } from 'fyo';
+import { Doc } from 'fyo/model/doc';
+import { Invoice } from 'models/baseModels/Invoice/Invoice';
+import { ModelNameEnum } from 'models/types';
+import { FieldTypeEnum, Schema, TargetField } from 'schemas/types';
+import { getValueMapFromList } from 'utils/index';
+import { TemplateFile } from 'utils/types';
+import { getSavePath, getTemplates, makePDF } from './ipcCalls';
+import { PrintValues } from './types';
+import { getDocFromNameIfExistsElseNew } from './ui';
+
+type PrintTemplateData = Record;
+type TemplateUpdateItem = { name: string; template: string; type: string };
+
+const printSettingsFields = [
+ 'logo',
+ 'displayLogo',
+ 'color',
+ 'font',
+ 'email',
+ 'phone',
+ 'address',
+];
+const accountingSettingsFields = ['companyName', 'gstin'];
+
+export async function getPrintTemplatePropValues(
+ doc: Doc
+): Promise {
+ const fyo = doc.fyo;
+ const values: PrintValues = { doc: {}, print: {} };
+ values.doc = await getPrintTemplateDocValues(doc);
+ (values.doc as PrintTemplateData).entryType = doc.schema.name;
+ (values.doc as PrintTemplateData).entryLabel = doc.schema.label;
+
+ const printSettings = await fyo.doc.getDoc(ModelNameEnum.PrintSettings);
+ const printValues = await getPrintTemplateDocValues(
+ printSettings,
+ printSettingsFields
+ );
+
+ const accountingSettings = await fyo.doc.getDoc(
+ ModelNameEnum.AccountingSettings
+ );
+ const accountingValues = await getPrintTemplateDocValues(
+ accountingSettings,
+ accountingSettingsFields
+ );
+
+ values.print = {
+ ...printValues,
+ ...accountingValues,
+ };
+
+ if (doc.schemaName?.endsWith('Invoice')) {
+ (values.doc as PrintTemplateData).totalDiscount =
+ formattedTotalDiscount(doc);
+ (values.doc as PrintTemplateData).showHSN = showHSN(doc);
+ }
+
+ return values;
+}
+
+export function getPrintTemplatePropHints(schemaName: string, fyo: Fyo) {
+ const hints: PrintTemplateData = {};
+ const schema = fyo.schemaMap[schemaName]!;
+ hints.doc = getPrintTemplateDocHints(schema, fyo);
+ (hints.doc as PrintTemplateData).entryType = fyo.t`Entry Type`;
+ (hints.doc as PrintTemplateData).entryLabel = fyo.t`Entry Label`;
+
+ const printSettingsHints = getPrintTemplateDocHints(
+ fyo.schemaMap[ModelNameEnum.PrintSettings]!,
+ fyo,
+ printSettingsFields
+ );
+ const accountingSettingsHints = getPrintTemplateDocHints(
+ fyo.schemaMap[ModelNameEnum.AccountingSettings]!,
+ fyo,
+ accountingSettingsFields
+ );
+
+ hints.print = {
+ ...printSettingsHints,
+ ...accountingSettingsHints,
+ };
+
+ if (schemaName?.endsWith('Invoice')) {
+ (hints.doc as PrintTemplateData).totalDiscount = fyo.t`Total Discount`;
+ (hints.doc as PrintTemplateData).showHSN = fyo.t`Show HSN`;
+ }
+
+ return hints;
+}
+
+function showHSN(doc: Doc): boolean {
+ if (!Array.isArray(doc.items)) {
+ return false;
+ }
+
+ return doc.items.map((i) => i.hsnCode).every(Boolean);
+}
+
+function formattedTotalDiscount(doc: Doc): string {
+ if (!(doc instanceof Invoice)) {
+ return '';
+ }
+
+ const totalDiscount = doc.getTotalDiscount();
+ if (!totalDiscount?.float) {
+ return '';
+ }
+
+ return doc.fyo.format(totalDiscount, ModelNameEnum.Currency);
+}
+
+function getPrintTemplateDocHints(
+ schema: Schema,
+ fyo: Fyo,
+ fieldnames?: string[],
+ linkLevel?: number
+): PrintTemplateData {
+ linkLevel ??= 0;
+ const hints: PrintTemplateData = {};
+ const links: PrintTemplateData = {};
+
+ let fields = schema.fields;
+ if (fieldnames) {
+ fields = fields.filter((f) => fieldnames.includes(f.fieldname));
+ }
+
+ for (const field of fields) {
+ const { fieldname, fieldtype, label, meta } = field;
+ if (fieldtype === FieldTypeEnum.Attachment || meta) {
+ continue;
+ }
+
+ hints[fieldname] = label ?? fieldname;
+ const { target } = field as TargetField;
+ const targetSchema = fyo.schemaMap[target];
+ if (fieldtype === FieldTypeEnum.Link && targetSchema && linkLevel < 2) {
+ links[fieldname] = getPrintTemplateDocHints(
+ targetSchema,
+ fyo,
+ undefined,
+ linkLevel + 1
+ );
+ }
+
+ if (fieldtype === FieldTypeEnum.Table && targetSchema) {
+ hints[fieldname] = [getPrintTemplateDocHints(targetSchema, fyo)];
+ }
+ }
+
+ if (Object.keys(links).length) {
+ hints.links = links;
+ }
+ return hints;
+}
+
+async function getPrintTemplateDocValues(doc: Doc, fieldnames?: string[]) {
+ const values: PrintTemplateData = {};
+ if (!(doc instanceof Doc)) {
+ return values;
+ }
+
+ let fields = doc.schema.fields;
+ if (fieldnames) {
+ fields = fields.filter((f) => fieldnames.includes(f.fieldname));
+ }
+
+ // Set Formatted Doc Data
+ for (const field of fields) {
+ const { fieldname, fieldtype, meta } = field;
+ if (fieldtype === FieldTypeEnum.Attachment || meta) {
+ continue;
+ }
+
+ const value = doc.get(fieldname);
+
+ if (!value) {
+ values[fieldname] = '';
+ continue;
+ }
+
+ if (!Array.isArray(value)) {
+ values[fieldname] = doc.fyo.format(value, field, doc);
+ continue;
+ }
+
+ const table: PrintTemplateData[] = [];
+ for (const row of value) {
+ const rowProps = await getPrintTemplateDocValues(row);
+ table.push(rowProps);
+ }
+
+ values[fieldname] = table;
+ }
+
+ // Set Formatted Doc Link Data
+ await doc.loadLinks();
+ const links: PrintTemplateData = {};
+ for (const [linkName, linkDoc] of Object.entries(doc.links ?? {})) {
+ if (fieldnames && !fieldnames.includes(linkName)) {
+ continue;
+ }
+
+ links[linkName] = await getPrintTemplateDocValues(linkDoc);
+ }
+
+ if (Object.keys(links).length) {
+ values.links = links;
+ }
+ return values;
+}
+
+export async function getPathAndMakePDF(name: string, innerHTML: string) {
+ const { filePath } = await getSavePath(name, 'pdf');
+ if (!filePath) {
+ return;
+ }
+
+ const html = constructPrintDocument(innerHTML);
+ await makePDF(html, filePath);
+}
+
+function constructPrintDocument(innerHTML: string) {
+ const html = document.createElement('html');
+ const head = document.createElement('head');
+ const body = document.createElement('body');
+ const style = getAllCSSAsStyleElem();
+
+ head.innerHTML = [
+ '',
+ 'Print Window',
+ ].join('\n');
+ head.append(style);
+
+ body.innerHTML = innerHTML;
+ html.append(head, body);
+ return html.outerHTML;
+}
+
+function getAllCSSAsStyleElem() {
+ const cssTexts = [];
+ for (const sheet of document.styleSheets) {
+ for (const rule of sheet.cssRules) {
+ cssTexts.push(rule.cssText);
+ }
+
+ // @ts-ignore
+ for (const rule of sheet.ownerRule ?? []) {
+ cssTexts.push(rule.cssText);
+ }
+ }
+
+ const styleElem = document.createElement('style');
+ styleElem.innerHTML = cssTexts.join('\n');
+ return styleElem;
+}
+
+export async function updatePrintTemplates(fyo: Fyo) {
+ const templateFiles = await getTemplates();
+ const existingTemplates = (await fyo.db.getAll(ModelNameEnum.PrintTemplate, {
+ fields: ['name', 'modified'],
+ filters: { isCustom: false },
+ })) as { name: string; modified: Date }[];
+
+ const nameModifiedMap = getValueMapFromList(
+ existingTemplates,
+ 'name',
+ 'modified'
+ );
+
+ const updateList: TemplateUpdateItem[] = [];
+ for (const templateFile of templateFiles) {
+ const updates = getPrintTemplateUpdateList(
+ templateFile,
+ nameModifiedMap,
+ fyo
+ );
+
+ updateList.push(...updates);
+ }
+
+ for (const { name, type, template } of updateList) {
+ const doc = await getDocFromNameIfExistsElseNew(
+ ModelNameEnum.PrintTemplate,
+ name
+ );
+
+ await doc.set({ name, type, template, isCustom: false });
+ await doc.sync();
+ }
+}
+
+function getPrintTemplateUpdateList(
+ { file, template, modified: modifiedString }: TemplateFile,
+ nameModifiedMap: Record,
+ fyo: Fyo
+): TemplateUpdateItem[] {
+ const templateList: TemplateUpdateItem[] = [];
+ const dbModified = new Date(modifiedString);
+
+ for (const { name, type } of getNameAndTypeFromTemplateFile(file, fyo)) {
+ const fileModified = nameModifiedMap[name];
+ if (fileModified && dbModified.valueOf() >= fileModified.valueOf()) {
+ continue;
+ }
+
+ templateList.push({
+ name,
+ type,
+ template,
+ });
+ }
+ return templateList;
+}
+
+function getNameAndTypeFromTemplateFile(
+ file: string,
+ fyo: Fyo
+): { name: string; type: string }[] {
+ /**
+ * Template File Name Format:
+ * TemplateName[.SchemaName].template.html
+ *
+ * If the SchemaName is absent then it is assumed
+ * that the SchemaName is:
+ * - SalesInvoice
+ * - PurchaseInvoice
+ */
+
+ const fileName = file.split('.template.html')[0];
+ const name = fileName.split('.')[0];
+ const schemaName = fileName.split('.')[1];
+
+ if (schemaName) {
+ const label = fyo.schemaMap[schemaName]?.label ?? schemaName;
+ return [{ name: `${name} - ${label}`, type: schemaName }];
+ }
+
+ return [ModelNameEnum.SalesInvoice, ModelNameEnum.PurchaseInvoice].map(
+ (schemaName) => {
+ const label = fyo.schemaMap[schemaName]?.label ?? schemaName;
+ return { name: `${name} - ${label}`, type: schemaName };
+ }
+ );
+}
+
+export const baseTemplate = `
+
+
+
+
+ {{ print.companyName }}
+
+
+ {{ doc.name }}
+
+
+
+
+ Edit the code in the Template Editor on the right
+ to create your own personalized custom template.
+
+
+
+`;
diff --git a/src/utils/refs.ts b/src/utils/refs.ts
index c2f06744..1b0ace7e 100644
--- a/src/utils/refs.ts
+++ b/src/utils/refs.ts
@@ -1,6 +1,7 @@
import { reactive, ref } from 'vue';
import { FocusedDocContextSet } from './misc';
+export const showSidebar = ref(true);
export const docsPathRef = ref('');
export const systemLanguageRef = ref('');
export const focusedDocsRef = reactive(
diff --git a/src/utils/sidebarConfig.ts b/src/utils/sidebarConfig.ts
index a60e603c..baea72fc 100644
--- a/src/utils/sidebarConfig.ts
+++ b/src/utils/sidebarConfig.ts
@@ -272,6 +272,11 @@ async function getCompleteSidebar(): Promise {
name: 'import-wizard',
route: '/import-wizard',
},
+ {
+ label: t`Print Templates`,
+ name: 'print-template',
+ route: `/list/PrintTemplate/${t`Print Templates`}`,
+ },
{
label: t`Settings`,
name: 'settings',
diff --git a/src/utils/types.ts b/src/utils/types.ts
index cc71f09a..57e30d97 100644
--- a/src/utils/types.ts
+++ b/src/utils/types.ts
@@ -85,3 +85,8 @@ export type ActionGroup = {
export type UIGroupedFields = Map>;
export type ExportFormat = 'csv' | 'json';
export type PeriodKey = 'This Year' | 'This Quarter' | 'This Month';
+
+export type PrintValues = {
+ print: Record;
+ doc: Record;
+};
diff --git a/src/utils/ui.ts b/src/utils/ui.ts
index e6423f80..91cd3d1b 100644
--- a/src/utils/ui.ts
+++ b/src/utils/ui.ts
@@ -14,10 +14,13 @@ import { handleErrorWithDialog } from 'src/errorHandling';
import { fyo } from 'src/initFyo';
import router from 'src/router';
import { IPC_ACTIONS } from 'utils/messages';
+import { SelectFileOptions } from 'utils/types';
import { App, createApp, h } from 'vue';
import { RouteLocationRaw } from 'vue-router';
import { stringifyCircular } from './';
import { evaluateHidden } from './doc';
+import { selectFile } from './ipcCalls';
+import { showSidebar } from './refs';
import {
ActionGroup,
MessageDialogOptions,
@@ -141,8 +144,8 @@ function replaceAndAppendMount(app: App, replaceId: string) {
parent!.append(clone);
}
-export function openSettings(tab: SettingsTab) {
- routeTo({ path: '/settings', query: { tab } });
+export async function openSettings(tab: SettingsTab) {
+ await routeTo({ path: '/settings', query: { tab } });
}
export async function routeTo(route: RouteLocationRaw) {
@@ -337,18 +340,13 @@ function getDeleteAction(doc: Doc): Action {
};
}
-async function openEdit(doc: Doc) {
- const isFormEdit = [
- ModelNameEnum.SalesInvoice,
- ModelNameEnum.PurchaseInvoice,
- ModelNameEnum.JournalEntry,
- ].includes(doc.schemaName as ModelNameEnum);
-
- if (isFormEdit) {
- return await routeTo(`/edit/${doc.schemaName}/${doc.name!}`);
+async function openEdit({ name, schemaName }: Doc) {
+ if (!name) {
+ return;
}
- await openQuickEdit({ schemaName: doc.schemaName, name: doc.name! });
+ const route = getFormRoute(schemaName, name);
+ return await routeTo(route);
}
function getDuplicateAction(doc: Doc): Action {
@@ -369,7 +367,7 @@ function getDuplicateAction(doc: Doc): Action {
label: t`Yes`,
async action() {
try {
- const dupe = await doc.duplicate();
+ const dupe = doc.duplicate();
await openEdit(dupe);
return true;
} catch (err) {
@@ -450,3 +448,126 @@ export function getFormRoute(
return `/list/${schemaName}?edit=1&schemaName=${schemaName}&name=${name}`;
}
+
+export async function getDocFromNameIfExistsElseNew(
+ schemaName: string,
+ name?: string
+) {
+ if (!name) {
+ return fyo.doc.getNewDoc(schemaName);
+ }
+
+ try {
+ return await fyo.doc.getDoc(schemaName, name);
+ } catch {
+ return fyo.doc.getNewDoc(schemaName);
+ }
+}
+
+export async function isPrintable(schemaName: string) {
+ const numTemplates = await fyo.db.count(ModelNameEnum.PrintTemplate, {
+ filters: { type: schemaName },
+ });
+ return numTemplates > 0;
+}
+
+export function toggleSidebar(value?: boolean) {
+ if (typeof value !== 'boolean') {
+ value = !showSidebar.value;
+ }
+
+ showSidebar.value = value;
+}
+
+export function focusOrSelectFormControl(
+ doc: Doc,
+ ref: any,
+ clear: boolean = true
+) {
+ const naming = doc.fyo.schemaMap[doc.schemaName]?.naming;
+ if (naming !== 'manual' || doc.inserted) {
+ return;
+ }
+
+ if (Array.isArray(ref) && ref.length > 0) {
+ ref = ref[0];
+ }
+
+ if (!clear && typeof ref?.select === 'function') {
+ ref.select();
+ return;
+ }
+
+ if (typeof ref?.clear === 'function') {
+ ref.clear();
+ }
+
+ if (typeof ref?.focus === 'function') {
+ ref.focus();
+ }
+
+ doc.name = '';
+}
+
+export async function selectTextFile(filters?: SelectFileOptions['filters']) {
+ const options = {
+ title: t`Select File`,
+ filters,
+ };
+ const { success, canceled, filePath, data, name } = await selectFile(options);
+
+ if (canceled || !success) {
+ await showToast({
+ type: 'error',
+ message: t`File selection failed`,
+ });
+ return {};
+ }
+
+ const text = new TextDecoder().decode(data);
+ if (!text) {
+ await showToast({
+ type: 'error',
+ message: t`Empty file selected`,
+ });
+
+ return {};
+ }
+
+ return { text, filePath, name };
+}
+
+export enum ShortcutKey {
+ enter = 'enter',
+ ctrl = 'ctrl',
+ pmod = 'pmod',
+ shift = 'shift',
+ alt = 'alt',
+ delete = 'delete',
+ esc = 'esc',
+}
+
+export function getShortcutKeyMap(
+ platform: string
+): Record {
+ if (platform === 'Mac') {
+ return {
+ [ShortcutKey.alt]: '⌥',
+ [ShortcutKey.ctrl]: '⌃',
+ [ShortcutKey.pmod]: '⌘',
+ [ShortcutKey.shift]: 'shift',
+ [ShortcutKey.delete]: 'delete',
+ [ShortcutKey.esc]: 'esc',
+ [ShortcutKey.enter]: 'return',
+ };
+ }
+ return {
+ [ShortcutKey.alt]: 'Alt',
+ [ShortcutKey.ctrl]: 'Ctrl',
+ [ShortcutKey.pmod]: 'Ctrl',
+ [ShortcutKey.shift]: '⇧',
+ [ShortcutKey.delete]: 'Backspace',
+ [ShortcutKey.esc]: 'Esc',
+ [ShortcutKey.enter]: 'Enter',
+ };
+}
diff --git a/templates/Basic.template.html b/templates/Basic.template.html
new file mode 100644
index 00000000..1ac7a80f
--- /dev/null
+++ b/templates/Basic.template.html
@@ -0,0 +1,148 @@
+
+
+
+
+
+
+
+ {{ print.companyName }}
+
+
+
+
+
+ {{ print.email }}
+ {{ print.phone }}
+
+
+
+
+ {{ print.links.address.addressDisplay }}
+ GSTIN: {{ print.gstin }}
+
+
+
+
+
+
+
+ {{ doc.name }}
+ {{ doc.date }}
+
+
+
+
+ {{ doc.party }}
+
+ {{ doc.links.party.links.address.addressDisplay }}
+
+
+ GSTIN: {{ doc.partyGSTIN }}
+
+
+
+
+
+
+
+
+ {{ t`Item` }}
+
+ {{ t`HSN/SAC` }}
+
+ {{ t`Quantity` }}
+ {{ t`Rate` }}
+ {{ t`Amount` }}
+
+
+
+
+ {{ row.item }}
+
+ {{ row.hsnCode }}
+
+ {{ row.quantity }}
+ {{ row.rate }}
+ {{ row.amount }}
+
+
+
+
+
+
diff --git a/templates/Business.template.html b/templates/Business.template.html
new file mode 100644
index 00000000..ae3138a0
--- /dev/null
+++ b/templates/Business.template.html
@@ -0,0 +1,130 @@
+
+
+
+
+
+
+
+
+
+ {{ print.companyName }}
+
+
+ {{ print.links.address.addressDisplay }}
+
+
+ GSTIN: {{ print.gstin }}
+
+
+
+
+
+
+
+
+
+ {{ doc.entryType === 'SalesInvoice' ? 'Invoice' : 'Bill' }}
+
+
+
{{ doc.name }}
+
{{ doc.date }}
+
+
+
+
+
+
+ {{ doc.entryType === 'SalesInvoice' ? 'Customer' : 'Supplier' }}
+
+
+
+
{{ doc.party }}
+
+ {{ doc.links.party.links.address.addressDisplay }}
+
+
GSTIN: {{ doc.links.party.gstin }}
+
+
+
+
+
+
+
+
+
+ {{ t`Item` }}
+ {{ t`HSN/SAC` }}
+ {{ t`Quantity` }}
+ {{ t`Rate` }}
+ {{ t`Amount` }}
+
+
+
+
+ {{ row.item }}
+ {{ row.hsnCode }}
+ {{ row.quantity }}
+ {{ row.rate }}
+ {{ row.amount }}
+
+
+
+
+
+
diff --git a/templates/Minimal.template.html b/templates/Minimal.template.html
new file mode 100644
index 00000000..6b158bb4
--- /dev/null
+++ b/templates/Minimal.template.html
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+ {{ print.companyName }}
+
+
{{ doc.date }}
+
+
+
+
+
+
+ {{ doc.entryType }}
+
+ {{ doc.name }}
+
+
+
+
+
+
+
+
+ {{ doc.entryType === 'SalesInvoice' ? 'To' : 'From' }}
+
+ {{ doc.party }}
+
+ {{ doc.links.party.links.address.addressDisplay ?? '' }}
+
+
+ GSTIN: {{ doc.links.party.gstin }}
+
+
+
+
+
+
+ {{ doc.entryType === 'SalesInvoice' ? 'From' : 'To' }}
+
+
+ {{ print.companyName }}
+
+
+ {{ print.links.address.addressDisplay }}
+
+
+ GSTIN: {{ print.gstin }}
+
+
+
+
+
+
+
+
+ {{ t`Item` }}
+ {{ t`HSN/SAC` }}
+ {{ t`Quantity` }}
+ {{ t`Rate` }}
+ {{ t`Amount`}}
+
+
+
+
+ {{ row.item }}
+ {{ row.hsnCode }}
+ {{ row.quantity }}
+ {{ row.rate }}
+ {{ row.amount }}
+
+
+
+
+
+
diff --git a/utils/messages.ts b/utils/messages.ts
index a69d9b48..7340d4fb 100644
--- a/utils/messages.ts
+++ b/utils/messages.ts
@@ -22,6 +22,7 @@ export enum IPC_ACTIONS {
SELECT_FILE = 'select-file',
GET_CREDS = 'get-creds',
GET_DB_LIST = 'get-db-list',
+ GET_TEMPLATES = 'get-templates',
DELETE_FILE = 'delete-file',
// Database messages
DB_CREATE = 'db-create',
diff --git a/utils/types.ts b/utils/types.ts
index 8bdfe203..af9a5e20 100644
--- a/utils/types.ts
+++ b/utils/types.ts
@@ -23,14 +23,18 @@ export interface VersionParts {
beta?: number;
}
-export type Creds = { errorLogUrl: string; telemetryUrl: string; tokenString: string };
+export type Creds = {
+ errorLogUrl: string;
+ telemetryUrl: string;
+ tokenString: string;
+};
export type UnexpectedLogObject = {
name: string;
message: string;
stack: string;
more: Record;
-}
+};
export interface SelectFileOptions {
title: string;
@@ -48,3 +52,5 @@ export interface SelectFileReturn {
export type PropertyEnum> = {
[key in keyof Required]: key;
};
+
+export type TemplateFile = { file: string; template: string; modified: string };
diff --git a/yarn.lock b/yarn.lock
index 9983d6af..a24f1143 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -962,6 +962,120 @@
"@babel/helper-validator-identifier" "^7.15.7"
to-fast-properties "^2.0.0"
+"@codemirror/autocomplete@^6.0.0", "@codemirror/autocomplete@^6.4.2":
+ version "6.4.2"
+ resolved "https://registry.yarnpkg.com/@codemirror/autocomplete/-/autocomplete-6.4.2.tgz#938b25223bd21f97b2a6d85474643355f98b505b"
+ integrity sha512-8WE2xp+D0MpWEv5lZ6zPW1/tf4AGb358T5GWYiKEuCP8MvFfT3tH2mIF9Y2yr2e3KbHuSvsVhosiEyqCpiJhZQ==
+ dependencies:
+ "@codemirror/language" "^6.0.0"
+ "@codemirror/state" "^6.0.0"
+ "@codemirror/view" "^6.6.0"
+ "@lezer/common" "^1.0.0"
+
+"@codemirror/commands@^6.0.0":
+ version "6.2.1"
+ resolved "https://registry.yarnpkg.com/@codemirror/commands/-/commands-6.2.1.tgz#ab5e979ad1458bbe395bf69ac601f461ac73cf08"
+ integrity sha512-FFiNKGuHA5O8uC6IJE5apI5rT9gyjlw4whqy4vlcX0wE/myxL6P1s0upwDhY4HtMWLOwzwsp0ap3bjdQhvfDOA==
+ dependencies:
+ "@codemirror/language" "^6.0.0"
+ "@codemirror/state" "^6.2.0"
+ "@codemirror/view" "^6.0.0"
+ "@lezer/common" "^1.0.0"
+
+"@codemirror/lang-css@^6.0.0":
+ version "6.1.0"
+ resolved "https://registry.yarnpkg.com/@codemirror/lang-css/-/lang-css-6.1.0.tgz#a40e6b772f4e98fd7c6f84061a0a838cabc3f082"
+ integrity sha512-GYn4TyMvQLrkrhdisFh8HCTDAjPY/9pzwN12hG9UdrTUxRUMicF+8GS24sFEYaleaG1KZClIFLCj0Rol/WO24w==
+ dependencies:
+ "@codemirror/autocomplete" "^6.0.0"
+ "@codemirror/language" "^6.0.0"
+ "@codemirror/state" "^6.0.0"
+ "@lezer/css" "^1.0.0"
+
+"@codemirror/lang-html@^6.0.0":
+ version "6.4.2"
+ resolved "https://registry.yarnpkg.com/@codemirror/lang-html/-/lang-html-6.4.2.tgz#3c7117e45bae009bc7bc08eef8a79b5d05930d83"
+ integrity sha512-bqCBASkteKySwtIbiV/WCtGnn/khLRbbiV5TE+d9S9eQJD7BA4c5dTRm2b3bVmSpilff5EYxvB4PQaZzM/7cNw==
+ dependencies:
+ "@codemirror/autocomplete" "^6.0.0"
+ "@codemirror/lang-css" "^6.0.0"
+ "@codemirror/lang-javascript" "^6.0.0"
+ "@codemirror/language" "^6.4.0"
+ "@codemirror/state" "^6.0.0"
+ "@codemirror/view" "^6.2.2"
+ "@lezer/common" "^1.0.0"
+ "@lezer/css" "^1.1.0"
+ "@lezer/html" "^1.3.0"
+
+"@codemirror/lang-javascript@^6.0.0", "@codemirror/lang-javascript@^6.1.2":
+ version "6.1.4"
+ resolved "https://registry.yarnpkg.com/@codemirror/lang-javascript/-/lang-javascript-6.1.4.tgz#8a41f4d213e1143b4eef6f65f8b77b349aaf894c"
+ integrity sha512-OxLf7OfOZBTMRMi6BO/F72MNGmgOd9B0vetOLvHsDACFXayBzW8fm8aWnDM0yuy68wTK03MBf4HbjSBNRG5q7A==
+ dependencies:
+ "@codemirror/autocomplete" "^6.0.0"
+ "@codemirror/language" "^6.6.0"
+ "@codemirror/lint" "^6.0.0"
+ "@codemirror/state" "^6.0.0"
+ "@codemirror/view" "^6.0.0"
+ "@lezer/common" "^1.0.0"
+ "@lezer/javascript" "^1.0.0"
+
+"@codemirror/lang-vue@^0.1.1":
+ version "0.1.1"
+ resolved "https://registry.yarnpkg.com/@codemirror/lang-vue/-/lang-vue-0.1.1.tgz#79567fb3be3f411354cd135af59d67f956cdb042"
+ integrity sha512-GIfc/MemCFKUdNSYGTFZDN8XsD2z0DUY7DgrK34on0dzdZ/CawZbi+SADYfVzWoPPdxngHzLhqlR5pSOqyPCvA==
+ dependencies:
+ "@codemirror/lang-html" "^6.0.0"
+ "@codemirror/lang-javascript" "^6.1.2"
+ "@codemirror/language" "^6.0.0"
+ "@lezer/common" "^1.0.0"
+ "@lezer/highlight" "^1.0.0"
+ "@lezer/lr" "^1.3.1"
+
+"@codemirror/language@^6.0.0", "@codemirror/language@^6.4.0", "@codemirror/language@^6.6.0":
+ version "6.6.0"
+ resolved "https://registry.yarnpkg.com/@codemirror/language/-/language-6.6.0.tgz#2204407174a38a68053715c19e28ad61f491779f"
+ integrity sha512-cwUd6lzt3MfNYOobdjf14ZkLbJcnv4WtndYaoBkbor/vF+rCNguMPK0IRtvZJG4dsWiaWPcK8x1VijhvSxnstg==
+ dependencies:
+ "@codemirror/state" "^6.0.0"
+ "@codemirror/view" "^6.0.0"
+ "@lezer/common" "^1.0.0"
+ "@lezer/highlight" "^1.0.0"
+ "@lezer/lr" "^1.0.0"
+ style-mod "^4.0.0"
+
+"@codemirror/lint@^6.0.0":
+ version "6.2.0"
+ resolved "https://registry.yarnpkg.com/@codemirror/lint/-/lint-6.2.0.tgz#25cdab7425fcda1b38a9d63f230f833c8b6b369f"
+ integrity sha512-KVCECmR2fFeYBr1ZXDVue7x3q5PMI0PzcIbA+zKufnkniMBo1325t0h1jM85AKp8l3tj67LRxVpZfgDxEXlQkg==
+ dependencies:
+ "@codemirror/state" "^6.0.0"
+ "@codemirror/view" "^6.0.0"
+ crelt "^1.0.5"
+
+"@codemirror/search@^6.0.0":
+ version "6.2.3"
+ resolved "https://registry.yarnpkg.com/@codemirror/search/-/search-6.2.3.tgz#fab933fef1b1de8ef40cda275c73d9ac7a1ff40f"
+ integrity sha512-V9n9233lopQhB1dyjsBK2Wc1i+8hcCqxl1wQ46c5HWWLePoe4FluV3TGHoZ04rBRlGjNyz9DTmpJErig8UE4jw==
+ dependencies:
+ "@codemirror/state" "^6.0.0"
+ "@codemirror/view" "^6.0.0"
+ crelt "^1.0.5"
+
+"@codemirror/state@^6.0.0", "@codemirror/state@^6.1.4", "@codemirror/state@^6.2.0":
+ version "6.2.0"
+ resolved "https://registry.yarnpkg.com/@codemirror/state/-/state-6.2.0.tgz#a0fb08403ced8c2a68d1d0acee926bd20be922f2"
+ integrity sha512-69QXtcrsc3RYtOtd+GsvczJ319udtBf1PTrr2KbLWM/e2CXUPnh0Nz9AUo8WfhSQ7GeL8dPVNUmhQVgpmuaNGA==
+
+"@codemirror/view@^6.0.0", "@codemirror/view@^6.2.2", "@codemirror/view@^6.6.0":
+ version "6.9.1"
+ resolved "https://registry.yarnpkg.com/@codemirror/view/-/view-6.9.1.tgz#2ce4c528974b6172a5a4a738b7b0a0f04a4c1140"
+ integrity sha512-bzfSjJn9dAADVpabLKWKNmMG4ibyTV2e3eOGowjElNPTdTkSbi6ixPYHm2u0ADcETfKsi2/R84Rkmi91dH9yEg==
+ dependencies:
+ "@codemirror/state" "^6.1.4"
+ style-mod "^4.0.0"
+ w3c-keyname "^2.2.4"
+
"@cspotcode/source-map-consumer@0.8.0":
version "0.8.0"
resolved "https://registry.yarnpkg.com/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz#33bf4b7b39c178821606f669bbc447a6a629786b"
@@ -1195,6 +1309,50 @@
"@jridgewell/resolve-uri" "^3.0.3"
"@jridgewell/sourcemap-codec" "^1.4.10"
+"@lezer/common@^1.0.0":
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/@lezer/common/-/common-1.0.2.tgz#8fb9b86bdaa2ece57e7d59e5ffbcb37d71815087"
+ integrity sha512-SVgiGtMnMnW3ActR8SXgsDhw7a0w0ChHSYAyAUxxrOiJ1OqYWEKk/xJd84tTSPo1mo6DXLObAJALNnd0Hrv7Ng==
+
+"@lezer/css@^1.0.0", "@lezer/css@^1.1.0":
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/@lezer/css/-/css-1.1.1.tgz#c36dcb0789317cb80c3740767dd3b85e071ad082"
+ integrity sha512-mSjx+unLLapEqdOYDejnGBokB5+AiJKZVclmud0MKQOKx3DLJ5b5VTCstgDDknR6iIV4gVrN6euzsCnj0A2gQA==
+ dependencies:
+ "@lezer/highlight" "^1.0.0"
+ "@lezer/lr" "^1.0.0"
+
+"@lezer/highlight@^1.0.0", "@lezer/highlight@^1.1.3":
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/@lezer/highlight/-/highlight-1.1.3.tgz#bf5a36c2ee227f526d74997ac91f7777e29bd25d"
+ integrity sha512-3vLKLPThO4td43lYRBygmMY18JN3CPh9w+XS2j8WC30vR4yZeFG4z1iFe4jXE43NtGqe//zHW5q8ENLlHvz9gw==
+ dependencies:
+ "@lezer/common" "^1.0.0"
+
+"@lezer/html@^1.3.0":
+ version "1.3.3"
+ resolved "https://registry.yarnpkg.com/@lezer/html/-/html-1.3.3.tgz#2eddae2ad000f9b184d9fc4686394d0fa0849993"
+ integrity sha512-04Fyvu66DjV2EjhDIG1kfDdktn5Pfw56SXPrzKNQH5B2m7BDfc6bDsz+ZJG8dLS3kIPEKbyyq1Sm2/kjeG0+AA==
+ dependencies:
+ "@lezer/common" "^1.0.0"
+ "@lezer/highlight" "^1.0.0"
+ "@lezer/lr" "^1.0.0"
+
+"@lezer/javascript@^1.0.0":
+ version "1.4.1"
+ resolved "https://registry.yarnpkg.com/@lezer/javascript/-/javascript-1.4.1.tgz#97a15042c76b5979af6a069fac83cf6485628cbf"
+ integrity sha512-Hqx36DJeYhKtdpc7wBYPR0XF56ZzIp0IkMO/zNNj80xcaFOV4Oj/P7TQc/8k2TxNhzl7tV5tXS8ZOCPbT4L3nA==
+ dependencies:
+ "@lezer/highlight" "^1.1.3"
+ "@lezer/lr" "^1.3.0"
+
+"@lezer/lr@^1.0.0", "@lezer/lr@^1.3.0", "@lezer/lr@^1.3.1":
+ version "1.3.3"
+ resolved "https://registry.yarnpkg.com/@lezer/lr/-/lr-1.3.3.tgz#0ac6c889f1235874f33c45a1b9785d7054f60708"
+ integrity sha512-JPQe3mwJlzEVqy67iQiiGozhcngbO8QBgpqZM6oL1Wj/dXckrEexpBLeFkq0edtW5IqnPRFxA24BHJni8Js69w==
+ dependencies:
+ "@lezer/common" "^1.0.0"
+
"@malept/cross-spawn-promise@^1.1.0":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz#504af200af6b98e198bce768bc1730c6936ae01d"
@@ -3903,6 +4061,19 @@ code-point-at@^1.0.0:
resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=
+codemirror@^6.0.1:
+ version "6.0.1"
+ resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-6.0.1.tgz#62b91142d45904547ee3e0e0e4c1a79158035a29"
+ integrity sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==
+ dependencies:
+ "@codemirror/autocomplete" "^6.0.0"
+ "@codemirror/commands" "^6.0.0"
+ "@codemirror/language" "^6.0.0"
+ "@codemirror/lint" "^6.0.0"
+ "@codemirror/search" "^6.0.0"
+ "@codemirror/state" "^6.0.0"
+ "@codemirror/view" "^6.0.0"
+
collection-visit@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0"
@@ -4295,6 +4466,11 @@ create-require@^1.1.0:
resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333"
integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==
+crelt@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/crelt/-/crelt-1.0.5.tgz#57c0d52af8c859e354bace1883eb2e1eb182bb94"
+ integrity sha512-+BO9wPPi+DWTDcNYhr/W90myha8ptzftZT+LwcmUbbok0rcP/fequmFYCw8NMoH7pkAZQzU78b3kYrlua5a9eA==
+
cross-spawn@^5.0.1:
version "5.1.0"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449"
@@ -11545,6 +11721,11 @@ strip-json-comments@~2.0.1:
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo=
+style-mod@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/style-mod/-/style-mod-4.0.0.tgz#97e7c2d68b592975f2ca7a63d0dd6fcacfe35a01"
+ integrity sha512-OPhtyEjyyN9x3nhPsu76f52yUGXiZcgvsrFVtvTkyGRQJ0XK+GPc6ov1z+lRpbeabka+MYEQxOYRnt5nF30aMw==
+
stylehacks@^4.0.0:
version "4.0.3"
resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-4.0.3.tgz#6718fcaf4d1e07d8a1318690881e8d96726a71d5"
@@ -12635,6 +12816,11 @@ vue@^3.2.40:
"@vue/server-renderer" "3.2.40"
"@vue/shared" "3.2.40"
+w3c-keyname@^2.2.4:
+ version "2.2.6"
+ resolved "https://registry.yarnpkg.com/w3c-keyname/-/w3c-keyname-2.2.6.tgz#8412046116bc16c5d73d4e612053ea10a189c85f"
+ integrity sha512-f+fciywl1SJEniZHD6H+kUO8gOnwIr7f4ijKA6+ZvJFjeGi1r4PDLl53Ayud9O/rk64RqgoQine0feoeOU0kXg==
+
watchpack-chokidar2@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/watchpack-chokidar2/-/watchpack-chokidar2-2.0.1.tgz#38500072ee6ece66f3769936950ea1771be1c957"