mirror of
https://github.com/frappe/books.git
synced 2025-01-22 14:48:25 +00:00
incr: add exchange rate widget
- set instance currency as readOnly - minor refactor in Base
This commit is contained in:
parent
5e458bc033
commit
a8532f05db
@ -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';
|
||||
|
||||
@ -191,6 +191,7 @@ export class Doc extends Observable<DocValue | Doc[]> {
|
||||
if (typeof fieldname === 'object') {
|
||||
return await this.setMultiple(fieldname as DocValueMap);
|
||||
}
|
||||
console.log(fieldname, value);
|
||||
|
||||
if (!this._canSet(fieldname, value)) {
|
||||
return false;
|
||||
|
@ -42,6 +42,14 @@ 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;
|
||||
}
|
||||
|
||||
async validate() {
|
||||
await super.validate();
|
||||
if (
|
||||
@ -126,10 +134,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() {
|
||||
@ -285,7 +295,15 @@ export abstract class Invoice extends Transactional {
|
||||
},
|
||||
dependsOn: ['party'],
|
||||
},
|
||||
exchangeRate: { formula: async () => await this.getExchangeRate() },
|
||||
exchangeRate: {
|
||||
formula: async () => {
|
||||
if (this.exchangeRate && this.exchangeRate !== 1) {
|
||||
return this.exchangeRate;
|
||||
}
|
||||
|
||||
return await this.getExchangeRate();
|
||||
},
|
||||
},
|
||||
netTotal: { formula: async () => this.getSum('items', 'amount', false) },
|
||||
baseNetTotal: {
|
||||
formula: async () => this.netTotal!.mul(this.exchangeRate!),
|
||||
|
@ -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;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -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;
|
||||
|
@ -88,6 +88,7 @@
|
||||
"label": "Currency",
|
||||
"fieldtype": "AutoComplete",
|
||||
"default": "INR",
|
||||
"readOnly": true,
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
|
@ -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() {
|
||||
|
102
src/components/Controls/ExchangeRate.vue
Normal file
102
src/components/Controls/ExchangeRate.vue
Normal 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>
|
@ -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"
|
||||
@ -256,6 +266,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 +295,7 @@ export default {
|
||||
Table,
|
||||
FormContainer,
|
||||
QuickEditForm,
|
||||
ExchangeRate,
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
@ -342,6 +354,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];
|
||||
@ -372,6 +390,7 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
log: console.log,
|
||||
routeTo,
|
||||
toggleInvoiceSettings() {
|
||||
if (!this.schemaName) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user