mirror of
https://github.com/frappe/books.git
synced 2024-12-23 11:29:03 +00:00
Merge pull request #558 from frappe/custom-templates
feat: Template Builder
This commit is contained in:
commit
bc1e8d21c4
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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));
|
||||
|
2
main.ts
2
main.ts
@ -4,7 +4,7 @@ import {
|
||||
app,
|
||||
BrowserWindow,
|
||||
BrowserWindowConstructorOptions,
|
||||
protocol
|
||||
protocol,
|
||||
} from 'electron';
|
||||
import Store from 'electron-store';
|
||||
import { autoUpdater } from 'electron-updater';
|
||||
|
41
main/getPrintTemplates.ts
Normal file
41
main/getPrintTemplates.ts
Normal file
@ -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;
|
||||
}
|
||||
}
|
@ -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
|
||||
*/
|
||||
|
@ -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 = {
|
||||
|
@ -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(),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
@ -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 = {};
|
||||
}
|
||||
|
81
models/baseModels/PrintTemplate.ts
Normal file
81
models/baseModels/PrintTemplate.ts
Normal file
@ -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;
|
||||
}
|
||||
}
|
@ -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';
|
||||
|
@ -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
|
||||
|
@ -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: `<span>${label}</span>`,
|
||||
};
|
||||
display(value): string {
|
||||
return movementTypeMap[value as MovementType] ?? '';
|
||||
},
|
||||
},
|
||||
],
|
||||
|
@ -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',
|
||||
|
@ -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",
|
||||
|
@ -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 });
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
34
schemas/app/PrintTemplate.json
Normal file
34
schemas/app/PrintTemplate.json
Normal file
@ -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
|
||||
}
|
||||
]
|
||||
}
|
@ -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,
|
||||
];
|
||||
|
@ -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() {
|
||||
|
@ -13,6 +13,7 @@
|
||||
</div>
|
||||
<div
|
||||
class="flex items-center justify-between pe-2 rounded"
|
||||
:style="containerStyles"
|
||||
:class="containerClasses"
|
||||
>
|
||||
<input
|
||||
|
@ -13,8 +13,10 @@
|
||||
:value="value"
|
||||
:placeholder="inputPlaceholder"
|
||||
:readonly="isReadOnly"
|
||||
:step="step"
|
||||
:max="df.maxvalue"
|
||||
:min="df.minvalue"
|
||||
:style="containerStyles"
|
||||
@blur="(e) => !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 },
|
||||
|
@ -44,6 +44,7 @@
|
||||
:placeholder="t`Custom Hex`"
|
||||
:class="[inputClasses, containerClasses]"
|
||||
:value="value"
|
||||
style="padding: 0"
|
||||
@change="(e) => setColorValue(e.target.value)"
|
||||
/>
|
||||
</div>
|
||||
|
@ -54,6 +54,9 @@ export default {
|
||||
input.value = '';
|
||||
}
|
||||
},
|
||||
select() {
|
||||
this.$refs.control.$refs?.input?.select()
|
||||
},
|
||||
focus() {
|
||||
this.$refs.control.focus();
|
||||
},
|
||||
|
@ -3,6 +3,9 @@
|
||||
<div class="flex flex-1 flex-col">
|
||||
<!-- Page Header (Title, Buttons, etc) -->
|
||||
<PageHeader :title="title" :border="false" :searchborder="searchborder">
|
||||
<template #left>
|
||||
<slot name="header-left" />
|
||||
</template>
|
||||
<slot name="header" />
|
||||
</PageHeader>
|
||||
|
||||
|
118
src/components/HorizontalResizer.vue
Normal file
118
src/components/HorizontalResizer.vue
Normal file
@ -0,0 +1,118 @@
|
||||
<template>
|
||||
<div
|
||||
class="h-full bg-gray-300 transition-opacity hover:opacity-100"
|
||||
:class="resizing ? 'opacity-100' : 'opacity-0'"
|
||||
style="width: 3px; cursor: col-resize; margin-left: -3px"
|
||||
@mousedown="onMouseDown"
|
||||
ref="hr"
|
||||
>
|
||||
<MouseFollower
|
||||
:show="resizing"
|
||||
placement="left"
|
||||
class="
|
||||
px-1
|
||||
py-0.5
|
||||
border
|
||||
rounded-md
|
||||
shadow
|
||||
text-sm text-center
|
||||
bg-gray-900
|
||||
text-gray-100
|
||||
"
|
||||
style="min-width: 2rem"
|
||||
>
|
||||
{{ value }}
|
||||
</MouseFollower>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import MouseFollower from './MouseFollower.vue';
|
||||
|
||||
export default defineComponent({
|
||||
emits: ['resize'],
|
||||
props: {
|
||||
initialX: { type: Number, required: true },
|
||||
minX: Number,
|
||||
maxX: Number,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
x: 0,
|
||||
delta: 0,
|
||||
xOnMouseDown: 0,
|
||||
resizing: false,
|
||||
listener: null,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
onMouseDown(e: MouseEvent) {
|
||||
e.preventDefault();
|
||||
|
||||
this.x = e.clientX;
|
||||
this.xOnMouseDown = this.initialX;
|
||||
this.setResizing(true);
|
||||
|
||||
document.addEventListener('mousemove', this.mouseMoveListener);
|
||||
document.addEventListener('mouseup', this.mouseUpListener);
|
||||
},
|
||||
mouseUpListener(e: MouseEvent) {
|
||||
e.preventDefault();
|
||||
|
||||
this.x = e.clientX;
|
||||
this.setResizing(false);
|
||||
|
||||
this.$emit('resize', this.value);
|
||||
this.removeListeners();
|
||||
},
|
||||
mouseMoveListener(e: MouseEvent) {
|
||||
e.preventDefault();
|
||||
this.delta = this.x - e.clientX;
|
||||
this.$emit('resize', this.value);
|
||||
},
|
||||
removeListeners() {
|
||||
document.removeEventListener('mousemove', this.mouseMoveListener);
|
||||
document.removeEventListener('mouseup', this.mouseUpListener);
|
||||
},
|
||||
setResizing(value: boolean) {
|
||||
this.resizing = value;
|
||||
|
||||
if (value) {
|
||||
this.delta = 0;
|
||||
document.body.style.cursor = 'col-resize';
|
||||
} else {
|
||||
document.body.style.cursor = '';
|
||||
}
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
value() {
|
||||
let value = this.delta + this.xOnMouseDown;
|
||||
if (typeof this.minX === 'number') {
|
||||
value = Math.max(this.minX, value);
|
||||
}
|
||||
|
||||
if (typeof this.maxX === 'number') {
|
||||
value = Math.min(this.maxX, value);
|
||||
}
|
||||
|
||||
return value;
|
||||
},
|
||||
minDelta() {
|
||||
if (typeof this.minX !== 'number') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.initialX - this.minX;
|
||||
},
|
||||
maxDelta() {
|
||||
if (typeof this.maxX !== 'number') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.maxX - this.initialX;
|
||||
},
|
||||
},
|
||||
components: { MouseFollower },
|
||||
});
|
||||
</script>
|
@ -8,14 +8,17 @@
|
||||
>
|
||||
<Transition name="spacer">
|
||||
<div
|
||||
v-if="!sidebar && platform === 'Mac' && languageDirection !== 'rtl'"
|
||||
v-if="!showSidebar && platform === 'Mac' && languageDirection !== 'rtl'"
|
||||
class="h-full"
|
||||
:class="sidebar ? '' : 'w-tl me-4 border-e'"
|
||||
:class="showSidebar ? '' : 'w-tl me-4 border-e'"
|
||||
/>
|
||||
</Transition>
|
||||
<h1 class="text-xl font-semibold select-none" v-if="title">
|
||||
{{ title }}
|
||||
</h1>
|
||||
<div class="flex items-stretch window-no-drag gap-2">
|
||||
<slot name="left" />
|
||||
</div>
|
||||
<div
|
||||
class="flex items-stretch window-no-drag gap-2 ms-auto"
|
||||
:class="platform === 'Mac' && languageDirection === 'rtl' ? 'me-18' : ''"
|
||||
@ -28,12 +31,13 @@
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { showSidebar } from 'src/utils/refs';
|
||||
import { Transition } from 'vue';
|
||||
import BackLink from './BackLink.vue';
|
||||
import SearchBar from './SearchBar.vue';
|
||||
|
||||
export default {
|
||||
inject: ['sidebar', 'languageDirection'],
|
||||
inject: ['languageDirection'],
|
||||
props: {
|
||||
title: { type: String, default: '' },
|
||||
backLink: { type: Boolean, default: true },
|
||||
@ -42,6 +46,9 @@ export default {
|
||||
searchborder: { type: Boolean, default: true },
|
||||
},
|
||||
components: { SearchBar, BackLink, Transition },
|
||||
setup() {
|
||||
return { showSidebar };
|
||||
},
|
||||
computed: {
|
||||
showBorder() {
|
||||
return !!this.$slots.default && this.searchborder;
|
||||
@ -59,7 +66,7 @@ export default {
|
||||
opacity: 0;
|
||||
width: 0px;
|
||||
margin-right: 0px;
|
||||
border-eight-width: 0px;
|
||||
border-right-width: 0px;
|
||||
}
|
||||
|
||||
.spacer-enter-to,
|
||||
@ -67,7 +74,7 @@ export default {
|
||||
opacity: 1;
|
||||
width: var(--w-trafficlights);
|
||||
margin-right: 1rem;
|
||||
border-eight-width: 1px;
|
||||
border-right-width: 1px;
|
||||
}
|
||||
|
||||
.spacer-enter-active,
|
||||
|
@ -1,28 +0,0 @@
|
||||
<template>
|
||||
<component :is="printComponent" :doc="doc" :print-settings="printSettings" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Basic from './Templates/Basic';
|
||||
import Minimal from './Templates/Minimal';
|
||||
import Business from './Templates/Business';
|
||||
|
||||
export default {
|
||||
name: 'InvoiceTemplate',
|
||||
props: ['doc', 'printSettings'],
|
||||
computed: {
|
||||
printComponent() {
|
||||
let type = this.printSettings.template;
|
||||
let templates = {
|
||||
Basic,
|
||||
Minimal,
|
||||
Business
|
||||
};
|
||||
if (!(type in templates)) {
|
||||
type = 'Basic';
|
||||
}
|
||||
return templates[type];
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
@ -1,109 +0,0 @@
|
||||
<script>
|
||||
export default {
|
||||
name: 'Base',
|
||||
props: { doc: Object, printSettings: Object },
|
||||
data: () => ({ party: null, companyAddress: null, partyAddress: null }),
|
||||
async mounted() {
|
||||
await this.printSettings.loadLinks();
|
||||
this.companyAddress = this.printSettings.getLink('address');
|
||||
|
||||
await this.doc.loadLinks();
|
||||
this.party = this.doc.getLink('party');
|
||||
this.partyAddress = this.party.getLink('address')?.addressDisplay ?? null;
|
||||
|
||||
if (this.fyo.store.isDevelopment) {
|
||||
window.bt = this;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getFormattedField(fieldname, doc) {
|
||||
doc ??= this.doc;
|
||||
const field = doc.fieldMap[fieldname];
|
||||
const value = doc.get(fieldname);
|
||||
if (Array.isArray(value)) {
|
||||
return this.getFormattedChildDocList(fieldname);
|
||||
}
|
||||
return this.fyo.format(value, field, doc);
|
||||
},
|
||||
getFormattedChildDocList(fieldname) {
|
||||
const formattedDocs = [];
|
||||
for (const childDoc of this.doc?.[fieldname] ?? {}) {
|
||||
formattedDocs.push(this.getFormattedChildDoc(childDoc));
|
||||
}
|
||||
return formattedDocs;
|
||||
},
|
||||
getFormattedChildDoc(childDoc) {
|
||||
const formattedChildDoc = {};
|
||||
for (const field of childDoc?.schema?.fields) {
|
||||
if (field.meta) {
|
||||
continue;
|
||||
}
|
||||
|
||||
formattedChildDoc[field.fieldname] = this.getFormattedField(
|
||||
field.fieldname,
|
||||
childDoc
|
||||
);
|
||||
}
|
||||
return formattedChildDoc;
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
currency() {
|
||||
return this.doc.isMultiCurrency
|
||||
? this.doc.currency
|
||||
: this.fyo.singles.SystemSettings.currency;
|
||||
},
|
||||
isSalesInvoice() {
|
||||
return this.doc.schemaName === 'SalesInvoice';
|
||||
},
|
||||
showHSN() {
|
||||
return this.doc.items.map((i) => i.hsnCode).every(Boolean);
|
||||
},
|
||||
totalDiscount() {
|
||||
return this.doc.getTotalDiscount();
|
||||
},
|
||||
formattedTotalDiscount() {
|
||||
if (!this.totalDiscount?.float) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const totalDiscount = this.fyo.format(this.totalDiscount, {
|
||||
fieldname: 'Total Discount',
|
||||
fieldtype: 'Currency',
|
||||
currency: this.currency,
|
||||
});
|
||||
|
||||
return `- ${totalDiscount}`;
|
||||
},
|
||||
printObject() {
|
||||
return {
|
||||
isSalesInvoice: this.isSalesInvoice,
|
||||
font: this.printSettings.font,
|
||||
color: this.printSettings.color,
|
||||
showHSN: this.showHSN,
|
||||
displayLogo: this.printSettings.displayLogo,
|
||||
displayTaxInvoice: this.printSettings.displayTaxInvoice,
|
||||
displayBatch: this.printSettings.displayBatch,
|
||||
discountAfterTax: this.doc.discountAfterTax,
|
||||
logo: this.printSettings.logo,
|
||||
companyName: this.fyo.singles.AccountingSettings.companyName,
|
||||
email: this.printSettings.email,
|
||||
phone: this.printSettings.phone,
|
||||
address: this.companyAddress?.addressDisplay,
|
||||
gstin: this.fyo.singles?.AccountingSettings?.gstin,
|
||||
invoiceName: this.doc.name,
|
||||
date: this.getFormattedField('date'),
|
||||
partyName: this.party?.name,
|
||||
partyAddress: this.partyAddress,
|
||||
partyGSTIN: this.party?.gstin,
|
||||
terms: this.doc.terms,
|
||||
netTotal: this.getFormattedField('netTotal'),
|
||||
items: this.getFormattedField('items'),
|
||||
taxes: this.getFormattedField('taxes'),
|
||||
grandTotal: this.getFormattedField('grandTotal'),
|
||||
totalDiscount: this.formattedTotalDiscount,
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
@ -1,172 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
class="bg-white border h-full"
|
||||
:style="{ 'font-family': printObject.font }"
|
||||
>
|
||||
<div>
|
||||
<div class="px-6 pt-6">
|
||||
<h2
|
||||
v-if="printObject.displayTaxInvoice"
|
||||
class="font-semibold text-black text-2xl mb-4"
|
||||
>
|
||||
{{ t`Tax Invoice` }}
|
||||
</h2>
|
||||
<div class="flex text-sm text-gray-900 border-b pb-4">
|
||||
<div class="w-1/3">
|
||||
<div v-if="printObject.displayLogo">
|
||||
<img
|
||||
class="h-12 max-w-32 object-contain"
|
||||
:src="printObject.logo"
|
||||
/>
|
||||
</div>
|
||||
<div class="text-xl text-gray-700 font-semibold" v-else>
|
||||
{{ printObject.companyName }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-1/3">
|
||||
<div>{{ printObject.email }}</div>
|
||||
<div class="mt-1">{{ printObject.phone }}</div>
|
||||
</div>
|
||||
<div class="w-1/3">
|
||||
<div v-if="printObject.address">
|
||||
{{ printObject.address }}
|
||||
</div>
|
||||
<div v-if="printObject.gstin">GSTIN: {{ printObject.gstin }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-8 px-6">
|
||||
<div class="flex justify-between">
|
||||
<div class="w-1/3">
|
||||
<h1 class="text-2xl font-semibold">
|
||||
{{ printObject.invoiceName }}
|
||||
</h1>
|
||||
<div class="py-2 text-base">
|
||||
{{ printObject.date }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-1/3" v-if="printObject.partyName">
|
||||
<div class="py-1 text-end text-lg font-semibold">
|
||||
{{ printObject.partyName }}
|
||||
</div>
|
||||
<div
|
||||
v-if="printObject.partyAddress"
|
||||
class="mt-1 text-xs text-gray-600 text-end"
|
||||
>
|
||||
{{ printObject.partyAddress }}
|
||||
</div>
|
||||
<div
|
||||
v-if="printObject.partyGSTIN"
|
||||
class="mt-1 text-xs text-gray-600 text-end"
|
||||
>
|
||||
GSTIN: {{ printObject.partyGSTIN }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 px-6 text-base">
|
||||
<div>
|
||||
<div class="text-gray-600 w-full flex border-b">
|
||||
<div class="py-4 w-5/12">Item</div>
|
||||
<div class="py-4 text-end w-2/12" v-if="printObject.showHSN">
|
||||
HSN/SAC
|
||||
</div>
|
||||
<div class="py-4 text-end w-2/12">Quantity</div>
|
||||
<div
|
||||
class="w-3/12 text-end py-4"
|
||||
v-if="printObject.displayBatch"
|
||||
>
|
||||
Batch
|
||||
</div>
|
||||
<div class="py-4 text-end w-3/12">Rate</div>
|
||||
<div class="py-4 text-end w-3/12">Amount</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex py-1 text-gray-900 w-full border-b"
|
||||
v-for="row in printObject.items"
|
||||
:key="row.name"
|
||||
>
|
||||
<div class="w-5/12 py-4">{{ row.item }}</div>
|
||||
<div class="w-2/12 text-end py-4" v-if="printObject.showHSN">
|
||||
{{ row.hsnCode }}
|
||||
</div>
|
||||
<div class="w-2/12 text-end py-4">{{ row.quantity }}</div>
|
||||
<div
|
||||
class="w-3/12 text-end py-4"
|
||||
v-if="printObject.displayBatch"
|
||||
>
|
||||
{{ row.batch }}
|
||||
</div>
|
||||
<div class="w-3/12 text-end py-4">{{ row.rate }}</div>
|
||||
<div class="w-3/12 text-end py-4">{{ row.amount }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-6 mt-2 flex justify-end text-base">
|
||||
<div class="w-1/2">
|
||||
<div
|
||||
class="text-sm tracking-widest text-gray-600 mt-2"
|
||||
v-if="printObject.terms"
|
||||
>
|
||||
Notes
|
||||
</div>
|
||||
<div class="my-4 text-lg whitespace-pre-line">
|
||||
{{ printObject.terms }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-1/2">
|
||||
<div class="flex ps-2 justify-between py-3 border-b">
|
||||
<div>{{ t`Subtotal` }}</div>
|
||||
<div>{{ printObject.netTotal }}</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex ps-2 justify-between py-3 border-b"
|
||||
v-if="printObject.totalDiscount && !printObject.discountAfterTax"
|
||||
>
|
||||
<div>{{ t`Discount` }}</div>
|
||||
<div>{{ printObject.totalDiscount }}</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex ps-2 justify-between py-3"
|
||||
v-for="tax in printObject.taxes"
|
||||
:key="tax.name"
|
||||
>
|
||||
<div>{{ tax.account }}</div>
|
||||
<div>{{ tax.amount }}</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex ps-2 justify-between py-3 border-t"
|
||||
v-if="printObject.totalDiscount && printObject.discountAfterTax"
|
||||
>
|
||||
<div>{{ t`Discount` }}</div>
|
||||
<div>{{ printObject.totalDiscount }}</div>
|
||||
</div>
|
||||
<div
|
||||
class="
|
||||
flex
|
||||
ps-2
|
||||
justify-between
|
||||
py-3
|
||||
border-t
|
||||
text-green-600
|
||||
font-semibold
|
||||
text-base
|
||||
"
|
||||
>
|
||||
<div>{{ t`Grand Total` }}</div>
|
||||
<div>{{ printObject.grandTotal }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BaseTemplate from './BaseTemplate.vue';
|
||||
|
||||
export default {
|
||||
name: 'Default',
|
||||
extends: BaseTemplate,
|
||||
};
|
||||
</script>
|
@ -1,165 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
class="bg-white border h-full"
|
||||
:style="{ 'font-family': printObject.font }"
|
||||
>
|
||||
<div class="bg-gray-100 px-12 py-10">
|
||||
<h2
|
||||
v-if="printObject.displayTaxInvoice"
|
||||
class="font-semibold text-gray-900 text-2xl mb-4"
|
||||
>
|
||||
{{ t`Tax Invoice` }}
|
||||
</h2>
|
||||
<div class="flex items-center">
|
||||
<div class="flex items-center rounded h-16">
|
||||
<div class="me-4" v-if="printObject.displayLogo">
|
||||
<img class="h-12 max-w-32 object-contain" :src="printObject.logo" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
class="font-semibold text-xl"
|
||||
:style="{ color: printObject.color }"
|
||||
>
|
||||
{{ printObject.companyName }}
|
||||
</div>
|
||||
<div class="text-sm text-gray-800" v-if="printObject.address">
|
||||
{{ printObject.address }}
|
||||
</div>
|
||||
<div class="text-sm text-gray-800" v-if="printObject.gstin">
|
||||
GSTIN: {{ printObject.gstin }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-8 text-lg">
|
||||
<div class="flex">
|
||||
<div class="w-1/3 font-semibold">
|
||||
{{ printObject.isSalesInvoice ? 'Invoice' : 'Bill' }}
|
||||
</div>
|
||||
<div class="w-2/3 text-gray-800">
|
||||
<div class="font-semibold">
|
||||
{{ printObject.invoiceName }}
|
||||
</div>
|
||||
<div>
|
||||
{{ printObject.date }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 flex">
|
||||
<div class="w-1/3 font-semibold">
|
||||
{{ printObject.isSalesInvoice ? 'Customer' : 'Supplier' }}
|
||||
</div>
|
||||
<div class="w-2/3 text-gray-800" v-if="printObject.partyName">
|
||||
<div class="font-semibold">
|
||||
{{ printObject.partyName }}
|
||||
</div>
|
||||
<div v-if="printObject.partyAddress">
|
||||
{{ printObject.partyAddress }}
|
||||
</div>
|
||||
<div v-if="printObject.partyGSTIN">
|
||||
GSTIN: {{ printObject.partyGSTIN }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-12 py-12 text-lg">
|
||||
<div class="mb-4 flex font-semibold">
|
||||
<div class="w-4/12">Item</div>
|
||||
<div class="w-2/12 text-end" v-if="printObject.showHSN">HSN/SAC</div>
|
||||
<div class="w-2/12 text-end">Quantity</div>
|
||||
<div class="w-3/12 text-end" v-if="printObject.displayBatch">
|
||||
Batch
|
||||
</div>
|
||||
<div class="w-3/12 text-end">Rate</div>
|
||||
<div class="w-3/12 text-end">Amount</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex py-1 text-gray-800"
|
||||
v-for="row in printObject.items"
|
||||
:key="row.name"
|
||||
>
|
||||
<div class="w-4/12">{{ row.item }}</div>
|
||||
<div class="w-2/12 text-end" v-if="printObject.showHSN">
|
||||
{{ row.hsnCode }}
|
||||
</div>
|
||||
<div class="w-2/12 text-end">{{ row.quantity }}</div>
|
||||
<div class="w-3/12 text-end" v-if="printObject.displayBatch">
|
||||
{{ row.batch }}
|
||||
</div>
|
||||
<div class="w-3/12 text-end">{{ row.rate }}</div>
|
||||
<div class="w-3/12 text-end">{{ row.amount }}</div>
|
||||
</div>
|
||||
<div class="mt-12">
|
||||
<div class="flex -mx-3">
|
||||
<div class="flex justify-end flex-1 py-3 bg-gray-100 gap-8 pe-6">
|
||||
<div class="text-end">
|
||||
<div class="text-gray-800">{{ t`Subtotal` }}</div>
|
||||
<div class="text-xl mt-2">
|
||||
{{ printObject.netTotal }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="text-end"
|
||||
v-if="printObject.totalDiscount && !printObject.discountAfterTax"
|
||||
>
|
||||
<div class="text-gray-800">{{ t`Discount` }}</div>
|
||||
<div class="text-xl mt-2">
|
||||
{{ printObject.totalDiscount }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="text-end"
|
||||
v-for="tax in printObject.taxes"
|
||||
:key="tax.name"
|
||||
>
|
||||
<div class="text-gray-800">
|
||||
{{ tax.account }}
|
||||
</div>
|
||||
<div class="text-xl mt-2">
|
||||
{{ tax.amount }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="text-end"
|
||||
v-if="printObject.totalDiscount && printObject.discountAfterTax"
|
||||
>
|
||||
<div class="text-gray-800">{{ t`Discount` }}</div>
|
||||
<div class="text-xl mt-2">
|
||||
{{ printObject.totalDiscount }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="py-3 px-4 text-end text-white"
|
||||
:style="{ backgroundColor: printObject.color }"
|
||||
>
|
||||
<div>
|
||||
<div>{{ t`Grand Total` }}</div>
|
||||
<div class="text-2xl mt-2 font-semibold">
|
||||
{{ printObject.grandTotal }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-12" v-if="printObject.terms">
|
||||
<div class="text-lg font-semibold">Notes</div>
|
||||
<div class="mt-4 text-lg whitespace-pre-line">
|
||||
{{ printObject.terms }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import BaseTemplate from './BaseTemplate.vue';
|
||||
|
||||
export default {
|
||||
name: 'Business',
|
||||
extends: BaseTemplate,
|
||||
};
|
||||
</script>
|
@ -1,194 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
class="bg-white border h-full"
|
||||
:style="{ 'font-family': printObject.font }"
|
||||
>
|
||||
<div class="flex flex-col w-full px-12 py-10 border-b">
|
||||
<h2
|
||||
v-if="printObject.displayTaxInvoice"
|
||||
class="
|
||||
font-semibold
|
||||
text-gray-800 text-sm
|
||||
tracking-widest
|
||||
uppercase
|
||||
mb-4
|
||||
"
|
||||
>
|
||||
{{ t`Tax Invoice` }}
|
||||
</h2>
|
||||
<div class="flex items-center justify-between w-full">
|
||||
<div class="flex items-center">
|
||||
<div class="flex items-center rounded h-16">
|
||||
<div class="me-4" v-if="printObject.displayLogo">
|
||||
<img
|
||||
class="h-12 max-w-32 object-contain"
|
||||
:src="printObject.logo"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
class="font-semibold text-xl"
|
||||
:style="{ color: printObject.color }"
|
||||
>
|
||||
{{ printObject.companyName }}
|
||||
</div>
|
||||
<div>
|
||||
{{ printObject.date }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-end">
|
||||
<div
|
||||
class="font-semibold text-xl"
|
||||
:style="{ color: printObject.color }"
|
||||
>
|
||||
{{
|
||||
printObject.isSalesInvoice
|
||||
? t`Sales Invoice`
|
||||
: t`Purchase Invoice`
|
||||
}}
|
||||
</div>
|
||||
<div>
|
||||
{{ printObject.invoiceName }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex px-12 py-10 border-b">
|
||||
<div class="w-1/2" v-if="printObject.partyName">
|
||||
<div
|
||||
class="uppercase text-sm font-semibold tracking-widest text-gray-800"
|
||||
>
|
||||
{{ printObject.isSalesInvoice ? 'To' : 'From' }}
|
||||
</div>
|
||||
<div class="mt-4 text-black leading-relaxed text-lg">
|
||||
{{ printObject.partyName }} <br />
|
||||
{{ printObject.partyAddress ?? '' }}
|
||||
</div>
|
||||
<div
|
||||
class="mt-4 text-black leading-relaxed text-lg"
|
||||
v-if="printObject.partyGSTIN"
|
||||
>
|
||||
GSTIN: {{ printObject.partyGSTIN }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-1/2" v-if="printObject.address">
|
||||
<div
|
||||
class="
|
||||
uppercase
|
||||
text-sm
|
||||
font-semibold
|
||||
tracking-widest
|
||||
text-gray-800
|
||||
ms-8
|
||||
"
|
||||
>
|
||||
{{ printObject.isSalesInvoice ? 'From' : 'To' }}
|
||||
</div>
|
||||
<div class="mt-4 ms-8 text-black leading-relaxed text-lg">
|
||||
{{ printObject.address }}
|
||||
</div>
|
||||
<div
|
||||
class="mt-4 ms-8 text-black leading-relaxed text-lg"
|
||||
v-if="printObject.gstin"
|
||||
>
|
||||
GSTIN: {{ printObject.gstin }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-12 py-10 border-b">
|
||||
<div
|
||||
class="
|
||||
mb-4
|
||||
flex
|
||||
uppercase
|
||||
text-sm
|
||||
tracking-widest
|
||||
font-semibold
|
||||
text-gray-800
|
||||
"
|
||||
>
|
||||
<div class="w-4/12">Item</div>
|
||||
<div class="w-2/12 text-end" v-if="printObject.showHSN">HSN/SAC</div>
|
||||
<div class="w-2/12 text-end">Quantity</div>
|
||||
<div class="w-3/12 text-end" v-if="printObject.displayBatch">
|
||||
Batch
|
||||
</div>
|
||||
<div class="w-3/12 text-end">Rate</div>
|
||||
<div class="w-3/12 text-end">Amount</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex py-1 text-lg"
|
||||
v-for="row in printObject.items"
|
||||
:key="row.name"
|
||||
>
|
||||
<div class="w-4/12">{{ row.item }}</div>
|
||||
<div class="w-2/12 text-end" v-if="printObject.showHSN">
|
||||
{{ row.hsnCode }}
|
||||
</div>
|
||||
<div class="w-2/12 text-end">{{ row.quantity }}</div>
|
||||
<div class="w-3/12 text-end" v-if="printObject.displayBatch">
|
||||
{{ row.batch }}
|
||||
</div>
|
||||
<div class="w-3/12 text-end">{{ row.rate }}</div>
|
||||
<div class="w-3/12 text-end">{{ row.amount }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex px-12 py-10">
|
||||
<div class="w-1/2" v-if="printObject.terms">
|
||||
<div
|
||||
class="uppercase text-sm tracking-widest font-semibold text-gray-800"
|
||||
>
|
||||
Notes
|
||||
</div>
|
||||
<div class="mt-4 text-lg whitespace-pre-line">
|
||||
{{ printObject.terms }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-1/2 text-lg">
|
||||
<div class="flex ps-2 justify-between py-1">
|
||||
<div>{{ t`Subtotal` }}</div>
|
||||
<div>{{ printObject.netTotal }}</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex ps-2 justify-between py-1"
|
||||
v-if="printObject.totalDiscount && !printObject.discountAfterTax"
|
||||
>
|
||||
<div>{{ t`Discount` }}</div>
|
||||
<div>{{ printObject.totalDiscount }}</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex ps-2 justify-between py-1"
|
||||
v-for="tax in printObject.taxes"
|
||||
:key="tax.name"
|
||||
>
|
||||
<div>{{ tax.account }}</div>
|
||||
<div>{{ tax.amount }}</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex ps-2 justify-between py-1"
|
||||
v-if="printObject.totalDiscount && printObject.discountAfterTax"
|
||||
>
|
||||
<div>{{ t`Discount` }}</div>
|
||||
<div>{{ printObject.totalDiscount }}</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex ps-2 justify-between py-1 font-semibold"
|
||||
:style="{ color: printObject.color }"
|
||||
>
|
||||
<div>{{ t`Grand Total` }}</div>
|
||||
<div>{{ printObject.grandTotal }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import BaseTemplate from './BaseTemplate.vue';
|
||||
|
||||
export default {
|
||||
name: 'Minimal',
|
||||
extends: BaseTemplate,
|
||||
};
|
||||
</script>
|
39
src/components/ShortcutKeys.vue
Normal file
39
src/components/ShortcutKeys.vue
Normal file
@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<div class="flex-shrink-0 flex items-center gap-2" style="width: fit-content">
|
||||
<kbd
|
||||
v-for="k in keys"
|
||||
:key="k"
|
||||
class="key-common"
|
||||
:class="{ 'key-styling': !simple }"
|
||||
>{{ keyMap[k] ?? k }}</kbd
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { getShortcutKeyMap } from 'src/utils/ui';
|
||||
import { defineComponent, PropType } from 'vue';
|
||||
export default defineComponent({
|
||||
props: {
|
||||
keys: { type: Array as PropType<string[]>, required: true },
|
||||
simple: { type: Boolean, default: false },
|
||||
},
|
||||
method() {},
|
||||
computed: {
|
||||
keyMap(): Record<string, string> {
|
||||
return getShortcutKeyMap(this.platform);
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style scoped>
|
||||
.key-common {
|
||||
font-family: monospace;
|
||||
font-weight: 600;
|
||||
@apply rounded-md px-1.5 py-0.5 bg-gray-200 text-gray-700
|
||||
tracking-tighter;
|
||||
}
|
||||
|
||||
.key-styling {
|
||||
@apply border-b-4 border-gray-400 shadow-md;
|
||||
}
|
||||
</style>
|
@ -22,29 +22,7 @@
|
||||
class="grid gap-4 items-start"
|
||||
style="grid-template-columns: 6rem auto"
|
||||
>
|
||||
<!-- <div class="w-2 text-base">{{ i + 1 }}.</div> -->
|
||||
<div
|
||||
class="
|
||||
text-base
|
||||
font-medium
|
||||
flex-shrink-0 flex
|
||||
items-center
|
||||
gap-1
|
||||
bg-gray-200
|
||||
text-gray-700
|
||||
px-1.5
|
||||
py-0.5
|
||||
rounded
|
||||
"
|
||||
style="width: fit-content"
|
||||
>
|
||||
<span
|
||||
v-for="k in s.shortcut"
|
||||
:key="k"
|
||||
class="tracking-tighter"
|
||||
>{{ k }}</span
|
||||
>
|
||||
</div>
|
||||
<ShortcutKeys class="text-base" :keys="s.shortcut" />
|
||||
<div class="whitespace-normal text-base">{{ s.description }}</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -63,8 +41,10 @@
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { t } from 'fyo';
|
||||
import { ShortcutKey } from 'src/utils/ui';
|
||||
import { defineComponent } from 'vue';
|
||||
import FormHeader from './FormHeader.vue';
|
||||
import ShortcutKeys from './ShortcutKeys.vue';
|
||||
|
||||
type Group = {
|
||||
label: string;
|
||||
@ -85,15 +65,15 @@ export default defineComponent({
|
||||
collapsed: false,
|
||||
shortcuts: [
|
||||
{
|
||||
shortcut: [this.pmod, 'K'],
|
||||
shortcut: [ShortcutKey.pmod, 'K'],
|
||||
description: t`Open Quick Search`,
|
||||
},
|
||||
{
|
||||
shortcut: [this.del],
|
||||
shortcut: [ShortcutKey.delete],
|
||||
description: t`Go back to the previous page`,
|
||||
},
|
||||
{
|
||||
shortcut: [this.shift, 'H'],
|
||||
shortcut: [ShortcutKey.shift, 'H'],
|
||||
description: t`Toggle sidebar`,
|
||||
},
|
||||
{
|
||||
@ -108,14 +88,14 @@ export default defineComponent({
|
||||
collapsed: false,
|
||||
shortcuts: [
|
||||
{
|
||||
shortcut: [this.pmod, 'S'],
|
||||
shortcut: [ShortcutKey.pmod, 'S'],
|
||||
description: [
|
||||
t`Save or Submit a doc.`,
|
||||
t`A doc is submitted only if it is submittable and is in the saved state.`,
|
||||
].join(' '),
|
||||
},
|
||||
{
|
||||
shortcut: [this.pmod, this.del],
|
||||
shortcut: [ShortcutKey.pmod, ShortcutKey.delete],
|
||||
description: [
|
||||
t`Cancel or Delete a doc.`,
|
||||
t`A doc is cancelled only if it is in the submitted state.`,
|
||||
@ -129,71 +109,58 @@ export default defineComponent({
|
||||
description: t`Applicable when Quick Search is open`,
|
||||
collapsed: false,
|
||||
shortcuts: [
|
||||
{ shortcut: [this.esc], description: t`Close Quick Search` },
|
||||
{ shortcut: [ShortcutKey.esc], description: t`Close Quick Search` },
|
||||
{
|
||||
shortcut: [this.pmod, '1'],
|
||||
shortcut: [ShortcutKey.pmod, '1'],
|
||||
description: t`Toggle the Docs filter`,
|
||||
},
|
||||
{
|
||||
shortcut: [this.pmod, '2'],
|
||||
shortcut: [ShortcutKey.pmod, '2'],
|
||||
description: t`Toggle the List filter`,
|
||||
},
|
||||
{
|
||||
shortcut: [this.pmod, '3'],
|
||||
shortcut: [ShortcutKey.pmod, '3'],
|
||||
description: t`Toggle the Create filter`,
|
||||
},
|
||||
{
|
||||
shortcut: [this.pmod, '4'],
|
||||
shortcut: [ShortcutKey.pmod, '4'],
|
||||
description: t`Toggle the Report filter`,
|
||||
},
|
||||
{
|
||||
shortcut: [this.pmod, '5'],
|
||||
shortcut: [ShortcutKey.pmod, '5'],
|
||||
description: t`Toggle the Page filter`,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t`Template Builder`,
|
||||
description: t`Applicable when Template Builder is open`,
|
||||
collapsed: false,
|
||||
shortcuts: [
|
||||
{
|
||||
shortcut: [ShortcutKey.ctrl, ShortcutKey.enter],
|
||||
description: t`Apply and view changes made to the print template`,
|
||||
},
|
||||
{
|
||||
shortcut: [ShortcutKey.ctrl, 'E'],
|
||||
description: t`Toggle Edit Mode`,
|
||||
},
|
||||
{
|
||||
shortcut: [ShortcutKey.ctrl, 'H'],
|
||||
description: t`Toggle Key Hints`,
|
||||
},
|
||||
{
|
||||
shortcut: [ShortcutKey.ctrl, '+'],
|
||||
description: t`Increase print template display scale`,
|
||||
},
|
||||
{
|
||||
shortcut: [ShortcutKey.ctrl, '-'],
|
||||
description: t`Decrease print template display scale`,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
},
|
||||
computed: {
|
||||
pmod() {
|
||||
if (this.isMac) {
|
||||
return '⌘';
|
||||
}
|
||||
|
||||
return 'Ctrl';
|
||||
},
|
||||
shift() {
|
||||
if (this.isMac) {
|
||||
return 'shift';
|
||||
}
|
||||
|
||||
return '⇧';
|
||||
},
|
||||
alt() {
|
||||
if (this.isMac) {
|
||||
return '⌥';
|
||||
}
|
||||
|
||||
return 'Alt';
|
||||
},
|
||||
del() {
|
||||
if (this.isMac) {
|
||||
return 'delete';
|
||||
}
|
||||
|
||||
return 'Backspace';
|
||||
},
|
||||
esc() {
|
||||
if (this.isMac) {
|
||||
return 'esc';
|
||||
}
|
||||
|
||||
return 'Esc';
|
||||
},
|
||||
isMac() {
|
||||
return this.platform === 'Mac';
|
||||
},
|
||||
},
|
||||
components: { FormHeader },
|
||||
components: { FormHeader, ShortcutKeys },
|
||||
});
|
||||
</script>
|
||||
|
@ -145,7 +145,7 @@
|
||||
</button>
|
||||
|
||||
<p
|
||||
v-if="fyo.store.isDevelopment"
|
||||
v-if="!fyo.store.isDevelopment"
|
||||
class="text-xs text-gray-500 select-none"
|
||||
>
|
||||
dev mode
|
||||
@ -165,7 +165,7 @@
|
||||
m-4
|
||||
rtl-rotate-180
|
||||
"
|
||||
@click="$emit('toggle-sidebar')"
|
||||
@click="() => toggleSidebar()"
|
||||
>
|
||||
<feather-icon name="chevrons-left" class="w-4 h-4" />
|
||||
</button>
|
||||
@ -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);
|
||||
},
|
||||
|
@ -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 });
|
||||
|
||||
|
@ -1,7 +1,16 @@
|
||||
<template>
|
||||
<FormContainer>
|
||||
<template #header-left v-if="hasDoc">
|
||||
<StatusBadge :status="status" class="h-8" />
|
||||
</template>
|
||||
<template #header v-if="hasDoc">
|
||||
<StatusBadge :status="status" />
|
||||
<Button
|
||||
v-if="!doc.isCancelled && !doc.dirty && isPrintable"
|
||||
:icon="true"
|
||||
@click="routeTo(`/print/${doc.schemaName}/${doc.name}`)"
|
||||
>
|
||||
{{ t`Print` }}
|
||||
</Button>
|
||||
<DropdownWithActions
|
||||
v-for="group of groupedActions"
|
||||
:key="group.label"
|
||||
@ -116,8 +125,11 @@ import { docsPathMap } from 'src/utils/misc';
|
||||
import { docsPathRef, focusedDocsRef } from 'src/utils/refs';
|
||||
import { ActionGroup, UIGroupedFields } from 'src/utils/types';
|
||||
import {
|
||||
getDocFromNameIfExistsElseNew,
|
||||
getFieldsGroupedByTabAndSection,
|
||||
getGroupedActionsForDoc,
|
||||
isPrintable,
|
||||
routeTo,
|
||||
} from 'src/utils/ui';
|
||||
import { computed, defineComponent, nextTick } from 'vue';
|
||||
import QuickEditForm from '../QuickEditForm.vue';
|
||||
@ -142,12 +154,14 @@ export default defineComponent({
|
||||
activeTab: 'Default',
|
||||
groupedFields: null,
|
||||
quickEditDoc: null,
|
||||
isPrintable: false,
|
||||
} as {
|
||||
errors: Record<string, string>;
|
||||
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) {
|
||||
|
@ -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 },
|
||||
});
|
||||
|
@ -1,11 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import { showSidebar } from 'src/utils/refs';
|
||||
import { toggleSidebar } from 'src/utils/ui';
|
||||
</script>
|
||||
<template>
|
||||
<div class="flex overflow-hidden">
|
||||
<Transition name="sidebar">
|
||||
<Sidebar
|
||||
v-show="sidebar"
|
||||
v-show="showSidebar"
|
||||
class="flex-shrink-0 border-e whitespace-nowrap w-sidebar"
|
||||
@change-db-file="$emit('change-db-file')"
|
||||
@toggle-sidebar="sidebar = !sidebar"
|
||||
/>
|
||||
</Transition>
|
||||
|
||||
@ -32,7 +35,7 @@
|
||||
|
||||
<!-- Show Sidebar Button -->
|
||||
<button
|
||||
v-show="!sidebar"
|
||||
v-show="!showSidebar"
|
||||
class="
|
||||
absolute
|
||||
bottom-0
|
||||
@ -43,31 +46,25 @@
|
||||
rtl-rotate-180
|
||||
p-1
|
||||
m-4
|
||||
opacity-0
|
||||
hover:opacity-100 hover:shadow-md
|
||||
"
|
||||
|
||||
@click="sidebar = !sidebar"
|
||||
@click="() => toggleSidebar()"
|
||||
>
|
||||
<feather-icon name="chevrons-right" class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { computed } from '@vue/reactivity';
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import Sidebar from '../components/Sidebar.vue';
|
||||
export default {
|
||||
export default defineComponent({
|
||||
name: 'Desk',
|
||||
emits: ['change-db-file'],
|
||||
data() {
|
||||
return { sidebar: true };
|
||||
},
|
||||
provide() {
|
||||
return { sidebar: computed(() => this.sidebar) };
|
||||
},
|
||||
components: {
|
||||
Sidebar,
|
||||
},
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
@ -386,7 +386,7 @@ import { fyo } from 'src/initFyo';
|
||||
import { getSavePath, saveData, selectFile } from 'src/utils/ipcCalls';
|
||||
import { docsPathMap } from 'src/utils/misc';
|
||||
import { docsPathRef } from 'src/utils/refs';
|
||||
import { showMessageDialog } from 'src/utils/ui';
|
||||
import { selectTextFile, showMessageDialog } from 'src/utils/ui';
|
||||
import { defineComponent } from 'vue';
|
||||
import Loading from '../components/Loading.vue';
|
||||
|
||||
@ -906,27 +906,14 @@ export default defineComponent({
|
||||
: '';
|
||||
},
|
||||
async selectFile(): Promise<void> {
|
||||
const options = {
|
||||
title: this.t`Select File`,
|
||||
filters: [{ name: 'CSV', extensions: ['csv'] }],
|
||||
};
|
||||
const { text, name, filePath } = await selectTextFile([
|
||||
{ name: 'CSV', extensions: ['csv'] },
|
||||
]);
|
||||
|
||||
const { success, canceled, filePath, data, name } = await selectFile(
|
||||
options
|
||||
);
|
||||
|
||||
if (!success && !canceled) {
|
||||
await showMessageDialog({
|
||||
message: this.t`File selection failed.`,
|
||||
});
|
||||
if (!text) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!success || canceled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const text = new TextDecoder().decode(data);
|
||||
const isValid = this.importer.selectFile(text);
|
||||
if (!isValid) {
|
||||
await showMessageDialog({
|
||||
|
@ -1,8 +1,15 @@
|
||||
<template>
|
||||
<FormContainer>
|
||||
<!-- Page Header (Title, Buttons, etc) -->
|
||||
<template #header-left v-if="doc">
|
||||
<StatusBadge :status="status" class="h-8" />
|
||||
<Barcode
|
||||
class="h-8"
|
||||
v-if="doc.canEdit && fyo.singles.InventorySettings?.enableBarcodes"
|
||||
@item-selected="(name) => doc.addItem(name)"
|
||||
/>
|
||||
</template>
|
||||
<template #header v-if="doc">
|
||||
<StatusBadge :status="status" />
|
||||
<ExchangeRate
|
||||
v-if="doc.isMultiCurrency"
|
||||
:disabled="doc?.isSubmitted || doc?.isCancelled"
|
||||
@ -13,10 +20,6 @@
|
||||
async (exchangeRate) => await doc.set('exchangeRate', exchangeRate)
|
||||
"
|
||||
/>
|
||||
<Barcode
|
||||
v-if="doc.canEdit && fyo.singles.InventorySettings?.enableBarcodes"
|
||||
@item-selected="(name) => doc.addItem(name)"
|
||||
/>
|
||||
<Button
|
||||
v-if="!doc.isCancelled && !doc.dirty"
|
||||
:icon="true"
|
||||
|
@ -37,7 +37,7 @@
|
||||
|
||||
<!-- Data Rows -->
|
||||
<div class="overflow-y-auto custom-scroll" v-if="dataSlice.length !== 0">
|
||||
<div v-for="(doc, i) in dataSlice" :key="doc.name">
|
||||
<div v-for="(row, i) in dataSlice" :key="row.name">
|
||||
<!-- Row Content -->
|
||||
<div class="flex hover:bg-gray-50 items-center">
|
||||
<p class="w-8 text-end me-4 text-gray-900">
|
||||
@ -46,7 +46,7 @@
|
||||
<Row
|
||||
gap="1rem"
|
||||
class="cursor-pointer text-gray-900 flex-1 h-row-mid"
|
||||
@click="$emit('openDoc', doc.name)"
|
||||
@click="$emit('openDoc', row.name)"
|
||||
:columnCount="columns.length"
|
||||
>
|
||||
<ListCell
|
||||
@ -56,7 +56,7 @@
|
||||
'text-end': isNumeric(column.fieldtype),
|
||||
'pe-4': c === columns.length - 1,
|
||||
}"
|
||||
:doc="doc"
|
||||
:row="row"
|
||||
:column="column"
|
||||
/>
|
||||
</Row>
|
||||
@ -95,7 +95,6 @@ import Paginator from 'src/components/Paginator.vue';
|
||||
import Row from 'src/components/Row';
|
||||
import { fyo } from 'src/initFyo';
|
||||
import { isNumeric } from 'src/utils';
|
||||
import { openQuickEdit, routeTo } from 'src/utils/ui';
|
||||
import { objectForEach } from 'utils/index';
|
||||
import { defineComponent, toRaw } from 'vue';
|
||||
import ListCell from './ListCell';
|
||||
|
@ -4,26 +4,52 @@
|
||||
<component v-else :is="customRenderer" />
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import { ColumnConfig, RenderData } from 'fyo/model/types';
|
||||
import { Field } from 'schemas/types';
|
||||
import { fyo } from 'src/initFyo';
|
||||
import { isNumeric } from 'src/utils';
|
||||
import { defineComponent, PropType } from 'vue';
|
||||
|
||||
export default {
|
||||
type Column = ColumnConfig | Field;
|
||||
|
||||
function isField(column: ColumnConfig | Field): column is Field {
|
||||
if ((column as ColumnConfig).display || (column as ColumnConfig).render) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ListCell',
|
||||
props: ['doc', 'column'],
|
||||
props: {
|
||||
row: { type: Object as PropType<RenderData>, required: true },
|
||||
column: { type: Object as PropType<Column>, required: true },
|
||||
},
|
||||
computed: {
|
||||
columnValue() {
|
||||
let { column, doc } = this;
|
||||
let value = doc[column.fieldname];
|
||||
return fyo.format(value, column, doc);
|
||||
columnValue(): string {
|
||||
const column = this.column;
|
||||
const value = this.row[this.column.fieldname];
|
||||
|
||||
if (isField(column)) {
|
||||
return fyo.format(value, column);
|
||||
}
|
||||
|
||||
return column.display?.(value, fyo) ?? '';
|
||||
},
|
||||
customRenderer() {
|
||||
if (!this.column.render) return;
|
||||
return this.column.render(this.doc);
|
||||
const { render } = this.column as ColumnConfig;
|
||||
|
||||
if (!render) {
|
||||
return;
|
||||
}
|
||||
|
||||
return render(this.row);
|
||||
},
|
||||
cellClass() {
|
||||
return isNumeric(this.column.fieldtype) ? 'justify-end' : '';
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
@ -1,161 +1,242 @@
|
||||
<template>
|
||||
<div class="flex">
|
||||
<div class="flex flex-col flex-1 bg-gray-25">
|
||||
<PageHeader class="z-10" :border="false">
|
||||
<Button
|
||||
class="text-gray-900 text-xs"
|
||||
@click="showCustomiser = !showCustomiser"
|
||||
>
|
||||
{{ t`Customise` }}
|
||||
</Button>
|
||||
<Button class="text-gray-900 text-xs" @click="makePDF">
|
||||
<PageHeader :border="true">
|
||||
<template #left>
|
||||
<AutoComplete
|
||||
v-if="templateList.length"
|
||||
:df="{
|
||||
fieldname: 'templateName',
|
||||
label: t`Template Name`,
|
||||
target: 'PrintTemplate',
|
||||
options: templateList,
|
||||
}"
|
||||
input-class="text-base py-0 h-8"
|
||||
class="w-56"
|
||||
:border="true"
|
||||
:value="templateName"
|
||||
@change="onTemplateNameChange"
|
||||
/>
|
||||
</template>
|
||||
<DropdownWithActions :actions="actions" :title="t`More`" />
|
||||
<Button class="text-xs" type="primary" @click="savePDF">
|
||||
{{ t`Save as PDF` }}
|
||||
</Button>
|
||||
</PageHeader>
|
||||
|
||||
<!-- Printview Preview -->
|
||||
<div
|
||||
v-if="doc && printSettings"
|
||||
class="flex justify-center flex-1 overflow-auto relative"
|
||||
>
|
||||
<div
|
||||
class="h-full shadow mb-4 absolute bg-white"
|
||||
style="
|
||||
width: 21cm;
|
||||
height: 29.7cm;
|
||||
transform: scale(0.65) translateY(-300px);
|
||||
"
|
||||
ref="printContainer"
|
||||
>
|
||||
<component
|
||||
class="flex-1"
|
||||
:is="printTemplate"
|
||||
v-bind="{ doc, printSettings }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Template Display Area -->
|
||||
<div class="overflow-auto custom-scroll p-4">
|
||||
<!-- Display Hints -->
|
||||
<div v-if="helperMessage" class="text-sm text-gray-700">
|
||||
{{ helperMessage }}
|
||||
</div>
|
||||
|
||||
<!-- Printview Customizer -->
|
||||
<Transition name="quickedit">
|
||||
<div class="border-s w-quick-edit" v-if="showCustomiser">
|
||||
<div
|
||||
class="px-4 flex items-center justify-between h-row-largest border-b"
|
||||
>
|
||||
<h2 class="font-semibold">{{ t`Customise` }}</h2>
|
||||
<Button :icon="true" @click="showCustomiser = false">
|
||||
<feather-icon name="x" class="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<TwoColumnForm
|
||||
:doc="printSettings"
|
||||
:autosave="true"
|
||||
class="border-none"
|
||||
<!-- Template Container -->
|
||||
<PrintContainer
|
||||
ref="printContainer"
|
||||
v-if="printProps"
|
||||
:template="printProps.template"
|
||||
:values="printProps.values"
|
||||
:scale="scale"
|
||||
/>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { ipcRenderer } from 'electron';
|
||||
import { Verb } from 'fyo/telemetry/types';
|
||||
<script lang="ts">
|
||||
import { Doc } from 'fyo/model/doc';
|
||||
import { Action } from 'fyo/model/types';
|
||||
import { PrintTemplate } from 'models/baseModels/PrintTemplate';
|
||||
import { ModelNameEnum } from 'models/types';
|
||||
import Button from 'src/components/Button.vue';
|
||||
import AutoComplete from 'src/components/Controls/AutoComplete.vue';
|
||||
import DropdownWithActions from 'src/components/DropdownWithActions.vue';
|
||||
import PageHeader from 'src/components/PageHeader.vue';
|
||||
import InvoiceTemplate from 'src/components/SalesInvoice/InvoiceTemplate.vue';
|
||||
import TwoColumnForm from 'src/components/TwoColumnForm.vue';
|
||||
import { handleErrorWithDialog } from 'src/errorHandling';
|
||||
import { fyo } from 'src/initFyo';
|
||||
import { makePDF } from 'src/utils/ipcCalls';
|
||||
import { IPC_ACTIONS } from 'utils/messages';
|
||||
import { getPrintTemplatePropValues } from 'src/utils/printTemplates';
|
||||
import { PrintValues } from 'src/utils/types';
|
||||
import { getFormRoute, openSettings, routeTo } from 'src/utils/ui';
|
||||
import { defineComponent } from 'vue';
|
||||
import PrintContainer from '../TemplateBuilder/PrintContainer.vue';
|
||||
|
||||
export default {
|
||||
export default defineComponent({
|
||||
name: 'PrintView',
|
||||
props: { schemaName: String, name: String },
|
||||
props: {
|
||||
schemaName: { type: String, required: true },
|
||||
name: { type: String, required: true },
|
||||
},
|
||||
components: {
|
||||
PageHeader,
|
||||
Button,
|
||||
TwoColumnForm,
|
||||
AutoComplete,
|
||||
PrintContainer,
|
||||
DropdownWithActions,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
doc: null,
|
||||
showCustomiser: false,
|
||||
printSettings: null,
|
||||
scale: 1,
|
||||
values: null,
|
||||
templateDoc: null,
|
||||
templateName: null,
|
||||
templateList: [],
|
||||
} as {
|
||||
doc: null | Doc;
|
||||
scale: number;
|
||||
values: null | PrintValues;
|
||||
templateDoc: null | PrintTemplate;
|
||||
templateName: null | string;
|
||||
templateList: string[];
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
this.doc = await fyo.doc.getDoc(this.schemaName, this.name);
|
||||
this.printSettings = await fyo.doc.getDoc('PrintSettings');
|
||||
|
||||
await this.setTemplateList();
|
||||
if (fyo.store.isDevelopment) {
|
||||
// @ts-ignore
|
||||
window.pv = this;
|
||||
}
|
||||
|
||||
await this.setTemplateFromDefault();
|
||||
if (!this.templateDoc && this.templateList.length) {
|
||||
await this.onTemplateNameChange(this.templateList[0]);
|
||||
}
|
||||
|
||||
if (this.doc) {
|
||||
this.values = await getPrintTemplatePropValues(this.doc as Doc);
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
printTemplate() {
|
||||
return InvoiceTemplate;
|
||||
helperMessage() {
|
||||
if (!this.templateList.length) {
|
||||
const label =
|
||||
this.fyo.schemaMap[this.schemaName]?.label ?? this.schemaName;
|
||||
|
||||
return this.t`No Print Templates not found for entry type ${label}`;
|
||||
}
|
||||
|
||||
if (!this.templateDoc) {
|
||||
return this.t`Please select a Print Template`;
|
||||
}
|
||||
|
||||
return '';
|
||||
},
|
||||
printProps(): null | { template: string; values: PrintValues } {
|
||||
const values = this.values;
|
||||
if (!values) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const template = this.templateDoc?.template;
|
||||
if (!template) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { values, template };
|
||||
},
|
||||
actions(): Action[] {
|
||||
const actions = [
|
||||
{
|
||||
label: this.t`Print Settings`,
|
||||
group: this.t`View`,
|
||||
async action() {
|
||||
await openSettings(ModelNameEnum.PrintSettings);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: this.t`New Template`,
|
||||
group: this.t`Create`,
|
||||
action: async () => {
|
||||
const doc = this.fyo.doc.getNewDoc(ModelNameEnum.PrintTemplate, {
|
||||
type: this.schemaName,
|
||||
});
|
||||
|
||||
const route = getFormRoute(doc.schemaName, doc.name!);
|
||||
await routeTo(route);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const templateDocName = this.templateDoc?.name;
|
||||
if (templateDocName) {
|
||||
actions.push({
|
||||
label: templateDocName,
|
||||
group: this.t`View`,
|
||||
action: async () => {
|
||||
const route = getFormRoute(
|
||||
ModelNameEnum.PrintTemplate,
|
||||
templateDocName
|
||||
);
|
||||
await routeTo(route);
|
||||
},
|
||||
});
|
||||
|
||||
actions.push({
|
||||
label: this.t`Duplicate Template`,
|
||||
group: this.t`Create`,
|
||||
action: async () => {
|
||||
const doc = this.fyo.doc.getNewDoc(ModelNameEnum.PrintTemplate, {
|
||||
type: this.schemaName,
|
||||
template: this.templateDoc?.template,
|
||||
});
|
||||
|
||||
const route = getFormRoute(doc.schemaName, doc.name!);
|
||||
await routeTo(route);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return actions;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
constructPrintDocument() {
|
||||
const html = document.createElement('html');
|
||||
const head = document.createElement('head');
|
||||
const body = document.createElement('body');
|
||||
const style = getAllCSSAsStyleElem();
|
||||
async onTemplateNameChange(value: string | null): Promise<void> {
|
||||
if (!value) {
|
||||
this.templateDoc = null;
|
||||
return;
|
||||
}
|
||||
|
||||
head.innerHTML = [
|
||||
'<meta charset="UTF-8">',
|
||||
'<title>Print Window</title>',
|
||||
].join('\n');
|
||||
head.append(style);
|
||||
|
||||
body.innerHTML = this.$refs.printContainer.innerHTML;
|
||||
html.append(head, body);
|
||||
return html.outerHTML;
|
||||
this.templateName = value;
|
||||
try {
|
||||
this.templateDoc = (await this.fyo.doc.getDoc(
|
||||
ModelNameEnum.PrintTemplate,
|
||||
this.templateName
|
||||
)) as PrintTemplate;
|
||||
} catch (error) {
|
||||
await handleErrorWithDialog(error);
|
||||
}
|
||||
},
|
||||
async makePDF() {
|
||||
const savePath = await this.getSavePath();
|
||||
if (!savePath) return;
|
||||
async setTemplateList(): Promise<void> {
|
||||
const list = (await this.fyo.db.getAllRaw(ModelNameEnum.PrintTemplate, {
|
||||
filters: { type: this.schemaName },
|
||||
})) as { name: string }[];
|
||||
|
||||
const html = this.constructPrintDocument();
|
||||
await makePDF(html, savePath);
|
||||
fyo.telemetry.log(Verb.Exported, 'SalesInvoice', { extension: 'pdf' });
|
||||
this.templateList = list.map(({ name }) => name);
|
||||
},
|
||||
async getSavePath() {
|
||||
const options = {
|
||||
title: this.t`Select folder`,
|
||||
defaultPath: `${this.name}.pdf`,
|
||||
async savePDF() {
|
||||
const printContainer = this.$refs.printContainer as {
|
||||
savePDF: (name?: string) => void;
|
||||
};
|
||||
|
||||
let { filePath } = await ipcRenderer.invoke(
|
||||
IPC_ACTIONS.GET_SAVE_FILEPATH,
|
||||
options
|
||||
);
|
||||
|
||||
if (filePath) {
|
||||
if (!filePath.endsWith('.pdf')) {
|
||||
filePath = filePath + '.pdf';
|
||||
}
|
||||
if (!printContainer?.savePDF) {
|
||||
return;
|
||||
}
|
||||
|
||||
return filePath;
|
||||
printContainer.savePDF(this.doc?.name);
|
||||
},
|
||||
async setTemplateFromDefault() {
|
||||
const defaultName =
|
||||
this.schemaName[0].toLowerCase() +
|
||||
this.schemaName.slice(1) +
|
||||
ModelNameEnum.PrintTemplate;
|
||||
const name = this.fyo.singles.Defaults?.get(defaultName);
|
||||
if (typeof name !== 'string') {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.onTemplateNameChange(name);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
function getAllCSSAsStyleElem() {
|
||||
const cssTexts = [];
|
||||
for (const sheet of document.styleSheets) {
|
||||
for (const rule of sheet.cssRules) {
|
||||
cssTexts.push(rule.cssText);
|
||||
}
|
||||
|
||||
for (const rule of sheet.ownerRule ?? []) {
|
||||
cssTexts.push(rule.cssText);
|
||||
}
|
||||
}
|
||||
|
||||
const styleElem = document.createElement('style');
|
||||
styleElem.innerHTML = cssTexts.join('\n');
|
||||
return styleElem;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
@ -161,12 +161,8 @@ export default defineComponent({
|
||||
try {
|
||||
await doc.sync();
|
||||
this.updateGroupedFields();
|
||||
} catch (err) {
|
||||
if (!(err instanceof Error)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await handleErrorWithDialog(err, doc);
|
||||
} catch (error) {
|
||||
await handleErrorWithDialog(error, doc);
|
||||
}
|
||||
},
|
||||
async onValueChange(field: Field, value: DocValue): Promise<void> {
|
||||
|
147
src/pages/TemplateBuilder/PrintContainer.vue
Normal file
147
src/pages/TemplateBuilder/PrintContainer.vue
Normal file
@ -0,0 +1,147 @@
|
||||
<template>
|
||||
<ScaledContainer
|
||||
:scale="Math.max(scale, 0.1)"
|
||||
ref="scaledContainer"
|
||||
class="mx-auto shadow-lg border"
|
||||
>
|
||||
<!-- Template -->
|
||||
<component
|
||||
v-if="!error"
|
||||
class="flex-1 bg-white"
|
||||
:doc="values.doc"
|
||||
:print="values.print"
|
||||
:is="templateComponent"
|
||||
/>
|
||||
|
||||
<!-- Compilation Error -->
|
||||
<div
|
||||
v-else
|
||||
class="
|
||||
h-full
|
||||
bg-red-100
|
||||
w-full
|
||||
text-2xl text-gray-900
|
||||
flex flex-col
|
||||
gap-4
|
||||
"
|
||||
>
|
||||
<h1 class="text-4xl font-bold text-red-500 p-4 border-b border-red-200">
|
||||
{{ t`Template Compilation Error` }}
|
||||
</h1>
|
||||
<p class="px-4 font-semibold">{{ error.message }}</p>
|
||||
<pre v-if="error.codeframe" class="px-4 text-xl text-gray-700">{{
|
||||
error.codeframe
|
||||
}}</pre>
|
||||
</div>
|
||||
</ScaledContainer>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import {
|
||||
compile,
|
||||
CompilerError,
|
||||
generateCodeFrame,
|
||||
SourceLocation,
|
||||
} from '@vue/compiler-dom';
|
||||
import { getPathAndMakePDF } from 'src/utils/printTemplates';
|
||||
import { PrintValues } from 'src/utils/types';
|
||||
import { defineComponent, PropType } from 'vue';
|
||||
import ScaledContainer from './ScaledContainer.vue';
|
||||
|
||||
export const baseSafeTemplate = `<main class="h-full w-full bg-white">
|
||||
<p class="p-4 text-red-500">
|
||||
<span class="font-bold">ERROR</span>: Template failed to load due to errors.
|
||||
</p>
|
||||
</main>
|
||||
`;
|
||||
|
||||
export default defineComponent({
|
||||
data() {
|
||||
return { error: null } as {
|
||||
error: null | { codeframe: string; message: string };
|
||||
};
|
||||
},
|
||||
props: {
|
||||
template: { type: String, required: true },
|
||||
scale: { type: Number, default: 0.65 },
|
||||
values: {
|
||||
type: Object as PropType<PrintValues>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
template(value: string) {
|
||||
this.compile(value);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.compile(this.template);
|
||||
},
|
||||
methods: {
|
||||
compile(template: string) {
|
||||
/**
|
||||
* Note: This is a hacky method to prevent
|
||||
* broken templates from reaching the `<component />`
|
||||
* element.
|
||||
*
|
||||
* It's required because the CompilerOptions doesn't
|
||||
* have an option to capture the errors.
|
||||
*
|
||||
* The compile function returns a code that can be
|
||||
* converted into a render function.
|
||||
*
|
||||
* This render function can be used instead
|
||||
* of passing the template to the `<component />` element
|
||||
* where it gets compiled again.
|
||||
*/
|
||||
this.error = null;
|
||||
return compile(template, {
|
||||
hoistStatic: true,
|
||||
onWarn: this.onError,
|
||||
onError: this.onError,
|
||||
});
|
||||
},
|
||||
onError({ message, loc }: CompilerError) {
|
||||
const codeframe = loc ? this.getCodeFrame(loc) : '';
|
||||
this.error = { codeframe, message };
|
||||
},
|
||||
getCodeFrame(loc: SourceLocation) {
|
||||
return generateCodeFrame(this.template, loc.start.offset, loc.end.offset);
|
||||
},
|
||||
async savePDF(name?: string) {
|
||||
/**
|
||||
* To be called through ref by the parent component.
|
||||
*/
|
||||
|
||||
// @ts-ignore
|
||||
const innerHTML = this.$refs.scaledContainer.$el.children[0].innerHTML;
|
||||
if (typeof innerHTML !== 'string') {
|
||||
return;
|
||||
}
|
||||
|
||||
await getPathAndMakePDF(name ?? this.t`Entry`, innerHTML);
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
templateComponent() {
|
||||
let template = this.template;
|
||||
if (this.error) {
|
||||
template = baseSafeTemplate;
|
||||
}
|
||||
|
||||
return {
|
||||
template,
|
||||
props: ['doc', 'print'],
|
||||
computed: {
|
||||
fyo() {
|
||||
return {};
|
||||
},
|
||||
platform() {
|
||||
return '';
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
components: { ScaledContainer },
|
||||
});
|
||||
</script>
|
49
src/pages/TemplateBuilder/ScaledContainer.vue
Normal file
49
src/pages/TemplateBuilder/ScaledContainer.vue
Normal file
@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<div class="overflow-hidden" :style="outerContainerStyle">
|
||||
<div :style="innerContainerStyle">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
/**
|
||||
* This Component is required because * CSS transforms (eg
|
||||
* scale) don't change the area taken by * an element.
|
||||
*
|
||||
* So to circumvent this, the outer element needs to have
|
||||
* the scaled dimensions without applying a CSS transform.
|
||||
*/
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
height: { type: String, default: '29.7cm' },
|
||||
width: { type: String, default: '21cm' },
|
||||
scale: { type: Number, default: 0.65 },
|
||||
},
|
||||
computed: {
|
||||
innerContainerStyle(): Record<string, string> {
|
||||
const style: Record<string, string> = {};
|
||||
style['width'] = this.width;
|
||||
style['height'] = this.height;
|
||||
style['transform'] = `scale(${this.scale})`;
|
||||
style['margin-top'] = `calc(-1 * (${this.height} * ${
|
||||
1 - this.scale
|
||||
}) / 2)`;
|
||||
style['margin-left'] = `calc(-1 * (${this.width} * ${
|
||||
1 - this.scale
|
||||
}) / 2)`;
|
||||
|
||||
return style;
|
||||
},
|
||||
outerContainerStyle(): Record<string, string> {
|
||||
const style: Record<string, string> = {};
|
||||
style['height'] = `calc(${this.scale} * ${this.height})`;
|
||||
style['width'] = `calc(${this.scale} * ${this.width})`;
|
||||
|
||||
return style;
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
685
src/pages/TemplateBuilder/TemplateBuilder.vue
Normal file
685
src/pages/TemplateBuilder/TemplateBuilder.vue
Normal file
@ -0,0 +1,685 @@
|
||||
<template>
|
||||
<div>
|
||||
<PageHeader :title="doc && doc.inserted ? doc.name : ''">
|
||||
<!-- Template Name -->
|
||||
<template #left v-if="doc && !doc.inserted">
|
||||
<FormControl
|
||||
ref="nameField"
|
||||
class="w-60 flex-shrink-0"
|
||||
size="small"
|
||||
:input-class="['font-semibold text-xl']"
|
||||
:df="fields.name"
|
||||
:border="true"
|
||||
:value="doc!.name"
|
||||
@change="async (value) => await doc?.set('name', value)"
|
||||
/>
|
||||
</template>
|
||||
<Button v-if="displayDoc && doc?.template" @click="savePDF">
|
||||
{{ t`Save as PDF` }}
|
||||
</Button>
|
||||
<Button
|
||||
v-if="doc && displayDoc"
|
||||
:title="t`Toggle Edit Mode`"
|
||||
:icon="true"
|
||||
@click="toggleEditMode"
|
||||
>
|
||||
<feather-icon name="edit" class="w-4 h-4" />
|
||||
</Button>
|
||||
<DropdownWithActions v-if="actions.length" :actions="actions" />
|
||||
<Button v-if="doc?.canSave" type="primary" @click="sync()">
|
||||
{{ t`Save` }}
|
||||
</Button>
|
||||
</PageHeader>
|
||||
|
||||
<!-- Template Builder Body -->
|
||||
<div
|
||||
v-if="doc"
|
||||
class="w-full bg-gray-50 grid"
|
||||
:style="templateBuilderBodyStyles"
|
||||
>
|
||||
<!-- Template Display Area -->
|
||||
<div
|
||||
class="overflow-auto no-scrollbar flex flex-col"
|
||||
style="height: calc(100vh - var(--h-row-largest) - 1px)"
|
||||
>
|
||||
<!-- Template Container -->
|
||||
<div v-if="canDisplayPreview" class="p-4 overflow-auto custom-scroll">
|
||||
<PrintContainer
|
||||
ref="printContainer"
|
||||
:template="doc.template!"
|
||||
:values="values!"
|
||||
:scale="scale"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Display Hints -->
|
||||
<p v-else-if="helperMessage" class="text-sm text-gray-700 p-4">
|
||||
{{ helperMessage }}
|
||||
</p>
|
||||
|
||||
<!-- Bottom Bar -->
|
||||
<div
|
||||
class="
|
||||
w-full
|
||||
sticky
|
||||
bottom-0
|
||||
flex
|
||||
bg-white
|
||||
border-t
|
||||
mt-auto
|
||||
flex-shrink-0
|
||||
"
|
||||
>
|
||||
<!-- Entry Type -->
|
||||
<FormControl
|
||||
:title="fields.type.label"
|
||||
class="w-40 border-r flex-shrink-0"
|
||||
:df="fields.type"
|
||||
:border="false"
|
||||
:value="doc.get('type')"
|
||||
:container-styles="{ 'border-radius': '0px' }"
|
||||
@change="async (value) => await setType(value)"
|
||||
/>
|
||||
|
||||
<!-- Display Doc -->
|
||||
<FormControl
|
||||
v-if="doc.type"
|
||||
:title="displayDocField.label"
|
||||
class="w-40 border-r flex-shrink-0"
|
||||
:df="displayDocField"
|
||||
:border="false"
|
||||
:value="displayDoc?.name"
|
||||
:container-styles="{ 'border-radius': '0px' }"
|
||||
@change="(value: string) => setDisplayDoc(value)"
|
||||
/>
|
||||
|
||||
<!-- Display Scale -->
|
||||
<div
|
||||
v-if="canDisplayPreview"
|
||||
class="flex ml-auto gap-2 px-2 w-36 justify-between flex-shrink-0"
|
||||
>
|
||||
<p class="text-sm text-gray-600 my-auto">{{ t`Display Scale` }}</p>
|
||||
<input
|
||||
type="number"
|
||||
class="
|
||||
my-auto
|
||||
w-10
|
||||
text-base text-end
|
||||
bg-transparent
|
||||
text-gray-800
|
||||
focus:text-gray-900
|
||||
"
|
||||
:value="scale"
|
||||
@change="setScale"
|
||||
@input="setScale"
|
||||
min="0.1"
|
||||
max="10"
|
||||
step="0.1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Input Panel Resizer -->
|
||||
<HorizontalResizer
|
||||
:initial-x="panelWidth"
|
||||
:min-x="22 * 16"
|
||||
:max-x="maxWidth"
|
||||
style="z-index: 5"
|
||||
@resize="(x: number) => panelWidth = x"
|
||||
/>
|
||||
|
||||
<!-- Template Panel -->
|
||||
<div
|
||||
class="border-l bg-white flex flex-col"
|
||||
style="height: calc(100vh - var(--h-row-largest) - 1px)"
|
||||
>
|
||||
<!-- Template Editor -->
|
||||
<div class="min-h-0">
|
||||
<TemplateEditor
|
||||
v-if="typeof doc.template === 'string' && hints"
|
||||
ref="templateEditor"
|
||||
class="overflow-auto custom-scroll h-full"
|
||||
:initial-value="doc.template"
|
||||
:disabled="!doc.isCustom"
|
||||
:hints="hints"
|
||||
@input="() => (templateChanged = true)"
|
||||
@blur="(value: string) => setTemplate(value)"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="templateChanged"
|
||||
class="flex gap-2 p-2 text-sm text-gray-600 items-center mt-auto"
|
||||
>
|
||||
<ShortcutKeys :keys="applyChangesShortcut" :simple="true" />
|
||||
{{ t` to apply changes` }}
|
||||
</div>
|
||||
|
||||
<!-- Value Key Hints Container -->
|
||||
<div
|
||||
class="border-t flex-shrink-0"
|
||||
:class="templateChanged ? '' : 'mt-auto'"
|
||||
v-if="hints"
|
||||
>
|
||||
<!-- Value Key Toggle -->
|
||||
<div
|
||||
class="
|
||||
flex
|
||||
justify-between
|
||||
items-center
|
||||
cursor-pointer
|
||||
select-none
|
||||
p-2
|
||||
"
|
||||
@click="toggleShowHints"
|
||||
>
|
||||
<h2 class="text-base text-gray-900 font-semibold">
|
||||
{{ t`Key Hints` }}
|
||||
</h2>
|
||||
<feather-icon
|
||||
:name="showHints ? 'chevron-up' : 'chevron-down'"
|
||||
class="w-4 h-4 text-gray-600 resize-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Value Key Hints -->
|
||||
<Transition name="hints">
|
||||
<div
|
||||
v-if="showHints"
|
||||
class="overflow-auto custom-scroll p-2 border-t"
|
||||
style="max-height: 30vh"
|
||||
>
|
||||
<TemplateBuilderHint :hints="hints" />
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { EditorView } from 'codemirror';
|
||||
import { Doc } from 'fyo/model/doc';
|
||||
import { PrintTemplate } from 'models/baseModels/PrintTemplate';
|
||||
import { ModelNameEnum } from 'models/types';
|
||||
import { saveExportData } from 'reports/commonExporter';
|
||||
import { Field, TargetField } from 'schemas/types';
|
||||
import Button from 'src/components/Button.vue';
|
||||
import FormControl from 'src/components/Controls/FormControl.vue';
|
||||
import DropdownWithActions from 'src/components/DropdownWithActions.vue';
|
||||
import HorizontalResizer from 'src/components/HorizontalResizer.vue';
|
||||
import PageHeader from 'src/components/PageHeader.vue';
|
||||
import ShortcutKeys from 'src/components/ShortcutKeys.vue';
|
||||
import { handleErrorWithDialog } from 'src/errorHandling';
|
||||
import { getSavePath } from 'src/utils/ipcCalls';
|
||||
import { docsPathMap } from 'src/utils/misc';
|
||||
import {
|
||||
baseTemplate,
|
||||
getPrintTemplatePropHints,
|
||||
getPrintTemplatePropValues,
|
||||
} from 'src/utils/printTemplates';
|
||||
import { docsPathRef, focusedDocsRef, showSidebar } from 'src/utils/refs';
|
||||
import { PrintValues } from 'src/utils/types';
|
||||
import {
|
||||
focusOrSelectFormControl,
|
||||
getActionsForDoc,
|
||||
getDocFromNameIfExistsElseNew,
|
||||
openSettings,
|
||||
selectTextFile,
|
||||
ShortcutKey,
|
||||
showMessageDialog,
|
||||
showToast,
|
||||
} from 'src/utils/ui';
|
||||
import { Shortcuts } from 'src/utils/vueUtils';
|
||||
import { getMapFromList } from 'utils/index';
|
||||
import { computed, defineComponent } from 'vue';
|
||||
import PrintContainer from './PrintContainer.vue';
|
||||
import TemplateBuilderHint from './TemplateBuilderHint.vue';
|
||||
import TemplateEditor from './TemplateEditor.vue';
|
||||
|
||||
export default defineComponent({
|
||||
props: { name: String },
|
||||
components: {
|
||||
PageHeader,
|
||||
Button,
|
||||
DropdownWithActions,
|
||||
PrintContainer,
|
||||
HorizontalResizer,
|
||||
TemplateEditor,
|
||||
FormControl,
|
||||
TemplateBuilderHint,
|
||||
ShortcutKeys,
|
||||
},
|
||||
inject: { shortcutManager: { from: 'shortcuts' } },
|
||||
provide() {
|
||||
return { doc: computed(() => this.doc) };
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
doc: null,
|
||||
editMode: false,
|
||||
showHints: false,
|
||||
hints: undefined,
|
||||
values: null,
|
||||
displayDoc: null,
|
||||
scale: 0.6,
|
||||
panelWidth: 22 /** rem */ * 16 /** px */,
|
||||
templateChanged: false,
|
||||
preEditMode: {
|
||||
scale: 0.6,
|
||||
showSidebar: true,
|
||||
panelWidth: 22 * 16,
|
||||
},
|
||||
} as {
|
||||
editMode: boolean;
|
||||
showHints: boolean;
|
||||
hints?: Record<string, unknown>;
|
||||
values: null | PrintValues;
|
||||
doc: PrintTemplate | null;
|
||||
displayDoc: PrintTemplate | null;
|
||||
scale: number;
|
||||
panelWidth: number;
|
||||
templateChanged: boolean;
|
||||
preEditMode: {
|
||||
scale: number;
|
||||
showSidebar: boolean;
|
||||
panelWidth: number;
|
||||
};
|
||||
};
|
||||
},
|
||||
async mounted() {
|
||||
await this.initialize();
|
||||
focusedDocsRef.add(this.doc);
|
||||
if (this.fyo.store.isDevelopment) {
|
||||
// @ts-ignore
|
||||
window.tb = this;
|
||||
}
|
||||
},
|
||||
async activated(): Promise<void> {
|
||||
docsPathRef.value = docsPathMap.PrintTemplate ?? '';
|
||||
this.shortcuts.ctrl.set(['Enter'], this.setTemplate);
|
||||
this.shortcuts.ctrl.set(['KeyE'], this.toggleEditMode);
|
||||
this.shortcuts.ctrl.set(['KeyH'], this.toggleShowHints);
|
||||
this.shortcuts.ctrl.set(['Equal'], () => this.setScale(this.scale + 0.1));
|
||||
this.shortcuts.ctrl.set(['Minus'], () => this.setScale(this.scale - 0.1));
|
||||
},
|
||||
deactivated(): void {
|
||||
docsPathRef.value = '';
|
||||
if (this.doc instanceof Doc) {
|
||||
focusedDocsRef.delete(this.doc);
|
||||
}
|
||||
this.shortcuts.ctrl.delete(['Enter']);
|
||||
this.shortcuts.ctrl.delete(['KeyE']);
|
||||
this.shortcuts.ctrl.delete(['KeyH']);
|
||||
this.shortcuts.ctrl.delete(['Equal']);
|
||||
this.shortcuts.ctrl.delete(['Minus']);
|
||||
},
|
||||
methods: {
|
||||
async initialize() {
|
||||
await this.setDoc();
|
||||
if (this.doc?.type) {
|
||||
this.hints = getPrintTemplatePropHints(this.doc.type, this.fyo);
|
||||
}
|
||||
|
||||
focusOrSelectFormControl(this.doc as Doc, this.$refs.nameField, false);
|
||||
|
||||
if (!this.doc?.template) {
|
||||
await this.doc?.set('template', baseTemplate);
|
||||
}
|
||||
|
||||
await this.setDisplayInitialDoc();
|
||||
},
|
||||
getTemplateEditorState() {
|
||||
const fallback = this.doc?.template ?? '';
|
||||
|
||||
if (!this.view) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
return this.view.state.doc.toString();
|
||||
},
|
||||
async setTemplate(value?: string) {
|
||||
this.templateChanged = false;
|
||||
if (!this.doc?.isCustom) {
|
||||
return;
|
||||
}
|
||||
|
||||
value ??= this.getTemplateEditorState();
|
||||
await this.doc?.set('template', value);
|
||||
},
|
||||
setScale(e: Event | number) {
|
||||
let value = this.scale;
|
||||
if (typeof e === 'number') {
|
||||
value = Number(e.toFixed(2));
|
||||
} else if (e instanceof Event && e.target instanceof HTMLInputElement) {
|
||||
value = Number(e.target.value);
|
||||
}
|
||||
|
||||
this.scale = Math.max(Math.min(value, 10), 0.15);
|
||||
},
|
||||
toggleShowHints() {
|
||||
this.showHints = !this.showHints;
|
||||
},
|
||||
async toggleEditMode() {
|
||||
if (!this.doc?.isCustom) {
|
||||
return;
|
||||
}
|
||||
|
||||
let message = this.t`Please set a Display Doc`;
|
||||
if (!this.displayDoc) {
|
||||
return showToast({ type: 'warning', message, duration: 1000 });
|
||||
}
|
||||
|
||||
this.editMode = !this.editMode;
|
||||
|
||||
if (this.editMode) {
|
||||
return this.enableEditMode();
|
||||
}
|
||||
|
||||
this.disableEditMode();
|
||||
},
|
||||
enableEditMode() {
|
||||
this.preEditMode.showSidebar = showSidebar.value;
|
||||
this.preEditMode.panelWidth = this.panelWidth;
|
||||
this.preEditMode.scale = this.scale;
|
||||
|
||||
this.panelWidth = Math.max(window.innerWidth / 2, this.panelWidth);
|
||||
showSidebar.value = false;
|
||||
this.scale = this.getEditModeScale();
|
||||
this.view?.focus();
|
||||
},
|
||||
disableEditMode() {
|
||||
showSidebar.value = this.preEditMode.showSidebar;
|
||||
this.panelWidth = this.preEditMode.panelWidth;
|
||||
this.scale = this.preEditMode.scale;
|
||||
},
|
||||
getEditModeScale(): number {
|
||||
// @ts-ignore
|
||||
const div = this.$refs.printContainer.$el;
|
||||
if (!(div instanceof HTMLDivElement)) {
|
||||
return this.scale;
|
||||
}
|
||||
|
||||
const padding = 16 * 2 /** p-4 */ + 16 * 0.6; /** w-scrollbar */
|
||||
const targetWidth = window.innerWidth / 2 - padding;
|
||||
const currentWidth = div.getBoundingClientRect().width;
|
||||
const targetScale = (targetWidth * this.scale) / currentWidth;
|
||||
|
||||
return Number(targetScale.toFixed(2));
|
||||
},
|
||||
async savePDF() {
|
||||
const printContainer = this.$refs.printContainer as {
|
||||
savePDF: (name?: string) => void;
|
||||
};
|
||||
|
||||
if (!printContainer?.savePDF) {
|
||||
return;
|
||||
}
|
||||
|
||||
printContainer.savePDF(this.displayDoc?.name);
|
||||
},
|
||||
async setDisplayInitialDoc() {
|
||||
const schemaName = this.doc?.type;
|
||||
if (!schemaName || this.displayDoc?.schemaName === schemaName) {
|
||||
return;
|
||||
}
|
||||
|
||||
const names = (await this.fyo.db.getAll(schemaName, {
|
||||
limit: 1,
|
||||
order: 'desc',
|
||||
orderBy: 'created',
|
||||
filters: { cancelled: false },
|
||||
})) as { name: string }[];
|
||||
|
||||
const name = names[0]?.name;
|
||||
if (!name) {
|
||||
const label = this.fyo.schemaMap[schemaName]?.label ?? schemaName;
|
||||
await showMessageDialog({
|
||||
message: this.t`No Display Entries Found`,
|
||||
detail: this
|
||||
.t`Please create a ${label} entry to view Template Preview`,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await this.setDisplayDoc(name);
|
||||
},
|
||||
async sync() {
|
||||
const doc = this.doc;
|
||||
if (!doc) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await doc.sync();
|
||||
} catch (error) {
|
||||
await handleErrorWithDialog(error, doc as Doc);
|
||||
}
|
||||
},
|
||||
async setDoc() {
|
||||
if (this.doc) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.doc = (await getDocFromNameIfExistsElseNew(
|
||||
ModelNameEnum.PrintTemplate,
|
||||
this.name
|
||||
)) as PrintTemplate;
|
||||
},
|
||||
async setType(value: unknown) {
|
||||
if (typeof value !== 'string') {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.doc?.set('type', value);
|
||||
await this.setDisplayInitialDoc();
|
||||
},
|
||||
async setDisplayDoc(value: string) {
|
||||
if (!value) {
|
||||
delete this.hints;
|
||||
this.values = null;
|
||||
this.displayDoc = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const schemaName = this.doc?.type;
|
||||
if (!schemaName) {
|
||||
return;
|
||||
}
|
||||
|
||||
const displayDoc = await getDocFromNameIfExistsElseNew(schemaName, value);
|
||||
this.hints = getPrintTemplatePropHints(schemaName, this.fyo);
|
||||
this.values = await getPrintTemplatePropValues(displayDoc);
|
||||
this.displayDoc = displayDoc;
|
||||
},
|
||||
async selectFile() {
|
||||
const { name: fileName, text } = await selectTextFile([
|
||||
{ name: 'Template', extensions: ['template.html', 'html'] },
|
||||
]);
|
||||
|
||||
if (!text) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.doc?.set('template', text);
|
||||
this.view?.dispatch({
|
||||
changes: { from: 0, to: this.view.state.doc.length, insert: text },
|
||||
});
|
||||
|
||||
if (this.doc?.inserted) {
|
||||
return;
|
||||
}
|
||||
|
||||
let name: string | null = null;
|
||||
if (fileName.endsWith('.template.html')) {
|
||||
name = fileName.split('.template.html')[0];
|
||||
}
|
||||
|
||||
if (!name && fileName.endsWith('.html')) {
|
||||
name = fileName.split('.html')[0];
|
||||
}
|
||||
|
||||
if (!name) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.doc?.set('name', name);
|
||||
},
|
||||
async saveFile() {
|
||||
const name = this.doc?.name;
|
||||
const template = this.getTemplateEditorState();
|
||||
|
||||
if (!name) {
|
||||
return await showToast({
|
||||
type: 'warning',
|
||||
message: this.t`Print Template Name not set`,
|
||||
});
|
||||
}
|
||||
|
||||
if (!template) {
|
||||
return await showToast({
|
||||
type: 'warning',
|
||||
message: this.t`Print Template is empty`,
|
||||
});
|
||||
}
|
||||
|
||||
const { canceled, filePath } = await getSavePath(name, 'template.html');
|
||||
if (canceled || !filePath) {
|
||||
return;
|
||||
}
|
||||
|
||||
await saveExportData(template, filePath, this.t`Template file saved`);
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
canDisplayPreview(): boolean {
|
||||
if (!this.displayDoc || !this.values) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.doc?.template) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
applyChangesShortcut() {
|
||||
return [ShortcutKey.ctrl, ShortcutKey.enter];
|
||||
},
|
||||
view(): EditorView | null {
|
||||
// @ts-ignore
|
||||
const { view } = this.$refs.templateEditor ?? {};
|
||||
if (view instanceof EditorView) {
|
||||
return view;
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
shortcuts(): Shortcuts {
|
||||
// @ts-ignore
|
||||
const shortcutManager = this.shortcutManager;
|
||||
if (shortcutManager instanceof Shortcuts) {
|
||||
return shortcutManager;
|
||||
}
|
||||
|
||||
// no-op (hopefully)
|
||||
throw Error('Shortcuts Not Found');
|
||||
},
|
||||
maxWidth() {
|
||||
return window.innerWidth - 12 * 16 - 100;
|
||||
},
|
||||
actions() {
|
||||
if (!this.doc) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const actions = getActionsForDoc(this.doc as Doc);
|
||||
actions.push({
|
||||
label: this.t`Print Settings`,
|
||||
group: this.t`View`,
|
||||
action: async () => {
|
||||
await openSettings(ModelNameEnum.PrintSettings);
|
||||
},
|
||||
});
|
||||
|
||||
if (this.doc.isCustom) {
|
||||
actions.push({
|
||||
label: this.t`Select Template File`,
|
||||
group: this.t`Action`,
|
||||
action: this.selectFile,
|
||||
});
|
||||
}
|
||||
|
||||
actions.push({
|
||||
label: this.t`Save Template File`,
|
||||
group: this.t`Action`,
|
||||
action: this.saveFile,
|
||||
});
|
||||
|
||||
return actions;
|
||||
},
|
||||
fields(): Record<string, Field> {
|
||||
return getMapFromList(
|
||||
this.fyo.schemaMap.PrintTemplate?.fields ?? [],
|
||||
'fieldname'
|
||||
);
|
||||
},
|
||||
displayDocField(): TargetField {
|
||||
const target = this.doc?.type ?? ModelNameEnum.SalesInvoice;
|
||||
return {
|
||||
fieldname: 'displayDoc',
|
||||
label: this.t`Display Doc`,
|
||||
fieldtype: 'Link',
|
||||
target,
|
||||
};
|
||||
},
|
||||
helperMessage() {
|
||||
if (!this.doc) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!this.doc.type) {
|
||||
return this.t`Select a Template type`;
|
||||
}
|
||||
|
||||
if (!this.displayDoc) {
|
||||
return this.t`Select a Display Doc to view the Template`;
|
||||
}
|
||||
|
||||
if (!this.doc.template) {
|
||||
return this.t`Set a Template value to see the Print Template`;
|
||||
}
|
||||
|
||||
return '';
|
||||
},
|
||||
templateBuilderBodyStyles(): Record<string, string> {
|
||||
const styles: Record<string, string> = {};
|
||||
|
||||
styles['grid-template-columns'] = `auto 0px ${this.panelWidth}px`;
|
||||
styles['height'] = 'calc(100vh - var(--h-row-largest) - 1px)';
|
||||
|
||||
return styles;
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.hints-enter-from,
|
||||
.hints-leave-to {
|
||||
opacity: 0;
|
||||
height: 0px;
|
||||
}
|
||||
.hints-enter-to,
|
||||
.hints-leave-from {
|
||||
opacity: 1;
|
||||
height: 30vh;
|
||||
}
|
||||
|
||||
.hints-enter-active,
|
||||
.hints-leave-active {
|
||||
transition: all 150ms ease-out;
|
||||
}
|
||||
</style>
|
110
src/pages/TemplateBuilder/TemplateBuilderHint.vue
Normal file
110
src/pages/TemplateBuilder/TemplateBuilderHint.vue
Normal file
@ -0,0 +1,110 @@
|
||||
<template>
|
||||
<div :class="level > 0 ? 'ms-2 ps-2 border-l' : ''">
|
||||
<template v-for="r of rows" :key="r.key">
|
||||
<div
|
||||
class="
|
||||
flex
|
||||
gap-2
|
||||
text-sm text-gray-600
|
||||
whitespace-nowrap
|
||||
overflow-auto
|
||||
no-scrollbar
|
||||
"
|
||||
:class="[typeof r.value === 'object' ? 'cursor-pointer' : '']"
|
||||
@click="r.collapsed = !r.collapsed"
|
||||
>
|
||||
<div class="">{{ getKey(r) }}</div>
|
||||
<div v-if="!r.isCollapsible" class="font-semibold text-gray-800">
|
||||
{{ r.value }}
|
||||
</div>
|
||||
<div
|
||||
v-else-if="Array.isArray(r.value)"
|
||||
class="
|
||||
text-blue-600
|
||||
bg-blue-50
|
||||
border-blue-200 border
|
||||
tracking-tighter
|
||||
rounded
|
||||
text-xs
|
||||
px-1
|
||||
"
|
||||
>
|
||||
Array
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="
|
||||
text-pink-600
|
||||
bg-pink-50
|
||||
border-pink-200 border
|
||||
tracking-tighter
|
||||
rounded
|
||||
text-xs
|
||||
px-1
|
||||
"
|
||||
>
|
||||
Object
|
||||
</div>
|
||||
|
||||
<feather-icon
|
||||
v-if="r.isCollapsible"
|
||||
:name="r.collapsed ? 'chevron-up' : 'chevron-down'"
|
||||
class="w-4 h-4 ms-auto"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="!r.collapsed && typeof r.value === 'object'">
|
||||
<TemplateBuilderHint
|
||||
:prefix="getKey(r)"
|
||||
:hints="Array.isArray(r.value) ? r.value[0] : r.value"
|
||||
:level="level + 1"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
type HintRow = {
|
||||
key: string;
|
||||
value: string | Record<string, unknown>;
|
||||
isCollapsible: boolean;
|
||||
collapsed: boolean;
|
||||
};
|
||||
export default defineComponent({
|
||||
name: 'TemplateBuilderHint',
|
||||
props: {
|
||||
prefix: { type: String, default: '' },
|
||||
hints: { type: Object, required: true },
|
||||
level: { type: Number, default: 0 },
|
||||
},
|
||||
data() {
|
||||
return { rows: [] } as {
|
||||
rows: HintRow[];
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.rows = Object.entries(this.hints)
|
||||
.map(([key, value]) => ({
|
||||
key,
|
||||
value,
|
||||
isCollapsible: typeof value === 'object',
|
||||
collapsed: this.level > 0,
|
||||
}))
|
||||
.sort((a, b) => Number(a.isCollapsible) - Number(b.isCollapsible));
|
||||
},
|
||||
methods: {
|
||||
getKey(row: HintRow) {
|
||||
const isArray = Array.isArray(row.value);
|
||||
if (isArray) {
|
||||
return `${this.prefix}.${row.key}[number]`;
|
||||
}
|
||||
|
||||
if (this.prefix.length) {
|
||||
return `${this.prefix}.${row.key}`;
|
||||
}
|
||||
|
||||
return row.key;
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
250
src/pages/TemplateBuilder/TemplateEditor.vue
Normal file
250
src/pages/TemplateBuilder/TemplateEditor.vue
Normal file
@ -0,0 +1,250 @@
|
||||
<template>
|
||||
<div ref="container" class="bg-white text-gray-900"></div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { autocompletion, CompletionContext } from '@codemirror/autocomplete';
|
||||
import { vue } from '@codemirror/lang-vue';
|
||||
import {
|
||||
HighlightStyle,
|
||||
syntaxHighlighting,
|
||||
syntaxTree,
|
||||
} from '@codemirror/language';
|
||||
import { Compartment, EditorState } from '@codemirror/state';
|
||||
import { EditorView, ViewUpdate } from '@codemirror/view';
|
||||
import { tags } from '@lezer/highlight';
|
||||
import { basicSetup } from 'codemirror';
|
||||
import { uicolors } from 'src/utils/colors';
|
||||
import { defineComponent, markRaw } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
data() {
|
||||
return { state: null, view: null, compartments: {} } as {
|
||||
state: EditorState | null;
|
||||
view: EditorView | null;
|
||||
compartments: Record<string, Compartment>;
|
||||
};
|
||||
},
|
||||
props: {
|
||||
initialValue: { type: String, required: true },
|
||||
disabled: { type: Boolean, default: false },
|
||||
hints: { type: Object },
|
||||
},
|
||||
watch: {
|
||||
disabled(value: boolean) {
|
||||
this.setDisabled(value);
|
||||
},
|
||||
},
|
||||
emits: ['input', 'blur'],
|
||||
mounted() {
|
||||
if (!this.view) {
|
||||
this.init();
|
||||
}
|
||||
|
||||
if (this.fyo.store.isDevelopment) {
|
||||
// @ts-ignore
|
||||
window.te = this;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
init() {
|
||||
const readOnly = new Compartment();
|
||||
const editable = new Compartment();
|
||||
|
||||
const highlightStyle = HighlightStyle.define([
|
||||
{ tag: tags.typeName, color: uicolors.pink[600] },
|
||||
{ tag: tags.angleBracket, color: uicolors.pink[600] },
|
||||
{ tag: tags.attributeName, color: uicolors.gray[600] },
|
||||
{ tag: tags.attributeValue, color: uicolors.blue[600] },
|
||||
{ tag: tags.comment, color: uicolors.gray[500], fontStyle: 'italic' },
|
||||
{ tag: tags.keyword, color: uicolors.pink[500] },
|
||||
{ tag: tags.variableName, color: uicolors.blue[700] },
|
||||
{ tag: tags.string, color: uicolors.pink[600] },
|
||||
{ tag: tags.content, color: uicolors.gray[700] },
|
||||
]);
|
||||
const completions = getCompletionsFromHints(this.hints ?? {});
|
||||
|
||||
const view = new EditorView({
|
||||
doc: this.initialValue,
|
||||
extensions: [
|
||||
EditorView.updateListener.of(this.updateListener),
|
||||
readOnly.of(EditorState.readOnly.of(this.disabled)),
|
||||
editable.of(EditorView.editable.of(!this.disabled)),
|
||||
basicSetup,
|
||||
vue(),
|
||||
syntaxHighlighting(highlightStyle),
|
||||
autocompletion({ override: [completions] }),
|
||||
],
|
||||
parent: this.container,
|
||||
});
|
||||
this.view = markRaw(view);
|
||||
|
||||
const compartments = { readOnly, editable };
|
||||
this.compartments = markRaw(compartments);
|
||||
},
|
||||
updateListener(update: ViewUpdate) {
|
||||
if (update.docChanged) {
|
||||
this.$emit('input', this.view?.state.doc.toString() ?? '');
|
||||
}
|
||||
|
||||
if (update.focusChanged && !this.view?.hasFocus) {
|
||||
this.$emit('blur', this.view?.state.doc.toString() ?? '');
|
||||
}
|
||||
},
|
||||
setDisabled(value: boolean) {
|
||||
const { readOnly, editable } = this.compartments;
|
||||
this.view?.dispatch({
|
||||
effects: [
|
||||
readOnly.reconfigure(EditorState.readOnly.of(value)),
|
||||
editable.reconfigure(EditorView.editable.of(!value)),
|
||||
],
|
||||
});
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
container() {
|
||||
const { container } = this.$refs;
|
||||
if (container instanceof HTMLDivElement) {
|
||||
return container;
|
||||
}
|
||||
|
||||
throw new Error('ref container is not a div element');
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
function getCompletionsFromHints(hints: Record<string, unknown>) {
|
||||
const options = hintsToCompletionOptions(hints);
|
||||
return function completions(context: CompletionContext) {
|
||||
let word = context.matchBefore(/\w*/);
|
||||
if (word == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const node = syntaxTree(context.state).resolveInner(context.pos);
|
||||
const aptLocation = ['ScriptAttributeValue', 'SingleExpression'];
|
||||
|
||||
if (!aptLocation.includes(node.name)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (word.from === word.to && !context.explicit) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
from: word.from,
|
||||
options,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
type CompletionOption = {
|
||||
label: string;
|
||||
type: string;
|
||||
detail: string;
|
||||
};
|
||||
|
||||
function hintsToCompletionOptions(
|
||||
hints: object,
|
||||
prefix?: string
|
||||
): CompletionOption[] {
|
||||
prefix ??= '';
|
||||
const list: CompletionOption[] = [];
|
||||
|
||||
for (const [key, value] of Object.entries(hints)) {
|
||||
const option = getCompletionOption(key, value, prefix);
|
||||
if (option === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Array.isArray(option)) {
|
||||
list.push(...option);
|
||||
continue;
|
||||
}
|
||||
|
||||
list.push(option);
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
function getCompletionOption(
|
||||
key: string,
|
||||
value: unknown,
|
||||
prefix: string
|
||||
): null | CompletionOption | CompletionOption[] {
|
||||
let label = key;
|
||||
if (prefix.length) {
|
||||
label = prefix + '.' + key;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return {
|
||||
label,
|
||||
type: 'variable',
|
||||
detail: 'Child Table',
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
return {
|
||||
label,
|
||||
type: 'variable',
|
||||
detail: value,
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
return hintsToCompletionOptions(value, label);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.cm-line {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.cm-gutter {
|
||||
@apply bg-gray-50;
|
||||
}
|
||||
|
||||
.cm-gutters {
|
||||
border: none black !important;
|
||||
border-right: 1px solid theme('colors.gray.200') !important;
|
||||
}
|
||||
|
||||
.cm-activeLine,
|
||||
.cm-activeLineGutter {
|
||||
background-color: #e5f3ff67 !important;
|
||||
}
|
||||
|
||||
.cm-tooltip-autocomplete {
|
||||
background-color: white !important;
|
||||
border: 1px solid theme('colors.gray.200') !important;
|
||||
@apply rounded shadow-lg overflow-hidden text-gray-900;
|
||||
}
|
||||
|
||||
.cm-tooltip-autocomplete [aria-selected] {
|
||||
color: #334155 !important;
|
||||
background-color: theme('colors.blue.100') !important;
|
||||
}
|
||||
|
||||
.cm-panels {
|
||||
border-top: 1px solid theme('colors.gray.200') !important;
|
||||
background-color: theme('colors.gray.50') !important;
|
||||
color: theme('colors.gray.800') !important;
|
||||
}
|
||||
|
||||
.cm-button {
|
||||
background-image: none !important;
|
||||
background-color: theme('colors.gray.200') !important;
|
||||
color: theme('colors.gray.700') !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.cm-textfield {
|
||||
border: 1px solid theme('colors.gray.200') !important;
|
||||
}
|
||||
</style>
|
@ -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) {
|
||||
|
@ -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',
|
||||
|
@ -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');
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -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;
|
||||
|
@ -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';
|
||||
|
@ -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<TemplateFile[]> {
|
||||
return await ipcRenderer.invoke(IPC_ACTIONS.GET_TEMPLATES);
|
||||
}
|
||||
|
||||
export async function selectFile(
|
||||
options: SelectFileOptions
|
||||
): Promise<SelectFileReturn> {
|
||||
|
@ -115,6 +115,7 @@ export const docsPathMap: Record<string, string | undefined> = {
|
||||
[ModelNameEnum.Party]: 'entries/party',
|
||||
[ModelNameEnum.Item]: 'entries/items',
|
||||
[ModelNameEnum.Tax]: 'entries/taxes',
|
||||
[ModelNameEnum.PrintTemplate]: 'miscellaneous/print-templates',
|
||||
|
||||
// Miscellaneous
|
||||
Search: 'miscellaneous/search',
|
||||
|
370
src/utils/printTemplates.ts
Normal file
370
src/utils/printTemplates.ts
Normal file
@ -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<string, unknown>;
|
||||
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<PrintValues> {
|
||||
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 = [
|
||||
'<meta charset="UTF-8">',
|
||||
'<title>Print Window</title>',
|
||||
].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<string, Date>,
|
||||
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 = `<main class="h-full w-full bg-white">
|
||||
|
||||
<!-- Edit This Code -->
|
||||
<header class="p-4 flex justify-between border-b">
|
||||
<h2
|
||||
class="font-semibold text-2xl"
|
||||
:style="{ color: print.color }"
|
||||
>
|
||||
{{ print.companyName }}
|
||||
</h2>
|
||||
<h2 class="font-semibold text-2xl" >
|
||||
{{ doc.name }}
|
||||
</h2>
|
||||
</header>
|
||||
|
||||
<div class="p-4 text-gray-600">
|
||||
Edit the code in the Template Editor on the right
|
||||
to create your own personalized custom template.
|
||||
</div>
|
||||
|
||||
</main>
|
||||
`;
|
@ -1,6 +1,7 @@
|
||||
import { reactive, ref } from 'vue';
|
||||
import { FocusedDocContextSet } from './misc';
|
||||
|
||||
export const showSidebar = ref(true);
|
||||
export const docsPathRef = ref<string>('');
|
||||
export const systemLanguageRef = ref<string>('');
|
||||
export const focusedDocsRef = reactive<FocusedDocContextSet>(
|
||||
|
@ -272,6 +272,11 @@ async function getCompleteSidebar(): Promise<SidebarConfig> {
|
||||
name: 'import-wizard',
|
||||
route: '/import-wizard',
|
||||
},
|
||||
{
|
||||
label: t`Print Templates`,
|
||||
name: 'print-template',
|
||||
route: `/list/PrintTemplate/${t`Print Templates`}`,
|
||||
},
|
||||
{
|
||||
label: t`Settings`,
|
||||
name: 'settings',
|
||||
|
@ -85,3 +85,8 @@ export type ActionGroup = {
|
||||
export type UIGroupedFields = Map<string, Map<string, Field[]>>;
|
||||
export type ExportFormat = 'csv' | 'json';
|
||||
export type PeriodKey = 'This Year' | 'This Quarter' | 'This Month';
|
||||
|
||||
export type PrintValues = {
|
||||
print: Record<string, unknown>;
|
||||
doc: Record<string, unknown>;
|
||||
};
|
||||
|
147
src/utils/ui.ts
147
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<Element>, 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<ShortcutKey, string> {
|
||||
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',
|
||||
};
|
||||
}
|
||||
|
148
templates/Basic.template.html
Normal file
148
templates/Basic.template.html
Normal file
@ -0,0 +1,148 @@
|
||||
<main class="bg-white h-full px-6" :style="{ 'font-family': print.font }">
|
||||
<!-- Invoice Header -->
|
||||
<header class="py-6 flex text-sm text-gray-900 border-b">
|
||||
<!-- Company Logo & Name -->
|
||||
<section class="w-1/3">
|
||||
<img
|
||||
v-if="print.displayLogo"
|
||||
class="h-12 max-w-32 object-contain"
|
||||
:src="print.logo"
|
||||
/>
|
||||
<div class="text-xl text-gray-700 font-semibold" v-else>
|
||||
{{ print.companyName }}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Company Contacts -->
|
||||
<section class="w-1/3">
|
||||
<div>{{ print.email }}</div>
|
||||
<div class="mt-1">{{ print.phone }}</div>
|
||||
</section>
|
||||
|
||||
<!-- Company Address & GSTIN -->
|
||||
<section class="w-1/3">
|
||||
<div v-if="print.address">{{ print.links.address.addressDisplay }}</div>
|
||||
<div v-if="print.gstin">GSTIN: {{ print.gstin }}</div>
|
||||
</section>
|
||||
</header>
|
||||
|
||||
<!-- Sub Heading Section -->
|
||||
<section class="mt-8 flex justify-between">
|
||||
<!-- Invoice Details -->
|
||||
<section class="w-1/3">
|
||||
<h2 class="text-2xl font-semibold">{{ doc.name }}</h2>
|
||||
<p class="py-2 text-base">{{ doc.date }}</p>
|
||||
</section>
|
||||
|
||||
<!-- Party Details -->
|
||||
<section class="w-1/3" v-if="doc.party">
|
||||
<h2 class="py-1 text-right text-lg font-semibold">{{ doc.party }}</h2>
|
||||
<p
|
||||
v-if="doc.links.party.address"
|
||||
class="mt-1 text-xs text-gray-600 text-right"
|
||||
>
|
||||
{{ doc.links.party.links.address.addressDisplay }}
|
||||
</p>
|
||||
<p
|
||||
v-if="doc.links.party.gstin"
|
||||
class="mt-1 text-xs text-gray-600 text-right"
|
||||
>
|
||||
GSTIN: {{ doc.partyGSTIN }}
|
||||
</p>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<!-- Items Table -->
|
||||
<section class="mt-8 text-base">
|
||||
<!-- Heading Row -->
|
||||
<section class="text-gray-600 w-full flex border-b">
|
||||
<div class="py-4 w-5/12">{{ t`Item` }}</div>
|
||||
<div class="py-4 text-right w-2/12" v-if="doc.showHSN">
|
||||
{{ t`HSN/SAC` }}
|
||||
</div>
|
||||
<div class="py-4 text-right w-1/12">{{ t`Quantity` }}</div>
|
||||
<div class="py-4 text-right w-3/12">{{ t`Rate` }}</div>
|
||||
<div class="py-4 text-right w-3/12">{{ t`Amount` }}</div>
|
||||
</section>
|
||||
|
||||
<!-- Body Rows -->
|
||||
<section
|
||||
class="flex py-1 text-gray-900 w-full border-b"
|
||||
v-for="row in doc.items"
|
||||
:key="row.name"
|
||||
>
|
||||
<div class="w-5/12 py-4">{{ row.item }}</div>
|
||||
<div class="w-2/12 text-right py-4" v-if="doc.showHSN">
|
||||
{{ row.hsnCode }}
|
||||
</div>
|
||||
<div class="w-1/12 text-right py-4">{{ row.quantity }}</div>
|
||||
<div class="w-3/12 text-right py-4">{{ row.rate }}</div>
|
||||
<div class="w-3/12 text-right py-4">{{ row.amount }}</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<!-- Invoice Footer -->
|
||||
<footer class="mt-8 flex justify-end text-base">
|
||||
<!-- Invoice Terms -->
|
||||
<section class="w-1/2">
|
||||
<h3 class="text-sm tracking-widest text-gray-600 mt-2" v-if="doc.terms">
|
||||
{{ t`Notes` }}
|
||||
</h3>
|
||||
<p class="my-4 text-lg whitespace-pre-line">{{ doc.terms }}</p>
|
||||
</section>
|
||||
|
||||
<!-- Totaled Amounts -->
|
||||
<section class="w-1/2">
|
||||
<!-- Subtotal -->
|
||||
<div class="flex pl-2 justify-between py-3 border-b">
|
||||
<h3>{{ t`Subtotal` }}</h3>
|
||||
<p>{{ doc.netTotal }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Discount (if applied before tax) -->
|
||||
<div
|
||||
class="flex pl-2 justify-between py-3 border-b"
|
||||
v-if="doc.totalDiscount && !doc.discountAfterTax"
|
||||
>
|
||||
<h3>{{ t`Discount` }}</h3>
|
||||
<p>{{ doc.totalDiscount }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Tax Breakdown -->
|
||||
<div
|
||||
class="flex pl-2 justify-between py-3"
|
||||
v-for="tax in doc.taxes"
|
||||
:key="tax.name"
|
||||
>
|
||||
<h3>{{ tax.account }}</h3>
|
||||
<p>{{ tax.amount }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Discount (if applied after tax) -->
|
||||
<div
|
||||
class="flex pl-2 justify-between py-3 border-t"
|
||||
v-if="doc.totalDiscount && doc.discountAfterTax"
|
||||
>
|
||||
<h3>{{ t`Discount` }}</h3>
|
||||
<p>{{ doc.totalDiscount }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Grand Total -->
|
||||
<div
|
||||
class="
|
||||
flex
|
||||
pl-2
|
||||
justify-between
|
||||
py-3
|
||||
border-t
|
||||
text-green-600
|
||||
font-semibold
|
||||
text-base
|
||||
"
|
||||
>
|
||||
<h3>{{ t`Grand Total` }}</h3>
|
||||
<p>{{ doc.grandTotal }}</p>
|
||||
</div>
|
||||
</section>
|
||||
</footer>
|
||||
</main>
|
130
templates/Business.template.html
Normal file
130
templates/Business.template.html
Normal file
@ -0,0 +1,130 @@
|
||||
<main class="bg-white h-full" :style="{ 'font-family': print.font }">
|
||||
<!-- Invoice Header -->
|
||||
<header class="bg-gray-100 px-12 py-10">
|
||||
<!-- Company Details -->
|
||||
<section class="flex items-center">
|
||||
<img
|
||||
v-if="print.displayLogo"
|
||||
class="h-12 max-w-32 object-contain mr-4"
|
||||
:src="print.logo"
|
||||
/>
|
||||
|
||||
<div>
|
||||
<p class="font-semibold text-xl" :style="{ color: print.color }">
|
||||
{{ print.companyName }}
|
||||
</p>
|
||||
<p class="text-sm text-gray-800" v-if="print.address">
|
||||
{{ print.links.address.addressDisplay }}
|
||||
</p>
|
||||
<p class="text-sm text-gray-800" v-if="print.gstin">
|
||||
GSTIN: {{ print.gstin }}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Sub Heading Section -->
|
||||
<div class="mt-8 text-lg">
|
||||
<!-- Doc Details -->
|
||||
<section class="flex">
|
||||
<h3 class="w-1/3 font-semibold">
|
||||
{{ doc.entryType === 'SalesInvoice' ? 'Invoice' : 'Bill' }}
|
||||
</h3>
|
||||
<div class="w-2/3 text-gray-800">
|
||||
<p class="font-semibold">{{ doc.name }}</p>
|
||||
<p>{{ doc.date }}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Party Details -->
|
||||
<section class="mt-4 flex">
|
||||
<h3 class="w-1/3 font-semibold">
|
||||
{{ doc.entryType === 'SalesInvoice' ? 'Customer' : 'Supplier' }}
|
||||
</h3>
|
||||
|
||||
<div class="w-2/3 text-gray-800" v-if="doc.party">
|
||||
<p class="font-semibold">{{ doc.party }}</p>
|
||||
<p v-if="doc.links.party.address">
|
||||
{{ doc.links.party.links.address.addressDisplay }}
|
||||
</p>
|
||||
<p v-if="doc.links.party.gstin">GSTIN: {{ doc.links.party.gstin }}</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Items Table -->
|
||||
<section class="px-12 pt-12 text-lg">
|
||||
<!-- Heading Row -->
|
||||
<section class="mb-4 flex font-semibold">
|
||||
<div class="w-4/12">{{ t`Item` }}</div>
|
||||
<div class="w-2/12 text-right" v-if="doc.showHSN">{{ t`HSN/SAC` }}</div>
|
||||
<div class="w-2/12 text-right">{{ t`Quantity` }}</div>
|
||||
<div class="w-3/12 text-right">{{ t`Rate` }}</div>
|
||||
<div class="w-3/12 text-right">{{ t`Amount` }}</div>
|
||||
</section>
|
||||
|
||||
<!-- Body Rows -->
|
||||
<section
|
||||
class="flex py-1 text-gray-800"
|
||||
v-for="row in doc.items"
|
||||
:key="row.name"
|
||||
>
|
||||
<div class="w-4/12">{{ row.item }}</div>
|
||||
<div class="w-2/12 text-right" v-if="doc.showHSN">{{ row.hsnCode }}</div>
|
||||
<div class="w-2/12 text-right">{{ row.quantity }}</div>
|
||||
<div class="w-3/12 text-right">{{ row.rate }}</div>
|
||||
<div class="w-3/12 text-right">{{ row.amount }}</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<!-- Invoice Footer -->
|
||||
<footer class="px-12 py-12 text-lg">
|
||||
<!-- Totaled Amounts -->
|
||||
<section class="flex -mx-3 justify-end flex-1 bg-gray-100 gap-8">
|
||||
<!-- Subtotal -->
|
||||
<div class="text-right py-3">
|
||||
<h3 class="text-gray-800">{{ t`Subtotal` }}</h3>
|
||||
<p class="text-xl mt-2">{{ doc.netTotal }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Discount (if applied before tax) -->
|
||||
<div
|
||||
class="text-right py-3"
|
||||
v-if="doc.totalDiscount && !doc.discountAfterTax"
|
||||
>
|
||||
<h3 class="text-gray-800">{{ t`Discount` }}</h3>
|
||||
<p class="text-xl mt-2">{{ doc.totalDiscount }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Tax Breakdown -->
|
||||
<div class="text-right py-3" v-for="tax in doc.taxes" :key="tax.name">
|
||||
<h3 class="text-gray-800">{{ tax.account }}</h3>
|
||||
<p class="text-xl mt-2">{{ tax.amount }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Discount (if applied after tax) -->
|
||||
<div
|
||||
class="text-right py-3"
|
||||
v-if="doc.totalDiscount && doc.discountAfterTax"
|
||||
>
|
||||
<h3 class="text-gray-800">{{ t`Discount` }}</h3>
|
||||
<p class="text-xl mt-2">{{ doc.totalDiscount }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Grand Total -->
|
||||
<div
|
||||
class="py-3 px-4 text-right text-white"
|
||||
:style="{ backgroundColor: print.color }"
|
||||
>
|
||||
<h3>{{ t`Grand Total` }}</h3>
|
||||
<p class="text-2xl mt-2 font-semibold">{{ doc.grandTotal }}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Invoice Terms -->
|
||||
<section class="mt-12" v-if="doc.terms">
|
||||
<h3 class="text-lg font-semibold">Notes</h3>
|
||||
<p class="mt-4 text-lg whitespace-pre-line">{{ doc.terms }}</p>
|
||||
</section>
|
||||
</footer>
|
||||
</main>
|
170
templates/Minimal.template.html
Normal file
170
templates/Minimal.template.html
Normal file
@ -0,0 +1,170 @@
|
||||
<main class="h-full" :style="{ 'font-family': print.font }">
|
||||
<!-- Invoice Header -->
|
||||
<header class="flex items-center justify-between w-full border-b px-12 py-10">
|
||||
<!-- Left Section -->
|
||||
<section class="flex items-center">
|
||||
<img
|
||||
v-if="print.displayLogo"
|
||||
class="h-12 max-w-32 object-contain mr-4"
|
||||
:src="print.logo"
|
||||
/>
|
||||
|
||||
<div>
|
||||
<p class="font-semibold text-xl" :style="{ color: print.color }">
|
||||
{{ print.companyName }}
|
||||
</p>
|
||||
<p>{{ doc.date }}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Right Section -->
|
||||
<section class="text-right">
|
||||
<p class="font-semibold text-xl" :style="{ color: print.color }">
|
||||
{{ doc.entryType }}
|
||||
</p>
|
||||
<p>{{ doc.name }}</p>
|
||||
</section>
|
||||
</header>
|
||||
|
||||
<!-- Party && Company Details -->
|
||||
<section class="flex px-12 py-10 border-b">
|
||||
<!-- Party Details -->
|
||||
<section class="w-1/2">
|
||||
<h3 class="uppercase text-sm font-semibold tracking-widest text-gray-800">
|
||||
{{ doc.entryType === 'SalesInvoice' ? 'To' : 'From' }}
|
||||
</h3>
|
||||
<p class="mt-4 text-black leading-relaxed text-lg">{{ doc.party }}</p>
|
||||
<p
|
||||
v-if="doc.links.party.address"
|
||||
class="mt-2 text-black leading-relaxed text-lg"
|
||||
>
|
||||
{{ doc.links.party.links.address.addressDisplay ?? '' }}
|
||||
</p>
|
||||
<p
|
||||
v-if="doc.links.party.gstin"
|
||||
class="mt-2 text-black leading-relaxed text-lg"
|
||||
>
|
||||
GSTIN: {{ doc.links.party.gstin }}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<!-- Company Details -->
|
||||
<section class="w-1/2">
|
||||
<h3
|
||||
class="
|
||||
uppercase
|
||||
text-sm
|
||||
font-semibold
|
||||
tracking-widest
|
||||
text-gray-800
|
||||
ml-8
|
||||
"
|
||||
>
|
||||
{{ doc.entryType === 'SalesInvoice' ? 'From' : 'To' }}
|
||||
</h3>
|
||||
<p class="mt-4 ml-8 text-black leading-relaxed text-lg">
|
||||
{{ print.companyName }}
|
||||
</p>
|
||||
<p
|
||||
v-if="print.address"
|
||||
class="mt-2 ml-8 text-black leading-relaxed text-lg"
|
||||
>
|
||||
{{ print.links.address.addressDisplay }}
|
||||
</p>
|
||||
<p
|
||||
v-if="print.gstin"
|
||||
class="mt-2 ml-8 text-black leading-relaxed text-lg"
|
||||
>
|
||||
GSTIN: {{ print.gstin }}
|
||||
</p>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<!-- Items Table -->
|
||||
<section class="px-12 py-10 border-b">
|
||||
<!-- Heading Row -->
|
||||
<section
|
||||
class="
|
||||
mb-4
|
||||
flex
|
||||
uppercase
|
||||
text-sm
|
||||
tracking-widest
|
||||
font-semibold
|
||||
text-gray-800
|
||||
"
|
||||
>
|
||||
<div class="w-4/12 text-left">{{ t`Item` }}</div>
|
||||
<div class="w-2/12 text-right" v-if="doc.showHSN">{{ t`HSN/SAC` }}</div>
|
||||
<div class="w-2/12 text-right">{{ t`Quantity` }}</div>
|
||||
<div class="w-3/12 text-right">{{ t`Rate` }}</div>
|
||||
<div class="w-3/12 text-right">{{ t`Amount`}}</div>
|
||||
</section>
|
||||
|
||||
<!-- Body Rows -->
|
||||
<section class="flex py-1 text-lg" v-for="row in doc.items" :key="row.name">
|
||||
<div class="w-4/12 text-left">{{ row.item }}</div>
|
||||
<div class="w-2/12 text-right" v-if="doc.showHSN">{{ row.hsnCode }}</div>
|
||||
<div class="w-2/12 text-right">{{ row.quantity }}</div>
|
||||
<div class="w-3/12 text-right">{{ row.rate }}</div>
|
||||
<div class="w-3/12 text-right">{{ row.amount }}</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<!-- Invoice Footer -->
|
||||
<footer class="flex px-12 py-10">
|
||||
<!-- Invoice Terms -->
|
||||
<section class="w-1/2" v-if="doc.terms">
|
||||
<h3 class="uppercase text-sm tracking-widest font-semibold text-gray-800">
|
||||
{{ t`Notes` }}
|
||||
</h3>
|
||||
<p class="mt-4 text-lg whitespace-pre-line">{{ doc.terms }}</p>
|
||||
</section>
|
||||
|
||||
<!-- Totaled Amounts -->
|
||||
<section class="w-1/2 text-lg ml-auto">
|
||||
<!-- Subtotal -->
|
||||
<div class="flex pl-2 justify-between py-1">
|
||||
<h3>{{ t`Subtotal` }}</h3>
|
||||
<p>{{ doc.netTotal }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Discount (if applied before tax) -->
|
||||
<div
|
||||
class="flex pl-2 justify-between py-1"
|
||||
v-if="doc.totalDiscount && !doc.discountAfterTax"
|
||||
>
|
||||
<h3>{{ t`Discount` }}</h3>
|
||||
<p>{{ doc.totalDiscount }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Tax Breakdown -->
|
||||
<div
|
||||
class="flex pl-2 justify-between py-1"
|
||||
v-for="tax in doc.taxes"
|
||||
:key="tax.name"
|
||||
>
|
||||
<h3>{{ tax.account }}</h3>
|
||||
<p>{{ tax.amount }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Discount (if applied after tax) -->
|
||||
<div
|
||||
class="flex pl-2 justify-between py-1"
|
||||
v-if="doc.totalDiscount && doc.discountAfterTax"
|
||||
>
|
||||
<h3>{{ t`Discount` }}</h3>
|
||||
<p>{{ doc.totalDiscount }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Grand Total -->
|
||||
<div
|
||||
class="flex pl-2 justify-between py-1 font-semibold"
|
||||
:style="{ color: print.color }"
|
||||
>
|
||||
<h3>{{ t`Grand Total` }}</h3>
|
||||
<p>{{ doc.grandTotal }}</p>
|
||||
</div>
|
||||
</section>
|
||||
</footer>
|
||||
</main>
|
@ -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',
|
||||
|
@ -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<string, unknown>;
|
||||
}
|
||||
};
|
||||
|
||||
export interface SelectFileOptions {
|
||||
title: string;
|
||||
@ -48,3 +52,5 @@ export interface SelectFileReturn {
|
||||
export type PropertyEnum<T extends Record<string, any>> = {
|
||||
[key in keyof Required<T>]: key;
|
||||
};
|
||||
|
||||
export type TemplateFile = { file: string; template: string; modified: string };
|
||||
|
186
yarn.lock
186
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"
|
||||
|
Loading…
Reference in New Issue
Block a user