2
0
mirror of https://github.com/frappe/books.git synced 2025-01-24 15:48:25 +00:00

Merge pull request #429 from 18alantom/multi-currency-invoicing

feat: multi currency invoicing
This commit is contained in:
Alan 2022-10-03 02:15:15 -07:00 committed by GitHub
commit 24b6021606
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 951 additions and 751 deletions

View File

@ -12,7 +12,7 @@ import {
OptionField, OptionField,
RawValue, RawValue,
Schema, Schema,
TargetField, TargetField
} from 'schemas/types'; } from 'schemas/types';
import { getIsNullOrUndef, getMapFromList, getRandomString } from 'utils'; import { getIsNullOrUndef, getMapFromList, getRandomString } from 'utils';
import { markRaw } from 'vue'; import { markRaw } from 'vue';
@ -23,7 +23,7 @@ import {
getMissingMandatoryMessage, getMissingMandatoryMessage,
getPreDefaultValues, getPreDefaultValues,
setChildDocIdx, setChildDocIdx,
shouldApplyFormula, shouldApplyFormula
} from './helpers'; } from './helpers';
import { setName } from './naming'; import { setName } from './naming';
import { import {
@ -41,7 +41,7 @@ import {
ReadOnlyMap, ReadOnlyMap,
RequiredMap, RequiredMap,
TreeViewSettings, TreeViewSettings,
ValidationMap, ValidationMap
} from './types'; } from './types';
import { validateOptions, validateRequired } from './validationFunction'; import { validateOptions, validateRequired } from './validationFunction';
@ -186,7 +186,8 @@ export class Doc extends Observable<DocValue | Doc[]> {
// set value and trigger change // set value and trigger change
async set( async set(
fieldname: string | DocValueMap, fieldname: string | DocValueMap,
value?: DocValue | Doc[] | DocValueMap[] value?: DocValue | Doc[] | DocValueMap[],
retriggerChildDocApplyChange: boolean = false
): Promise<boolean> { ): Promise<boolean> {
if (typeof fieldname === 'object') { if (typeof fieldname === 'object') {
return await this.setMultiple(fieldname as DocValueMap); return await this.setMultiple(fieldname as DocValueMap);
@ -216,7 +217,7 @@ export class Doc extends Observable<DocValue | Doc[]> {
await this._applyChange(fieldname); await this._applyChange(fieldname);
await this.parentdoc._applyChange(this.parentFieldname as string); await this.parentdoc._applyChange(this.parentFieldname as string);
} else { } else {
await this._applyChange(fieldname); await this._applyChange(fieldname, retriggerChildDocApplyChange);
} }
return true; return true;
@ -259,8 +260,11 @@ export class Doc extends Observable<DocValue | Doc[]> {
return !areDocValuesEqual(currentValue as DocValue, value as DocValue); return !areDocValuesEqual(currentValue as DocValue, value as DocValue);
} }
async _applyChange(fieldname: string): Promise<boolean> { async _applyChange(
await this._applyFormula(fieldname); fieldname: string,
retriggerChildDocApplyChange?: boolean
): Promise<boolean> {
await this._applyFormula(fieldname, retriggerChildDocApplyChange);
await this.trigger('change', { await this.trigger('change', {
doc: this, doc: this,
changed: fieldname, changed: fieldname,
@ -616,37 +620,62 @@ export class Doc extends Observable<DocValue | Doc[]> {
} }
} }
async _applyFormula(fieldname?: string): Promise<boolean> { async _applyFormula(
fieldname?: string,
retriggerChildDocApplyChange?: boolean
): Promise<boolean> {
const doc = this; const doc = this;
let changed = false; let changed = await this._callAllTableFieldsApplyFormula(fieldname);
changed = (await this._applyFormulaForFields(doc, fieldname)) || changed;
const childDocs = this.tableFields if (changed && retriggerChildDocApplyChange) {
.map((f) => (this.get(f.fieldname) as Doc[]) ?? []) await this._callAllTableFieldsApplyFormula(fieldname);
.flat(); await this._applyFormulaForFields(doc, fieldname);
// children
for (const row of childDocs) {
changed ||= (await row?._applyFormula()) ?? false;
} }
// parent or child row return changed;
}
async _callAllTableFieldsApplyFormula(
changedFieldname?: string
): Promise<boolean> {
let changed = false;
for (const { fieldname } of this.tableFields) {
const childDocs = this.get(fieldname) as Doc[];
if (!childDocs) {
continue;
}
changed =
(await this._callChildDocApplyFormula(childDocs, changedFieldname)) ||
changed;
}
return changed;
}
async _callChildDocApplyFormula(
childDocs: Doc[],
fieldname?: string
): Promise<boolean> {
let changed: boolean = false;
for (const childDoc of childDocs) {
if (!childDoc._applyFormula) {
continue;
}
changed = (await childDoc._applyFormula(fieldname)) || changed;
}
return changed;
}
async _applyFormulaForFields(doc: Doc, fieldname?: string) {
const formulaFields = Object.keys(this.formulas).map( const formulaFields = Object.keys(this.formulas).map(
(fn) => this.fieldMap[fn] (fn) => this.fieldMap[fn]
); );
changed ||= await this._applyFormulaForFields(
formulaFields,
doc,
fieldname
);
return changed;
}
async _applyFormulaForFields(
formulaFields: Field[],
doc: Doc,
fieldname?: string
) {
let changed = false; let changed = false;
for (const field of formulaFields) { for (const field of formulaFields) {
const shouldApply = shouldApplyFormula(field, doc, fieldname); const shouldApply = shouldApplyFormula(field, doc, fieldname);
@ -662,7 +691,7 @@ export class Doc extends Observable<DocValue | Doc[]> {
} }
doc[field.fieldname] = newVal; doc[field.fieldname] = newVal;
changed = true; changed ||= true;
} }
return changed; return changed;

View File

@ -7,6 +7,16 @@ import { SelectOption } from 'schemas/types';
import { getCountryInfo } from 'utils/misc'; import { getCountryInfo } from 'utils/misc';
export default class SystemSettings extends Doc { export default class SystemSettings extends Doc {
dateFormat?: string;
locale?: string;
displayPrecision?: number;
internalPrecision?: number;
hideGetStarted?: boolean;
countryCode?: string;
currency?: string;
version?: string;
instanceId?: string;
validations: ValidationMap = { validations: ValidationMap = {
async displayPrecision(value: DocValue) { async displayPrecision(value: DocValue) {
if ((value as number) >= 0 && (value as number) <= 9) { if ((value as number) >= 0 && (value as number) <= 9) {

View File

@ -70,7 +70,7 @@ function formatCurrency(
doc: Doc | null, doc: Doc | null,
fyo: Fyo fyo: Fyo
): string { ): string {
const currency = getCurrency(field, doc, fyo); const currency = getCurrency(value as Money, field, doc, fyo);
let valueString; let valueString;
try { try {
@ -128,7 +128,20 @@ function getNumberFormatter(fyo: Fyo) {
})); }));
} }
function getCurrency(field: Field, doc: Doc | null, fyo: Fyo): string { function getCurrency(
value: Money,
field: Field,
doc: Doc | null,
fyo: Fyo
): string {
const currency = value?.getCurrency?.();
const defaultCurrency =
fyo.singles.SystemSettings?.currency ?? DEFAULT_CURRENCY;
if (currency && currency !== defaultCurrency) {
return currency;
}
let getCurrency = doc?.getCurrencies?.[field.fieldname]; let getCurrency = doc?.getCurrencies?.[field.fieldname];
if (getCurrency !== undefined) { if (getCurrency !== undefined) {
return getCurrency(); return getCurrency();
@ -139,7 +152,7 @@ function getCurrency(field: Field, doc: Doc | null, fyo: Fyo): string {
return getCurrency(); return getCurrency();
} }
return (fyo.singles.SystemSettings?.currency as string) ?? DEFAULT_CURRENCY; return defaultCurrency;
} }
function getField(df: string | Field): Field { function getField(df: string | Field): Field {

View File

@ -1,11 +1,20 @@
import { DocValue } from 'fyo/core/types'; import { Fyo } from 'fyo';
import { DocValue, DocValueMap } from 'fyo/core/types';
import { Doc } from 'fyo/model/doc'; import { Doc } from 'fyo/model/doc';
import { DefaultMap, FiltersMap, FormulaMap, HiddenMap } from 'fyo/model/types'; import {
CurrenciesMap,
DefaultMap,
FiltersMap,
FormulaMap,
HiddenMap
} from 'fyo/model/types';
import { DEFAULT_CURRENCY } from 'fyo/utils/consts';
import { ValidationError } from 'fyo/utils/errors'; import { ValidationError } from 'fyo/utils/errors';
import { getExchangeRate } from 'models/helpers'; import { getExchangeRate } from 'models/helpers';
import { Transactional } from 'models/Transactional/Transactional'; import { Transactional } from 'models/Transactional/Transactional';
import { ModelNameEnum } from 'models/types'; import { ModelNameEnum } from 'models/types';
import { Money } from 'pesa'; import { Money } from 'pesa';
import { FieldTypeEnum, Schema } from 'schemas/types';
import { getIsNullOrUndef } from 'utils'; import { getIsNullOrUndef } from 'utils';
import { InvoiceItem } from '../InvoiceItem/InvoiceItem'; import { InvoiceItem } from '../InvoiceItem/InvoiceItem';
import { Party } from '../Party/Party'; import { Party } from '../Party/Party';
@ -42,6 +51,23 @@ export abstract class Invoice extends Transactional {
return !!this.fyo.singles?.AccountingSettings?.enableDiscounting; return !!this.fyo.singles?.AccountingSettings?.enableDiscounting;
} }
get isMultiCurrency() {
if (!this.currency) {
return false;
}
return this.fyo.singles.SystemSettings!.currency !== this.currency;
}
get companyCurrency() {
return this.fyo.singles.SystemSettings?.currency ?? DEFAULT_CURRENCY;
}
constructor(schema: Schema, data: DocValueMap, fyo: Fyo) {
super(schema, data, fyo);
this._setGetCurrencies();
}
async validate() { async validate() {
await super.validate(); await super.validate();
if ( if (
@ -126,10 +152,12 @@ export abstract class Invoice extends Transactional {
if (this.currency === currency) { if (this.currency === currency) {
return 1.0; return 1.0;
} }
return await getExchangeRate({ const exchangeRate = await getExchangeRate({
fromCurrency: this.currency!, fromCurrency: this.currency!,
toCurrency: currency as string, toCurrency: currency as string,
}); });
return parseFloat(exchangeRate.toFixed(2));
} }
async getTaxSummary() { async getTaxSummary() {
@ -139,7 +167,6 @@ export abstract class Invoice extends Transactional {
account: string; account: string;
rate: number; rate: number;
amount: Money; amount: Money;
baseAmount: Money;
[key: string]: DocValue; [key: string]: DocValue;
} }
> = {}; > = {};
@ -157,7 +184,6 @@ export abstract class Invoice extends Transactional {
account, account,
rate, rate,
amount: this.fyo.pesa(0), amount: this.fyo.pesa(0),
baseAmount: this.fyo.pesa(0),
}; };
let amount = item.amount!; let amount = item.amount!;
@ -172,9 +198,7 @@ export abstract class Invoice extends Transactional {
return Object.keys(taxes) return Object.keys(taxes)
.map((account) => { .map((account) => {
const tax = taxes[account]; return taxes[account];
tax.baseAmount = tax.amount.mul(this.exchangeRate!);
return tax;
}) })
.filter((tax) => !tax.amount.isZero()); .filter((tax) => !tax.amount.isZero());
} }
@ -285,15 +309,28 @@ export abstract class Invoice extends Transactional {
}, },
dependsOn: ['party'], dependsOn: ['party'],
}, },
exchangeRate: { formula: async () => await this.getExchangeRate() }, exchangeRate: {
netTotal: { formula: async () => this.getSum('items', 'amount', false) }, formula: async () => {
baseNetTotal: { if (
formula: async () => this.netTotal!.mul(this.exchangeRate!), this.currency ===
(this.fyo.singles.SystemSettings?.currency ?? DEFAULT_CURRENCY)
) {
return 1;
}
if (this.exchangeRate && this.exchangeRate !== 1) {
return this.exchangeRate;
}
return await this.getExchangeRate();
}, },
},
netTotal: { formula: async () => this.getSum('items', 'amount', false) },
taxes: { formula: async () => await this.getTaxSummary() }, taxes: { formula: async () => await this.getTaxSummary() },
grandTotal: { formula: async () => await this.getGrandTotal() }, grandTotal: { formula: async () => await this.getGrandTotal() },
baseGrandTotal: { baseGrandTotal: {
formula: async () => (this.grandTotal as Money).mul(this.exchangeRate!), formula: async () =>
(this.grandTotal as Money).mul(this.exchangeRate! ?? 1),
}, },
outstandingAmount: { outstandingAmount: {
formula: async () => { formula: async () => {
@ -345,4 +382,25 @@ export abstract class Invoice extends Transactional {
role: doc.isSales ? 'Customer' : 'Supplier', role: doc.isSales ? 'Customer' : 'Supplier',
}), }),
}; };
getCurrencies: CurrenciesMap = {
baseGrandTotal: () => this.companyCurrency,
outstandingAmount: () => this.companyCurrency,
};
_getCurrency() {
if (this.exchangeRate === 1) {
return this.companyCurrency;
}
return this.currency ?? DEFAULT_CURRENCY;
}
_setGetCurrencies() {
const currencyFields = this.schema.fields.filter(
({ fieldtype }) => fieldtype === FieldTypeEnum.Currency
);
for (const { fieldname } of currencyFields) {
this.getCurrencies[fieldname] ??= this._getCurrency.bind(this);
}
}
} }

View File

@ -1,21 +1,23 @@
import { DocValue } from 'fyo/core/types'; import { Fyo } from 'fyo';
import { DocValue, DocValueMap } from 'fyo/core/types';
import { Doc } from 'fyo/model/doc'; import { Doc } from 'fyo/model/doc';
import { import {
CurrenciesMap,
FiltersMap, FiltersMap,
FormulaMap, FormulaMap,
HiddenMap, HiddenMap,
ValidationMap ValidationMap
} from 'fyo/model/types'; } from 'fyo/model/types';
import { DEFAULT_CURRENCY } from 'fyo/utils/consts';
import { ValidationError } from 'fyo/utils/errors'; import { ValidationError } from 'fyo/utils/errors';
import { ModelNameEnum } from 'models/types'; import { ModelNameEnum } from 'models/types';
import { Money } from 'pesa'; import { Money } from 'pesa';
import { FieldTypeEnum, Schema } from 'schemas/types';
import { Invoice } from '../Invoice/Invoice'; import { Invoice } from '../Invoice/Invoice';
export abstract class InvoiceItem extends Doc { export abstract class InvoiceItem extends Doc {
account?: string; account?: string;
amount?: Money; amount?: Money;
baseAmount?: Money;
exchangeRate?: number;
parentdoc?: Invoice; parentdoc?: Invoice;
rate?: Money; rate?: Money;
quantity?: number; quantity?: number;
@ -39,6 +41,23 @@ export abstract class InvoiceItem extends Doc {
return !!this.fyo.singles?.AccountingSettings?.enableDiscounting; return !!this.fyo.singles?.AccountingSettings?.enableDiscounting;
} }
get currency() {
return this.parentdoc?.currency ?? DEFAULT_CURRENCY;
}
get exchangeRate() {
return this.parentdoc?.exchangeRate ?? 1;
}
get isMultiCurrency() {
return this.parentdoc?.isMultiCurrency ?? false;
}
constructor(schema: Schema, data: DocValueMap, fyo: Fyo) {
super(schema, data, fyo);
this._setGetCurrencies();
}
async getTotalTaxRate(): Promise<number> { async getTotalTaxRate(): Promise<number> {
if (!this.tax) { if (!this.tax) {
return 0; return 0;
@ -73,7 +92,7 @@ export abstract class InvoiceItem extends Doc {
fieldname !== 'itemTaxedTotal' && fieldname !== 'itemTaxedTotal' &&
fieldname !== 'itemDiscountedTotal' fieldname !== 'itemDiscountedTotal'
) { ) {
return rate ?? this.fyo.pesa(0); return rate?.div(this.exchangeRate) ?? this.fyo.pesa(0);
} }
const quantity = this.quantity ?? 0; const quantity = this.quantity ?? 0;
@ -102,17 +121,14 @@ export abstract class InvoiceItem extends Doc {
return rateFromTotals ?? rate ?? this.fyo.pesa(0); return rateFromTotals ?? rate ?? this.fyo.pesa(0);
}, },
dependsOn: [ dependsOn: [
'party',
'exchangeRate',
'item', 'item',
'itemTaxedTotal', 'itemTaxedTotal',
'itemDiscountedTotal', 'itemDiscountedTotal',
'setItemDiscountAmount', 'setItemDiscountAmount',
], ],
}, },
baseRate: {
formula: () =>
(this.rate as Money).mul(this.parentdoc!.exchangeRate as number),
dependsOn: ['item', 'rate'],
},
quantity: { quantity: {
formula: async () => { formula: async () => {
if (!this.item) { if (!this.item) {
@ -157,11 +173,6 @@ export abstract class InvoiceItem extends Doc {
formula: () => (this.rate as Money).mul(this.quantity as number), formula: () => (this.rate as Money).mul(this.quantity as number),
dependsOn: ['item', 'rate', 'quantity'], dependsOn: ['item', 'rate', 'quantity'],
}, },
baseAmount: {
formula: () =>
(this.amount as Money).mul(this.parentdoc!.exchangeRate as number),
dependsOn: ['item', 'amount', 'rate', 'quantity'],
},
hsnCode: { hsnCode: {
formula: async () => formula: async () =>
await this.fyo.getValue('Item', this.item as string, 'hsnCode'), await this.fyo.getValue('Item', this.item as string, 'hsnCode'),
@ -338,6 +349,24 @@ export abstract class InvoiceItem extends Doc {
return { for: doc.isSales ? 'Sales' : 'Purchases' }; return { for: doc.isSales ? 'Sales' : 'Purchases' };
}, },
}; };
getCurrencies: CurrenciesMap = {};
_getCurrency() {
if (this.exchangeRate === 1) {
return this.fyo.singles.SystemSettings?.currency ?? DEFAULT_CURRENCY;
}
return this.currency;
}
_setGetCurrencies() {
const currencyFields = this.schema.fields.filter(
({ fieldtype }) => fieldtype === FieldTypeEnum.Currency
);
for (const { fieldname } of currencyFields) {
this.getCurrencies[fieldname] ??= this._getCurrency.bind(this);
}
}
} }
function getDiscountedTotalBeforeTaxation( function getDiscountedTotalBeforeTaxation(

View File

@ -87,7 +87,11 @@ export class Party extends Doc {
dependsOn: ['role'], dependsOn: ['role'],
}, },
currency: { currency: {
formula: async () => this.fyo.singles.SystemSettings!.currency as string, formula: async () => {
if (!this.currency) {
return this.fyo.singles.SystemSettings!.currency as string;
}
},
}, },
}; };

View File

@ -10,16 +10,17 @@ export class PurchaseInvoice extends Invoice {
items?: PurchaseInvoiceItem[]; items?: PurchaseInvoiceItem[];
async getPosting() { async getPosting() {
const exchangeRate = this.exchangeRate ?? 1;
const posting: LedgerPosting = new LedgerPosting(this, this.fyo); const posting: LedgerPosting = new LedgerPosting(this, this.fyo);
await posting.credit(this.account!, this.baseGrandTotal!); await posting.credit(this.account!, this.baseGrandTotal!);
for (const item of this.items!) { for (const item of this.items!) {
await posting.debit(item.account!, item.baseAmount!); await posting.debit(item.account!, item.amount!.mul(exchangeRate));
} }
if (this.taxes) { if (this.taxes) {
for (const tax of this.taxes) { for (const tax of this.taxes) {
await posting.debit(tax.account!, tax.baseAmount!); await posting.debit(tax.account!, tax.amount!.mul(exchangeRate));
} }
} }
@ -27,7 +28,7 @@ export class PurchaseInvoice extends Invoice {
const discountAccount = this.fyo.singles.AccountingSettings const discountAccount = this.fyo.singles.AccountingSettings
?.discountAccount as string | undefined; ?.discountAccount as string | undefined;
if (discountAccount && discountAmount.isPositive()) { if (discountAccount && discountAmount.isPositive()) {
await posting.credit(discountAccount, discountAmount); await posting.credit(discountAccount, discountAmount.mul(exchangeRate));
} }
await posting.makeRoundOffEntry(); await posting.makeRoundOffEntry();
@ -46,7 +47,7 @@ export class PurchaseInvoice extends Invoice {
getTransactionStatusColumn(), getTransactionStatusColumn(),
'party', 'party',
'date', 'date',
'grandTotal', 'baseGrandTotal',
'outstandingAmount', 'outstandingAmount',
], ],
}; };

View File

@ -10,16 +10,17 @@ export class SalesInvoice extends Invoice {
items?: SalesInvoiceItem[]; items?: SalesInvoiceItem[];
async getPosting() { async getPosting() {
const exchangeRate = this.exchangeRate ?? 1;
const posting: LedgerPosting = new LedgerPosting(this, this.fyo); const posting: LedgerPosting = new LedgerPosting(this, this.fyo);
await posting.debit(this.account!, this.baseGrandTotal!); await posting.debit(this.account!, this.baseGrandTotal!);
for (const item of this.items!) { for (const item of this.items!) {
await posting.credit(item.account!, item.baseAmount!); await posting.credit(item.account!, item.amount!.mul(exchangeRate));
} }
if (this.taxes) { if (this.taxes) {
for (const tax of this.taxes!) { for (const tax of this.taxes!) {
await posting.credit(tax.account!, tax.baseAmount!); await posting.credit(tax.account!, tax.amount!.mul(exchangeRate));
} }
} }
@ -27,7 +28,7 @@ export class SalesInvoice extends Invoice {
const discountAccount = this.fyo.singles.AccountingSettings const discountAccount = this.fyo.singles.AccountingSettings
?.discountAccount as string | undefined; ?.discountAccount as string | undefined;
if (discountAccount && discountAmount.isPositive()) { if (discountAccount && discountAmount.isPositive()) {
await posting.debit(discountAccount, discountAmount); await posting.debit(discountAccount, discountAmount.mul(exchangeRate));
} }
await posting.makeRoundOffEntry(); await posting.makeRoundOffEntry();
@ -46,7 +47,7 @@ export class SalesInvoice extends Invoice {
getTransactionStatusColumn(), getTransactionStatusColumn(),
'party', 'party',
'date', 'date',
'grandTotal', 'baseGrandTotal',
'outstandingAmount', 'outstandingAmount',
], ],
}; };

View File

@ -1,21 +1,48 @@
import { Fyo } from 'fyo';
import { DocValueMap } from 'fyo/core/types';
import { Doc } from 'fyo/model/doc'; import { Doc } from 'fyo/model/doc';
import { FormulaMap } from 'fyo/model/types'; import { CurrenciesMap } from 'fyo/model/types';
import { DEFAULT_CURRENCY } from 'fyo/utils/consts';
import { Money } from 'pesa'; import { Money } from 'pesa';
import { FieldTypeEnum, Schema } from 'schemas/types';
import { Invoice } from '../Invoice/Invoice';
export class TaxSummary extends Doc { export class TaxSummary extends Doc {
account?: string; account?: string;
rate?: number; rate?: number;
amount?: Money; amount?: Money;
baseAmount?: Money; parentdoc?: Invoice;
formulas: FormulaMap = { get exchangeRate() {
baseAmount: { return this.parentdoc?.exchangeRate ?? 1;
formula: async () => { }
const amount = this.amount as Money;
const exchangeRate = (this.parentdoc?.exchangeRate ?? 1) as number; get currency() {
return amount.mul(exchangeRate); return this.parentdoc?.currency ?? DEFAULT_CURRENCY;
}, }
dependsOn: ['amount'],
}, constructor(schema: Schema, data: DocValueMap, fyo: Fyo) {
}; super(schema, data, fyo);
this._setGetCurrencies();
}
getCurrencies: CurrenciesMap = {};
_getCurrency() {
if (this.exchangeRate === 1) {
return this.fyo.singles.SystemSettings?.currency ?? DEFAULT_CURRENCY;
}
return this.currency;
}
_setGetCurrencies() {
const currencyFields = this.schema.fields.filter(
({ fieldtype }) => fieldtype === FieldTypeEnum.Currency
);
const getCurrency = this._getCurrency.bind(this);
for (const { fieldname } of currencyFields) {
this.getCurrencies[fieldname] ??= getCurrency;
}
}
} }

View File

@ -1,13 +1,12 @@
import { Fyo, t } from 'fyo'; import { Fyo, t } from 'fyo';
import { Doc } from 'fyo/model/doc'; import { Doc } from 'fyo/model/doc';
import { Action, ColumnConfig, DocStatus, RenderData } from 'fyo/model/types'; import { Action, ColumnConfig, DocStatus, RenderData } from 'fyo/model/types';
import { NotFoundError } from 'fyo/utils/errors';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { Money } from 'pesa'; import { Money } from 'pesa';
import { Router } from 'vue-router'; import { Router } from 'vue-router';
import { import {
AccountRootType, AccountRootType,
AccountRootTypeEnum, AccountRootTypeEnum
} from './baseModels/Account/types'; } from './baseModels/Account/types';
import { InvoiceStatus, ModelNameEnum } from './types'; import { InvoiceStatus, ModelNameEnum } from './types';
@ -196,14 +195,12 @@ export async function getExchangeRate({
toCurrency: string; toCurrency: string;
date?: string; date?: string;
}) { }) {
if (!date) { if (!fetch) {
date = DateTime.local().toISODate(); return 1;
} }
if (!fromCurrency || !toCurrency) { if (!date) {
throw new NotFoundError( date = DateTime.local().toISODate();
'Please provide `fromCurrency` and `toCurrency` to get exchange rate.'
);
} }
const cacheKey = `currencyExchangeRate:${date}:${fromCurrency}:${toCurrency}`; const cacheKey = `currencyExchangeRate:${date}:${fromCurrency}:${toCurrency}`;
@ -215,27 +212,24 @@ export async function getExchangeRate({
); );
} }
if (!exchangeRate && fetch) { if (exchangeRate && exchangeRate !== 1) {
return exchangeRate;
}
try { try {
const res = await fetch( const res = await fetch(
`https://api.vatcomply.com/rates?date=${date}&base=${fromCurrency}&symbols=${toCurrency}` `https://api.vatcomply.com/rates?date=${date}&base=${fromCurrency}&symbols=${toCurrency}`
); );
const data = await res.json(); const data = await res.json();
exchangeRate = data.rates[toCurrency]; exchangeRate = data.rates[toCurrency];
} catch (error) {
console.error(error);
exchangeRate ??= 1;
}
if (localStorage) { if (localStorage) {
localStorage.setItem(cacheKey, String(exchangeRate)); localStorage.setItem(cacheKey, String(exchangeRate));
} }
} catch (error) {
console.error(error);
throw new NotFoundError(
`Could not fetch exchange rate for ${fromCurrency} -> ${toCurrency}`,
false
);
}
} else {
exchangeRate = 1;
}
return exchangeRate; return exchangeRate;
} }
@ -256,3 +250,6 @@ export function isCredit(rootType: AccountRootType) {
return true; return true;
} }
} }
// @ts-ignore
window.gex = getExchangeRate;

View File

@ -218,6 +218,11 @@ async function generateB2bData(report: BaseGSTR): Promise<B2BCustomer[]> {
? ModelNameEnum.SalesInvoiceItem ? ModelNameEnum.SalesInvoiceItem
: ModelNameEnum.PurchaseInvoiceItem; : ModelNameEnum.PurchaseInvoiceItem;
const parentSchemaName =
report.gstrType === 'GSTR-1'
? ModelNameEnum.SalesInvoice
: ModelNameEnum.PurchaseInvoice;
for (const row of report.gstrRows ?? []) { for (const row of report.gstrRows ?? []) {
const invRecord: B2BInvRecord = { const invRecord: B2BInvRecord = {
inum: row.invNo, inum: row.invNo,
@ -229,20 +234,29 @@ async function generateB2bData(report: BaseGSTR): Promise<B2BCustomer[]> {
itms: [], itms: [],
}; };
const exchangeRate = (
await fyo.db.getAllRaw(parentSchemaName, {
fields: ['exchangeRate'],
filters: { name: invRecord.inum },
})
)[0].exchangeRate as number;
const items = await fyo.db.getAllRaw(schemaName, { const items = await fyo.db.getAllRaw(schemaName, {
fields: ['baseAmount', 'tax', 'hsnCode'], fields: ['amount', 'tax', 'hsnCode'],
filters: { parent: invRecord.inum as string }, filters: { parent: invRecord.inum },
}); });
items.forEach((item) => { items.forEach((item) => {
const hsnCode = item.hsnCode as number; const hsnCode = item.hsnCode as number;
const tax = item.tax as string; const tax = item.tax as string;
const baseAmount = (item.baseAmount ?? 0) as string; const baseAmount = fyo
.pesa((item.amount as string) ?? 0)
.mul(exchangeRate);
const itemRecord: B2BItmRecord = { const itemRecord: B2BItmRecord = {
num: hsnCode, num: hsnCode,
itm_det: { itm_det: {
txval: fyo.pesa(baseAmount).float, txval: baseAmount.float,
rt: GST[tax], rt: GST[tax],
csamt: 0, csamt: 0,
camt: fyo camt: fyo
@ -292,6 +306,11 @@ async function generateB2clData(
? ModelNameEnum.SalesInvoiceItem ? ModelNameEnum.SalesInvoiceItem
: ModelNameEnum.PurchaseInvoiceItem; : ModelNameEnum.PurchaseInvoiceItem;
const parentSchemaName =
report.gstrType === 'GSTR-1'
? ModelNameEnum.SalesInvoice
: ModelNameEnum.PurchaseInvoice;
for (const row of report.gstrRows ?? []) { for (const row of report.gstrRows ?? []) {
const invRecord: B2CLInvRecord = { const invRecord: B2CLInvRecord = {
inum: row.invNo, inum: row.invNo,
@ -300,20 +319,29 @@ async function generateB2clData(
itms: [], itms: [],
}; };
const exchangeRate = (
await fyo.db.getAllRaw(parentSchemaName, {
fields: ['exchangeRate'],
filters: { name: invRecord.inum },
})
)[0].exchangeRate as number;
const items = await fyo.db.getAllRaw(schemaName, { const items = await fyo.db.getAllRaw(schemaName, {
fields: ['hsnCode', 'tax', 'baseAmount'], fields: ['amount', 'tax', 'hsnCode'],
filters: { parent: invRecord.inum }, filters: { parent: invRecord.inum },
}); });
items.forEach((item) => { items.forEach((item) => {
const hsnCode = item.hsnCode as number; const hsnCode = item.hsnCode as number;
const tax = item.tax as string; const tax = item.tax as string;
const baseAmount = (item.baseAmount ?? 0) as string; const baseAmount = fyo
.pesa((item.amount as string) ?? 0)
.mul(exchangeRate);
const itemRecord: B2CLItmRecord = { const itemRecord: B2CLItmRecord = {
num: hsnCode, num: hsnCode,
itm_det: { itm_det: {
txval: fyo.pesa(baseAmount).float, txval: baseAmount.float,
rt: GST[tax] ?? 0, rt: GST[tax] ?? 0,
csamt: 0, csamt: 0,
iamt: fyo iamt: fyo

131
schemas/app/Invoice.json Normal file
View File

@ -0,0 +1,131 @@
{
"name": "Invoice",
"label": "Invoice",
"isAbstract": true,
"isSingle": false,
"isChild": false,
"isSubmittable": true,
"fields": [
{
"label": "Invoice No",
"fieldname": "name",
"fieldtype": "Data",
"required": true,
"readOnly": true
},
{
"fieldname": "date",
"label": "Date",
"fieldtype": "Date",
"required": true
},
{
"fieldname": "party",
"label": "Party",
"fieldtype": "Link",
"target": "Party",
"create": true,
"required": true
},
{
"fieldname": "account",
"label": "Account",
"fieldtype": "Link",
"target": "Account",
"create": true,
"required": true
},
{
"fieldname": "currency",
"label": "Customer Currency",
"fieldtype": "Link",
"target": "Currency"
},
{
"fieldname": "exchangeRate",
"label": "Exchange Rate",
"fieldtype": "Float",
"default": 1,
"readOnly": true
},
{
"fieldname": "netTotal",
"label": "Net Total",
"fieldtype": "Currency",
"readOnly": true
},
{
"fieldname": "taxes",
"label": "Taxes",
"fieldtype": "Table",
"target": "TaxSummary",
"readOnly": true
},
{
"fieldname": "grandTotal",
"label": "Grand Total",
"fieldtype": "Currency",
"readOnly": true
},
{
"fieldname": "baseGrandTotal",
"label": "Base Grand Total",
"fieldtype": "Currency",
"readOnly": true
},
{
"fieldname": "outstandingAmount",
"label": "Outstanding Amount",
"fieldtype": "Currency",
"readOnly": true
},
{
"fieldname": "setDiscountAmount",
"label": "Set Discount Amount",
"fieldtype": "Check",
"default": false
},
{
"fieldname": "discountAmount",
"label": "Discount Amount",
"fieldtype": "Currency",
"readOnly": false
},
{
"fieldname": "discountPercent",
"label": "Discount Percent",
"fieldtype": "Float",
"readOnly": false
},
{
"fieldname": "discountAfterTax",
"label": "Discount After Tax",
"fieldtype": "Check",
"default": false,
"readOnly": false
},
{
"fieldname": "entryCurrency",
"label": "Entry Currency",
"fieldtype": "Select",
"options": [
{
"value": "Party",
"label": "Party"
},
{
"value": "Company",
"label": "Company"
}
],
"default": "Party"
},
{
"fieldname": "terms",
"label": "Notes",
"placeholder": "Add invoice terms",
"fieldtype": "Text"
}
],
"keywordFields": ["name", "party"]
}

View File

@ -0,0 +1,109 @@
{
"name": "InvoiceItem",
"label": "Invoice Item",
"isAbstract": true,
"isChild": true,
"fields": [
{
"fieldname": "item",
"label": "Item",
"fieldtype": "Link",
"target": "Item",
"create": true,
"required": true
},
{
"fieldname": "description",
"label": "Description",
"fieldtype": "Text"
},
{
"fieldname": "quantity",
"label": "Quantity",
"fieldtype": "Float",
"required": true,
"default": 1
},
{
"fieldname": "rate",
"label": "Rate",
"fieldtype": "Currency",
"required": true
},
{
"fieldname": "account",
"label": "Account",
"fieldtype": "Link",
"target": "Account",
"required": true
},
{
"fieldname": "tax",
"label": "Tax",
"fieldtype": "Link",
"create": true,
"target": "Tax"
},
{
"fieldname": "amount",
"label": "Amount",
"fieldtype": "Currency",
"readOnly": true
},
{
"fieldname": "setItemDiscountAmount",
"label": "Set Discount Amount",
"fieldtype": "Check",
"default": false
},
{
"fieldname": "itemDiscountAmount",
"label": "Discount Amount",
"fieldtype": "Currency",
"readOnly": false
},
{
"fieldname": "itemDiscountPercent",
"label": "Discount Percent",
"fieldtype": "Float",
"readOnly": false
},
{
"fieldname": "itemDiscountedTotal",
"label": "Discounted Amount",
"fieldtype": "Currency",
"readOnly": false,
"computed": true
},
{
"fieldname": "itemTaxedTotal",
"label": "Taxed Amount",
"fieldtype": "Currency",
"readOnly": false,
"computed": true
},
{
"fieldname": "hsnCode",
"label": "HSN/SAC",
"fieldtype": "Int",
"placeholder": "HSN/SAC Code"
}
],
"tableFields": ["item", "tax", "quantity", "rate", "amount"],
"keywordFields": ["item", "tax"],
"quickEditFields": [
"item",
"account",
"description",
"hsnCode",
"tax",
"quantity",
"rate",
"amount",
"setItemDiscountAmount",
"itemDiscountAmount",
"itemDiscountPercent",
"itemDiscountedTotal",
"itemTaxedTotal"
]
}

View File

@ -1,54 +1,10 @@
{ {
"name": "PurchaseInvoice", "name": "PurchaseInvoice",
"label": "Purchase Invoice", "label": "Purchase Invoice",
"extends": "Invoice",
"naming": "numberSeries", "naming": "numberSeries",
"isSingle": false,
"isChild": false,
"isSubmittable": true,
"showTitle": true, "showTitle": true,
"fields": [ "fields": [
{
"label": "Bill No",
"fieldname": "name",
"fieldtype": "Data",
"required": true,
"readOnly": true
},
{
"fieldname": "date",
"label": "Date",
"fieldtype": "Date",
"required": true
},
{
"fieldname": "party",
"label": "Party",
"fieldtype": "Link",
"target": "Party",
"create": true,
"required": true
},
{
"fieldname": "account",
"label": "Account",
"fieldtype": "Link",
"target": "Account",
"create": true,
"required": true
},
{
"fieldname": "currency",
"label": "Supplier Currency",
"fieldtype": "Link",
"target": "Currency",
"hidden": true
},
{
"fieldname": "exchangeRate",
"label": "Exchange Rate",
"fieldtype": "Float",
"default": 1
},
{ {
"fieldname": "items", "fieldname": "items",
"label": "Items", "label": "Items",
@ -57,75 +13,6 @@
"required": true, "required": true,
"edit": true "edit": true
}, },
{
"fieldname": "netTotal",
"label": "Net Total",
"fieldtype": "Currency",
"readOnly": true
},
{
"fieldname": "baseNetTotal",
"label": "Net Total (Company Currency)",
"fieldtype": "Currency",
"readOnly": true
},
{
"fieldname": "taxes",
"label": "Taxes",
"fieldtype": "Table",
"target": "TaxSummary",
"readOnly": true
},
{
"fieldname": "grandTotal",
"label": "Grand Total",
"fieldtype": "Currency",
"readOnly": true
},
{
"fieldname": "baseGrandTotal",
"label": "Grand Total (Company Currency)",
"fieldtype": "Currency",
"readOnly": true
},
{
"fieldname": "outstandingAmount",
"label": "Outstanding Amount",
"fieldtype": "Currency",
"readOnly": true,
"filter": true
},
{
"fieldname": "setDiscountAmount",
"label": "Set Discount Amount",
"fieldtype": "Check",
"default": false
},
{
"fieldname": "discountAmount",
"label": "Discount Amount",
"fieldtype": "Currency",
"readOnly": false
},
{
"fieldname": "discountPercent",
"label": "Discount Percent",
"fieldtype": "Float",
"readOnly": false
},
{
"fieldname": "discountAfterTax",
"label": "Discount After Tax",
"fieldtype": "Check",
"default": false,
"readOnly": false
},
{
"fieldname": "terms",
"label": "Terms",
"placeholder": "Add invoice terms",
"fieldtype": "Text"
},
{ {
"fieldname": "numberSeries", "fieldname": "numberSeries",
"label": "Number Series", "label": "Number Series",

View File

@ -1,124 +1,5 @@
{ {
"name": "PurchaseInvoiceItem", "name": "PurchaseInvoiceItem",
"label": "Purchase Invoice Item", "label": "Purchase Invoice Item",
"isChild": true, "extends": "InvoiceItem"
"fields": [
{
"fieldname": "item",
"label": "Item",
"fieldtype": "Link",
"target": "Item",
"create": true,
"required": true
},
{
"fieldname": "description",
"label": "Description",
"fieldtype": "Text",
"hidden": true
},
{
"fieldname": "quantity",
"label": "Quantity",
"fieldtype": "Float",
"required": true,
"default": 1
},
{
"fieldname": "rate",
"label": "Rate",
"fieldtype": "Currency",
"required": true
},
{
"fieldname": "baseRate",
"label": "Rate (Company Currency)",
"fieldtype": "Currency",
"readOnly": true
},
{
"fieldname": "account",
"label": "Account",
"fieldtype": "Link",
"target": "Account",
"create": true,
"required": true,
"readOnly": true
},
{
"fieldname": "tax",
"label": "Tax",
"fieldtype": "Link",
"create": true,
"target": "Tax"
},
{
"fieldname": "amount",
"label": "Amount",
"fieldtype": "Currency",
"readOnly": true
},
{
"fieldname": "baseAmount",
"label": "Amount (Company Currency)",
"fieldtype": "Currency",
"readOnly": true
},
{
"fieldname": "setItemDiscountAmount",
"label": "Set Discount Amount",
"fieldtype": "Check",
"default": false
},
{
"fieldname": "itemDiscountAmount",
"label": "Discount Amount",
"fieldtype": "Currency",
"readOnly": false
},
{
"fieldname": "itemDiscountPercent",
"label": "Discount Percent",
"fieldtype": "Float",
"readOnly": false
},
{
"fieldname": "itemDiscountedTotal",
"label": "Discounted Amount",
"fieldtype": "Currency",
"readOnly": false,
"computed": true
},
{
"fieldname": "itemTaxedTotal",
"label": "Taxed Amount",
"fieldtype": "Currency",
"readOnly": false,
"computed": true
},
{
"fieldname": "hsnCode",
"label": "HSN/SAC",
"fieldtype": "Int",
"placeholder": "HSN/SAC Code",
"hidden": true
}
],
"tableFields": ["item", "tax", "quantity", "rate", "amount"],
"keywordFields": ["item", "tax"],
"quickEditFields": [
"item",
"account",
"description",
"hsnCode",
"tax",
"quantity",
"rate",
"amount",
"setItemDiscountAmount",
"itemDiscountAmount",
"itemDiscountPercent",
"itemDiscountedTotal",
"itemTaxedTotal"
]
} }

View File

@ -1,53 +1,10 @@
{ {
"name": "SalesInvoice", "name": "SalesInvoice",
"label": "Sales Invoice", "label": "Sales Invoice",
"extends": "Invoice",
"naming": "numberSeries", "naming": "numberSeries",
"isSingle": false, "showTitle": true,
"isChild": false,
"isSubmittable": true,
"fields": [ "fields": [
{
"label": "Invoice No",
"fieldname": "name",
"fieldtype": "Data",
"required": true,
"readOnly": true
},
{
"fieldname": "date",
"label": "Date",
"fieldtype": "Date",
"required": true
},
{
"fieldname": "party",
"label": "Party",
"fieldtype": "Link",
"target": "Party",
"create": true,
"required": true
},
{
"fieldname": "account",
"label": "Account",
"fieldtype": "Link",
"target": "Account",
"create": true,
"required": true
},
{
"fieldname": "currency",
"label": "Customer Currency",
"fieldtype": "Link",
"target": "Currency"
},
{
"fieldname": "exchangeRate",
"label": "Exchange Rate",
"fieldtype": "Float",
"default": 1,
"readOnly": true
},
{ {
"fieldname": "items", "fieldname": "items",
"label": "Items", "label": "Items",
@ -56,75 +13,6 @@
"required": true, "required": true,
"edit": true "edit": true
}, },
{
"fieldname": "netTotal",
"label": "Net Total",
"fieldtype": "Currency",
"readOnly": true
},
{
"fieldname": "baseNetTotal",
"label": "Net Total (Company Currency)",
"fieldtype": "Currency",
"readOnly": true
},
{
"fieldname": "taxes",
"label": "Taxes",
"fieldtype": "Table",
"target": "TaxSummary",
"readOnly": true
},
{
"fieldname": "grandTotal",
"label": "Grand Total",
"fieldtype": "Currency",
"readOnly": true
},
{
"fieldname": "baseGrandTotal",
"label": "Grand Total (Company Currency)",
"fieldtype": "Currency",
"readOnly": true
},
{
"fieldname": "outstandingAmount",
"label": "Outstanding Amount",
"fieldtype": "Currency",
"readOnly": true,
"filter": true
},
{
"fieldname": "setDiscountAmount",
"label": "Set Discount Amount",
"fieldtype": "Check",
"default": false
},
{
"fieldname": "discountAmount",
"label": "Discount Amount",
"fieldtype": "Currency",
"readOnly": false
},
{
"fieldname": "discountPercent",
"label": "Discount Percent",
"fieldtype": "Float",
"readOnly": false
},
{
"fieldname": "discountAfterTax",
"label": "Discount After Tax",
"fieldtype": "Check",
"default": false,
"readOnly": false
},
{
"fieldname": "terms",
"label": "Notes",
"placeholder": "Add invoice terms",
"fieldtype": "Text"
},
{ {
"fieldname": "numberSeries", "fieldname": "numberSeries",
"label": "Number Series", "label": "Number Series",

View File

@ -1,120 +1,5 @@
{ {
"name": "SalesInvoiceItem", "name": "SalesInvoiceItem",
"label": "Sales Invoice Item", "label": "Sales Invoice Item",
"isChild": true, "extends": "InvoiceItem"
"fields": [
{
"fieldname": "item",
"label": "Item",
"fieldtype": "Link",
"target": "Item",
"create": true,
"required": true
},
{
"fieldname": "description",
"label": "Description",
"fieldtype": "Text"
},
{
"fieldname": "quantity",
"label": "Quantity",
"fieldtype": "Float",
"required": true,
"default": 1
},
{
"fieldname": "rate",
"label": "Rate",
"fieldtype": "Currency",
"required": true
},
{
"fieldname": "baseRate",
"label": "Rate (Company Currency)",
"fieldtype": "Currency",
"readOnly": true
},
{
"fieldname": "account",
"label": "Account",
"fieldtype": "Link",
"target": "Account",
"required": true
},
{
"fieldname": "tax",
"label": "Tax",
"fieldtype": "Link",
"create": true,
"target": "Tax"
},
{
"fieldname": "amount",
"label": "Amount",
"fieldtype": "Currency",
"readOnly": true
},
{
"fieldname": "baseAmount",
"label": "Amount (Company Currency)",
"fieldtype": "Currency",
"readOnly": true
},
{
"fieldname": "setItemDiscountAmount",
"label": "Set Discount Amount",
"fieldtype": "Check",
"default": false
},
{
"fieldname": "itemDiscountAmount",
"label": "Discount Amount",
"fieldtype": "Currency",
"readOnly": false
},
{
"fieldname": "itemDiscountPercent",
"label": "Discount Percent",
"fieldtype": "Float",
"readOnly": false
},
{
"fieldname": "itemDiscountedTotal",
"label": "Discounted Amount",
"fieldtype": "Currency",
"readOnly": false,
"computed": true
},
{
"fieldname": "itemTaxedTotal",
"label": "Taxed Amount",
"fieldtype": "Currency",
"readOnly": false,
"computed": true
},
{
"fieldname": "hsnCode",
"label": "HSN/SAC",
"fieldtype": "Int",
"placeholder": "HSN/SAC Code"
}
],
"tableFields": ["item", "tax", "quantity", "rate", "amount"],
"keywordFields": ["item", "tax"],
"quickEditFields": [
"item",
"account",
"description",
"hsnCode",
"tax",
"quantity",
"rate",
"amount",
"setItemDiscountAmount",
"itemDiscountAmount",
"itemDiscountPercent",
"itemDiscountedTotal",
"itemTaxedTotal"
]
} }

View File

@ -21,12 +21,6 @@
"label": "Amount", "label": "Amount",
"fieldtype": "Currency", "fieldtype": "Currency",
"required": true "required": true
},
{
"fieldname": "baseAmount",
"label": "Amount (Company Currency)",
"fieldtype": "Currency",
"readOnly": true
} }
] ]
} }

View File

@ -88,6 +88,7 @@
"label": "Currency", "label": "Currency",
"fieldtype": "AutoComplete", "fieldtype": "AutoComplete",
"default": "INR", "default": "INR",
"readOnly": true,
"required": true "required": true
}, },
{ {

View File

@ -7,6 +7,8 @@ import Color from './app/Color.json';
import CompanySettings from './app/CompanySettings.json'; import CompanySettings from './app/CompanySettings.json';
import Currency from './app/Currency.json'; import Currency from './app/Currency.json';
import GetStarted from './app/GetStarted.json'; import GetStarted from './app/GetStarted.json';
import Invoice from './app/Invoice.json';
import InvoiceItem from './app/InvoiceItem.json';
import Item from './app/Item.json'; import Item from './app/Item.json';
import JournalEntry from './app/JournalEntry.json'; import JournalEntry from './app/JournalEntry.json';
import JournalEntryAccount from './app/JournalEntryAccount.json'; import JournalEntryAccount from './app/JournalEntryAccount.json';
@ -73,11 +75,13 @@ export const appSchemas: Schema[] | SchemaStub[] = [
JournalEntry as Schema, JournalEntry as Schema,
JournalEntryAccount as Schema, JournalEntryAccount as Schema,
PurchaseInvoice as Schema, Invoice as Schema,
PurchaseInvoiceItem as Schema,
SalesInvoice as Schema, SalesInvoice as Schema,
SalesInvoiceItem as Schema, PurchaseInvoice as Schema,
InvoiceItem as Schema,
SalesInvoiceItem as SchemaStub,
PurchaseInvoiceItem as SchemaStub,
Tax as Schema, Tax as Schema,
TaxDetail as Schema, TaxDetail as Schema,

View File

@ -151,13 +151,16 @@ export default {
}, },
methods: { methods: {
getInputClassesFromProp(classes) { getInputClassesFromProp(classes) {
if (this.inputClass) { if (!this.inputClass) {
return classes;
}
if (typeof this.inputClass === 'function') { if (typeof this.inputClass === 'function') {
classes = this.inputClass(classes); classes = this.inputClass(classes);
} else { } else {
classes.push(this.inputClass); classes.push(this.inputClass);
} }
}
return classes; return classes;
}, },
focus() { focus() {

View File

@ -0,0 +1,102 @@
<template>
<div class="flex items-center bg-gray-100 rounded-md textsm px-1">
<div
class="rate-container"
:class="disabled ? 'bg-gray-100' : 'bg-gray-25'"
>
<input type="number" v-model="fromValue" :disabled="disabled" min="0" />
<p>{{ left }}</p>
</div>
<p class="mx-1 text-gray-600">=</p>
<div
class="rate-container"
:class="disabled ? 'bg-gray-100' : 'bg-gray-25'"
>
<input
type="number"
ref="toValue"
:value="isSwapped ? fromValue / exchangeRate : exchangeRate * fromValue"
:disabled="disabled"
min="0"
@change="rightChange"
/>
<p>{{ right }}</p>
</div>
<button
class="bg-green100 px-2 ml-1 -mr-0.5 h-full border-l"
@click="swap"
v-if="!disabled"
>
<feather-icon name="refresh-cw" class="w-3 h-3 text-gray-600" />
</button>
</div>
</template>
<script>
import { defineComponent } from 'vue';
export default defineComponent({
emits: ['change'],
props: {
disabled: { type: Boolean, default: false },
fromCurrency: { type: String, default: 'USD' },
toCurrency: { type: String, default: 'INR' },
exchangeRate: { type: Number, default: 75 },
},
data() {
return { fromValue: 1, isSwapped: false };
},
methods: {
swap() {
this.isSwapped = !this.isSwapped;
},
rightChange(e) {
let value = this.$refs.toValue.value;
if (e) {
value = e.target.value;
}
value = parseFloat(value);
let exchangeRate = value / this.fromValue;
if (this.isSwapped) {
exchangeRate = this.fromValue / value;
}
this.$emit('change', exchangeRate);
},
},
computed: {
left() {
if (this.isSwapped) {
return this.toCurrency;
}
return this.fromCurrency;
},
right() {
if (this.isSwapped) {
return this.fromCurrency;
}
return this.toCurrency;
},
},
});
</script>
<style scoped>
input[type='number'] {
@apply w-12 outline-none bg-transparent p-0.5;
}
.rate-container {
@apply flex items-center rounded-md border border-gray-100 text-gray-900
text-sm outline-none focus-within:bg-gray-50 px-1 focus-within:border-gray-200;
}
.rate-container > p {
@apply text-xs text-gray-600;
}
</style>

View File

@ -1,15 +1,8 @@
<script> <script>
import { fyo } from 'src/initFyo';
export default { export default {
name: 'Base', name: 'Base',
props: { doc: Object, printSettings: Object }, props: { doc: Object, printSettings: Object },
data: () => ({ party: null, companyAddress: null, partyAddress: null }), data: () => ({ party: null, companyAddress: null, partyAddress: null }),
methods: {
format(row, fieldname) {
const value = row.get(fieldname);
return fyo.format(value, fyo.getField(row.schemaName, fieldname));
},
},
async mounted() { async mounted() {
await this.printSettings.loadLink('address'); await this.printSettings.loadLink('address');
this.companyAddress = this.printSettings.getLink('address'); this.companyAddress = this.printSettings.getLink('address');
@ -17,8 +10,49 @@ export default {
await this.doc.loadLink('party'); await this.doc.loadLink('party');
this.party = this.doc.getLink('party'); this.party = this.doc.getLink('party');
this.partyAddress = this.party.getLink('address')?.addressDisplay ?? null; 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: { computed: {
currency() {
return this.doc.isMultiCurrency
? this.doc.currency
: this.fyo.singles.SystemSettings.currency;
},
isSalesInvoice() { isSalesInvoice() {
return this.doc.schemaName === 'SalesInvoice'; return this.doc.schemaName === 'SalesInvoice';
}, },
@ -28,6 +62,46 @@ export default {
totalDiscount() { totalDiscount() {
return this.doc.getTotalDiscount(); 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,
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> </script>

View File

@ -1,36 +1,31 @@
<template> <template>
<div <div
class="bg-white border h-full" class="bg-white border h-full"
:style="{ 'font-family': printSettings.font }" :style="{ 'font-family': printObject.font }"
> >
<div> <div>
<div class="px-6 pt-6" v-if="printSettings"> <div class="px-6 pt-6">
<div class="flex text-sm text-gray-900 border-b pb-4"> <div class="flex text-sm text-gray-900 border-b pb-4">
<div class="w-1/3"> <div class="w-1/3">
<div v-if="printSettings.displayLogo"> <div v-if="printObject.displayLogo">
<img <img
class="h-12 max-w-32 object-contain" class="h-12 max-w-32 object-contain"
:src="printSettings.logo" :src="printObject.logo"
/> />
</div> </div>
<div class="text-xl text-gray-700 font-semibold" v-else> <div class="text-xl text-gray-700 font-semibold" v-else>
{{ fyo.singles.AccountingSettings.companyName }} {{ printObject.companyName }}
</div> </div>
</div> </div>
<div class="w-1/3"> <div class="w-1/3">
<div>{{ printSettings.email }}</div> <div>{{ printObject.email }}</div>
<div class="mt-1">{{ printSettings.phone }}</div> <div class="mt-1">{{ printObject.phone }}</div>
</div> </div>
<div class="w-1/3"> <div class="w-1/3">
<div v-if="companyAddress">{{ companyAddress.addressDisplay }}</div> <div v-if="printObject.address">
<div {{ printObject.address }}
v-if="
fyo.singles.AccountingSettings &&
fyo.singles.AccountingSettings.gstin
"
>
GSTIN: {{ fyo.singles.AccountingSettings.gstin }}
</div> </div>
<div v-if="printObject.gstin">GSTIN: {{ printObject.gstin }}</div>
</div> </div>
</div> </div>
</div> </div>
@ -38,27 +33,27 @@
<div class="flex justify-between"> <div class="flex justify-between">
<div class="w-1/3"> <div class="w-1/3">
<h1 class="text-2xl font-semibold"> <h1 class="text-2xl font-semibold">
{{ doc.name }} {{ printObject.invoiceName }}
</h1> </h1>
<div class="py-2 text-base"> <div class="py-2 text-base">
{{ fyo.format(doc.date, 'Date') }} {{ printObject.date }}
</div> </div>
</div> </div>
<div class="w-1/3" v-if="party"> <div class="w-1/3" v-if="printObject.partyName">
<div class="py-1 text-right text-lg font-semibold"> <div class="py-1 text-right text-lg font-semibold">
{{ party.name }} {{ printObject.partyName }}
</div> </div>
<div <div
v-if="partyAddress" v-if="printObject.partyAddress"
class="mt-1 text-xs text-gray-600 text-right" class="mt-1 text-xs text-gray-600 text-right"
> >
{{ partyAddress }} {{ printObject.partyAddress }}
</div> </div>
<div <div
v-if="party && party.gstin" v-if="printObject.partyGSTIN"
class="mt-1 text-xs text-gray-600 text-right" class="mt-1 text-xs text-gray-600 text-right"
> >
GSTIN: {{ party.gstin }} GSTIN: {{ printObject.partyGSTIN }}
</div> </div>
</div> </div>
</div> </div>
@ -67,64 +62,67 @@
<div> <div>
<div class="text-gray-600 w-full flex border-b"> <div class="text-gray-600 w-full flex border-b">
<div class="py-4 w-5/12">Item</div> <div class="py-4 w-5/12">Item</div>
<div class="py-4 text-right w-2/12" v-if="showHSN">HSN/SAC</div> <div class="py-4 text-right w-2/12" v-if="printObject.showHSN">
HSN/SAC
</div>
<div class="py-4 text-right w-1/12">Quantity</div> <div class="py-4 text-right w-1/12">Quantity</div>
<div class="py-4 text-right w-3/12">Rate</div> <div class="py-4 text-right w-3/12">Rate</div>
<div class="py-4 text-right w-3/12">Amount</div> <div class="py-4 text-right w-3/12">Amount</div>
</div> </div>
<div <div
class="flex py-1 text-gray-900 w-full border-b" class="flex py-1 text-gray-900 w-full border-b"
v-for="row in doc.items" v-for="row in printObject.items"
:key="row.name" :key="row.name"
> >
<div class="w-5/12 py-4">{{ row.item }}</div> <div class="w-5/12 py-4">{{ row.item }}</div>
<div class="w-2/12 text-right py-4" v-if="showHSN"> <div class="w-2/12 text-right py-4" v-if="printObject.showHSN">
{{ row.hsnCode }} {{ row.hsnCode }}
</div> </div>
<div class="w-1/12 text-right py-4"> <div class="w-1/12 text-right py-4">{{ row.quantity }}</div>
{{ format(row, 'quantity') }} <div class="w-3/12 text-right py-4">{{ row.rate }}</div>
</div> <div class="w-3/12 text-right py-4">{{ row.amount }}</div>
<div class="w-3/12 text-right py-4">{{ format(row, 'rate') }}</div>
<div class="w-3/12 text-right py-4">
{{ format(row, 'amount') }}
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="px-6 mt-2 flex justify-end text-base"> <div class="px-6 mt-2 flex justify-end text-base">
<div class="w-1/2 bg-pink"> <div class="w-1/2">
<div class="text-sm tracking-widest text-gray-600 mt-2">Notes</div> <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"> <div class="my-4 text-lg whitespace-pre-line">
{{ doc.terms }} {{ printObject.terms }}
</div> </div>
</div> </div>
<div class="w-1/2"> <div class="w-1/2">
<div class="flex pl-2 justify-between py-3 border-b"> <div class="flex pl-2 justify-between py-3 border-b">
<div>{{ t`Subtotal` }}</div> <div>{{ t`Subtotal` }}</div>
<div>{{ fyo.format(doc.netTotal, 'Currency') }}</div> <div>{{ printObject.netTotal }}</div>
</div> </div>
<div <div
class="flex pl-2 justify-between py-3 border-b" class="flex pl-2 justify-between py-3 border-b"
v-if="totalDiscount?.float > 0 && !doc.discountAfterTax" v-if="printObject.totalDiscount && !printObject.discountAfterTax"
> >
<div>{{ t`Discount` }}</div> <div>{{ t`Discount` }}</div>
<div>{{ `- ${fyo.format(totalDiscount, 'Currency')}` }}</div> <div>{{ printObject.totalDiscount }}</div>
</div> </div>
<div <div
class="flex pl-2 justify-between py-3" class="flex pl-2 justify-between py-3"
v-for="tax in doc.taxes" v-for="tax in printObject.taxes"
:key="tax.name" :key="tax.name"
> >
<div>{{ tax.account }}</div> <div>{{ tax.account }}</div>
<div>{{ fyo.format(tax.amount, 'Currency') }}</div> <div>{{ tax.amount }}</div>
</div> </div>
<div <div
class="flex pl-2 justify-between py-3 border-t" class="flex pl-2 justify-between py-3 border-t"
v-if="totalDiscount?.float > 0 && doc.discountAfterTax" v-if="printObject.totalDiscount && printObject.discountAfterTax"
> >
<div>{{ t`Discount` }}</div> <div>{{ t`Discount` }}</div>
<div>{{ `- ${fyo.format(totalDiscount, 'Currency')}` }}</div> <div>{{ printObject.totalDiscount }}</div>
</div> </div>
<div <div
class=" class="
@ -139,7 +137,7 @@
" "
> >
<div>{{ t`Grand Total` }}</div> <div>{{ t`Grand Total` }}</div>
<div>{{ fyo.format(doc.grandTotal, 'Currency') }}</div> <div>{{ printObject.grandTotal }}</div>
</div> </div>
</div> </div>
</div> </div>
@ -147,11 +145,10 @@
</template> </template>
<script> <script>
import Base from './BaseTemplate.vue'; import BaseTemplate from './BaseTemplate.vue';
export default { export default {
name: 'Default', name: 'Default',
extends: Base, extends: BaseTemplate,
props: ['doc', 'printSettings'],
}; };
</script> </script>

View File

@ -1,65 +1,58 @@
<template> <template>
<div <div
class="bg-white border h-full" class="bg-white border h-full"
:style="{ 'font-family': printSettings.font }" :style="{ 'font-family': printObject.font }"
> >
<div class="bg-gray-100 px-12 py-10"> <div class="bg-gray-100 px-12 py-10">
<div class="flex items-center"> <div class="flex items-center">
<div class="flex items-center rounded h-16"> <div class="flex items-center rounded h-16">
<div class="mr-4" v-if="printSettings.displayLogo"> <div class="mr-4" v-if="printObject.displayLogo">
<img <img class="h-12 max-w-32 object-contain" :src="printObject.logo" />
class="h-12 max-w-32 object-contain"
:src="printSettings.logo"
/>
</div> </div>
</div> </div>
<div> <div>
<div <div
class="font-semibold text-xl" class="font-semibold text-xl"
:style="{ color: printSettings.color }" :style="{ color: printObject.color }"
> >
{{ fyo.singles.AccountingSettings.companyName }} {{ printObject.companyName }}
</div> </div>
<div class="text-sm text-gray-800" v-if="companyAddress"> <div class="text-sm text-gray-800" v-if="printObject.address">
{{ companyAddress.addressDisplay }} {{ printObject.address }}
</div> </div>
<div <div class="text-sm text-gray-800" v-if="printObject.gstin">
class="text-sm text-gray-800" GSTIN: {{ printObject.gstin }}
v-if="
fyo.singles.AccountingSettings &&
fyo.singles.AccountingSettings.gstin
"
>
GSTIN: {{ fyo.singles.AccountingSettings.gstin }}
</div> </div>
</div> </div>
</div> </div>
<div class="mt-8 text-lg"> <div class="mt-8 text-lg">
<div class="flex"> <div class="flex">
<div class="w-1/3 font-semibold"> <div class="w-1/3 font-semibold">
{{ isSalesInvoice ? 'Invoice' : 'Bill' }} {{ printObject.isSalesInvoice ? 'Invoice' : 'Bill' }}
</div> </div>
<div class="w-2/3 text-gray-800"> <div class="w-2/3 text-gray-800">
<div class="font-semibold"> <div class="font-semibold">
{{ doc.name }} {{ printObject.invoiceName }}
</div> </div>
<div> <div>
{{ fyo.format(doc.date, 'Date') }} {{ printObject.date }}
</div> </div>
</div> </div>
</div> </div>
<div class="mt-4 flex"> <div class="mt-4 flex">
<div class="w-1/3 font-semibold"> <div class="w-1/3 font-semibold">
{{ isSalesInvoice ? 'Customer' : 'Supplier' }} {{ printObject.isSalesInvoice ? 'Customer' : 'Supplier' }}
</div> </div>
<div class="w-2/3 text-gray-800" v-if="party"> <div class="w-2/3 text-gray-800" v-if="printObject.partyName">
<div class="font-semibold"> <div class="font-semibold">
{{ party.name }} {{ printObject.partyName }}
</div> </div>
<div v-if="partyAddress"> <div v-if="printObject.partyAddress">
{{ partyAddress }} {{ printObject.partyAddress }}
</div>
<div v-if="printObject.partyGSTIN">
GSTIN: {{ printObject.partyGSTIN }}
</div> </div>
<div v-if="party && party.gstin">GSTIN: {{ party.gstin }}</div>
</div> </div>
</div> </div>
</div> </div>
@ -67,21 +60,23 @@
<div class="px-12 py-12 text-lg"> <div class="px-12 py-12 text-lg">
<div class="mb-4 flex font-semibold"> <div class="mb-4 flex font-semibold">
<div class="w-4/12">Item</div> <div class="w-4/12">Item</div>
<div class="w-2/12 text-right" v-if="showHSN">HSN/SAC</div> <div class="w-2/12 text-right" v-if="printObject.showHSN">HSN/SAC</div>
<div class="w-2/12 text-right">Quantity</div> <div class="w-2/12 text-right">Quantity</div>
<div class="w-3/12 text-right">Rate</div> <div class="w-3/12 text-right">Rate</div>
<div class="w-3/12 text-right">Amount</div> <div class="w-3/12 text-right">Amount</div>
</div> </div>
<div <div
class="flex py-1 text-gray-800" class="flex py-1 text-gray-800"
v-for="row in doc.items" v-for="row in printObject.items"
:key="row.name" :key="row.name"
> >
<div class="w-4/12">{{ row.item }}</div> <div class="w-4/12">{{ row.item }}</div>
<div class="w-2/12 text-right" v-if="showHSN">{{ row.hsnCode }}</div> <div class="w-2/12 text-right" v-if="printObject.showHSN">
<div class="w-2/12 text-right">{{ format(row, 'quantity') }}</div> {{ row.hsnCode }}
<div class="w-3/12 text-right">{{ format(row, 'rate') }}</div> </div>
<div class="w-3/12 text-right">{{ format(row, 'amount') }}</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>
</div> </div>
<div class="mt-12"> <div class="mt-12">
<div class="flex -mx-3"> <div class="flex -mx-3">
@ -89,67 +84,70 @@
<div class="text-right"> <div class="text-right">
<div class="text-gray-800">{{ t`Subtotal` }}</div> <div class="text-gray-800">{{ t`Subtotal` }}</div>
<div class="text-xl mt-2"> <div class="text-xl mt-2">
{{ fyo.format(doc.netTotal, 'Currency') }} {{ printObject.netTotal }}
</div> </div>
</div> </div>
<div <div
class="text-right" class="text-right"
v-if="totalDiscount?.float > 0 && !doc.discountAfterTax" v-if="printObject.totalDiscount && !printObject.discountAfterTax"
> >
<div class="text-gray-800">{{ t`Discount` }}</div> <div class="text-gray-800">{{ t`Discount` }}</div>
<div class="text-xl mt-2"> <div class="text-xl mt-2">
{{ `- ${fyo.format(totalDiscount, 'Currency')}` }} {{ printObject.totalDiscount }}
</div> </div>
</div> </div>
<div class="text-right" v-for="tax in doc.taxes" :key="tax.name"> <div
class="text-right"
v-for="tax in printObject.taxes"
:key="tax.name"
>
<div class="text-gray-800"> <div class="text-gray-800">
{{ tax.account }} {{ tax.account }}
</div> </div>
<div class="text-xl mt-2"> <div class="text-xl mt-2">
{{ fyo.format(tax.amount, 'Currency') }} {{ tax.amount }}
</div> </div>
</div> </div>
<div <div
class="text-right" class="text-right"
v-if="totalDiscount?.float > 0 && !doc.discountAfterTax" v-if="printObject.totalDiscount && printObject.discountAfterTax"
> >
<div class="text-gray-800">{{ t`Discount` }}</div> <div class="text-gray-800">{{ t`Discount` }}</div>
<div class="text-xl mt-2"> <div class="text-xl mt-2">
{{ `- ${fyo.format(totalDiscount, 'Currency')}` }} {{ printObject.totalDiscount }}
</div> </div>
</div> </div>
</div> </div>
<div <div
class="py-3 px-4 text-right text-white" class="py-3 px-4 text-right text-white"
:style="{ backgroundColor: printSettings.color }" :style="{ backgroundColor: printObject.color }"
> >
<div> <div>
<div>{{ t`Grand Total` }}</div> <div>{{ t`Grand Total` }}</div>
<div class="text-2xl mt-2 font-semibold"> <div class="text-2xl mt-2 font-semibold">
{{ fyo.format(doc.grandTotal, 'Currency') }} {{ printObject.grandTotal }}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="mt-12" v-if="doc.terms"> <div class="mt-12" v-if="printObject.terms">
<div class="text-lg font-semibold">Notes</div> <div class="text-lg font-semibold">Notes</div>
<div class="mt-4 text-lg whitespace-pre-line"> <div class="mt-4 text-lg whitespace-pre-line">
{{ doc.terms }} {{ printObject.terms }}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import Base from './BaseTemplate.vue'; import BaseTemplate from './BaseTemplate.vue';
export default { export default {
name: 'Business', name: 'Business',
extends: Base, extends: BaseTemplate,
}; };
</script> </script>

View File

@ -1,65 +1,60 @@
<template> <template>
<div <div
class="bg-white border h-full" class="bg-white border h-full"
:style="{ 'font-family': printSettings.font }" :style="{ 'font-family': printObject.font }"
> >
<div class="flex items-center justify-between px-12 py-10 border-b"> <div class="flex items-center justify-between px-12 py-10 border-b">
<div class="flex items-center"> <div class="flex items-center">
<div class="flex items-center rounded h-16"> <div class="flex items-center rounded h-16">
<div class="mr-4" v-if="printSettings.displayLogo"> <div class="mr-4" v-if="printObject.displayLogo">
<img <img class="h-12 max-w-32 object-contain" :src="printObject.logo" />
class="h-12 max-w-32 object-contain"
:src="printSettings.logo"
/>
</div> </div>
</div> </div>
<div> <div>
<div <div
class="font-semibold text-xl" class="font-semibold text-xl"
:style="{ color: printSettings.color }" :style="{ color: printObject.color }"
> >
{{ fyo.singles.AccountingSettings.companyName }} {{ printObject.companyName }}
</div> </div>
<div> <div>
{{ fyo.format(doc.date, 'Date') }} {{ printObject.date }}
</div> </div>
</div> </div>
</div> </div>
<div class="text-right"> <div class="text-right">
<div <div
class="font-semibold text-xl" class="font-semibold text-xl"
:style="{ color: printSettings.color }" :style="{ color: printObject.color }"
> >
{{ {{
doc.schemaName === 'SalesInvoice' printObject.isSalesInvoice ? t`Sales Invoice` : t`Purchase Invoice`
? t`Sales Invoice`
: t`Purchase Invoice`
}} }}
</div> </div>
<div> <div>
{{ doc.name }} {{ printObject.invoiceName }}
</div> </div>
</div> </div>
</div> </div>
<div class="flex px-12 py-10 border-b"> <div class="flex px-12 py-10 border-b">
<div class="w-1/2" v-if="party"> <div class="w-1/2" v-if="printObject.partyName">
<div <div
class="uppercase text-sm font-semibold tracking-widest text-gray-800" class="uppercase text-sm font-semibold tracking-widest text-gray-800"
> >
{{ isSalesInvoice ? 'To' : 'From' }} {{ printObject.isSalesInvoice ? 'To' : 'From' }}
</div> </div>
<div class="mt-4 text-black leading-relaxed text-lg"> <div class="mt-4 text-black leading-relaxed text-lg">
{{ party.name }} <br /> {{ printObject.partyName }} <br />
{{ partyAddress ? partyAddress : '' }} {{ printObject.partyAddress ?? '' }}
</div> </div>
<div <div
class="mt-4 text-black leading-relaxed text-lg" class="mt-4 text-black leading-relaxed text-lg"
v-if="party && party.gstin" v-if="printObject.partyGSTIN"
> >
GSTIN: {{ party.gstin }} GSTIN: {{ printObject.partyGSTIN }}
</div> </div>
</div> </div>
<div class="w-1/2" v-if="companyAddress"> <div class="w-1/2" v-if="printObject.address">
<div <div
class=" class="
uppercase uppercase
@ -70,19 +65,16 @@
ml-8 ml-8
" "
> >
{{ isSalesInvoice ? 'From' : 'To' }} {{ printObject.isSalesInvoice ? 'From' : 'To' }}
</div> </div>
<div class="mt-4 ml-8 text-black leading-relaxed text-lg"> <div class="mt-4 ml-8 text-black leading-relaxed text-lg">
{{ companyAddress.addressDisplay }} {{ printObject.address }}
</div> </div>
<div <div
class="mt-4 ml-8 text-black leading-relaxed text-lg" class="mt-4 ml-8 text-black leading-relaxed text-lg"
v-if=" v-if="printObject.gstin"
fyo.singles.AccountingSettings &&
fyo.singles.AccountingSettings.gstin
"
> >
GSTIN: {{ fyo.singles.AccountingSettings.gstin }} GSTIN: {{ printObject.gstin }}
</div> </div>
</div> </div>
</div> </div>
@ -99,74 +91,79 @@
" "
> >
<div class="w-4/12">Item</div> <div class="w-4/12">Item</div>
<div class="w-2/12 text-right" v-if="showHSN">HSN/SAC</div> <div class="w-2/12 text-right" v-if="printObject.showHSN">HSN/SAC</div>
<div class="w-2/12 text-right">Quantity</div> <div class="w-2/12 text-right">Quantity</div>
<div class="w-3/12 text-right">Rate</div> <div class="w-3/12 text-right">Rate</div>
<div class="w-3/12 text-right">Amount</div> <div class="w-3/12 text-right">Amount</div>
</div> </div>
<div class="flex py-1 text-lg" v-for="row in doc.items" :key="row.name"> <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-4/12">{{ row.item }}</div>
<div class="w-2/12 text-right" v-if="showHSN">{{ row.hsnCode }}</div> <div class="w-2/12 text-right" v-if="printObject.showHSN">
<div class="w-2/12 text-right">{{ format(row, 'quantity') }}</div> {{ row.hsnCode }}
<div class="w-3/12 text-right">{{ format(row, 'rate') }}</div> </div>
<div class="w-3/12 text-right">{{ format(row, 'amount') }}</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>
</div> </div>
</div> </div>
<div class="flex px-12 py-10"> <div class="flex px-12 py-10">
<div class="w-1/2" v-if="doc.terms"> <div class="w-1/2" v-if="printObject.terms">
<div <div
class="uppercase text-sm tracking-widest font-semibold text-gray-800" class="uppercase text-sm tracking-widest font-semibold text-gray-800"
> >
Notes Notes
</div> </div>
<div class="mt-4 text-lg whitespace-pre-line"> <div class="mt-4 text-lg whitespace-pre-line">
{{ doc.terms }} {{ printObject.terms }}
</div> </div>
</div> </div>
<div class="w-1/2 text-lg"> <div class="w-1/2 text-lg">
<div class="flex pl-2 justify-between py-1"> <div class="flex pl-2 justify-between py-1">
<div>{{ t`Subtotal` }}</div> <div>{{ t`Subtotal` }}</div>
<div>{{ fyo.format(doc.netTotal, 'Currency') }}</div> <div>{{ printObject.netTotal }}</div>
</div> </div>
<div <div
class="flex pl-2 justify-between py-1" class="flex pl-2 justify-between py-1"
v-if="totalDiscount?.float > 0 && !doc.discountAfterTax" v-if="printObject.totalDiscount && !printObject.discountAfterTax"
> >
<div>{{ t`Discount` }}</div> <div>{{ t`Discount` }}</div>
<div>{{ `- ${fyo.format(totalDiscount, 'Currency')}` }}</div> <div>{{ printObject.totalDiscount }}</div>
</div> </div>
<div <div
class="flex pl-2 justify-between py-1" class="flex pl-2 justify-between py-1"
v-for="tax in doc.taxes" v-for="tax in printObject.taxes"
:key="tax.name" :key="tax.name"
> >
<div>{{ tax.account }}</div> <div>{{ tax.account }}</div>
<div>{{ fyo.format(tax.amount, 'Currency') }}</div> <div>{{ tax.amount }}</div>
</div> </div>
<div <div
class="flex pl-2 justify-between py-1" class="flex pl-2 justify-between py-1"
v-if="totalDiscount?.float > 0 && doc.discountAfterTax" v-if="printObject.totalDiscount && printObject.discountAfterTax"
> >
<div>{{ t`Discount` }}</div> <div>{{ t`Discount` }}</div>
<div>{{ `- ${fyo.format(totalDiscount, 'Currency')}` }}</div> <div>{{ printObject.totalDiscount }}</div>
</div> </div>
<div <div
class="flex pl-2 justify-between py-1 font-semibold" class="flex pl-2 justify-between py-1 font-semibold"
:style="{ color: printSettings.color }" :style="{ color: printObject.color }"
> >
<div>{{ t`Grand Total` }}</div> <div>{{ t`Grand Total` }}</div>
<div>{{ fyo.format(doc.grandTotal, 'Currency') }}</div> <div>{{ printObject.grandTotal }}</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import Base from './BaseTemplate.vue'; import BaseTemplate from './BaseTemplate.vue';
export default { export default {
name: 'Minimal', name: 'Minimal',
extends: Base, extends: BaseTemplate,
}; };
</script> </script>

View File

@ -3,6 +3,16 @@
<!-- Page Header (Title, Buttons, etc) --> <!-- Page Header (Title, Buttons, etc) -->
<template #header v-if="doc"> <template #header v-if="doc">
<StatusBadge :status="status" /> <StatusBadge :status="status" />
<ExchangeRate
v-if="doc.isMultiCurrency"
:disabled="doc?.isSubmitted || doc?.isCancelled"
:from-currency="fromCurrency"
:to-currency="toCurrency"
:exchange-rate="doc.exchangeRate"
@change="
async (exchangeRate) => await doc.set('exchangeRate', exchangeRate)
"
/>
<Button <Button
v-if="!doc.isCancelled && !doc.dirty" v-if="!doc.isCancelled && !doc.dirty"
:icon="true" :icon="true"
@ -67,8 +77,8 @@
:border="true" :border="true"
:df="getField('party')" :df="getField('party')"
:value="doc.party" :value="doc.party"
@change="(value) => doc.set('party', value)" @change="(value) => doc.set('party', value, true)"
@new-doc="(party) => doc.set('party', party.name)" @new-doc="(party) => doc.set('party', party.name, true)"
:read-only="doc?.submitted" :read-only="doc?.submitted"
/> />
<FormControl <FormControl
@ -175,10 +185,14 @@
<div>{{ tax.account }}</div> <div>{{ tax.account }}</div>
<div> <div>
{{ {{
fyo.format(tax.amount, { fyo.format(
tax.amount,
{
fieldtype: 'Currency', fieldtype: 'Currency',
currency: doc.currency, fieldname: 'amount',
}) },
tax
)
}} }}
</div> </div>
</div> </div>
@ -220,6 +234,21 @@
<div>{{ formattedValue('grandTotal') }}</div> <div>{{ formattedValue('grandTotal') }}</div>
</div> </div>
<!-- Base Grand Total -->
<div
v-if="doc.isMultiCurrency"
class="
flex
justify-between
text-green-600
font-semibold
text-base
"
>
<div>{{ t`Base Grand Total` }}</div>
<div>{{ formattedValue('baseGrandTotal') }}</div>
</div>
<!-- Outstanding Amount --> <!-- Outstanding Amount -->
<hr v-if="doc.outstandingAmount?.float > 0" /> <hr v-if="doc.outstandingAmount?.float > 0" />
<div <div
@ -256,6 +285,7 @@ import { computed } from '@vue/reactivity';
import { getDocStatus } from 'models/helpers'; import { getDocStatus } from 'models/helpers';
import { ModelNameEnum } from 'models/types'; import { ModelNameEnum } from 'models/types';
import Button from 'src/components/Button.vue'; import Button from 'src/components/Button.vue';
import ExchangeRate from 'src/components/Controls/ExchangeRate.vue';
import FormControl from 'src/components/Controls/FormControl.vue'; import FormControl from 'src/components/Controls/FormControl.vue';
import Table from 'src/components/Controls/Table.vue'; import Table from 'src/components/Controls/Table.vue';
import DropdownWithActions from 'src/components/DropdownWithActions.vue'; import DropdownWithActions from 'src/components/DropdownWithActions.vue';
@ -284,6 +314,7 @@ export default {
Table, Table,
FormContainer, FormContainer,
QuickEditForm, QuickEditForm,
ExchangeRate,
}, },
provide() { provide() {
return { return {
@ -342,6 +373,12 @@ export default {
itemDiscountAmount() { itemDiscountAmount() {
return this.doc.getItemDiscountAmount(); return this.doc.getItemDiscountAmount();
}, },
fromCurrency() {
return this.doc?.currency ?? this.toCurrency;
},
toCurrency() {
return fyo.singles.SystemSettings.currency;
},
}, },
activated() { activated() {
docsPath.value = docsPathMap[this.schemaName]; docsPath.value = docsPathMap[this.schemaName];

View File

@ -56,6 +56,12 @@
</p> </p>
<TwoColumnForm :doc="doc" :read-only="loading" /> <TwoColumnForm :doc="doc" :read-only="loading" />
<Button
v-if="fyo.store.isDevelopment"
class="m-4 text-sm min-w-28"
@click="fill"
>Fill</Button
>
</div> </div>
</template> </template>
<template #secondaryButton>{{ t`Cancel` }}</template> <template #secondaryButton>{{ t`Cancel` }}</template>
@ -67,6 +73,7 @@
</template> </template>
<script> <script>
import Button from 'src/components/Button.vue';
import FormControl from 'src/components/Controls/FormControl.vue'; import FormControl from 'src/components/Controls/FormControl.vue';
import TwoColumnForm from 'src/components/TwoColumnForm.vue'; import TwoColumnForm from 'src/components/TwoColumnForm.vue';
import { getErrorMessage } from 'src/utils'; import { getErrorMessage } from 'src/utils';
@ -95,6 +102,7 @@ export default {
TwoColumnForm, TwoColumnForm,
FormControl, FormControl,
Slide, Slide,
Button,
}, },
async mounted() { async mounted() {
this.doc = await getSetupWizardDoc(); this.doc = await getSetupWizardDoc();
@ -103,6 +111,13 @@ export default {
}); });
}, },
methods: { methods: {
async fill() {
await this.doc.set('companyName', "Lin's Things");
await this.doc.set('email', 'lin@lthings.com');
await this.doc.set('fullname', 'Lin Slovenly');
await this.doc.set('bankName', 'Max Finance');
await this.doc.set('country', 'India');
},
getField(fieldname) { getField(fieldname) {
return this.doc.schema?.fields.find((f) => f.fieldname === fieldname); return this.doc.schema?.fields.find((f) => f.fieldname === fieldname);
}, },