2
0
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:
18alantom 2022-09-29 15:11:05 +05:30
parent 5e458bc033
commit a8532f05db
8 changed files with 184 additions and 39 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';
@ -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;

View File

@ -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!),

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

@ -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

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

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

@ -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) {