Skip to content

Commit

Permalink
Merge pull request #429 from 18alantom/multi-currency-invoicing
Browse files Browse the repository at this point in the history
feat: multi currency invoicing
  • Loading branch information
18alantom committed Oct 3, 2022
2 parents 0048d58 + 2523656 commit 24b6021
Show file tree
Hide file tree
Showing 28 changed files with 950 additions and 750 deletions.
87 changes: 58 additions & 29 deletions fyo/model/doc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
OptionField,
RawValue,
Schema,
TargetField,
TargetField
} from 'schemas/types';
import { getIsNullOrUndef, getMapFromList, getRandomString } from 'utils';
import { markRaw } from 'vue';
Expand All @@ -23,7 +23,7 @@ import {
getMissingMandatoryMessage,
getPreDefaultValues,
setChildDocIdx,
shouldApplyFormula,
shouldApplyFormula
} from './helpers';
import { setName } from './naming';
import {
Expand All @@ -41,7 +41,7 @@ import {
ReadOnlyMap,
RequiredMap,
TreeViewSettings,
ValidationMap,
ValidationMap
} from './types';
import { validateOptions, validateRequired } from './validationFunction';

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 = await this._callAllTableFieldsApplyFormula(fieldname);
changed = (await this._applyFormulaForFields(doc, fieldname)) || changed;

if (changed && retriggerChildDocApplyChange) {
await this._callAllTableFieldsApplyFormula(fieldname);
await this._applyFormulaForFields(doc, fieldname);
}

return changed;
}

async _callAllTableFieldsApplyFormula(
changedFieldname?: string
): Promise<boolean> {
let changed = false;

const childDocs = this.tableFields
.map((f) => (this.get(f.fieldname) as Doc[]) ?? [])
.flat();
for (const { fieldname } of this.tableFields) {
const childDocs = this.get(fieldname) as Doc[];
if (!childDocs) {
continue;
}

// children
for (const row of childDocs) {
changed ||= (await row?._applyFormula()) ?? false;
changed =
(await this._callChildDocApplyFormula(childDocs, changedFieldname)) ||
changed;
}

// parent or child row
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,
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]
);

let changed = false;
for (const field of formulaFields) {
const shouldApply = shouldApplyFormula(field, doc, fieldname);
Expand All @@ -662,7 +691,7 @@ export class Doc extends Observable<DocValue | Doc[]> {
}

doc[field.fieldname] = newVal;
changed = true;
changed ||= true;
}

return changed;
Expand Down
10 changes: 10 additions & 0 deletions fyo/models/SystemSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
19 changes: 16 additions & 3 deletions fyo/utils/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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();
Expand All @@ -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 {
Expand Down
84 changes: 71 additions & 13 deletions models/baseModels/Invoice/Invoice.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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 (
Expand Down Expand Up @@ -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() {
Expand All @@ -139,7 +167,6 @@ export abstract class Invoice extends Transactional {
account: string;
rate: number;
amount: Money;
baseAmount: Money;
[key: string]: DocValue;
}
> = {};
Expand All @@ -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!;
Expand All @@ -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());
}
Expand Down Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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);
}
}
}
Loading

0 comments on commit 24b6021

Please sign in to comment.