2
0
mirror of https://github.com/frappe/books.git synced 2025-01-09 09:50:27 +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,
RawValue,
Schema,
TargetField,
TargetField
} from 'schemas/types';
import { getIsNullOrUndef, getMapFromList, getRandomString } from 'utils';
import { markRaw } from 'vue';
@ -23,7 +23,7 @@ import {
getMissingMandatoryMessage,
getPreDefaultValues,
setChildDocIdx,
shouldApplyFormula,
shouldApplyFormula
} from './helpers';
import { setName } from './naming';
import {
@ -41,7 +41,7 @@ import {
ReadOnlyMap,
RequiredMap,
TreeViewSettings,
ValidationMap,
ValidationMap
} from './types';
import { validateOptions, validateRequired } from './validationFunction';
@ -186,7 +186,8 @@ export class Doc extends Observable<DocValue | Doc[]> {
// set value and trigger change
async set(
fieldname: string | DocValueMap,
value?: DocValue | Doc[] | DocValueMap[]
value?: DocValue | Doc[] | DocValueMap[],
retriggerChildDocApplyChange: boolean = false
): Promise<boolean> {
if (typeof fieldname === 'object') {
return await this.setMultiple(fieldname as DocValueMap);
@ -216,7 +217,7 @@ export class Doc extends Observable<DocValue | Doc[]> {
await this._applyChange(fieldname);
await this.parentdoc._applyChange(this.parentFieldname as string);
} else {
await this._applyChange(fieldname);
await this._applyChange(fieldname, retriggerChildDocApplyChange);
}
return true;
@ -259,8 +260,11 @@ export class Doc extends Observable<DocValue | Doc[]> {
return !areDocValuesEqual(currentValue as DocValue, value as DocValue);
}
async _applyChange(fieldname: string): Promise<boolean> {
await this._applyFormula(fieldname);
async _applyChange(
fieldname: string,
retriggerChildDocApplyChange?: boolean
): Promise<boolean> {
await this._applyFormula(fieldname, retriggerChildDocApplyChange);
await this.trigger('change', {
doc: this,
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;
let changed = false;
let changed = await this._callAllTableFieldsApplyFormula(fieldname);
changed = (await this._applyFormulaForFields(doc, fieldname)) || changed;
const childDocs = this.tableFields
.map((f) => (this.get(f.fieldname) as Doc[]) ?? [])
.flat();
// children
for (const row of childDocs) {
changed ||= (await row?._applyFormula()) ?? false;
if (changed && retriggerChildDocApplyChange) {
await this._callAllTableFieldsApplyFormula(fieldname);
await this._applyFormulaForFields(doc, fieldname);
}
// 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(
(fn) => this.fieldMap[fn]
);
changed ||= await this._applyFormulaForFields(
formulaFields,
doc,
fieldname
);
return changed;
}
async _applyFormulaForFields(
formulaFields: Field[],
doc: Doc,
fieldname?: string
) {
let changed = false;
for (const field of formulaFields) {
const shouldApply = shouldApplyFormula(field, doc, fieldname);
@ -662,7 +691,7 @@ export class Doc extends Observable<DocValue | Doc[]> {
}
doc[field.fieldname] = newVal;
changed = true;
changed ||= true;
}
return changed;

View File

@ -7,6 +7,16 @@ import { SelectOption } from 'schemas/types';
import { getCountryInfo } from 'utils/misc';
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 = {
async displayPrecision(value: DocValue) {
if ((value as number) >= 0 && (value as number) <= 9) {

View File

@ -70,7 +70,7 @@ function formatCurrency(
doc: Doc | null,
fyo: Fyo
): string {
const currency = getCurrency(field, doc, fyo);
const currency = getCurrency(value as Money, field, doc, fyo);
let valueString;
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];
if (getCurrency !== undefined) {
return getCurrency();
@ -139,7 +152,7 @@ function getCurrency(field: Field, doc: Doc | null, fyo: Fyo): string {
return getCurrency();
}
return (fyo.singles.SystemSettings?.currency as string) ?? DEFAULT_CURRENCY;
return defaultCurrency;
}
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 { 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 { getExchangeRate } from 'models/helpers';
import { Transactional } from 'models/Transactional/Transactional';
import { ModelNameEnum } from 'models/types';
import { Money } from 'pesa';
import { FieldTypeEnum, Schema } from 'schemas/types';
import { getIsNullOrUndef } from 'utils';
import { InvoiceItem } from '../InvoiceItem/InvoiceItem';
import { Party } from '../Party/Party';
@ -42,6 +51,23 @@ export abstract class Invoice extends Transactional {
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() {
await super.validate();
if (
@ -126,10 +152,12 @@ export abstract class Invoice extends Transactional {
if (this.currency === currency) {
return 1.0;
}
return await getExchangeRate({
const exchangeRate = await getExchangeRate({
fromCurrency: this.currency!,
toCurrency: currency as string,
});
return parseFloat(exchangeRate.toFixed(2));
}
async getTaxSummary() {
@ -139,7 +167,6 @@ export abstract class Invoice extends Transactional {
account: string;
rate: number;
amount: Money;
baseAmount: Money;
[key: string]: DocValue;
}
> = {};
@ -157,7 +184,6 @@ export abstract class Invoice extends Transactional {
account,
rate,
amount: this.fyo.pesa(0),
baseAmount: this.fyo.pesa(0),
};
let amount = item.amount!;
@ -172,9 +198,7 @@ export abstract class Invoice extends Transactional {
return Object.keys(taxes)
.map((account) => {
const tax = taxes[account];
tax.baseAmount = tax.amount.mul(this.exchangeRate!);
return tax;
return taxes[account];
})
.filter((tax) => !tax.amount.isZero());
}
@ -285,15 +309,28 @@ export abstract class Invoice extends Transactional {
},
dependsOn: ['party'],
},
exchangeRate: { formula: async () => await this.getExchangeRate() },
netTotal: { formula: async () => this.getSum('items', 'amount', false) },
baseNetTotal: {
formula: async () => this.netTotal!.mul(this.exchangeRate!),
exchangeRate: {
formula: async () => {
if (
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() },
grandTotal: { formula: async () => await this.getGrandTotal() },
baseGrandTotal: {
formula: async () => (this.grandTotal as Money).mul(this.exchangeRate!),
formula: async () =>
(this.grandTotal as Money).mul(this.exchangeRate! ?? 1),
},
outstandingAmount: {
formula: async () => {
@ -345,4 +382,25 @@ export abstract class Invoice extends Transactional {
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 {
CurrenciesMap,
FiltersMap,
FormulaMap,
HiddenMap,
ValidationMap
} from 'fyo/model/types';
import { DEFAULT_CURRENCY } from 'fyo/utils/consts';
import { ValidationError } from 'fyo/utils/errors';
import { ModelNameEnum } from 'models/types';
import { Money } from 'pesa';
import { FieldTypeEnum, Schema } from 'schemas/types';
import { Invoice } from '../Invoice/Invoice';
export abstract class InvoiceItem extends Doc {
account?: string;
amount?: Money;
baseAmount?: Money;
exchangeRate?: number;
parentdoc?: Invoice;
rate?: Money;
quantity?: number;
@ -39,6 +41,23 @@ export abstract class InvoiceItem extends Doc {
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> {
if (!this.tax) {
return 0;
@ -73,7 +92,7 @@ export abstract class InvoiceItem extends Doc {
fieldname !== 'itemTaxedTotal' &&
fieldname !== 'itemDiscountedTotal'
) {
return rate ?? this.fyo.pesa(0);
return rate?.div(this.exchangeRate) ?? this.fyo.pesa(0);
}
const quantity = this.quantity ?? 0;
@ -102,17 +121,14 @@ export abstract class InvoiceItem extends Doc {
return rateFromTotals ?? rate ?? this.fyo.pesa(0);
},
dependsOn: [
'party',
'exchangeRate',
'item',
'itemTaxedTotal',
'itemDiscountedTotal',
'setItemDiscountAmount',
],
},
baseRate: {
formula: () =>
(this.rate as Money).mul(this.parentdoc!.exchangeRate as number),
dependsOn: ['item', 'rate'],
},
quantity: {
formula: async () => {
if (!this.item) {
@ -157,11 +173,6 @@ export abstract class InvoiceItem extends Doc {
formula: () => (this.rate as Money).mul(this.quantity as number),
dependsOn: ['item', 'rate', 'quantity'],
},
baseAmount: {
formula: () =>
(this.amount as Money).mul(this.parentdoc!.exchangeRate as number),
dependsOn: ['item', 'amount', 'rate', 'quantity'],
},
hsnCode: {
formula: async () =>
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' };
},
};
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(

View File

@ -87,7 +87,11 @@ export class Party extends Doc {
dependsOn: ['role'],
},
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[];
async getPosting() {
const exchangeRate = this.exchangeRate ?? 1;
const posting: LedgerPosting = new LedgerPosting(this, this.fyo);
await posting.credit(this.account!, this.baseGrandTotal!);
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) {
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
?.discountAccount as string | undefined;
if (discountAccount && discountAmount.isPositive()) {
await posting.credit(discountAccount, discountAmount);
await posting.credit(discountAccount, discountAmount.mul(exchangeRate));
}
await posting.makeRoundOffEntry();
@ -46,7 +47,7 @@ export class PurchaseInvoice extends Invoice {
getTransactionStatusColumn(),
'party',
'date',
'grandTotal',
'baseGrandTotal',
'outstandingAmount',
],
};

View File

@ -10,16 +10,17 @@ export class SalesInvoice extends Invoice {
items?: SalesInvoiceItem[];
async getPosting() {
const exchangeRate = this.exchangeRate ?? 1;
const posting: LedgerPosting = new LedgerPosting(this, this.fyo);
await posting.debit(this.account!, this.baseGrandTotal!);
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) {
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
?.discountAccount as string | undefined;
if (discountAccount && discountAmount.isPositive()) {
await posting.debit(discountAccount, discountAmount);
await posting.debit(discountAccount, discountAmount.mul(exchangeRate));
}
await posting.makeRoundOffEntry();
@ -46,7 +47,7 @@ export class SalesInvoice extends Invoice {
getTransactionStatusColumn(),
'party',
'date',
'grandTotal',
'baseGrandTotal',
'outstandingAmount',
],
};

View File

@ -1,21 +1,48 @@
import { Fyo } from 'fyo';
import { DocValueMap } from 'fyo/core/types';
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 { FieldTypeEnum, Schema } from 'schemas/types';
import { Invoice } from '../Invoice/Invoice';
export class TaxSummary extends Doc {
account?: string;
rate?: number;
amount?: Money;
baseAmount?: Money;
parentdoc?: Invoice;
formulas: FormulaMap = {
baseAmount: {
formula: async () => {
const amount = this.amount as Money;
const exchangeRate = (this.parentdoc?.exchangeRate ?? 1) as number;
return amount.mul(exchangeRate);
},
dependsOn: ['amount'],
},
};
get exchangeRate() {
return this.parentdoc?.exchangeRate ?? 1;
}
get currency() {
return this.parentdoc?.currency ?? DEFAULT_CURRENCY;
}
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 { Doc } from 'fyo/model/doc';
import { Action, ColumnConfig, DocStatus, RenderData } from 'fyo/model/types';
import { NotFoundError } from 'fyo/utils/errors';
import { DateTime } from 'luxon';
import { Money } from 'pesa';
import { Router } from 'vue-router';
import {
AccountRootType,
AccountRootTypeEnum,
AccountRootTypeEnum
} from './baseModels/Account/types';
import { InvoiceStatus, ModelNameEnum } from './types';
@ -196,14 +195,12 @@ export async function getExchangeRate({
toCurrency: string;
date?: string;
}) {
if (!date) {
date = DateTime.local().toISODate();
if (!fetch) {
return 1;
}
if (!fromCurrency || !toCurrency) {
throw new NotFoundError(
'Please provide `fromCurrency` and `toCurrency` to get exchange rate.'
);
if (!date) {
date = DateTime.local().toISODate();
}
const cacheKey = `currencyExchangeRate:${date}:${fromCurrency}:${toCurrency}`;
@ -215,26 +212,23 @@ export async function getExchangeRate({
);
}
if (!exchangeRate && fetch) {
try {
const res = await fetch(
` https://api.vatcomply.com/rates?date=${date}&base=${fromCurrency}&symbols=${toCurrency}`
);
const data = await res.json();
exchangeRate = data.rates[toCurrency];
if (exchangeRate && exchangeRate !== 1) {
return exchangeRate;
}
if (localStorage) {
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;
try {
const res = await fetch(
`https://api.vatcomply.com/rates?date=${date}&base=${fromCurrency}&symbols=${toCurrency}`
);
const data = await res.json();
exchangeRate = data.rates[toCurrency];
} catch (error) {
console.error(error);
exchangeRate ??= 1;
}
if (localStorage) {
localStorage.setItem(cacheKey, String(exchangeRate));
}
return exchangeRate;
@ -256,3 +250,6 @@ export function isCredit(rootType: AccountRootType) {
return true;
}
}
// @ts-ignore
window.gex = getExchangeRate;

View File

@ -218,6 +218,11 @@ async function generateB2bData(report: BaseGSTR): Promise<B2BCustomer[]> {
? ModelNameEnum.SalesInvoiceItem
: ModelNameEnum.PurchaseInvoiceItem;
const parentSchemaName =
report.gstrType === 'GSTR-1'
? ModelNameEnum.SalesInvoice
: ModelNameEnum.PurchaseInvoice;
for (const row of report.gstrRows ?? []) {
const invRecord: B2BInvRecord = {
inum: row.invNo,
@ -229,20 +234,29 @@ async function generateB2bData(report: BaseGSTR): Promise<B2BCustomer[]> {
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, {
fields: ['baseAmount', 'tax', 'hsnCode'],
filters: { parent: invRecord.inum as string },
fields: ['amount', 'tax', 'hsnCode'],
filters: { parent: invRecord.inum },
});
items.forEach((item) => {
const hsnCode = item.hsnCode as number;
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 = {
num: hsnCode,
itm_det: {
txval: fyo.pesa(baseAmount).float,
txval: baseAmount.float,
rt: GST[tax],
csamt: 0,
camt: fyo
@ -292,6 +306,11 @@ async function generateB2clData(
? ModelNameEnum.SalesInvoiceItem
: ModelNameEnum.PurchaseInvoiceItem;
const parentSchemaName =
report.gstrType === 'GSTR-1'
? ModelNameEnum.SalesInvoice
: ModelNameEnum.PurchaseInvoice;
for (const row of report.gstrRows ?? []) {
const invRecord: B2CLInvRecord = {
inum: row.invNo,
@ -300,20 +319,29 @@ async function generateB2clData(
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, {
fields: ['hsnCode', 'tax', 'baseAmount'],
fields: ['amount', 'tax', 'hsnCode'],
filters: { parent: invRecord.inum },
});
items.forEach((item) => {
const hsnCode = item.hsnCode as number;
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 = {
num: hsnCode,
itm_det: {
txval: fyo.pesa(baseAmount).float,
txval: baseAmount.float,
rt: GST[tax] ?? 0,
csamt: 0,
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",
"label": "Purchase Invoice",
"extends": "Invoice",
"naming": "numberSeries",
"isSingle": false,
"isChild": false,
"isSubmittable": true,
"showTitle": true,
"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",
"label": "Items",
@ -57,75 +13,6 @@
"required": 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",
"label": "Number Series",

View File

@ -1,124 +1,5 @@
{
"name": "PurchaseInvoiceItem",
"label": "Purchase Invoice Item",
"isChild": true,
"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"
]
"extends": "InvoiceItem"
}

View File

@ -1,53 +1,10 @@
{
"name": "SalesInvoice",
"label": "Sales Invoice",
"extends": "Invoice",
"naming": "numberSeries",
"isSingle": false,
"isChild": false,
"isSubmittable": true,
"showTitle": 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": "items",
"label": "Items",
@ -56,75 +13,6 @@
"required": 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",
"label": "Number Series",

View File

@ -1,120 +1,5 @@
{
"name": "SalesInvoiceItem",
"label": "Sales Invoice Item",
"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": "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"
]
"extends": "InvoiceItem"
}

View File

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

View File

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

View File

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

View File

@ -151,13 +151,16 @@ export default {
},
methods: {
getInputClassesFromProp(classes) {
if (this.inputClass) {
if (typeof this.inputClass === 'function') {
classes = this.inputClass(classes);
} else {
classes.push(this.inputClass);
}
if (!this.inputClass) {
return classes;
}
if (typeof this.inputClass === 'function') {
classes = this.inputClass(classes);
} else {
classes.push(this.inputClass);
}
return classes;
},
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>
import { fyo } from 'src/initFyo';
export default {
name: 'Base',
props: { doc: Object, printSettings: Object },
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() {
await this.printSettings.loadLink('address');
this.companyAddress = this.printSettings.getLink('address');
@ -17,8 +10,49 @@ export default {
await this.doc.loadLink('party');
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';
},
@ -28,6 +62,46 @@ export default {
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,
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>

View File

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

View File

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

View File

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

View File

@ -3,6 +3,16 @@
<!-- Page Header (Title, Buttons, etc) -->
<template #header v-if="doc">
<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
v-if="!doc.isCancelled && !doc.dirty"
:icon="true"
@ -67,8 +77,8 @@
:border="true"
:df="getField('party')"
:value="doc.party"
@change="(value) => doc.set('party', value)"
@new-doc="(party) => doc.set('party', party.name)"
@change="(value) => doc.set('party', value, true)"
@new-doc="(party) => doc.set('party', party.name, true)"
:read-only="doc?.submitted"
/>
<FormControl
@ -175,10 +185,14 @@
<div>{{ tax.account }}</div>
<div>
{{
fyo.format(tax.amount, {
fieldtype: 'Currency',
currency: doc.currency,
})
fyo.format(
tax.amount,
{
fieldtype: 'Currency',
fieldname: 'amount',
},
tax
)
}}
</div>
</div>
@ -220,6 +234,21 @@
<div>{{ formattedValue('grandTotal') }}</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 -->
<hr v-if="doc.outstandingAmount?.float > 0" />
<div
@ -256,6 +285,7 @@ import { computed } from '@vue/reactivity';
import { getDocStatus } from 'models/helpers';
import { ModelNameEnum } from 'models/types';
import Button from 'src/components/Button.vue';
import ExchangeRate from 'src/components/Controls/ExchangeRate.vue';
import FormControl from 'src/components/Controls/FormControl.vue';
import Table from 'src/components/Controls/Table.vue';
import DropdownWithActions from 'src/components/DropdownWithActions.vue';
@ -284,6 +314,7 @@ export default {
Table,
FormContainer,
QuickEditForm,
ExchangeRate,
},
provide() {
return {
@ -342,6 +373,12 @@ export default {
itemDiscountAmount() {
return this.doc.getItemDiscountAmount();
},
fromCurrency() {
return this.doc?.currency ?? this.toCurrency;
},
toCurrency() {
return fyo.singles.SystemSettings.currency;
},
},
activated() {
docsPath.value = docsPathMap[this.schemaName];

View File

@ -56,6 +56,12 @@
</p>
<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>
</template>
<template #secondaryButton>{{ t`Cancel` }}</template>
@ -67,6 +73,7 @@
</template>
<script>
import Button from 'src/components/Button.vue';
import FormControl from 'src/components/Controls/FormControl.vue';
import TwoColumnForm from 'src/components/TwoColumnForm.vue';
import { getErrorMessage } from 'src/utils';
@ -95,6 +102,7 @@ export default {
TwoColumnForm,
FormControl,
Slide,
Button,
},
async mounted() {
this.doc = await getSetupWizardDoc();
@ -103,6 +111,13 @@ export default {
});
},
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) {
return this.doc.schema?.fields.find((f) => f.fieldname === fieldname);
},