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: 'log_creds.txt', to: '../creds/log_creds.txt' },
|
||||||
{ from: 'translations', to: '../translations' },
|
{ from: 'translations', to: '../translations' },
|
||||||
|
{ from: 'templates', to: '../templates' },
|
||||||
]
|
]
|
||||||
mac:
|
mac:
|
||||||
type: distribution
|
type: distribution
|
||||||
|
@ -13,7 +13,7 @@ import { TelemetryManager } from './telemetry/telemetry';
|
|||||||
import {
|
import {
|
||||||
DEFAULT_CURRENCY,
|
DEFAULT_CURRENCY,
|
||||||
DEFAULT_DISPLAY_PRECISION,
|
DEFAULT_DISPLAY_PRECISION,
|
||||||
DEFAULT_INTERNAL_PRECISION
|
DEFAULT_INTERNAL_PRECISION,
|
||||||
} from './utils/consts';
|
} from './utils/consts';
|
||||||
import * as errors from './utils/errors';
|
import * as errors from './utils/errors';
|
||||||
import { format } from './utils/format';
|
import { format } from './utils/format';
|
||||||
@ -88,7 +88,7 @@ export class Fyo {
|
|||||||
return this.db.fieldMap;
|
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);
|
return format(value, field, doc ?? null, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { Fyo } from 'fyo';
|
||||||
import { DocValue, DocValueMap } from 'fyo/core/types';
|
import { DocValue, DocValueMap } from 'fyo/core/types';
|
||||||
import SystemSettings from 'fyo/models/SystemSettings';
|
import SystemSettings from 'fyo/models/SystemSettings';
|
||||||
import { FieldType, Schema, SelectOption } from 'schemas/types';
|
import { FieldType, Schema, SelectOption } from 'schemas/types';
|
||||||
@ -76,13 +77,12 @@ export interface RenderData {
|
|||||||
[key: string]: DocValue | Schema
|
[key: string]: DocValue | Schema
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ColumnConfig {
|
export type ColumnConfig = {
|
||||||
label: string;
|
label: string;
|
||||||
fieldtype: FieldType;
|
fieldtype: FieldType;
|
||||||
fieldname?: string;
|
fieldname: string;
|
||||||
size?: string;
|
|
||||||
render?: (doc: RenderData) => { template: string };
|
render?: (doc: RenderData) => { template: string };
|
||||||
getValue?: (doc: Doc) => string;
|
display?: (value: unknown, fyo: Fyo) => string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ListViewColumn = string | ColumnConfig;
|
export type ListViewColumn = string | ColumnConfig;
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
import { Fyo } from 'fyo';
|
import { Fyo } from 'fyo';
|
||||||
import { DocValue } from 'fyo/core/types';
|
|
||||||
import { Doc } from 'fyo/model/doc';
|
import { Doc } from 'fyo/model/doc';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { Money } from 'pesa';
|
import { Money } from 'pesa';
|
||||||
import { Field, FieldType, FieldTypeEnum } from 'schemas/types';
|
import { Field, FieldType, FieldTypeEnum } from 'schemas/types';
|
||||||
import { getIsNullOrUndef, safeParseFloat } from 'utils';
|
import { getIsNullOrUndef, safeParseFloat, titleCase } from 'utils';
|
||||||
import {
|
import {
|
||||||
DEFAULT_CURRENCY,
|
DEFAULT_CURRENCY,
|
||||||
DEFAULT_DATE_FORMAT,
|
DEFAULT_DATE_FORMAT,
|
||||||
@ -13,7 +12,7 @@ import {
|
|||||||
} from './consts';
|
} from './consts';
|
||||||
|
|
||||||
export function format(
|
export function format(
|
||||||
value: DocValue,
|
value: unknown,
|
||||||
df: string | Field | null,
|
df: string | Field | null,
|
||||||
doc: Doc | null,
|
doc: Doc | null,
|
||||||
fyo: Fyo
|
fyo: Fyo
|
||||||
@ -45,7 +44,7 @@ export function format(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (field.fieldtype === FieldTypeEnum.Check) {
|
if (field.fieldtype === FieldTypeEnum.Check) {
|
||||||
return Boolean(value).toString();
|
return titleCase(Boolean(value).toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (getIsNullOrUndef(value)) {
|
if (getIsNullOrUndef(value)) {
|
||||||
@ -55,26 +54,31 @@ export function format(
|
|||||||
return String(value);
|
return String(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
function toDatetime(value: DocValue) {
|
function toDatetime(value: unknown): DateTime | null {
|
||||||
if (typeof value === 'string') {
|
if (typeof value === 'string') {
|
||||||
return DateTime.fromISO(value);
|
return DateTime.fromISO(value);
|
||||||
} else if (value instanceof Date) {
|
} else if (value instanceof Date) {
|
||||||
return DateTime.fromJSDate(value);
|
return DateTime.fromJSDate(value);
|
||||||
} else {
|
} else if (typeof value === 'number') {
|
||||||
return DateTime.fromSeconds(value as 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) {
|
if (value == null) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
const dateFormat =
|
const dateFormat =
|
||||||
(fyo.singles.SystemSettings?.dateFormat as string) ?? DEFAULT_DATE_FORMAT;
|
(fyo.singles.SystemSettings?.dateFormat as string) ?? DEFAULT_DATE_FORMAT;
|
||||||
const formattedDatetime = toDatetime(value).toFormat(
|
const dateTime = toDatetime(value);
|
||||||
`${dateFormat} HH:mm:ss`
|
if (!dateTime) {
|
||||||
);
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const formattedDatetime = dateTime.toFormat(`${dateFormat} HH:mm:ss`);
|
||||||
|
|
||||||
if (value === 'Invalid DateTime') {
|
if (value === 'Invalid DateTime') {
|
||||||
return '';
|
return '';
|
||||||
@ -83,7 +87,7 @@ function formatDatetime(value: DocValue, fyo: Fyo): string {
|
|||||||
return formattedDatetime;
|
return formattedDatetime;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDate(value: DocValue, fyo: Fyo): string {
|
function formatDate(value: unknown, fyo: Fyo): string {
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
@ -91,9 +95,12 @@ function formatDate(value: DocValue, fyo: Fyo): string {
|
|||||||
const dateFormat =
|
const dateFormat =
|
||||||
(fyo.singles.SystemSettings?.dateFormat as string) ?? DEFAULT_DATE_FORMAT;
|
(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') {
|
if (value === 'Invalid DateTime') {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
@ -102,7 +109,7 @@ function formatDate(value: DocValue, fyo: Fyo): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function formatCurrency(
|
function formatCurrency(
|
||||||
value: DocValue,
|
value: unknown,
|
||||||
field: Field,
|
field: Field,
|
||||||
doc: Doc | null,
|
doc: Doc | null,
|
||||||
fyo: Fyo
|
fyo: Fyo
|
||||||
@ -125,7 +132,7 @@ function formatCurrency(
|
|||||||
return valueString;
|
return valueString;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatNumber(value: DocValue, fyo: Fyo): string {
|
function formatNumber(value: unknown, fyo: Fyo): string {
|
||||||
const numberFormatter = getNumberFormatter(fyo);
|
const numberFormatter = getNumberFormatter(fyo);
|
||||||
if (typeof value === 'number') {
|
if (typeof value === 'number') {
|
||||||
value = fyo.pesa(value.toFixed(20));
|
value = fyo.pesa(value.toFixed(20));
|
||||||
|
2
main.ts
2
main.ts
@ -4,7 +4,7 @@ import {
|
|||||||
app,
|
app,
|
||||||
BrowserWindow,
|
BrowserWindow,
|
||||||
BrowserWindowConstructorOptions,
|
BrowserWindowConstructorOptions,
|
||||||
protocol
|
protocol,
|
||||||
} from 'electron';
|
} from 'electron';
|
||||||
import Store from 'electron-store';
|
import Store from 'electron-store';
|
||||||
import { autoUpdater } from 'electron-updater';
|
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 { IPC_ACTIONS } from '../utils/messages';
|
||||||
import { getUrlAndTokenString, sendError } from './contactMothership';
|
import { getUrlAndTokenString, sendError } from './contactMothership';
|
||||||
import { getLanguageMap } from './getLanguageMap';
|
import { getLanguageMap } from './getLanguageMap';
|
||||||
|
import { getTemplates } from './getPrintTemplates';
|
||||||
import {
|
import {
|
||||||
getConfigFilesWithModified,
|
getConfigFilesWithModified,
|
||||||
getErrorHandledReponse,
|
getErrorHandledReponse,
|
||||||
@ -117,7 +118,7 @@ export default function registerIpcMainActionListeners(main: Main) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
ipcMain.handle(IPC_ACTIONS.GET_CREDS, async (event) => {
|
ipcMain.handle(IPC_ACTIONS.GET_CREDS, async (event) => {
|
||||||
return await getUrlAndTokenString();
|
return getUrlAndTokenString();
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle(IPC_ACTIONS.DELETE_FILE, async (_, filePath) => {
|
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
|
* Database Related Actions
|
||||||
*/
|
*/
|
||||||
|
@ -7,7 +7,9 @@ import {
|
|||||||
RequiredMap,
|
RequiredMap,
|
||||||
TreeViewSettings,
|
TreeViewSettings,
|
||||||
ReadOnlyMap,
|
ReadOnlyMap,
|
||||||
|
FormulaMap,
|
||||||
} from 'fyo/model/types';
|
} from 'fyo/model/types';
|
||||||
|
import { ModelNameEnum } from 'models/types';
|
||||||
import { QueryFilter } from 'utils/db/types';
|
import { QueryFilter } from 'utils/db/types';
|
||||||
import { AccountRootType, AccountRootTypeEnum, AccountType } from './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 = {
|
static filters: FiltersMap = {
|
||||||
parentAccount: (doc: Doc) => {
|
parentAccount: (doc: Doc) => {
|
||||||
const filter: QueryFilter = {
|
const filter: QueryFilter = {
|
||||||
|
@ -3,6 +3,7 @@ import { FiltersMap, HiddenMap } from 'fyo/model/types';
|
|||||||
import { ModelNameEnum } from 'models/types';
|
import { ModelNameEnum } from 'models/types';
|
||||||
|
|
||||||
export class Defaults extends Doc {
|
export class Defaults extends Doc {
|
||||||
|
// Number Series
|
||||||
salesInvoiceNumberSeries?: string;
|
salesInvoiceNumberSeries?: string;
|
||||||
purchaseInvoiceNumberSeries?: string;
|
purchaseInvoiceNumberSeries?: string;
|
||||||
journalEntryNumberSeries?: string;
|
journalEntryNumberSeries?: string;
|
||||||
@ -11,12 +12,23 @@ export class Defaults extends Doc {
|
|||||||
shipmentNumberSeries?: string;
|
shipmentNumberSeries?: string;
|
||||||
purchaseReceiptNumberSeries?: string;
|
purchaseReceiptNumberSeries?: string;
|
||||||
|
|
||||||
|
// Terms
|
||||||
salesInvoiceTerms?: string;
|
salesInvoiceTerms?: string;
|
||||||
purchaseInvoiceTerms?: string;
|
purchaseInvoiceTerms?: string;
|
||||||
shipmentTerms?: string;
|
shipmentTerms?: string;
|
||||||
purchaseReceiptTerms?: string;
|
purchaseReceiptTerms?: string;
|
||||||
|
|
||||||
|
// Print Templates
|
||||||
|
salesInvoicePrintTemplate?: string;
|
||||||
|
purchaseInvoicePrintTemplate?: string;
|
||||||
|
journalEntryPrintTemplate?: string;
|
||||||
|
paymentPrintTemplate?: string;
|
||||||
|
shipmentPrintTemplate?: string;
|
||||||
|
purchaseReceiptPrintTemplate?: string;
|
||||||
|
stockMovementPrintTemplate?: string;
|
||||||
|
|
||||||
static commonFilters = {
|
static commonFilters = {
|
||||||
|
// Number Series
|
||||||
salesInvoiceNumberSeries: () => ({
|
salesInvoiceNumberSeries: () => ({
|
||||||
referenceType: ModelNameEnum.SalesInvoice,
|
referenceType: ModelNameEnum.SalesInvoice,
|
||||||
}),
|
}),
|
||||||
@ -38,6 +50,18 @@ export class Defaults extends Doc {
|
|||||||
purchaseReceiptNumberSeries: () => ({
|
purchaseReceiptNumberSeries: () => ({
|
||||||
referenceType: ModelNameEnum.PurchaseReceipt,
|
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;
|
static filters: FiltersMap = this.commonFilters;
|
||||||
@ -53,6 +77,9 @@ export class Defaults extends Doc {
|
|||||||
purchaseReceiptNumberSeries: this.getInventoryHidden(),
|
purchaseReceiptNumberSeries: this.getInventoryHidden(),
|
||||||
shipmentTerms: this.getInventoryHidden(),
|
shipmentTerms: this.getInventoryHidden(),
|
||||||
purchaseReceiptTerms: 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 { DEFAULT_CURRENCY } from 'fyo/utils/consts';
|
||||||
import { ValidationError } from 'fyo/utils/errors';
|
import { ValidationError } from 'fyo/utils/errors';
|
||||||
import { addItem, getExchangeRate, getNumberSeries } from 'models/helpers';
|
import { addItem, getExchangeRate, getNumberSeries } from 'models/helpers';
|
||||||
import { validateBatch } from 'models/inventory/helpers';
|
|
||||||
import { InventorySettings } from 'models/inventory/InventorySettings';
|
import { InventorySettings } from 'models/inventory/InventorySettings';
|
||||||
import { StockTransfer } from 'models/inventory/StockTransfer';
|
import { StockTransfer } from 'models/inventory/StockTransfer';
|
||||||
import { Transactional } from 'models/Transactional/Transactional';
|
import { Transactional } from 'models/Transactional/Transactional';
|
||||||
|
@ -70,8 +70,8 @@ export class JournalEntry extends Transactional {
|
|||||||
'name',
|
'name',
|
||||||
{
|
{
|
||||||
label: t`Status`,
|
label: t`Status`,
|
||||||
|
fieldname: 'status',
|
||||||
fieldtype: 'Select',
|
fieldtype: 'Select',
|
||||||
size: 'small',
|
|
||||||
render(doc) {
|
render(doc) {
|
||||||
const status = getDocStatus(doc);
|
const status = getDocStatus(doc);
|
||||||
const color = statusColor[status] ?? 'gray';
|
const color = statusColor[status] ?? 'gray';
|
||||||
|
@ -2,7 +2,5 @@ import { Doc } from 'fyo/model/doc';
|
|||||||
import { HiddenMap } from 'fyo/model/types';
|
import { HiddenMap } from 'fyo/model/types';
|
||||||
|
|
||||||
export class PrintSettings extends Doc {
|
export class PrintSettings extends Doc {
|
||||||
override hidden: HiddenMap = {
|
override hidden: HiddenMap = {};
|
||||||
displayBatch: () => !this.fyo.singles.InventorySettings?.enableBatches,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
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`,
|
label: t`Status`,
|
||||||
fieldname: 'status',
|
fieldname: 'status',
|
||||||
fieldtype: 'Select',
|
fieldtype: 'Select',
|
||||||
size: 'small',
|
|
||||||
render(doc) {
|
render(doc) {
|
||||||
const status = getDocStatus(doc);
|
const status = getDocStatus(doc);
|
||||||
const color = statusColor[status] ?? 'gray';
|
const color = statusColor[status] ?? 'gray';
|
||||||
|
@ -29,6 +29,7 @@ import { StockLedgerEntry } from './inventory/StockLedgerEntry';
|
|||||||
import { StockMovement } from './inventory/StockMovement';
|
import { StockMovement } from './inventory/StockMovement';
|
||||||
import { StockMovementItem } from './inventory/StockMovementItem';
|
import { StockMovementItem } from './inventory/StockMovementItem';
|
||||||
|
|
||||||
|
import { PrintTemplate } from './baseModels/PrintTemplate';
|
||||||
export const models = {
|
export const models = {
|
||||||
Account,
|
Account,
|
||||||
AccountingLedgerEntry,
|
AccountingLedgerEntry,
|
||||||
@ -48,6 +49,7 @@ export const models = {
|
|||||||
SalesInvoice,
|
SalesInvoice,
|
||||||
SalesInvoiceItem,
|
SalesInvoiceItem,
|
||||||
SetupWizard,
|
SetupWizard,
|
||||||
|
PrintTemplate,
|
||||||
Tax,
|
Tax,
|
||||||
TaxSummary,
|
TaxSummary,
|
||||||
// Inventory Models
|
// Inventory Models
|
||||||
|
@ -80,6 +80,13 @@ export class StockMovement extends Transfer {
|
|||||||
};
|
};
|
||||||
|
|
||||||
static getListViewSettings(fyo: Fyo): ListViewSettings {
|
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 {
|
return {
|
||||||
formRoute: (name) => `/edit/StockMovement/${name}`,
|
formRoute: (name) => `/edit/StockMovement/${name}`,
|
||||||
columns: [
|
columns: [
|
||||||
@ -90,20 +97,8 @@ export class StockMovement extends Transfer {
|
|||||||
label: fyo.t`Movement Type`,
|
label: fyo.t`Movement Type`,
|
||||||
fieldname: 'movementType',
|
fieldname: 'movementType',
|
||||||
fieldtype: 'Select',
|
fieldtype: 'Select',
|
||||||
size: 'small',
|
display(value): string {
|
||||||
render(doc) {
|
return movementTypeMap[value as MovementType] ?? '';
|
||||||
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>`,
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -6,7 +6,6 @@ export enum ModelNameEnum {
|
|||||||
Address = 'Address',
|
Address = 'Address',
|
||||||
Batch= 'Batch',
|
Batch= 'Batch',
|
||||||
Color = 'Color',
|
Color = 'Color',
|
||||||
CompanySettings = 'CompanySettings',
|
|
||||||
Currency = 'Currency',
|
Currency = 'Currency',
|
||||||
GetStarted = 'GetStarted',
|
GetStarted = 'GetStarted',
|
||||||
Defaults = 'Defaults',
|
Defaults = 'Defaults',
|
||||||
@ -21,6 +20,7 @@ export enum ModelNameEnum {
|
|||||||
Payment = 'Payment',
|
Payment = 'Payment',
|
||||||
PaymentFor = 'PaymentFor',
|
PaymentFor = 'PaymentFor',
|
||||||
PrintSettings = 'PrintSettings',
|
PrintSettings = 'PrintSettings',
|
||||||
|
PrintTemplate = 'PrintTemplate',
|
||||||
PurchaseInvoice = 'PurchaseInvoice',
|
PurchaseInvoice = 'PurchaseInvoice',
|
||||||
PurchaseInvoiceItem = 'PurchaseInvoiceItem',
|
PurchaseInvoiceItem = 'PurchaseInvoiceItem',
|
||||||
SalesInvoice = 'SalesInvoice',
|
SalesInvoice = 'SalesInvoice',
|
||||||
|
@ -20,8 +20,11 @@
|
|||||||
"test": "scripts/test.sh"
|
"test": "scripts/test.sh"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@codemirror/autocomplete": "^6.4.2",
|
||||||
|
"@codemirror/lang-vue": "^0.1.1",
|
||||||
"@popperjs/core": "^2.10.2",
|
"@popperjs/core": "^2.10.2",
|
||||||
"better-sqlite3": "^7.5.3",
|
"better-sqlite3": "^7.5.3",
|
||||||
|
"codemirror": "^6.0.1",
|
||||||
"core-js": "^3.19.0",
|
"core-js": "^3.19.0",
|
||||||
"electron-store": "^8.0.1",
|
"electron-store": "^8.0.1",
|
||||||
"feather-icons": "^4.28.0",
|
"feather-icons": "^4.28.0",
|
||||||
|
@ -162,7 +162,7 @@ async function exportReport(extention: ExportExtention, report: BaseGSTR) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await saveExportData(data, filePath, report.fyo);
|
await saveExportData(data, filePath);
|
||||||
report.fyo.telemetry.log(Verb.Exported, report.reportName, { extention });
|
report.fyo.telemetry.log(Verb.Exported, report.reportName, { extention });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,7 +52,7 @@ async function exportReport(extention: ExportExtention, report: Report) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await saveExportData(data, filePath, report.fyo);
|
await saveExportData(data, filePath);
|
||||||
report.fyo.telemetry.log(Verb.Exported, report.reportName, { extention });
|
report.fyo.telemetry.log(Verb.Exported, report.reportName, { extention });
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -178,7 +178,12 @@ function getValueFromCell(cell: ReportCell, displayPrecision: number) {
|
|||||||
return rawValue;
|
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);
|
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",
|
"label": "Purchase Receipt Terms",
|
||||||
"fieldtype": "Text",
|
"fieldtype": "Text",
|
||||||
"section": "Terms"
|
"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,
|
"create": true,
|
||||||
"section": "Contacts"
|
"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",
|
"fieldname": "color",
|
||||||
"label": "Color",
|
"label": "Color",
|
||||||
@ -136,30 +114,6 @@
|
|||||||
"label": "Display Logo in Invoice",
|
"label": "Display Logo in Invoice",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"section": "Customizations"
|
"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 AccountingLedgerEntry from './app/AccountingLedgerEntry.json';
|
||||||
import AccountingSettings from './app/AccountingSettings.json';
|
import AccountingSettings from './app/AccountingSettings.json';
|
||||||
import Address from './app/Address.json';
|
import Address from './app/Address.json';
|
||||||
|
import Batch from './app/Batch.json';
|
||||||
import Color from './app/Color.json';
|
import Color from './app/Color.json';
|
||||||
import CompanySettings from './app/CompanySettings.json';
|
|
||||||
import Currency from './app/Currency.json';
|
import Currency from './app/Currency.json';
|
||||||
import Defaults from './app/Defaults.json';
|
import Defaults from './app/Defaults.json';
|
||||||
import GetStarted from './app/GetStarted.json';
|
import GetStarted from './app/GetStarted.json';
|
||||||
|
import InventorySettings from './app/inventory/InventorySettings.json';
|
||||||
import Location from './app/inventory/Location.json';
|
import Location from './app/inventory/Location.json';
|
||||||
import PurchaseReceipt from './app/inventory/PurchaseReceipt.json';
|
import PurchaseReceipt from './app/inventory/PurchaseReceipt.json';
|
||||||
import PurchaseReceiptItem from './app/inventory/PurchaseReceiptItem.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 StockMovementItem from './app/inventory/StockMovementItem.json';
|
||||||
import StockTransfer from './app/inventory/StockTransfer.json';
|
import StockTransfer from './app/inventory/StockTransfer.json';
|
||||||
import StockTransferItem from './app/inventory/StockTransferItem.json';
|
import StockTransferItem from './app/inventory/StockTransferItem.json';
|
||||||
|
import UOMConversionItem from './app/inventory/UOMConversionItem.json';
|
||||||
import Invoice from './app/Invoice.json';
|
import Invoice from './app/Invoice.json';
|
||||||
import InvoiceItem from './app/InvoiceItem.json';
|
import InvoiceItem from './app/InvoiceItem.json';
|
||||||
import Item from './app/Item.json';
|
import Item from './app/Item.json';
|
||||||
@ -28,6 +30,7 @@ import Party from './app/Party.json';
|
|||||||
import Payment from './app/Payment.json';
|
import Payment from './app/Payment.json';
|
||||||
import PaymentFor from './app/PaymentFor.json';
|
import PaymentFor from './app/PaymentFor.json';
|
||||||
import PrintSettings from './app/PrintSettings.json';
|
import PrintSettings from './app/PrintSettings.json';
|
||||||
|
import PrintTemplate from './app/PrintTemplate.json';
|
||||||
import PurchaseInvoice from './app/PurchaseInvoice.json';
|
import PurchaseInvoice from './app/PurchaseInvoice.json';
|
||||||
import PurchaseInvoiceItem from './app/PurchaseInvoiceItem.json';
|
import PurchaseInvoiceItem from './app/PurchaseInvoiceItem.json';
|
||||||
import SalesInvoice from './app/SalesInvoice.json';
|
import SalesInvoice from './app/SalesInvoice.json';
|
||||||
@ -37,7 +40,6 @@ import Tax from './app/Tax.json';
|
|||||||
import TaxDetail from './app/TaxDetail.json';
|
import TaxDetail from './app/TaxDetail.json';
|
||||||
import TaxSummary from './app/TaxSummary.json';
|
import TaxSummary from './app/TaxSummary.json';
|
||||||
import UOM from './app/UOM.json';
|
import UOM from './app/UOM.json';
|
||||||
import UOMConversionItem from './app/inventory/UOMConversionItem.json';
|
|
||||||
import PatchRun from './core/PatchRun.json';
|
import PatchRun from './core/PatchRun.json';
|
||||||
import SingleValue from './core/SingleValue.json';
|
import SingleValue from './core/SingleValue.json';
|
||||||
import SystemSettings from './core/SystemSettings.json';
|
import SystemSettings from './core/SystemSettings.json';
|
||||||
@ -46,8 +48,6 @@ import child from './meta/child.json';
|
|||||||
import submittable from './meta/submittable.json';
|
import submittable from './meta/submittable.json';
|
||||||
import tree from './meta/tree.json';
|
import tree from './meta/tree.json';
|
||||||
import { Schema, SchemaStub } from './types';
|
import { Schema, SchemaStub } from './types';
|
||||||
import InventorySettings from './app/inventory/InventorySettings.json';
|
|
||||||
import Batch from './app/Batch.json'
|
|
||||||
|
|
||||||
export const coreSchemas: Schema[] = [
|
export const coreSchemas: Schema[] = [
|
||||||
PatchRun as Schema,
|
PatchRun as Schema,
|
||||||
@ -66,6 +66,7 @@ export const appSchemas: Schema[] | SchemaStub[] = [
|
|||||||
Misc as Schema,
|
Misc as Schema,
|
||||||
SetupWizard as Schema,
|
SetupWizard as Schema,
|
||||||
GetStarted as Schema,
|
GetStarted as Schema,
|
||||||
|
PrintTemplate as Schema,
|
||||||
|
|
||||||
Color as Schema,
|
Color as Schema,
|
||||||
Currency as Schema,
|
Currency as Schema,
|
||||||
@ -73,7 +74,6 @@ export const appSchemas: Schema[] | SchemaStub[] = [
|
|||||||
NumberSeries as Schema,
|
NumberSeries as Schema,
|
||||||
|
|
||||||
PrintSettings as Schema,
|
PrintSettings as Schema,
|
||||||
CompanySettings as Schema,
|
|
||||||
|
|
||||||
Account as Schema,
|
Account as Schema,
|
||||||
AccountingSettings as Schema,
|
AccountingSettings as Schema,
|
||||||
@ -116,5 +116,5 @@ export const appSchemas: Schema[] | SchemaStub[] = [
|
|||||||
PurchaseReceipt as Schema,
|
PurchaseReceipt as Schema,
|
||||||
PurchaseReceiptItem 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 { initializeInstance } from './utils/initialization';
|
||||||
import { checkForUpdates } from './utils/ipcCalls';
|
import { checkForUpdates } from './utils/ipcCalls';
|
||||||
import { updateConfigFiles } from './utils/misc';
|
import { updateConfigFiles } from './utils/misc';
|
||||||
|
import { updatePrintTemplates } from './utils/printTemplates';
|
||||||
import { Search } from './utils/search';
|
import { Search } from './utils/search';
|
||||||
import { setGlobalShortcuts } from './utils/shortcuts';
|
import { setGlobalShortcuts } from './utils/shortcuts';
|
||||||
import { routeTo } from './utils/ui';
|
import { routeTo } from './utils/ui';
|
||||||
@ -128,7 +129,7 @@ export default {
|
|||||||
'companyName'
|
'companyName'
|
||||||
);
|
);
|
||||||
await this.setSearcher();
|
await this.setSearcher();
|
||||||
await updateConfigFiles(fyo);
|
updateConfigFiles(fyo);
|
||||||
},
|
},
|
||||||
async setSearcher() {
|
async setSearcher() {
|
||||||
this.searcher = new Search(fyo);
|
this.searcher = new Search(fyo);
|
||||||
@ -160,6 +161,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await initializeInstance(filePath, false, countryCode, fyo);
|
await initializeInstance(filePath, false, countryCode, fyo);
|
||||||
|
await updatePrintTemplates(fyo);
|
||||||
await this.setDesk(filePath);
|
await this.setDesk(filePath);
|
||||||
},
|
},
|
||||||
async setDeskRoute() {
|
async setDeskRoute() {
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="flex items-center justify-between pe-2 rounded"
|
class="flex items-center justify-between pe-2 rounded"
|
||||||
|
:style="containerStyles"
|
||||||
:class="containerClasses"
|
:class="containerClasses"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
|
@ -13,8 +13,10 @@
|
|||||||
:value="value"
|
:value="value"
|
||||||
:placeholder="inputPlaceholder"
|
:placeholder="inputPlaceholder"
|
||||||
:readonly="isReadOnly"
|
:readonly="isReadOnly"
|
||||||
|
:step="step"
|
||||||
:max="df.maxvalue"
|
:max="df.maxvalue"
|
||||||
:min="df.minvalue"
|
:min="df.minvalue"
|
||||||
|
:style="containerStyles"
|
||||||
@blur="(e) => !isReadOnly && triggerChange(e.target.value)"
|
@blur="(e) => !isReadOnly && triggerChange(e.target.value)"
|
||||||
@focus="(e) => !isReadOnly && $emit('focus', e)"
|
@focus="(e) => !isReadOnly && $emit('focus', e)"
|
||||||
@input="(e) => !isReadOnly && $emit('input', e)"
|
@input="(e) => !isReadOnly && $emit('input', e)"
|
||||||
@ -32,6 +34,7 @@ export default {
|
|||||||
name: 'Base',
|
name: 'Base',
|
||||||
props: {
|
props: {
|
||||||
df: Object,
|
df: Object,
|
||||||
|
step: { type: Number, default: 1 },
|
||||||
value: [String, Number, Boolean, Object],
|
value: [String, Number, Boolean, Object],
|
||||||
inputClass: [Function, String, Object],
|
inputClass: [Function, String, Object],
|
||||||
border: { type: Boolean, default: false },
|
border: { type: Boolean, default: false },
|
||||||
@ -39,6 +42,7 @@ export default {
|
|||||||
size: String,
|
size: String,
|
||||||
showLabel: Boolean,
|
showLabel: Boolean,
|
||||||
autofocus: Boolean,
|
autofocus: Boolean,
|
||||||
|
containerStyles: { type: Object, default: () => ({}) },
|
||||||
textRight: { type: [null, Boolean], default: null },
|
textRight: { type: [null, Boolean], default: null },
|
||||||
readOnly: { type: [null, Boolean], default: null },
|
readOnly: { type: [null, Boolean], default: null },
|
||||||
required: { type: [null, Boolean], default: null },
|
required: { type: [null, Boolean], default: null },
|
||||||
|
@ -44,6 +44,7 @@
|
|||||||
:placeholder="t`Custom Hex`"
|
:placeholder="t`Custom Hex`"
|
||||||
:class="[inputClasses, containerClasses]"
|
:class="[inputClasses, containerClasses]"
|
||||||
:value="value"
|
:value="value"
|
||||||
|
style="padding: 0"
|
||||||
@change="(e) => setColorValue(e.target.value)"
|
@change="(e) => setColorValue(e.target.value)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -54,6 +54,9 @@ export default {
|
|||||||
input.value = '';
|
input.value = '';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
select() {
|
||||||
|
this.$refs.control.$refs?.input?.select()
|
||||||
|
},
|
||||||
focus() {
|
focus() {
|
||||||
this.$refs.control.focus();
|
this.$refs.control.focus();
|
||||||
},
|
},
|
||||||
|
@ -3,6 +3,9 @@
|
|||||||
<div class="flex flex-1 flex-col">
|
<div class="flex flex-1 flex-col">
|
||||||
<!-- Page Header (Title, Buttons, etc) -->
|
<!-- Page Header (Title, Buttons, etc) -->
|
||||||
<PageHeader :title="title" :border="false" :searchborder="searchborder">
|
<PageHeader :title="title" :border="false" :searchborder="searchborder">
|
||||||
|
<template #left>
|
||||||
|
<slot name="header-left" />
|
||||||
|
</template>
|
||||||
<slot name="header" />
|
<slot name="header" />
|
||||||
</PageHeader>
|
</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">
|
<Transition name="spacer">
|
||||||
<div
|
<div
|
||||||
v-if="!sidebar && platform === 'Mac' && languageDirection !== 'rtl'"
|
v-if="!showSidebar && platform === 'Mac' && languageDirection !== 'rtl'"
|
||||||
class="h-full"
|
class="h-full"
|
||||||
:class="sidebar ? '' : 'w-tl me-4 border-e'"
|
:class="showSidebar ? '' : 'w-tl me-4 border-e'"
|
||||||
/>
|
/>
|
||||||
</Transition>
|
</Transition>
|
||||||
<h1 class="text-xl font-semibold select-none" v-if="title">
|
<h1 class="text-xl font-semibold select-none" v-if="title">
|
||||||
{{ title }}
|
{{ title }}
|
||||||
</h1>
|
</h1>
|
||||||
|
<div class="flex items-stretch window-no-drag gap-2">
|
||||||
|
<slot name="left" />
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
class="flex items-stretch window-no-drag gap-2 ms-auto"
|
class="flex items-stretch window-no-drag gap-2 ms-auto"
|
||||||
:class="platform === 'Mac' && languageDirection === 'rtl' ? 'me-18' : ''"
|
:class="platform === 'Mac' && languageDirection === 'rtl' ? 'me-18' : ''"
|
||||||
@ -28,12 +31,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
|
import { showSidebar } from 'src/utils/refs';
|
||||||
import { Transition } from 'vue';
|
import { Transition } from 'vue';
|
||||||
import BackLink from './BackLink.vue';
|
import BackLink from './BackLink.vue';
|
||||||
import SearchBar from './SearchBar.vue';
|
import SearchBar from './SearchBar.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
inject: ['sidebar', 'languageDirection'],
|
inject: ['languageDirection'],
|
||||||
props: {
|
props: {
|
||||||
title: { type: String, default: '' },
|
title: { type: String, default: '' },
|
||||||
backLink: { type: Boolean, default: true },
|
backLink: { type: Boolean, default: true },
|
||||||
@ -42,6 +46,9 @@ export default {
|
|||||||
searchborder: { type: Boolean, default: true },
|
searchborder: { type: Boolean, default: true },
|
||||||
},
|
},
|
||||||
components: { SearchBar, BackLink, Transition },
|
components: { SearchBar, BackLink, Transition },
|
||||||
|
setup() {
|
||||||
|
return { showSidebar };
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
showBorder() {
|
showBorder() {
|
||||||
return !!this.$slots.default && this.searchborder;
|
return !!this.$slots.default && this.searchborder;
|
||||||
@ -59,7 +66,7 @@ export default {
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
width: 0px;
|
width: 0px;
|
||||||
margin-right: 0px;
|
margin-right: 0px;
|
||||||
border-eight-width: 0px;
|
border-right-width: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.spacer-enter-to,
|
.spacer-enter-to,
|
||||||
@ -67,7 +74,7 @@ export default {
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
width: var(--w-trafficlights);
|
width: var(--w-trafficlights);
|
||||||
margin-right: 1rem;
|
margin-right: 1rem;
|
||||||
border-eight-width: 1px;
|
border-right-width: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.spacer-enter-active,
|
.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"
|
class="grid gap-4 items-start"
|
||||||
style="grid-template-columns: 6rem auto"
|
style="grid-template-columns: 6rem auto"
|
||||||
>
|
>
|
||||||
<!-- <div class="w-2 text-base">{{ i + 1 }}.</div> -->
|
<ShortcutKeys class="text-base" :keys="s.shortcut" />
|
||||||
<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>
|
|
||||||
<div class="whitespace-normal text-base">{{ s.description }}</div>
|
<div class="whitespace-normal text-base">{{ s.description }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -63,8 +41,10 @@
|
|||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { t } from 'fyo';
|
import { t } from 'fyo';
|
||||||
|
import { ShortcutKey } from 'src/utils/ui';
|
||||||
import { defineComponent } from 'vue';
|
import { defineComponent } from 'vue';
|
||||||
import FormHeader from './FormHeader.vue';
|
import FormHeader from './FormHeader.vue';
|
||||||
|
import ShortcutKeys from './ShortcutKeys.vue';
|
||||||
|
|
||||||
type Group = {
|
type Group = {
|
||||||
label: string;
|
label: string;
|
||||||
@ -85,15 +65,15 @@ export default defineComponent({
|
|||||||
collapsed: false,
|
collapsed: false,
|
||||||
shortcuts: [
|
shortcuts: [
|
||||||
{
|
{
|
||||||
shortcut: [this.pmod, 'K'],
|
shortcut: [ShortcutKey.pmod, 'K'],
|
||||||
description: t`Open Quick Search`,
|
description: t`Open Quick Search`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
shortcut: [this.del],
|
shortcut: [ShortcutKey.delete],
|
||||||
description: t`Go back to the previous page`,
|
description: t`Go back to the previous page`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
shortcut: [this.shift, 'H'],
|
shortcut: [ShortcutKey.shift, 'H'],
|
||||||
description: t`Toggle sidebar`,
|
description: t`Toggle sidebar`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -108,14 +88,14 @@ export default defineComponent({
|
|||||||
collapsed: false,
|
collapsed: false,
|
||||||
shortcuts: [
|
shortcuts: [
|
||||||
{
|
{
|
||||||
shortcut: [this.pmod, 'S'],
|
shortcut: [ShortcutKey.pmod, 'S'],
|
||||||
description: [
|
description: [
|
||||||
t`Save or Submit a doc.`,
|
t`Save or Submit a doc.`,
|
||||||
t`A doc is submitted only if it is submittable and is in the saved state.`,
|
t`A doc is submitted only if it is submittable and is in the saved state.`,
|
||||||
].join(' '),
|
].join(' '),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
shortcut: [this.pmod, this.del],
|
shortcut: [ShortcutKey.pmod, ShortcutKey.delete],
|
||||||
description: [
|
description: [
|
||||||
t`Cancel or Delete a doc.`,
|
t`Cancel or Delete a doc.`,
|
||||||
t`A doc is cancelled only if it is in the submitted state.`,
|
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`,
|
description: t`Applicable when Quick Search is open`,
|
||||||
collapsed: false,
|
collapsed: false,
|
||||||
shortcuts: [
|
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`,
|
description: t`Toggle the Docs filter`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
shortcut: [this.pmod, '2'],
|
shortcut: [ShortcutKey.pmod, '2'],
|
||||||
description: t`Toggle the List filter`,
|
description: t`Toggle the List filter`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
shortcut: [this.pmod, '3'],
|
shortcut: [ShortcutKey.pmod, '3'],
|
||||||
description: t`Toggle the Create filter`,
|
description: t`Toggle the Create filter`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
shortcut: [this.pmod, '4'],
|
shortcut: [ShortcutKey.pmod, '4'],
|
||||||
description: t`Toggle the Report filter`,
|
description: t`Toggle the Report filter`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
shortcut: [this.pmod, '5'],
|
shortcut: [ShortcutKey.pmod, '5'],
|
||||||
description: t`Toggle the Page filter`,
|
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: {
|
components: { FormHeader, ShortcutKeys },
|
||||||
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 },
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
@ -145,7 +145,7 @@
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<p
|
<p
|
||||||
v-if="fyo.store.isDevelopment"
|
v-if="!fyo.store.isDevelopment"
|
||||||
class="text-xs text-gray-500 select-none"
|
class="text-xs text-gray-500 select-none"
|
||||||
>
|
>
|
||||||
dev mode
|
dev mode
|
||||||
@ -165,7 +165,7 @@
|
|||||||
m-4
|
m-4
|
||||||
rtl-rotate-180
|
rtl-rotate-180
|
||||||
"
|
"
|
||||||
@click="$emit('toggle-sidebar')"
|
@click="() => toggleSidebar()"
|
||||||
>
|
>
|
||||||
<feather-icon name="chevrons-left" class="w-4 h-4" />
|
<feather-icon name="chevrons-left" class="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
@ -182,7 +182,7 @@ import { fyo } from 'src/initFyo';
|
|||||||
import { openLink } from 'src/utils/ipcCalls';
|
import { openLink } from 'src/utils/ipcCalls';
|
||||||
import { docsPathRef } from 'src/utils/refs';
|
import { docsPathRef } from 'src/utils/refs';
|
||||||
import { getSidebarConfig } from 'src/utils/sidebarConfig';
|
import { getSidebarConfig } from 'src/utils/sidebarConfig';
|
||||||
import { routeTo } from 'src/utils/ui';
|
import { routeTo, toggleSidebar } from 'src/utils/ui';
|
||||||
import router from '../router';
|
import router from '../router';
|
||||||
import Icon from './Icon.vue';
|
import Icon from './Icon.vue';
|
||||||
import Modal from './Modal.vue';
|
import Modal from './Modal.vue';
|
||||||
@ -191,7 +191,7 @@ import ShortcutsHelper from './ShortcutsHelper.vue';
|
|||||||
export default {
|
export default {
|
||||||
components: [Button],
|
components: [Button],
|
||||||
inject: ['languageDirection', 'shortcuts'],
|
inject: ['languageDirection', 'shortcuts'],
|
||||||
emits: ['change-db-file', 'toggle-sidebar'],
|
emits: ['change-db-file'],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
companyName: '',
|
companyName: '',
|
||||||
@ -222,7 +222,7 @@ export default {
|
|||||||
|
|
||||||
this.shortcuts.shift.set(['KeyH'], () => {
|
this.shortcuts.shift.set(['KeyH'], () => {
|
||||||
if (document.body === document.activeElement) {
|
if (document.body === document.activeElement) {
|
||||||
this.$emit('toggle-sidebar');
|
this.toggleSidebar();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.shortcuts.set(['F1'], () => this.openDocumentation());
|
this.shortcuts.set(['F1'], () => this.openDocumentation());
|
||||||
@ -234,6 +234,7 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
routeTo,
|
routeTo,
|
||||||
reportIssue,
|
reportIssue,
|
||||||
|
toggleSidebar,
|
||||||
openDocumentation() {
|
openDocumentation() {
|
||||||
openLink('https://docs.frappebooks.com/' + docsPathRef.value);
|
openLink('https://docs.frappebooks.com/' + docsPathRef.value);
|
||||||
},
|
},
|
||||||
|
@ -94,11 +94,15 @@ export async function handleError(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function handleErrorWithDialog(
|
export async function handleErrorWithDialog(
|
||||||
error: Error,
|
error: unknown,
|
||||||
doc?: Doc,
|
doc?: Doc,
|
||||||
reportError?: false,
|
reportError?: false,
|
||||||
dontThrow?: false
|
dontThrow?: false
|
||||||
) {
|
) {
|
||||||
|
if (!(error instanceof Error)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const errorMessage = getErrorMessage(error, doc);
|
const errorMessage = getErrorMessage(error, doc);
|
||||||
await handleError(false, error, { errorMessage, doc });
|
await handleError(false, error, { errorMessage, doc });
|
||||||
|
|
||||||
|
@ -1,7 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<FormContainer>
|
<FormContainer>
|
||||||
|
<template #header-left v-if="hasDoc">
|
||||||
|
<StatusBadge :status="status" class="h-8" />
|
||||||
|
</template>
|
||||||
<template #header v-if="hasDoc">
|
<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
|
<DropdownWithActions
|
||||||
v-for="group of groupedActions"
|
v-for="group of groupedActions"
|
||||||
:key="group.label"
|
:key="group.label"
|
||||||
@ -116,8 +125,11 @@ import { docsPathMap } from 'src/utils/misc';
|
|||||||
import { docsPathRef, focusedDocsRef } from 'src/utils/refs';
|
import { docsPathRef, focusedDocsRef } from 'src/utils/refs';
|
||||||
import { ActionGroup, UIGroupedFields } from 'src/utils/types';
|
import { ActionGroup, UIGroupedFields } from 'src/utils/types';
|
||||||
import {
|
import {
|
||||||
|
getDocFromNameIfExistsElseNew,
|
||||||
getFieldsGroupedByTabAndSection,
|
getFieldsGroupedByTabAndSection,
|
||||||
getGroupedActionsForDoc,
|
getGroupedActionsForDoc,
|
||||||
|
isPrintable,
|
||||||
|
routeTo,
|
||||||
} from 'src/utils/ui';
|
} from 'src/utils/ui';
|
||||||
import { computed, defineComponent, nextTick } from 'vue';
|
import { computed, defineComponent, nextTick } from 'vue';
|
||||||
import QuickEditForm from '../QuickEditForm.vue';
|
import QuickEditForm from '../QuickEditForm.vue';
|
||||||
@ -142,12 +154,14 @@ export default defineComponent({
|
|||||||
activeTab: 'Default',
|
activeTab: 'Default',
|
||||||
groupedFields: null,
|
groupedFields: null,
|
||||||
quickEditDoc: null,
|
quickEditDoc: null,
|
||||||
|
isPrintable: false,
|
||||||
} as {
|
} as {
|
||||||
errors: Record<string, string>;
|
errors: Record<string, string>;
|
||||||
docOrNull: null | Doc;
|
docOrNull: null | Doc;
|
||||||
activeTab: string;
|
activeTab: string;
|
||||||
groupedFields: null | UIGroupedFields;
|
groupedFields: null | UIGroupedFields;
|
||||||
quickEditDoc: null | Doc;
|
quickEditDoc: null | Doc;
|
||||||
|
isPrintable: boolean;
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
@ -159,6 +173,7 @@ export default defineComponent({
|
|||||||
await this.setDoc();
|
await this.setDoc();
|
||||||
focusedDocsRef.add(this.docOrNull);
|
focusedDocsRef.add(this.docOrNull);
|
||||||
this.updateGroupedFields();
|
this.updateGroupedFields();
|
||||||
|
this.isPrintable = await isPrintable(this.schemaName);
|
||||||
},
|
},
|
||||||
activated(): void {
|
activated(): void {
|
||||||
docsPathRef.value = docsPathMap[this.schemaName] ?? '';
|
docsPathRef.value = docsPathMap[this.schemaName] ?? '';
|
||||||
@ -166,7 +181,9 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
deactivated(): void {
|
deactivated(): void {
|
||||||
docsPathRef.value = '';
|
docsPathRef.value = '';
|
||||||
focusedDocsRef.add(this.docOrNull);
|
if (this.docOrNull) {
|
||||||
|
focusedDocsRef.delete(this.doc);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
hasDoc(): boolean {
|
hasDoc(): boolean {
|
||||||
@ -238,6 +255,7 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
routeTo,
|
||||||
updateGroupedFields(): void {
|
updateGroupedFields(): void {
|
||||||
if (!this.hasDoc) {
|
if (!this.hasDoc) {
|
||||||
return;
|
return;
|
||||||
@ -253,10 +271,6 @@ export default defineComponent({
|
|||||||
await this.doc.sync();
|
await this.doc.sync();
|
||||||
this.updateGroupedFields();
|
this.updateGroupedFields();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!(err instanceof Error)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await handleErrorWithDialog(err, this.doc);
|
await handleErrorWithDialog(err, this.doc);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -265,10 +279,6 @@ export default defineComponent({
|
|||||||
await this.doc.submit();
|
await this.doc.submit();
|
||||||
this.updateGroupedFields();
|
this.updateGroupedFields();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!(err instanceof Error)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await handleErrorWithDialog(err, this.doc);
|
await handleErrorWithDialog(err, this.doc);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -277,18 +287,10 @@ export default defineComponent({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.name) {
|
this.docOrNull = await getDocFromNameIfExistsElseNew(
|
||||||
await this.setDocFromName(this.name);
|
this.schemaName,
|
||||||
} else {
|
this.name
|
||||||
this.docOrNull = this.fyo.doc.getNewDoc(this.schemaName);
|
);
|
||||||
}
|
|
||||||
},
|
|
||||||
async setDocFromName(name: string) {
|
|
||||||
try {
|
|
||||||
this.docOrNull = await this.fyo.doc.getDoc(this.schemaName, name);
|
|
||||||
} catch (err) {
|
|
||||||
this.docOrNull = this.fyo.doc.getNewDoc(this.schemaName);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
async toggleQuickEditDoc(doc: Doc | null) {
|
async toggleQuickEditDoc(doc: Doc | null) {
|
||||||
if (this.quickEditDoc && doc) {
|
if (this.quickEditDoc && doc) {
|
||||||
|
@ -44,6 +44,7 @@ import { DocValue } from 'fyo/core/types';
|
|||||||
import { Doc } from 'fyo/model/doc';
|
import { Doc } from 'fyo/model/doc';
|
||||||
import { Field } from 'schemas/types';
|
import { Field } from 'schemas/types';
|
||||||
import FormControl from 'src/components/Controls/FormControl.vue';
|
import FormControl from 'src/components/Controls/FormControl.vue';
|
||||||
|
import { focusOrSelectFormControl } from 'src/utils/ui';
|
||||||
import { defineComponent, PropType } from 'vue';
|
import { defineComponent, PropType } from 'vue';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
@ -61,26 +62,7 @@ export default defineComponent({
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.focusOnNameField();
|
focusOrSelectFormControl(this.doc, this.$refs.nameField);
|
||||||
},
|
|
||||||
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();
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
components: { FormControl },
|
components: { FormControl },
|
||||||
});
|
});
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { showSidebar } from 'src/utils/refs';
|
||||||
|
import { toggleSidebar } from 'src/utils/ui';
|
||||||
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div class="flex overflow-hidden">
|
<div class="flex overflow-hidden">
|
||||||
<Transition name="sidebar">
|
<Transition name="sidebar">
|
||||||
<Sidebar
|
<Sidebar
|
||||||
v-show="sidebar"
|
v-show="showSidebar"
|
||||||
class="flex-shrink-0 border-e whitespace-nowrap w-sidebar"
|
class="flex-shrink-0 border-e whitespace-nowrap w-sidebar"
|
||||||
@change-db-file="$emit('change-db-file')"
|
@change-db-file="$emit('change-db-file')"
|
||||||
@toggle-sidebar="sidebar = !sidebar"
|
|
||||||
/>
|
/>
|
||||||
</Transition>
|
</Transition>
|
||||||
|
|
||||||
@ -32,7 +35,7 @@
|
|||||||
|
|
||||||
<!-- Show Sidebar Button -->
|
<!-- Show Sidebar Button -->
|
||||||
<button
|
<button
|
||||||
v-show="!sidebar"
|
v-show="!showSidebar"
|
||||||
class="
|
class="
|
||||||
absolute
|
absolute
|
||||||
bottom-0
|
bottom-0
|
||||||
@ -43,31 +46,25 @@
|
|||||||
rtl-rotate-180
|
rtl-rotate-180
|
||||||
p-1
|
p-1
|
||||||
m-4
|
m-4
|
||||||
|
opacity-0
|
||||||
hover:opacity-100 hover:shadow-md
|
hover:opacity-100 hover:shadow-md
|
||||||
"
|
"
|
||||||
|
@click="() => toggleSidebar()"
|
||||||
@click="sidebar = !sidebar"
|
|
||||||
>
|
>
|
||||||
<feather-icon name="chevrons-right" class="w-4 h-4" />
|
<feather-icon name="chevrons-right" class="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script lang="ts">
|
||||||
import { computed } from '@vue/reactivity';
|
import { defineComponent } from 'vue';
|
||||||
import Sidebar from '../components/Sidebar.vue';
|
import Sidebar from '../components/Sidebar.vue';
|
||||||
export default {
|
export default defineComponent({
|
||||||
name: 'Desk',
|
name: 'Desk',
|
||||||
emits: ['change-db-file'],
|
emits: ['change-db-file'],
|
||||||
data() {
|
|
||||||
return { sidebar: true };
|
|
||||||
},
|
|
||||||
provide() {
|
|
||||||
return { sidebar: computed(() => this.sidebar) };
|
|
||||||
},
|
|
||||||
components: {
|
components: {
|
||||||
Sidebar,
|
Sidebar,
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
@ -386,7 +386,7 @@ import { fyo } from 'src/initFyo';
|
|||||||
import { getSavePath, saveData, selectFile } from 'src/utils/ipcCalls';
|
import { getSavePath, saveData, selectFile } from 'src/utils/ipcCalls';
|
||||||
import { docsPathMap } from 'src/utils/misc';
|
import { docsPathMap } from 'src/utils/misc';
|
||||||
import { docsPathRef } from 'src/utils/refs';
|
import { docsPathRef } from 'src/utils/refs';
|
||||||
import { showMessageDialog } from 'src/utils/ui';
|
import { selectTextFile, showMessageDialog } from 'src/utils/ui';
|
||||||
import { defineComponent } from 'vue';
|
import { defineComponent } from 'vue';
|
||||||
import Loading from '../components/Loading.vue';
|
import Loading from '../components/Loading.vue';
|
||||||
|
|
||||||
@ -906,27 +906,14 @@ export default defineComponent({
|
|||||||
: '';
|
: '';
|
||||||
},
|
},
|
||||||
async selectFile(): Promise<void> {
|
async selectFile(): Promise<void> {
|
||||||
const options = {
|
const { text, name, filePath } = await selectTextFile([
|
||||||
title: this.t`Select File`,
|
{ name: 'CSV', extensions: ['csv'] },
|
||||||
filters: [{ name: 'CSV', extensions: ['csv'] }],
|
]);
|
||||||
};
|
|
||||||
|
|
||||||
const { success, canceled, filePath, data, name } = await selectFile(
|
if (!text) {
|
||||||
options
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!success && !canceled) {
|
|
||||||
await showMessageDialog({
|
|
||||||
message: this.t`File selection failed.`,
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!success || canceled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const text = new TextDecoder().decode(data);
|
|
||||||
const isValid = this.importer.selectFile(text);
|
const isValid = this.importer.selectFile(text);
|
||||||
if (!isValid) {
|
if (!isValid) {
|
||||||
await showMessageDialog({
|
await showMessageDialog({
|
||||||
|
@ -1,8 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<FormContainer>
|
<FormContainer>
|
||||||
<!-- Page Header (Title, Buttons, etc) -->
|
<!-- 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">
|
<template #header v-if="doc">
|
||||||
<StatusBadge :status="status" />
|
|
||||||
<ExchangeRate
|
<ExchangeRate
|
||||||
v-if="doc.isMultiCurrency"
|
v-if="doc.isMultiCurrency"
|
||||||
:disabled="doc?.isSubmitted || doc?.isCancelled"
|
:disabled="doc?.isSubmitted || doc?.isCancelled"
|
||||||
@ -13,10 +20,6 @@
|
|||||||
async (exchangeRate) => await doc.set('exchangeRate', exchangeRate)
|
async (exchangeRate) => await doc.set('exchangeRate', exchangeRate)
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
<Barcode
|
|
||||||
v-if="doc.canEdit && fyo.singles.InventorySettings?.enableBarcodes"
|
|
||||||
@item-selected="(name) => doc.addItem(name)"
|
|
||||||
/>
|
|
||||||
<Button
|
<Button
|
||||||
v-if="!doc.isCancelled && !doc.dirty"
|
v-if="!doc.isCancelled && !doc.dirty"
|
||||||
:icon="true"
|
:icon="true"
|
||||||
|
@ -37,7 +37,7 @@
|
|||||||
|
|
||||||
<!-- Data Rows -->
|
<!-- Data Rows -->
|
||||||
<div class="overflow-y-auto custom-scroll" v-if="dataSlice.length !== 0">
|
<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 -->
|
<!-- Row Content -->
|
||||||
<div class="flex hover:bg-gray-50 items-center">
|
<div class="flex hover:bg-gray-50 items-center">
|
||||||
<p class="w-8 text-end me-4 text-gray-900">
|
<p class="w-8 text-end me-4 text-gray-900">
|
||||||
@ -46,7 +46,7 @@
|
|||||||
<Row
|
<Row
|
||||||
gap="1rem"
|
gap="1rem"
|
||||||
class="cursor-pointer text-gray-900 flex-1 h-row-mid"
|
class="cursor-pointer text-gray-900 flex-1 h-row-mid"
|
||||||
@click="$emit('openDoc', doc.name)"
|
@click="$emit('openDoc', row.name)"
|
||||||
:columnCount="columns.length"
|
:columnCount="columns.length"
|
||||||
>
|
>
|
||||||
<ListCell
|
<ListCell
|
||||||
@ -56,7 +56,7 @@
|
|||||||
'text-end': isNumeric(column.fieldtype),
|
'text-end': isNumeric(column.fieldtype),
|
||||||
'pe-4': c === columns.length - 1,
|
'pe-4': c === columns.length - 1,
|
||||||
}"
|
}"
|
||||||
:doc="doc"
|
:row="row"
|
||||||
:column="column"
|
:column="column"
|
||||||
/>
|
/>
|
||||||
</Row>
|
</Row>
|
||||||
@ -95,7 +95,6 @@ import Paginator from 'src/components/Paginator.vue';
|
|||||||
import Row from 'src/components/Row';
|
import Row from 'src/components/Row';
|
||||||
import { fyo } from 'src/initFyo';
|
import { fyo } from 'src/initFyo';
|
||||||
import { isNumeric } from 'src/utils';
|
import { isNumeric } from 'src/utils';
|
||||||
import { openQuickEdit, routeTo } from 'src/utils/ui';
|
|
||||||
import { objectForEach } from 'utils/index';
|
import { objectForEach } from 'utils/index';
|
||||||
import { defineComponent, toRaw } from 'vue';
|
import { defineComponent, toRaw } from 'vue';
|
||||||
import ListCell from './ListCell';
|
import ListCell from './ListCell';
|
||||||
|
@ -4,26 +4,52 @@
|
|||||||
<component v-else :is="customRenderer" />
|
<component v-else :is="customRenderer" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script lang="ts">
|
||||||
|
import { ColumnConfig, RenderData } from 'fyo/model/types';
|
||||||
|
import { Field } from 'schemas/types';
|
||||||
import { fyo } from 'src/initFyo';
|
import { fyo } from 'src/initFyo';
|
||||||
import { isNumeric } from 'src/utils';
|
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',
|
name: 'ListCell',
|
||||||
props: ['doc', 'column'],
|
props: {
|
||||||
|
row: { type: Object as PropType<RenderData>, required: true },
|
||||||
|
column: { type: Object as PropType<Column>, required: true },
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
columnValue() {
|
columnValue(): string {
|
||||||
let { column, doc } = this;
|
const column = this.column;
|
||||||
let value = doc[column.fieldname];
|
const value = this.row[this.column.fieldname];
|
||||||
return fyo.format(value, column, doc);
|
|
||||||
|
if (isField(column)) {
|
||||||
|
return fyo.format(value, column);
|
||||||
|
}
|
||||||
|
|
||||||
|
return column.display?.(value, fyo) ?? '';
|
||||||
},
|
},
|
||||||
customRenderer() {
|
customRenderer() {
|
||||||
if (!this.column.render) return;
|
const { render } = this.column as ColumnConfig;
|
||||||
return this.column.render(this.doc);
|
|
||||||
|
if (!render) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return render(this.row);
|
||||||
},
|
},
|
||||||
cellClass() {
|
cellClass() {
|
||||||
return isNumeric(this.column.fieldtype) ? 'justify-end' : '';
|
return isNumeric(this.column.fieldtype) ? 'justify-end' : '';
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,161 +1,242 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<div class="flex flex-col flex-1 bg-gray-25">
|
<div class="flex flex-col flex-1 bg-gray-25">
|
||||||
<PageHeader class="z-10" :border="false">
|
<PageHeader :border="true">
|
||||||
<Button
|
<template #left>
|
||||||
class="text-gray-900 text-xs"
|
<AutoComplete
|
||||||
@click="showCustomiser = !showCustomiser"
|
v-if="templateList.length"
|
||||||
>
|
:df="{
|
||||||
{{ t`Customise` }}
|
fieldname: 'templateName',
|
||||||
</Button>
|
label: t`Template Name`,
|
||||||
<Button class="text-gray-900 text-xs" @click="makePDF">
|
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` }}
|
{{ t`Save as PDF` }}
|
||||||
</Button>
|
</Button>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
|
|
||||||
<!-- Printview Preview -->
|
<!-- Template Display Area -->
|
||||||
<div
|
<div class="overflow-auto custom-scroll p-4">
|
||||||
v-if="doc && printSettings"
|
<!-- Display Hints -->
|
||||||
class="flex justify-center flex-1 overflow-auto relative"
|
<div v-if="helperMessage" class="text-sm text-gray-700">
|
||||||
>
|
{{ helperMessage }}
|
||||||
<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>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Printview Customizer -->
|
<!-- Template Container -->
|
||||||
<Transition name="quickedit">
|
<PrintContainer
|
||||||
<div class="border-s w-quick-edit" v-if="showCustomiser">
|
ref="printContainer"
|
||||||
<div
|
v-if="printProps"
|
||||||
class="px-4 flex items-center justify-between h-row-largest border-b"
|
:template="printProps.template"
|
||||||
>
|
:values="printProps.values"
|
||||||
<h2 class="font-semibold">{{ t`Customise` }}</h2>
|
:scale="scale"
|
||||||
<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"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Transition>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script lang="ts">
|
||||||
import { ipcRenderer } from 'electron';
|
import { Doc } from 'fyo/model/doc';
|
||||||
import { Verb } from 'fyo/telemetry/types';
|
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 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 PageHeader from 'src/components/PageHeader.vue';
|
||||||
import InvoiceTemplate from 'src/components/SalesInvoice/InvoiceTemplate.vue';
|
import { handleErrorWithDialog } from 'src/errorHandling';
|
||||||
import TwoColumnForm from 'src/components/TwoColumnForm.vue';
|
|
||||||
import { fyo } from 'src/initFyo';
|
import { fyo } from 'src/initFyo';
|
||||||
import { makePDF } from 'src/utils/ipcCalls';
|
import { getPrintTemplatePropValues } from 'src/utils/printTemplates';
|
||||||
import { IPC_ACTIONS } from 'utils/messages';
|
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',
|
name: 'PrintView',
|
||||||
props: { schemaName: String, name: String },
|
props: {
|
||||||
|
schemaName: { type: String, required: true },
|
||||||
|
name: { type: String, required: true },
|
||||||
|
},
|
||||||
components: {
|
components: {
|
||||||
PageHeader,
|
PageHeader,
|
||||||
Button,
|
Button,
|
||||||
TwoColumnForm,
|
AutoComplete,
|
||||||
|
PrintContainer,
|
||||||
|
DropdownWithActions,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
doc: null,
|
doc: null,
|
||||||
showCustomiser: false,
|
scale: 1,
|
||||||
printSettings: null,
|
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() {
|
async mounted() {
|
||||||
this.doc = await fyo.doc.getDoc(this.schemaName, this.name);
|
this.doc = await fyo.doc.getDoc(this.schemaName, this.name);
|
||||||
this.printSettings = await fyo.doc.getDoc('PrintSettings');
|
await this.setTemplateList();
|
||||||
|
|
||||||
if (fyo.store.isDevelopment) {
|
if (fyo.store.isDevelopment) {
|
||||||
|
// @ts-ignore
|
||||||
window.pv = this;
|
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: {
|
computed: {
|
||||||
printTemplate() {
|
helperMessage() {
|
||||||
return InvoiceTemplate;
|
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: {
|
methods: {
|
||||||
constructPrintDocument() {
|
async onTemplateNameChange(value: string | null): Promise<void> {
|
||||||
const html = document.createElement('html');
|
if (!value) {
|
||||||
const head = document.createElement('head');
|
this.templateDoc = null;
|
||||||
const body = document.createElement('body');
|
return;
|
||||||
const style = getAllCSSAsStyleElem();
|
|
||||||
|
|
||||||
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;
|
|
||||||
},
|
|
||||||
async makePDF() {
|
|
||||||
const savePath = await this.getSavePath();
|
|
||||||
if (!savePath) return;
|
|
||||||
|
|
||||||
const html = this.constructPrintDocument();
|
|
||||||
await makePDF(html, savePath);
|
|
||||||
fyo.telemetry.log(Verb.Exported, 'SalesInvoice', { extension: 'pdf' });
|
|
||||||
},
|
|
||||||
async getSavePath() {
|
|
||||||
const options = {
|
|
||||||
title: this.t`Select folder`,
|
|
||||||
defaultPath: `${this.name}.pdf`,
|
|
||||||
};
|
|
||||||
|
|
||||||
let { filePath } = await ipcRenderer.invoke(
|
|
||||||
IPC_ACTIONS.GET_SAVE_FILEPATH,
|
|
||||||
options
|
|
||||||
);
|
|
||||||
|
|
||||||
if (filePath) {
|
|
||||||
if (!filePath.endsWith('.pdf')) {
|
|
||||||
filePath = filePath + '.pdf';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return filePath;
|
this.templateName = value;
|
||||||
|
try {
|
||||||
|
this.templateDoc = (await this.fyo.doc.getDoc(
|
||||||
|
ModelNameEnum.PrintTemplate,
|
||||||
|
this.templateName
|
||||||
|
)) as PrintTemplate;
|
||||||
|
} catch (error) {
|
||||||
|
await handleErrorWithDialog(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async setTemplateList(): Promise<void> {
|
||||||
|
const list = (await this.fyo.db.getAllRaw(ModelNameEnum.PrintTemplate, {
|
||||||
|
filters: { type: this.schemaName },
|
||||||
|
})) as { name: string }[];
|
||||||
|
|
||||||
|
this.templateList = list.map(({ name }) => name);
|
||||||
|
},
|
||||||
|
async savePDF() {
|
||||||
|
const printContainer = this.$refs.printContainer as {
|
||||||
|
savePDF: (name?: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!printContainer?.savePDF) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
</script>
|
||||||
|
@ -161,12 +161,8 @@ export default defineComponent({
|
|||||||
try {
|
try {
|
||||||
await doc.sync();
|
await doc.sync();
|
||||||
this.updateGroupedFields();
|
this.updateGroupedFields();
|
||||||
} catch (err) {
|
} catch (error) {
|
||||||
if (!(err instanceof Error)) {
|
await handleErrorWithDialog(error, doc);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await handleErrorWithDialog(err, doc);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async onValueChange(field: Field, value: DocValue): Promise<void> {
|
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;
|
window.fyo = fyo;
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
window.DateTime = DateTime;
|
window.DateTime = DateTime;
|
||||||
|
// @ts-ignore
|
||||||
|
window.ipcRenderer = ipcRenderer;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPlatformName(platform: string) {
|
function getPlatformName(platform: string) {
|
||||||
|
@ -10,12 +10,8 @@ import PrintView from 'src/pages/PrintView/PrintView.vue';
|
|||||||
import QuickEditForm from 'src/pages/QuickEditForm.vue';
|
import QuickEditForm from 'src/pages/QuickEditForm.vue';
|
||||||
import Report from 'src/pages/Report.vue';
|
import Report from 'src/pages/Report.vue';
|
||||||
import Settings from 'src/pages/Settings/Settings.vue';
|
import Settings from 'src/pages/Settings/Settings.vue';
|
||||||
import {
|
import TemplateBuilder from 'src/pages/TemplateBuilder/TemplateBuilder.vue';
|
||||||
createRouter,
|
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router';
|
||||||
createWebHistory,
|
|
||||||
RouteLocationRaw,
|
|
||||||
RouteRecordRaw,
|
|
||||||
} from 'vue-router';
|
|
||||||
|
|
||||||
function getCommonFormItems(): RouteRecordRaw[] {
|
function getCommonFormItems(): RouteRecordRaw[] {
|
||||||
return [
|
return [
|
||||||
@ -127,6 +123,12 @@ const routes: RouteRecordRaw[] = [
|
|||||||
name: 'Import Wizard',
|
name: 'Import Wizard',
|
||||||
component: ImportWizard,
|
component: ImportWizard,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/template-builder/:name',
|
||||||
|
name: 'Template Builder',
|
||||||
|
component: TemplateBuilder,
|
||||||
|
props: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/settings',
|
path: '/settings',
|
||||||
name: 'Settings',
|
name: 'Settings',
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
outline-color: theme('colors.pink.500');
|
outline-color: theme('colors.pink.400');
|
||||||
font-variation-settings: 'slnt' 0deg;
|
font-variation-settings: 'slnt' 0deg;
|
||||||
}
|
}
|
||||||
.italic {
|
.italic {
|
||||||
@ -133,19 +133,19 @@ input[type='number']::-webkit-inner-spin-button {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.custom-scroll::-webkit-scrollbar-track:vertical {
|
.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 {
|
.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 {
|
.custom-scroll::-webkit-scrollbar-thumb {
|
||||||
background: theme('colors.gray.200');
|
background: theme('colors.gray.100');
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-scroll::-webkit-scrollbar-thumb:hover {
|
.custom-scroll::-webkit-scrollbar-thumb:hover {
|
||||||
background: theme('colors.gray.400');
|
background: theme('colors.gray.200');
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -47,9 +47,9 @@ function evaluateFieldMeta(
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
const hiddenFunction = doc?.[meta]?.[field.fieldname];
|
const evalFunction = doc?.[meta]?.[field.fieldname];
|
||||||
if (hiddenFunction !== undefined) {
|
if (evalFunction !== undefined) {
|
||||||
return hiddenFunction();
|
return evalFunction();
|
||||||
}
|
}
|
||||||
|
|
||||||
return defaultValue;
|
return defaultValue;
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { Fyo } from 'fyo';
|
import { Fyo } from 'fyo';
|
||||||
import { ConfigFile, ConfigKeys } from 'fyo/core/types';
|
import { ConfigFile, ConfigKeys } from 'fyo/core/types';
|
||||||
import { Doc } from 'fyo/model/doc';
|
|
||||||
import { getRegionalModels, models } from 'models/index';
|
import { getRegionalModels, models } from 'models/index';
|
||||||
import { ModelNameEnum } from 'models/types';
|
import { ModelNameEnum } from 'models/types';
|
||||||
import { TargetField } from 'schemas/types';
|
import { TargetField } from 'schemas/types';
|
||||||
|
@ -6,7 +6,7 @@ import { t } from 'fyo';
|
|||||||
import { BaseError } from 'fyo/utils/errors';
|
import { BaseError } from 'fyo/utils/errors';
|
||||||
import { BackendResponse } from 'utils/ipc/types';
|
import { BackendResponse } from 'utils/ipc/types';
|
||||||
import { IPC_ACTIONS, IPC_MESSAGES } from 'utils/messages';
|
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 { setLanguageMap } from './language';
|
||||||
import { showMessageDialog, showToast } from './ui';
|
import { showMessageDialog, showToast } from './ui';
|
||||||
|
|
||||||
@ -14,6 +14,10 @@ export function reloadWindow() {
|
|||||||
return ipcRenderer.send(IPC_MESSAGES.RELOAD_MAIN_WINDOW);
|
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(
|
export async function selectFile(
|
||||||
options: SelectFileOptions
|
options: SelectFileOptions
|
||||||
): Promise<SelectFileReturn> {
|
): Promise<SelectFileReturn> {
|
||||||
|
@ -115,6 +115,7 @@ export const docsPathMap: Record<string, string | undefined> = {
|
|||||||
[ModelNameEnum.Party]: 'entries/party',
|
[ModelNameEnum.Party]: 'entries/party',
|
||||||
[ModelNameEnum.Item]: 'entries/items',
|
[ModelNameEnum.Item]: 'entries/items',
|
||||||
[ModelNameEnum.Tax]: 'entries/taxes',
|
[ModelNameEnum.Tax]: 'entries/taxes',
|
||||||
|
[ModelNameEnum.PrintTemplate]: 'miscellaneous/print-templates',
|
||||||
|
|
||||||
// Miscellaneous
|
// Miscellaneous
|
||||||
Search: 'miscellaneous/search',
|
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 { reactive, ref } from 'vue';
|
||||||
import { FocusedDocContextSet } from './misc';
|
import { FocusedDocContextSet } from './misc';
|
||||||
|
|
||||||
|
export const showSidebar = ref(true);
|
||||||
export const docsPathRef = ref<string>('');
|
export const docsPathRef = ref<string>('');
|
||||||
export const systemLanguageRef = ref<string>('');
|
export const systemLanguageRef = ref<string>('');
|
||||||
export const focusedDocsRef = reactive<FocusedDocContextSet>(
|
export const focusedDocsRef = reactive<FocusedDocContextSet>(
|
||||||
|
@ -272,6 +272,11 @@ async function getCompleteSidebar(): Promise<SidebarConfig> {
|
|||||||
name: 'import-wizard',
|
name: 'import-wizard',
|
||||||
route: '/import-wizard',
|
route: '/import-wizard',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: t`Print Templates`,
|
||||||
|
name: 'print-template',
|
||||||
|
route: `/list/PrintTemplate/${t`Print Templates`}`,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: t`Settings`,
|
label: t`Settings`,
|
||||||
name: 'settings',
|
name: 'settings',
|
||||||
|
@ -85,3 +85,8 @@ export type ActionGroup = {
|
|||||||
export type UIGroupedFields = Map<string, Map<string, Field[]>>;
|
export type UIGroupedFields = Map<string, Map<string, Field[]>>;
|
||||||
export type ExportFormat = 'csv' | 'json';
|
export type ExportFormat = 'csv' | 'json';
|
||||||
export type PeriodKey = 'This Year' | 'This Quarter' | 'This Month';
|
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 { fyo } from 'src/initFyo';
|
||||||
import router from 'src/router';
|
import router from 'src/router';
|
||||||
import { IPC_ACTIONS } from 'utils/messages';
|
import { IPC_ACTIONS } from 'utils/messages';
|
||||||
|
import { SelectFileOptions } from 'utils/types';
|
||||||
import { App, createApp, h } from 'vue';
|
import { App, createApp, h } from 'vue';
|
||||||
import { RouteLocationRaw } from 'vue-router';
|
import { RouteLocationRaw } from 'vue-router';
|
||||||
import { stringifyCircular } from './';
|
import { stringifyCircular } from './';
|
||||||
import { evaluateHidden } from './doc';
|
import { evaluateHidden } from './doc';
|
||||||
|
import { selectFile } from './ipcCalls';
|
||||||
|
import { showSidebar } from './refs';
|
||||||
import {
|
import {
|
||||||
ActionGroup,
|
ActionGroup,
|
||||||
MessageDialogOptions,
|
MessageDialogOptions,
|
||||||
@ -141,8 +144,8 @@ function replaceAndAppendMount(app: App<Element>, replaceId: string) {
|
|||||||
parent!.append(clone);
|
parent!.append(clone);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function openSettings(tab: SettingsTab) {
|
export async function openSettings(tab: SettingsTab) {
|
||||||
routeTo({ path: '/settings', query: { tab } });
|
await routeTo({ path: '/settings', query: { tab } });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function routeTo(route: RouteLocationRaw) {
|
export async function routeTo(route: RouteLocationRaw) {
|
||||||
@ -337,18 +340,13 @@ function getDeleteAction(doc: Doc): Action {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openEdit(doc: Doc) {
|
async function openEdit({ name, schemaName }: Doc) {
|
||||||
const isFormEdit = [
|
if (!name) {
|
||||||
ModelNameEnum.SalesInvoice,
|
return;
|
||||||
ModelNameEnum.PurchaseInvoice,
|
|
||||||
ModelNameEnum.JournalEntry,
|
|
||||||
].includes(doc.schemaName as ModelNameEnum);
|
|
||||||
|
|
||||||
if (isFormEdit) {
|
|
||||||
return await routeTo(`/edit/${doc.schemaName}/${doc.name!}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await openQuickEdit({ schemaName: doc.schemaName, name: doc.name! });
|
const route = getFormRoute(schemaName, name);
|
||||||
|
return await routeTo(route);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDuplicateAction(doc: Doc): Action {
|
function getDuplicateAction(doc: Doc): Action {
|
||||||
@ -369,7 +367,7 @@ function getDuplicateAction(doc: Doc): Action {
|
|||||||
label: t`Yes`,
|
label: t`Yes`,
|
||||||
async action() {
|
async action() {
|
||||||
try {
|
try {
|
||||||
const dupe = await doc.duplicate();
|
const dupe = doc.duplicate();
|
||||||
await openEdit(dupe);
|
await openEdit(dupe);
|
||||||
return true;
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -450,3 +448,126 @@ export function getFormRoute(
|
|||||||
|
|
||||||
return `/list/${schemaName}?edit=1&schemaName=${schemaName}&name=${name}`;
|
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',
|
SELECT_FILE = 'select-file',
|
||||||
GET_CREDS = 'get-creds',
|
GET_CREDS = 'get-creds',
|
||||||
GET_DB_LIST = 'get-db-list',
|
GET_DB_LIST = 'get-db-list',
|
||||||
|
GET_TEMPLATES = 'get-templates',
|
||||||
DELETE_FILE = 'delete-file',
|
DELETE_FILE = 'delete-file',
|
||||||
// Database messages
|
// Database messages
|
||||||
DB_CREATE = 'db-create',
|
DB_CREATE = 'db-create',
|
||||||
|
@ -23,14 +23,18 @@ export interface VersionParts {
|
|||||||
beta?: number;
|
beta?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Creds = { errorLogUrl: string; telemetryUrl: string; tokenString: string };
|
export type Creds = {
|
||||||
|
errorLogUrl: string;
|
||||||
|
telemetryUrl: string;
|
||||||
|
tokenString: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type UnexpectedLogObject = {
|
export type UnexpectedLogObject = {
|
||||||
name: string;
|
name: string;
|
||||||
message: string;
|
message: string;
|
||||||
stack: string;
|
stack: string;
|
||||||
more: Record<string, unknown>;
|
more: Record<string, unknown>;
|
||||||
}
|
};
|
||||||
|
|
||||||
export interface SelectFileOptions {
|
export interface SelectFileOptions {
|
||||||
title: string;
|
title: string;
|
||||||
@ -48,3 +52,5 @@ export interface SelectFileReturn {
|
|||||||
export type PropertyEnum<T extends Record<string, any>> = {
|
export type PropertyEnum<T extends Record<string, any>> = {
|
||||||
[key in keyof Required<T>]: key;
|
[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"
|
"@babel/helper-validator-identifier" "^7.15.7"
|
||||||
to-fast-properties "^2.0.0"
|
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":
|
"@cspotcode/source-map-consumer@0.8.0":
|
||||||
version "0.8.0"
|
version "0.8.0"
|
||||||
resolved "https://registry.yarnpkg.com/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz#33bf4b7b39c178821606f669bbc447a6a629786b"
|
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/resolve-uri" "^3.0.3"
|
||||||
"@jridgewell/sourcemap-codec" "^1.4.10"
|
"@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":
|
"@malept/cross-spawn-promise@^1.1.0":
|
||||||
version "1.1.1"
|
version "1.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz#504af200af6b98e198bce768bc1730c6936ae01d"
|
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"
|
resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
|
||||||
integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=
|
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:
|
collection-visit@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0"
|
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"
|
resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333"
|
||||||
integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==
|
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:
|
cross-spawn@^5.0.1:
|
||||||
version "5.1.0"
|
version "5.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449"
|
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"
|
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
|
||||||
integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo=
|
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:
|
stylehacks@^4.0.0:
|
||||||
version "4.0.3"
|
version "4.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-4.0.3.tgz#6718fcaf4d1e07d8a1318690881e8d96726a71d5"
|
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/server-renderer" "3.2.40"
|
||||||
"@vue/shared" "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:
|
watchpack-chokidar2@^2.0.1:
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/watchpack-chokidar2/-/watchpack-chokidar2-2.0.1.tgz#38500072ee6ece66f3769936950ea1771be1c957"
|
resolved "https://registry.yarnpkg.com/watchpack-chokidar2/-/watchpack-chokidar2-2.0.1.tgz#38500072ee6ece66f3769936950ea1771be1c957"
|
||||||
|
Loading…
Reference in New Issue
Block a user