2
0
mirror of https://github.com/frappe/books.git synced 2024-09-20 11:29:00 +00:00

Merge pull request #268 from 18alantom/make-money

refactor: currency handling to use pesa
This commit is contained in:
Alan 2022-01-11 11:33:18 +05:30 committed by GitHub
commit 6a9fd904b5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 1831 additions and 1268 deletions

View File

@ -1,5 +1,4 @@
import frappe from 'frappejs';
import { round } from 'frappejs/utils/numberFormat';
export default class LedgerPosting {
constructor({ reference, party, date, description }) {
@ -16,13 +15,13 @@ export default class LedgerPosting {
async debit(account, amount, referenceType, referenceName) {
const entry = this.getEntry(account, referenceType, referenceName);
entry.debit += amount;
entry.debit = entry.debit.add(amount);
await this.setAccountBalanceChange(account, 'debit', amount);
}
async credit(account, amount, referenceType, referenceName) {
const entry = this.getEntry(account, referenceType, referenceName);
entry.credit += amount;
entry.credit = entry.credit.add(amount);
await this.setAccountBalanceChange(account, 'credit', amount);
}
@ -30,16 +29,16 @@ export default class LedgerPosting {
const debitAccounts = ['Asset', 'Expense'];
const { rootType } = await frappe.getDoc('Account', accountName);
if (debitAccounts.indexOf(rootType) === -1) {
const change = type == 'credit' ? amount : -1 * amount;
const change = type == 'credit' ? amount : amount.neg();
this.accountEntries.push({
name: accountName,
balanceChange: change
balanceChange: change,
});
} else {
const change = type == 'debit' ? amount : -1 * amount;
const change = type == 'debit' ? amount : amount.neg();
this.accountEntries.push({
name: accountName,
balanceChange: change
balanceChange: change,
});
}
}
@ -54,8 +53,8 @@ export default class LedgerPosting {
referenceName: referenceName || this.reference.name,
description: this.description,
reverted: this.reverted,
debit: 0,
credit: 0
debit: frappe.pesa(0),
credit: frappe.pesa(0),
};
this.entries.push(entry);
@ -78,8 +77,8 @@ export default class LedgerPosting {
fields: ['name'],
filters: {
referenceName: this.reference.name,
reverted: 0
}
reverted: 0,
},
});
for (let entry of data) {
@ -96,24 +95,23 @@ export default class LedgerPosting {
entry.reverted = 1;
}
for (let entry of this.accountEntries) {
entry.balanceChange = -1 * entry.balanceChange;
entry.balanceChange = entry.balanceChange.neg();
}
await this.insertEntries();
}
makeRoundOffEntry() {
let { debit, credit } = this.getTotalDebitAndCredit();
let precision = this.getPrecision();
let difference = round(debit - credit, precision);
let absoluteValue = Math.abs(difference);
let difference = debit.sub(credit);
let absoluteValue = difference.abs();
let allowance = 0.5;
if (absoluteValue === 0) {
if (absoluteValue.eq(0)) {
return;
}
let roundOffAccount = this.getRoundOffAccount();
if (absoluteValue <= allowance) {
if (difference > 0) {
if (absoluteValue.lte(allowance)) {
if (difference.gt(0)) {
this.credit(roundOffAccount, absoluteValue);
} else {
this.debit(roundOffAccount, absoluteValue);
@ -123,49 +121,44 @@ export default class LedgerPosting {
validateEntries() {
let { debit, credit } = this.getTotalDebitAndCredit();
if (debit !== credit) {
if (debit.neq(credit)) {
throw new Error(
`Total Debit (${debit}) must be equal to Total Credit (${credit})`
`Total Debit: ${frappe.format(
debit,
'Currency'
)} must be equal to Total Credit: ${frappe.format(credit, 'Currency')}`
);
}
}
getTotalDebitAndCredit() {
let debit = 0;
let credit = 0;
let debit = frappe.pesa(0);
let credit = frappe.pesa(0);
for (let entry of this.entries) {
debit += entry.debit;
credit += entry.credit;
debit = debit.add(entry.debit);
credit = credit.add(entry.credit);
}
let precision = this.getPrecision();
debit = round(debit, precision);
credit = round(credit, precision);
return { debit, credit };
}
async insertEntries() {
for (let entry of this.entries) {
let entryDoc = frappe.newDoc({
doctype: 'AccountingLedgerEntry'
doctype: 'AccountingLedgerEntry',
});
Object.assign(entryDoc, entry);
await entryDoc.insert();
}
for (let entry of this.accountEntries) {
let entryDoc = await frappe.getDoc('Account', entry.name);
entryDoc.balance += entry.balanceChange;
entryDoc.balance = entryDoc.balance.add(entry.balanceChange);
await entryDoc.update();
}
}
getPrecision() {
return frappe.SystemSettings.floatPrecision;
}
getRoundOffAccount() {
return frappe.AccountingSettings.roundOffAccount;
}
};
}

File diff suppressed because it is too large Load Diff

View File

@ -68,7 +68,6 @@ export default {
fieldname: 'balance',
label: 'Balance',
fieldtype: 'Currency',
default: '0',
readOnly: 1,
},
{

View File

@ -32,23 +32,5 @@ export default {
fieldname: 'symbol',
fieldtype: 'Data',
},
{
fieldname: 'numberFormat',
fieldtype: 'Select',
label: 'Number Format',
placeholder: 'Number Format',
options: [
'#,###.##',
'#.###,##',
'# ###.##',
'# ###,##',
"#'###.##",
'#, ###.##',
'#,##,###.##',
'#,###.###',
'#.###',
'#,###',
],
},
],
};

View File

@ -104,9 +104,8 @@ export default {
fieldname: 'rate',
label: 'Rate',
fieldtype: 'Currency',
placeholder: '0.00',
validate(value) {
if (!value) {
if (value.lte(0)) {
throw new frappe.errors.ValidationError(
'Rate must be greater than 0'
);

View File

@ -6,9 +6,9 @@ export default class JournalEntryServer extends BaseDocument {
let entries = new LedgerPosting({ reference: this });
for (let row of this.accounts) {
if (row.debit) {
if (!row.debit.isZero()) {
entries.debit(row.account, row.debit);
} else if (row.credit) {
} else if (!row.credit.isZero()) {
entries.credit(row.account, row.credit);
}
}
@ -31,4 +31,4 @@ export default class JournalEntryServer extends BaseDocument {
async afterRevert() {
await this.getPosting().postReverse();
}
};
}

View File

@ -10,34 +10,35 @@ export default {
target: 'Account',
required: 1,
groupBy: 'rootType',
getFilters: () => ({ isGroup: 0 })
getFilters: () => ({ isGroup: 0 }),
},
{
fieldname: 'debit',
label: 'Debit',
fieldtype: 'Currency',
formula: autoDebitCredit('debit')
formula: autoDebitCredit('debit'),
},
{
fieldname: 'credit',
label: 'Credit',
fieldtype: 'Currency',
formula: autoDebitCredit('credit')
}
formula: autoDebitCredit('credit'),
},
],
tableFields: ['account', 'debit', 'credit']
tableFields: ['account', 'debit', 'credit'],
};
function autoDebitCredit(type = 'debit') {
function autoDebitCredit(type) {
let otherType = type === 'debit' ? 'credit' : 'debit';
return (row, doc) => {
if (row[type] == 0) return null;
if (row[otherType]) return null;
let totalType = doc.getSum('accounts', type);
let totalOtherType = doc.getSum('accounts', otherType);
if (totalType < totalOtherType) {
return totalOtherType - totalType;
return (row, doc) => {
if (!row[otherType].isZero()) return frappe.pesa(0);
let totalType = doc.getSum('accounts', type, false);
let totalOtherType = doc.getSum('accounts', otherType, false);
if (totalType.lt(totalOtherType)) {
return totalOtherType.sub(totalType);
}
};
}

View File

@ -1,5 +1,5 @@
import BaseDocument from 'frappejs/model/document';
import frappe from 'frappejs';
import BaseDocument from 'frappejs/model/document';
export default class PartyServer extends BaseDocument {
beforeInsert() {
@ -8,8 +8,8 @@ export default class PartyServer extends BaseDocument {
method: 'show-dialog',
args: {
title: 'Invalid Entry',
message: 'Select a single party type.'
}
message: 'Select a single party type.',
},
});
throw new Error();
}
@ -23,14 +23,18 @@ export default class PartyServer extends BaseDocument {
let isCustomer = this.customer;
let doctype = isCustomer ? 'SalesInvoice' : 'PurchaseInvoice';
let partyField = isCustomer ? 'customer' : 'supplier';
let { totalOutstanding } = await frappe.db.knex
.sum({ totalOutstanding: 'outstandingAmount' })
const outstandingAmounts = await frappe.db.knex
.select('outstandingAmount')
.from(doctype)
.where('submitted', 1)
.andWhere(partyField, this.name)
.first();
.andWhere(partyField, this.name);
await this.set('outstandingAmount', this.round(totalOutstanding));
const totalOutstanding = outstandingAmounts
.map(({ outstandingAmount }) => frappe.pesa(outstandingAmount))
.reduce((a, b) => a.add(b), frappe.pesa(0));
await this.set('outstandingAmount', totalOutstanding);
await this.update();
}
};
}

View File

@ -109,35 +109,30 @@ export default {
label: 'Amount',
fieldtype: 'Currency',
required: 1,
formula: (doc) => doc.getSum('for', 'amount'),
formula: (doc) => doc.getSum('for', 'amount', false),
validate(value, doc) {
if (value < 0) {
if (value.isNegative()) {
throw new frappe.errors.ValidationError(
frappe._(
`Payment amount cannot be less than zero. Amount has been reset.`
)
frappe._(`Payment amount cannot be less than zero.`)
);
}
if (doc.for.length === 0) return;
const amount = doc.getSum('for', 'amount');
const amount = doc.getSum('for', 'amount', false);
if (value > amount) {
if (value.gt(amount)) {
throw new frappe.errors.ValidationError(
frappe._(
`Payment amount cannot exceed ${frappe.format(
amount,
'Currency'
)}. Amount has been reset.`
)}.`
)
);
} else if (value === 0) {
} else if (value.isZero()) {
throw new frappe.errors.ValidationError(
frappe._(
`Payment amount cannot be ${frappe.format(
value,
'Currency'
)}. Amount has been reset.`
`Payment amount cannot be ${frappe.format(value, 'Currency')}.`
)
);
}
@ -147,7 +142,6 @@ export default {
fieldname: 'writeoff',
label: 'Write Off / Refund',
fieldtype: 'Currency',
default: 0,
},
{
fieldname: 'for',

View File

@ -46,9 +46,9 @@ export default class PaymentServer extends BaseDocument {
}
updateAmountOnReferenceUpdate() {
this.amount = 0;
this.amount = frappe.pesa(0);
for (let paymentReference of this.for) {
this.amount += paymentReference.amount;
this.amount = this.amount.add(paymentReference.amount);
}
}
@ -73,18 +73,19 @@ export default class PaymentServer extends BaseDocument {
if (!this.for?.length) return;
const referenceAmountTotal = this.for
.map(({ amount }) => amount)
.reduce((a, b) => a + b, 0);
.reduce((a, b) => a.add(b), frappe.pesa(0));
if (this.amount + (this.writeoff ?? 0) < referenceAmountTotal) {
if (this.amount.add(this.writeoff ?? 0).lt(referenceAmountTotal)) {
const writeoff = frappe.format(this.writeoff, 'Currency');
const payment = frappe.format(this.amount, 'Currency');
const refAmount = frappe.format(referenceAmountTotal, 'Currency');
const writeoffString =
this.writeoff > 0 ? `and writeoff: ${writeoff} ` : '';
const writeoffString = this.writeoff.gt(0)
? `and writeoff: ${writeoff} `
: '';
throw new Error(
frappe._(
`Amount: ${payment} ${writeoffString}is less than the total amount allocated to references: ${refAmount}`
`Amount: ${payment} ${writeoffString}is less than the total amount allocated to references: ${refAmount}.`
)
);
}
@ -113,18 +114,28 @@ export default class PaymentServer extends BaseDocument {
if (outstandingAmount == null) {
outstandingAmount = baseGrandTotal;
}
if (this.amount <= 0 || this.amount > outstandingAmount) {
if (this.amount.lte(0) || this.amount.gt(outstandingAmount)) {
let message = frappe._(
`Payment amount (${this.amount}) should be less than Outstanding amount (${outstandingAmount}).`
`Payment amount: ${frappe.format(
this.amount,
'Currency'
)} should be less than Outstanding amount: ${frappe.format(
outstandingAmount,
'Currency'
)}.`
);
if (this.amount <= 0) {
const amt = this.amount < 0 ? ` (${this.amount})` : '';
message = frappe._(`Payment amount${amt} should be greater than 0.`);
if (this.amount.lte(0)) {
const amt = frappe.format(this.amount, 'Currency');
message = frappe._(
`Payment amount: ${amt} should be greater than 0.`
);
}
throw new frappe.errors.ValidationError(message);
} else {
// update outstanding amounts in invoice and party
let newOutstanding = outstandingAmount - this.amount;
let newOutstanding = outstandingAmount.sub(this.amount);
await referenceDoc.set('outstandingAmount', newOutstanding);
await referenceDoc.update();
let party = await frappe.getDoc('Party', this.party);
@ -147,7 +158,9 @@ export default class PaymentServer extends BaseDocument {
async updateReferenceOutstandingAmount() {
await this.for.forEach(async ({ amount, referenceType, referenceName }) => {
const refDoc = await frappe.getDoc(referenceType, referenceName);
refDoc.update({ outstandingAmount: refDoc.outstandingAmount + amount });
refDoc.update({
outstandingAmount: refDoc.outstandingAmount.add(amount),
});
});
}
}

View File

@ -1,4 +1,6 @@
import frappe from 'frappejs';
import { _ } from 'frappejs/utils';
const referenceTypeMap = {
SalesInvoice: _('Invoice'),
PurchaseInvoice: _('Bill'),
@ -38,12 +40,13 @@ export default {
fieldname: 'amount',
label: 'Amount',
fieldtype: 'Currency',
placeholder: '0.00',
formula: (row, doc) => {
return doc.getFrom(
row.referenceType,
row.referenceName,
'outstandingAmount'
return (
doc.getFrom(
row.referenceType,
row.referenceName,
'outstandingAmount'
) || frappe.pesa(0)
);
},
required: 1,

View File

@ -1,5 +1,5 @@
import { getActions } from '../Transaction/Transaction';
import InvoiceTemplate from '../SalesInvoice/InvoiceTemplate.vue';
import { getActions } from '../Transaction/Transaction';
import PurchaseInvoice from './PurchaseInvoiceDocument';
export default {
@ -54,14 +54,17 @@ export default {
fieldtype: 'Link',
target: 'Currency',
hidden: 1,
formula: (doc) => doc.getFrom('Party', doc.supplier, 'currency'),
formula: (doc) =>
doc.getFrom('Party', doc.supplier, 'currency') ||
frappe.AccountingSettings.currency,
formulaDependsOn: ['supplier'],
},
{
fieldname: 'exchangeRate',
label: 'Exchange Rate',
fieldtype: 'Float',
formula: (doc) => doc.getExchangeRate(),
default: 1,
formula: async (doc) => await doc.getExchangeRate(),
required: true,
},
{
@ -75,7 +78,7 @@ export default {
fieldname: 'netTotal',
label: 'Net Total',
fieldtype: 'Currency',
formula: (doc) => doc.getSum('items', 'amount'),
formula: (doc) => doc.getSum('items', 'amount', false),
readOnly: 1,
getCurrency: (doc) => doc.currency,
},
@ -83,7 +86,7 @@ export default {
fieldname: 'baseNetTotal',
label: 'Net Total (Company Currency)',
fieldtype: 'Currency',
formula: (doc) => doc.netTotal * doc.exchangeRate,
formula: (doc) => doc.netTotal.mul(doc.exchangeRate),
readOnly: 1,
},
{
@ -106,7 +109,7 @@ export default {
fieldname: 'baseGrandTotal',
label: 'Grand Total (Company Currency)',
fieldtype: 'Currency',
formula: (doc) => doc.grandTotal * doc.exchangeRate,
formula: (doc) => doc.grandTotal.mul(doc.exchangeRate),
readOnly: 1,
},
{

View File

@ -32,7 +32,7 @@ export default {
label: 'Quantity',
fieldtype: 'Float',
required: 1,
formula: () => 1,
default: 1,
},
{
fieldname: 'rate',
@ -40,9 +40,9 @@ export default {
fieldtype: 'Currency',
required: 1,
formula: async (row, doc) => {
const baseRate = (await doc.getFrom('Item', row.item, 'rate')) || 0;
const exchangeRate = doc.exchangeRate ?? 1;
return baseRate / exchangeRate;
const baseRate =
(await doc.getFrom('Item', row.item, 'rate')) || frappe.pesa(0);
return baseRate.div(doc.exchangeRate);
},
getCurrency: (row, doc) => doc.currency,
},
@ -50,7 +50,7 @@ export default {
fieldname: 'baseRate',
label: 'Rate (Company Currency)',
fieldtype: 'Currency',
formula: (row, doc) => row.rate * doc.exchangeRate,
formula: (row, doc) => row.rate.mul(doc.exchangeRate),
readOnly: 1,
},
{
@ -76,7 +76,7 @@ export default {
label: 'Amount',
fieldtype: 'Currency',
readOnly: 1,
formula: (row) => row.quantity * row.rate,
formula: (row) => row.rate.mul(row.quantity),
getCurrency: (row, doc) => doc.currency,
},
{
@ -84,7 +84,7 @@ export default {
label: 'Amount (Company Currency)',
fieldtype: 'Currency',
readOnly: 1,
formula: (row, doc) => row.amount * doc.exchangeRate,
formula: (row, doc) => row.amount.mul(doc.exchangeRate),
},
],
};

View File

@ -53,13 +53,16 @@ export default {
label: 'Customer Currency',
fieldtype: 'Link',
target: 'Currency',
formula: (doc) => doc.getFrom('Party', doc.customer, 'currency'),
formula: (doc) =>
doc.getFrom('Party', doc.customer, 'currency') ||
frappe.AccountingSettings.currency,
formulaDependsOn: ['customer'],
},
{
fieldname: 'exchangeRate',
label: 'Exchange Rate',
fieldtype: 'Float',
default: 1,
formula: (doc) => doc.getExchangeRate(),
readOnly: true,
},
@ -74,7 +77,7 @@ export default {
fieldname: 'netTotal',
label: 'Net Total',
fieldtype: 'Currency',
formula: (doc) => doc.getSum('items', 'amount'),
formula: (doc) => doc.getSum('items', 'amount', false),
readOnly: 1,
getCurrency: (doc) => doc.currency,
},
@ -82,7 +85,7 @@ export default {
fieldname: 'baseNetTotal',
label: 'Net Total (Company Currency)',
fieldtype: 'Currency',
formula: (doc) => doc.netTotal * doc.exchangeRate,
formula: (doc) => doc.netTotal.mul(doc.exchangeRate),
readOnly: 1,
},
{
@ -105,7 +108,7 @@ export default {
fieldname: 'baseGrandTotal',
label: 'Grand Total (Company Currency)',
fieldtype: 'Currency',
formula: (doc) => doc.grandTotal * doc.exchangeRate,
formula: (doc) => doc.grandTotal.mul(doc.exchangeRate),
readOnly: 1,
},
{

View File

@ -34,7 +34,16 @@ export default {
label: 'Quantity',
fieldtype: 'Float',
required: 1,
formula: (row) => row.quantity || 1,
default: 1,
validate(value, doc) {
if (value >= 0) {
return;
}
throw new frappe.errors.ValidationError(
frappe._(`Quantity (${value}) cannot be less than zero.`)
);
},
},
{
fieldname: 'rate',
@ -42,18 +51,29 @@ export default {
fieldtype: 'Currency',
required: 1,
formula: async (row, doc) => {
const baseRate = (await doc.getFrom('Item', row.item, 'rate')) || 0;
const exchangeRate = doc.exchangeRate ?? 1;
return baseRate / exchangeRate;
const baseRate =
(await doc.getFrom('Item', row.item, 'rate')) || frappe.pesa(0);
return baseRate.div(doc.exchangeRate);
},
getCurrency: (row, doc) => doc.currency,
formulaDependsOn: ['item'],
validate(value, doc) {
if (value.gte(0)) {
return;
}
throw new frappe.errors.ValidationError(
frappe._(
`Rate (${frappe.format(value, 'Currency')}) cannot be less zero.`
)
);
},
},
{
fieldname: 'baseRate',
label: 'Rate (Company Currency)',
fieldtype: 'Currency',
formula: (row, doc) => row.rate * doc.exchangeRate,
formula: (row, doc) => row.rate.mul(doc.exchangeRate),
readOnly: 1,
},
{
@ -78,7 +98,7 @@ export default {
label: 'Amount',
fieldtype: 'Currency',
readOnly: 1,
formula: (row) => row.quantity * row.rate,
formula: (row) => row.rate.mul(row.quantity),
getCurrency: (row, doc) => doc.currency,
},
{
@ -86,7 +106,7 @@ export default {
label: 'Amount (Company Currency)',
fieldtype: 'Currency',
readOnly: 1,
formula: (row, doc) => row.amount * doc.exchangeRate,
formula: (row, doc) => row.amount.mul(doc.exchangeRate),
},
],
};

View File

@ -8,26 +8,26 @@ export default {
label: 'Tax Account',
fieldtype: 'Link',
target: 'Account',
required: 1
required: 1,
},
{
fieldname: 'rate',
label: 'Rate',
fieldtype: 'Float',
required: 1
required: 1,
},
{
fieldname: 'amount',
label: 'Amount',
fieldtype: 'Currency',
required: 1
required: 1,
},
{
fieldname: 'baseAmount',
label: 'Amount (Company Currency)',
fieldtype: 'Currency',
formula: (row, doc) => row.amount * doc.exchangeRate,
readOnly: 1
}
]
formula: (row, doc) => row.amount.mul(doc.exchangeRate),
readOnly: 1,
},
],
};

View File

@ -1,11 +1,10 @@
import BaseDocument from 'frappejs/model/document';
import frappe from 'frappejs';
import { round } from 'frappejs/utils/numberFormat';
import BaseDocument from 'frappejs/model/document';
import { getExchangeRate } from '../../../accounting/exchangeRate';
export default class TransactionDocument extends BaseDocument {
async getExchangeRate() {
if (!this.currency) return;
if (!this.currency) return 1.0;
let accountingSettings = await frappe.getSingle('AccountingSettings');
const companyCurrency = accountingSettings.currency;
@ -14,7 +13,7 @@ export default class TransactionDocument extends BaseDocument {
}
return await getExchangeRate({
fromCurrency: this.currency,
toCurrency: companyCurrency
toCurrency: companyCurrency,
});
}
@ -22,31 +21,30 @@ export default class TransactionDocument extends BaseDocument {
let taxes = {};
for (let row of this.items) {
if (row.tax) {
let tax = await this.getTax(row.tax);
for (let d of tax.details) {
let amount = (row.amount * d.rate) / 100;
taxes[d.account] = taxes[d.account] || {
account: d.account,
rate: d.rate,
amount: 0
};
// collect amount
taxes[d.account].amount += amount;
}
if (!row.tax) {
continue;
}
const tax = await this.getTax(row.tax);
for (let d of tax.details) {
taxes[d.account] = taxes[d.account] || {
account: d.account,
rate: d.rate,
amount: frappe.pesa(0),
};
const amount = row.amount.mul(d.rate).div(100);
taxes[d.account].amount = taxes[d.account].amount.add(amount);
}
}
return (
Object.keys(taxes)
.map(account => {
let tax = taxes[account];
tax.baseAmount = round(tax.amount * this.exchangeRate, 2);
return tax;
})
// clear rows with 0 amount
.filter(tax => tax.amount)
);
return Object.keys(taxes)
.map((account) => {
const tax = taxes[account];
tax.baseAmount = tax.amount.mul(this.exchangeRate);
return tax;
})
.filter((tax) => !tax.amount.isZero());
}
async getTax(tax) {
@ -56,13 +54,8 @@ export default class TransactionDocument extends BaseDocument {
}
async getGrandTotal() {
let grandTotal = this.netTotal;
if (this.taxes) {
for (let row of this.taxes) {
grandTotal += row.amount;
}
}
return grandTotal;
return (this.taxes || [])
.map(({ amount }) => amount)
.reduce((a, b) => a.add(b), this.netTotal);
}
};
}

View File

@ -0,0 +1,43 @@
import frappe from 'frappejs';
function getTablesToConvert() {
// Do not change loops to map, doesn't work for some reason.
const toConvert = [];
for (let key in frappe.models) {
const model = frappe.models[key];
const fieldsToConvert = [];
for (let i in model.fields) {
const field = model.fields[i];
if (field.fieldtype === 'Currency') {
fieldsToConvert.push(field.fieldname);
}
}
if (fieldsToConvert.length > 0 && !model.isSingle && !model.basedOn) {
toConvert.push({ name: key, fields: fieldsToConvert });
}
}
return toConvert;
}
export default async function execute() {
const toConvert = getTablesToConvert();
for (let { name, fields } of toConvert) {
const rows = await frappe.db.knex(name);
const convertedRows = rows.map((row) => {
for (let field of fields) {
row[field] = frappe.pesa(row[field] ?? 0).store;
}
if ('numberFormat' in row) {
delete row.numberFormat;
}
return row;
});
await frappe.db.prestigeTheTable(name, convertedRows);
}
}

View File

@ -3,5 +3,10 @@
"version": "0.0.3",
"fileName": "makePaymentRefIdNullable",
"beforeMigrate": true
},
{
"version": "0.0.4",
"fileName": "convertCurrencyToStrings",
"beforeMigrate": true
}
]

View File

@ -1,6 +1,6 @@
import frappe from 'frappejs';
import { DateTime } from 'luxon';
import { unique } from 'frappejs/utils';
import { convertPesaValuesToFloat } from '../../src/utils';
export async function getData({
rootType,
@ -8,7 +8,7 @@ export async function getData({
fromDate,
toDate,
periodicity = 'Monthly',
accumulateValues = false
accumulateValues = false,
}) {
let accounts = await getAccounts(rootType);
let fiscalYear = await getFiscalYear();
@ -17,19 +17,19 @@ export async function getData({
for (let account of accounts) {
const entries = ledgerEntries.filter(
entry => entry.account === account.name
(entry) => entry.account === account.name
);
for (let entry of entries) {
let periodKey = getPeriodKey(entry.date, periodicity);
if (!account[periodKey]) {
account[periodKey] = 0.0;
account[periodKey] = frappe.pesa(0.0);
}
const multiplier = balanceMustBe === 'Debit' ? 1 : -1;
const value = (entry.debit - entry.credit) * multiplier;
account[periodKey] += value;
const value = entry.debit.sub(entry.credit).mul(multiplier);
account[periodKey] = value.add(account[periodKey]);
}
}
@ -40,28 +40,34 @@ export async function getData({
for (let account of accounts) {
if (!account[periodKey]) {
account[periodKey] = 0.0;
account[periodKey] = frappe.pesa(0.0);
}
account[periodKey] += account[previousPeriodKey] || 0.0;
account[periodKey] = account[periodKey].add(
account[previousPeriodKey] ?? 0
);
}
});
}
// calculate totalRow
let totalRow = {
account: `Total ${rootType} (${balanceMustBe})`
account: `Total ${rootType} (${balanceMustBe})`,
};
periodList.forEach(periodKey => {
periodList.forEach((periodKey) => {
if (!totalRow[periodKey]) {
totalRow[periodKey] = 0.0;
totalRow[periodKey] = frappe.pesa(0.0);
}
for (let account of accounts) {
totalRow[periodKey] += account[periodKey] || 0.0;
totalRow[periodKey] = totalRow[periodKey].add(account[periodKey] ?? 0.0);
}
});
convertPesaValuesToFloat(totalRow);
accounts.forEach(convertPesaValuesToFloat);
return { accounts, totalRow, periodList };
}
@ -71,42 +77,50 @@ export async function getTrialBalance({ rootType, fromDate, toDate }) {
for (let account of accounts) {
const accountEntries = ledgerEntries.filter(
entry => entry.account === account.name
(entry) => entry.account === account.name
);
// opening
const beforePeriodEntries = accountEntries.filter(
entry => entry.date < fromDate
(entry) => entry.date < fromDate
);
account.opening = beforePeriodEntries.reduce(
(acc, entry) => acc.add(entry.debit).sub(entry.credit),
frappe.pesa(0)
);
account.opening = beforePeriodEntries.reduce((acc, entry) => {
return acc + (entry.debit - entry.credit);
}, 0);
if (account.opening >= 0) {
if (account.opening.gte(0)) {
account.openingDebit = account.opening;
account.openingCredit = frappe.pesa(0);
} else {
account.openingCredit = -account.opening;
account.openingCredit = account.opening.neg();
account.openingDebit = frappe.pesa(0);
}
// debit / credit
const periodEntries = accountEntries.filter(
entry => entry.date >= fromDate && entry.date < toDate
(entry) => entry.date >= fromDate && entry.date < toDate
);
account.debit = periodEntries.reduce(
(acc, entry) => acc.add(entry.debit),
frappe.pesa(0)
);
account.debit = periodEntries.reduce((acc, entry) => acc + entry.debit, 0);
account.credit = periodEntries.reduce(
(acc, entry) => acc + entry.credit,
0
(acc, entry) => acc.add(entry.credit),
frappe.pesa(0)
);
// closing
account.closing = account.opening + account.debit - account.credit;
account.closing = account.opening.add(account.debit).sub(account.credit);
if (account.closing >= 0) {
if (account.closing.gte(0)) {
account.closingDebit = account.closing;
account.closingCredit = frappe.pesa(0);
} else {
account.closingCredit = -account.closing;
account.closingCredit = account.closing.neg();
account.closingDebit = frappe.pesa(0);
}
if (account.debit != 0 || account.credit != 0) {
if (account.debit.neq(0) || account.credit.neq(0)) {
setParentEntry(account, account.parentAccount);
}
}
@ -114,13 +128,13 @@ export async function getTrialBalance({ rootType, fromDate, toDate }) {
function setParentEntry(leafAccount, parentName) {
for (let acc of accounts) {
if (acc.name === parentName) {
acc.debit += leafAccount.debit;
acc.credit += leafAccount.credit;
acc.closing = acc.opening + acc.debit - acc.credit;
if (acc.closing >= 0) {
acc.debit = acc.debit.add(leafAccount.debit);
acc.credit = acc.credit.add(leafAccount.credit);
acc.closing = acc.opening.add(acc.debit).sub(acc.credit);
if (acc.closing.gte(0)) {
acc.closingDebit = acc.closing;
} else {
acc.closingCredit = -acc.closing;
acc.closingCredit = acc.closing.neg();
}
if (acc.parentAccount) {
setParentEntry(leafAccount, acc.parentAccount);
@ -131,6 +145,7 @@ export async function getTrialBalance({ rootType, fromDate, toDate }) {
}
}
accounts.forEach(convertPesaValuesToFloat);
return accounts;
}
@ -143,7 +158,7 @@ export function getPeriodList(fromDate, toDate, periodicity, fiscalYear) {
Monthly: 1,
Quarterly: 3,
'Half Yearly': 6,
Yearly: 12
Yearly: 12,
}[periodicity];
let startDate = DateTime.fromISO(fromDate).startOf('month');
@ -173,13 +188,13 @@ function getPeriodKey(date, periodicity) {
1: `Jan ${year} - Mar ${year}`,
2: `Apr ${year} - Jun ${year}`,
3: `Jun ${year} - Sep ${year}`,
4: `Oct ${year} - Dec ${year}`
4: `Oct ${year} - Dec ${year}`,
}[quarter];
},
'Half Yearly': () => {
return {
1: `Apr ${year} - Sep ${year}`,
2: `Oct ${year} - Mar ${year}`
2: `Oct ${year} - Mar ${year}`,
}[[2, 3].includes(quarter) ? 1 : 2];
},
Yearly: () => {
@ -187,7 +202,7 @@ function getPeriodKey(date, periodicity) {
return `${year} - ${year + 1}`;
}
return `${year - 1} - ${year}`;
}
},
}[periodicity];
return getKey();
@ -200,7 +215,7 @@ function setIndentLevel(accounts, parentAccount, level) {
level = 0;
}
accounts.forEach(account => {
accounts.forEach((account) => {
if (
account.parentAccount === parentAccount &&
account.indent === undefined
@ -220,7 +235,7 @@ function sortAccounts(accounts) {
pushToOut(null);
function pushToOut(parentAccount) {
accounts.forEach(account => {
accounts.forEach((account) => {
if (account.parentAccount === parentAccount && !pushed[account.name]) {
out.push(account);
pushed[account.name] = 1;
@ -247,9 +262,9 @@ async function getLedgerEntries(fromDate, toDate, accounts) {
doctype: 'AccountingLedgerEntry',
fields: ['account', 'debit', 'credit', 'date'],
filters: {
account: ['in', accounts.map(d => d.name)],
date: dateFilter()
}
account: ['in', accounts.map((d) => d.name)],
date: dateFilter(),
},
});
return ledgerEntries;
@ -260,14 +275,14 @@ async function getAccounts(rootType) {
doctype: 'Account',
fields: ['name', 'parentAccount', 'isGroup'],
filters: {
rootType
}
rootType,
},
});
accounts = setIndentLevel(accounts);
accounts = sortAccounts(accounts);
accounts.forEach(account => {
accounts.forEach((account) => {
account.account = account.name;
});
@ -280,12 +295,12 @@ async function getFiscalYear() {
);
return {
start: fiscalYearStart,
end: fiscalYearEnd
end: fiscalYearEnd,
};
}
export default {
getData,
getTrialBalance,
getPeriodList
getPeriodList,
};

View File

@ -29,7 +29,14 @@ class GeneralLedger {
],
filters: filters,
})
).filter((d) => !d.reverted || (d.reverted && params.reverted));
)
.filter((d) => !d.reverted || (d.reverted && params.reverted))
.map((row) => {
row.debit = row.debit.float;
row.credit = row.credit.float;
return row;
});
return this.appendOpeningEntry(data);
}

View File

@ -1,5 +1,6 @@
import frappe from 'frappejs';
import { stateCodeMap } from '../../accounting/gst';
import { convertPesaValuesToFloat } from '../../src/utils';
class BaseGSTR {
async getCompleteReport(gstrType, filters) {
@ -30,6 +31,7 @@ class BaseGSTR {
});
}
tableData.forEach(convertPesaValuesToFloat);
return tableData;
} else {
return [];
@ -63,7 +65,7 @@ class BaseGSTR {
ledgerEntry.taxes?.forEach((tax) => {
row.rate += tax.rate;
const taxAmt = (tax.rate * ledgerEntry.netTotal) / 100;
const taxAmt = ledgerEntry.netTotal.percent(tax.rate);
switch (tax.account) {
case 'IGST': {

View File

@ -10,14 +10,17 @@
:value="value"
:placeholder="inputPlaceholder"
:readonly="isReadOnly"
@blur="e => triggerChange(e.target.value)"
@focus="e => $emit('focus', e)"
@input="e => $emit('input', e)"
:max="df.maxValue"
:min="df.minValue"
@blur="(e) => triggerChange(e.target.value)"
@focus="(e) => $emit('focus', e)"
@input="(e) => $emit('input', e)"
/>
</div>
</template>
<script>
import { showMessageDialog } from '../../utils';
export default {
name: 'Base',
props: [
@ -28,18 +31,18 @@ export default {
'size',
'showLabel',
'readOnly',
'autofocus'
'autofocus',
],
inject: {
doctype: {
default: null
default: null,
},
name: {
default: null
default: null,
},
doc: {
default: null
}
default: null,
},
},
mounted() {
if (this.autofocus) {
@ -55,9 +58,9 @@ export default {
{
'px-3 py-2': this.size !== 'small',
'px-2 py-1': this.size === 'small',
'pointer-events-none': this.isReadOnly
'pointer-events-none': this.isReadOnly,
},
'focus:outline-none focus:bg-gray-200 rounded w-full text-gray-900 placeholder-gray-400'
'focus:outline-none focus:bg-gray-200 rounded w-full text-gray-900 placeholder-gray-400',
];
return this.getInputClassesFromProp(classes);
@ -70,7 +73,7 @@ export default {
return this.readOnly;
}
return this.df.readOnly;
}
},
},
methods: {
getInputClassesFromProp(classes) {
@ -90,9 +93,11 @@ export default {
},
triggerChange(value) {
value = this.parse(value);
if (value === '') {
value = null;
}
this.$emit('change', value);
},
parse(value) {
@ -100,7 +105,13 @@ export default {
},
isNumeric(df) {
return ['Int', 'Float', 'Currency'].includes(df.fieldtype);
}
}
},
},
};
</script>
<style>
input[type='number']::-webkit-inner-spin-button {
appearance: none;
}
</style>

View File

@ -8,12 +8,12 @@
ref="input"
:class="inputClasses"
:type="inputType"
:value="value"
:value="value.round()"
:placeholder="inputPlaceholder"
:readonly="isReadOnly"
@blur="onBlur"
@focus="onFocus"
@input="e => $emit('input', e)"
@input="(e) => $emit('input', e)"
/>
<div
v-show="!showInput"
@ -37,7 +37,7 @@ export default {
data() {
return {
showInput: false,
currencySymbol: ''
currencySymbol: '',
};
},
methods: {
@ -45,21 +45,29 @@ export default {
this.showInput = true;
this.$emit('focus', e);
},
parse(value) {
return frappe.pesa(value);
},
onBlur(e) {
let { value } = e.target;
if (value !== 0 && !value) {
value = frappe.pesa(0).round();
}
this.showInput = false;
this.triggerChange(e.target.value);
this.triggerChange(value);
},
activateInput() {
this.showInput = true;
this.$nextTick(() => {
this.focus();
});
}
},
},
computed: {
formattedValue() {
return frappe.format(this.value, this.df, this.doc);
}
}
},
},
};
</script>

View File

@ -4,11 +4,16 @@ import Int from './Int';
export default {
name: 'Float',
extends: Int,
computed: {
inputType() {
return 'number';
}
},
methods: {
parse(value) {
let parsedValue = parseFloat(value);
return isNaN(parsedValue) ? 0 : parsedValue;
}
}
},
},
};
</script>

View File

@ -4,11 +4,16 @@ import Data from './Data';
export default {
name: 'Int',
extends: Data,
computed: {
inputType() {
return 'number';
},
},
methods: {
parse(value) {
let parsedValue = parseInt(value, 10);
return isNaN(parsedValue) ? 0 : parsedValue;
}
}
},
},
};
</script>

View File

@ -20,22 +20,29 @@
v-for="df in tableFields"
:df="df"
:value="row[df.fieldname]"
@change="value => row.set(df.fieldname, value)"
@new-doc="doc => row.set(df.fieldname, doc.name)"
@change="(value) => onChange(df, value)"
@new-doc="(doc) => row.set(df.fieldname, doc.name)"
/>
<div
class="text-sm text-red-600 mb-2 pl-2 col-span-full"
v-if="Object.values(errors).length"
>
{{ getErrorString() }}
</div>
</Row>
</template>
<script>
import FormControl from './FormControl';
import { getErrorMessage } from '../../utils';
import Row from '@/components/Row';
export default {
name: 'TableRow',
props: ['row', 'tableFields', 'size', 'ratio', 'isNumeric'],
components: {
Row
Row,
},
data: () => ({ hovering: false }),
data: () => ({ hovering: false, errors: {} }),
beforeCreate() {
this.$options.components.FormControl = FormControl;
},
@ -43,8 +50,28 @@ export default {
return {
doctype: this.row.doctype,
name: this.row.name,
doc: this.row
doc: this.row,
};
}
},
methods: {
onChange(df, value) {
if (value == null) {
return;
}
this.$set(this.errors, df.fieldname, null);
const oldValue = this.row.get(df.fieldname);
if (oldValue === value) {
return;
}
this.row.set(df.fieldname, value).catch((e) => {
this.$set(this.errors, df.fieldname, getErrorMessage(e, this.row));
});
},
getErrorString() {
return Object.values(this.errors).join(' ');
},
},
};
</script>

View File

@ -162,6 +162,7 @@ let TwoColumnForm = {
let oldValue = this.doc.get(df.fieldname);
this.$set(this.errors, df.fieldname, null);
if (oldValue === value) {
return;
}
@ -171,9 +172,6 @@ let TwoColumnForm = {
return this.doc.rename(value);
}
// reset error messages
this.$set(this.errors, df.fieldname, null);
this.doc.set(df.fieldname, value).catch((e) => {
// set error message for this field
this.$set(this.errors, df.fieldname, getErrorMessage(e, this.doc));

View File

@ -6,7 +6,7 @@ import regionalModelUpdates from '../models/regionalModelUpdates';
import postStart from '../server/postStart';
import { DB_CONN_FAILURE } from './messages';
import migrate from './migrate';
import { getSavePath } from './utils';
import { callInitializeMoneyMaker, getSavePath } from './utils';
export async function createNewDatabase() {
const { canceled, filePath } = await getSavePath('books', 'db');
@ -49,6 +49,9 @@ export async function connectToLocalDatabase(filePath) {
return { connectionSuccess: false, reason: DB_CONN_FAILURE.CANT_CONNECT };
}
// first init no currency, for migratory needs
await callInitializeMoneyMaker();
try {
await runRegionalModelUpdates();
} catch (error) {
@ -87,6 +90,9 @@ export async function connectToLocalDatabase(filePath) {
// set last selected file
config.set('lastSelectedFilePath', filePath);
// second init with currency, normal usage
await callInitializeMoneyMaker();
return { connectionSuccess: true, reason: '' };
}

View File

@ -143,7 +143,7 @@ export default {
heatLine: 1,
},
tooltipOptions: {
formatTooltipY: (value) => frappe.format(value, 'Currency'),
formatTooltipY: (value) => frappe.format(value ?? 0, 'Currency'),
},
data: {
labels: periodList.map((p) => p.split(' ')[0]),

View File

@ -84,9 +84,10 @@ export default {
.select('name')
.from('Account')
.where('rootType', 'Expense');
let topExpenses = await frappe.db.knex
.select({
total: frappe.db.knex.raw('sum(??) - sum(??)', ['debit', 'credit']),
total: frappe.db.knex.raw('sum(cast(?? as real)) - sum(cast(?? as real))', ['debit', 'credit']),
})
.select('account')
.from('AccountingLedgerEntry')

View File

@ -5,7 +5,7 @@
<PeriodSelector
slot="action"
:value="period"
@change="value => (period = value)"
@change="(value) => (period = value)"
/>
</SectionHeader>
<div v-show="hasData" class="chart-wrapper" ref="profit-and-loss"></div>
@ -28,14 +28,14 @@ export default {
name: 'ProfitAndLoss',
components: {
PeriodSelector,
SectionHeader
SectionHeader,
},
data: () => ({ period: 'This Year', hasData: false }),
activated() {
this.render();
},
watch: {
period: 'render'
period: 'render',
},
methods: {
async render() {
@ -47,11 +47,11 @@ export default {
let res = await pl.run({
fromDate,
toDate,
periodicity
periodicity,
});
let totalRow = res.rows[res.rows.length - 1];
this.hasData = res.columns.some(key => totalRow[key] !== 0);
this.hasData = res.columns.some((key) => totalRow[key] !== 0);
if (!this.hasData) return;
this.$nextTick(() => this.renderChart(res));
},
@ -66,10 +66,10 @@ export default {
axisOptions: {
xAxisMode: 'tick',
shortenYAxisNumbers: true,
xIsSeries: true
xIsSeries: true,
},
tooltipOptions: {
formatTooltipY: value => frappe.format(value, 'Currency')
formatTooltipY: (value) => frappe.format(value ?? 0, 'Currency'),
},
data: {
labels: res.columns,
@ -77,12 +77,12 @@ export default {
{
name: 'Income',
chartType: 'bar',
values: res.columns.map(key => totalRow[key])
}
]
}
values: res.columns.map((key) => totalRow[key]),
},
],
},
});
}
}
},
},
};
</script>

View File

@ -11,7 +11,7 @@
v-if="invoice.hasData"
slot="action"
:value="$data[invoice.periodKey]"
@change="value => ($data[invoice.periodKey] = value)"
@change="(value) => ($data[invoice.periodKey] = value)"
/>
<Button
v-else
@ -73,14 +73,14 @@ import Button from '@/components/Button';
import PeriodSelector from './PeriodSelector';
import SectionHeader from './SectionHeader';
import { getDatesAndPeriodicity } from './getDatesAndPeriodicity';
import { routeTo } from '@/utils'
import { routeTo } from '@/utils';
export default {
name: 'UnpaidInvoices',
components: {
PeriodSelector,
SectionHeader,
Button
Button,
},
data: () => ({
invoices: [
@ -93,7 +93,7 @@ export default {
color: 'blue',
periodKey: 'salesInvoicePeriod',
hasData: false,
barWidth: 40
barWidth: 40,
},
{
title: 'Bills',
@ -104,22 +104,22 @@ export default {
color: 'gray',
periodKey: 'purchaseInvoicePeriod',
hasData: false,
barWidth: 60
}
barWidth: 60,
},
],
salesInvoicePeriod: 'This Year',
purchaseInvoicePeriod: 'This Year'
purchaseInvoicePeriod: 'This Year',
}),
watch: {
salesInvoicePeriod: 'calculateInvoiceTotals',
purchaseInvoicePeriod: 'calculateInvoiceTotals'
purchaseInvoicePeriod: 'calculateInvoiceTotals',
},
activated() {
this.calculateInvoiceTotals();
},
methods: {
async calculateInvoiceTotals() {
let promises = this.invoices.map(async d => {
let promises = this.invoices.map(async (d) => {
let { fromDate, toDate } = await getDatesAndPeriodicity(
this.$data[d.periodKey]
);
@ -133,11 +133,11 @@ export default {
.first();
let { total, outstanding } = result;
d.total = total;
d.unpaid = outstanding;
d.total = total ?? 0;
d.unpaid = outstanding ?? 0;
d.paid = total - outstanding;
d.hasData = (d.total || 0) !== 0;
d.barWidth = (d.paid / d.total) * 100;
d.hasData = d.total !== 0;
d.barWidth = (d.paid / (d.total || 1)) * 100;
return d;
});
@ -146,7 +146,7 @@ export default {
async newInvoice(invoice) {
let doc = await frappe.getNewDoc(invoice.doctype);
routeTo(`/edit/${invoice.doctype}/${doc.name}`);
}
}
},
},
};
</script>

View File

@ -134,7 +134,7 @@ export default {
label: _('System'),
icon: 'system',
description:
'Setup system defaults like date format and currency precision',
'Setup system defaults like date format and display precision',
fieldname: 'systemSetup',
action() {
openSettings('System');

View File

@ -1,8 +1,10 @@
import config from '@/config';
import frappe from 'frappejs';
import { DEFAULT_LOCALE } from 'frappejs/utils/consts';
import countryList from '~/fixtures/countryInfo.json';
import generateTaxes from '../../../models/doctype/Tax/RegionalEntries';
import regionalModelUpdates from '../../../models/regionalModelUpdates';
import { callInitializeMoneyMaker } from '../../utils';
export default async function setupCompany(setupWizardValues) {
const {
@ -17,6 +19,10 @@ export default async function setupCompany(setupWizardValues) {
} = setupWizardValues;
const accountingSettings = frappe.AccountingSettings;
const currency = countryList[country]['currency'];
const locale = countryList[country]['locale'] ?? DEFAULT_LOCALE;
await callInitializeMoneyMaker(currency);
await accountingSettings.update({
companyName,
country,
@ -25,7 +31,7 @@ export default async function setupCompany(setupWizardValues) {
bankName,
fiscalYearStart,
fiscalYearEnd,
currency: countryList[country]['currency'],
currency,
});
const printSettings = await frappe.getSingle('PrintSettings');
@ -43,6 +49,8 @@ export default async function setupCompany(setupWizardValues) {
await accountingSettings.update({ setupComplete: 1 });
frappe.AccountingSettings = accountingSettings;
(await frappe.getSingle('SystemSettings')).update({ locale });
}
async function setupGlobalCurrencies(countries) {
@ -55,7 +63,6 @@ async function setupGlobalCurrencies(countries) {
currency_fraction_units: fractionUnits,
smallest_currency_fraction_value: smallestValue,
currency_symbol: symbol,
number_format: numberFormat,
} = country;
if (!currency || queue.includes(currency)) {
@ -69,7 +76,6 @@ async function setupGlobalCurrencies(countries) {
fractionUnits,
smallestValue,
symbol,
numberFormat: numberFormat || '#,###.##',
};
const doc = checkAndCreateDoc(docObject);

View File

@ -3,7 +3,7 @@ import Toast from '@/components/Toast';
import router from '@/router';
import { ipcRenderer } from 'electron';
import frappe from 'frappejs';
import { _ } from 'frappejs/utils';
import { isPesa, _ } from 'frappejs/utils';
import lodash from 'lodash';
import Vue from 'vue';
import { IPC_ACTIONS, IPC_MESSAGES } from './messages';
@ -152,7 +152,7 @@ export function openQuickEdit({ doctype, name, hideFields, defaults = {} }) {
}
export function getErrorMessage(e, doc) {
let errorMessage = e.message || _('An error occurred');
let errorMessage = e.message || _('An error occurred.');
const { doctype, name } = doc;
const canElaborate = doctype && name;
if (e.type === frappe.errors.LinkValidationError && canElaborate) {
@ -258,7 +258,7 @@ export function getInvoiceStatus(doc) {
if (!doc.submitted) {
status = 'Draft';
}
if (doc.submitted === 1 && doc.outstandingAmount === 0.0) {
if (doc.submitted === 1 && doc.outstandingAmount.isZero()) {
status = 'Paid';
}
if (doc.cancelled === 1) {
@ -351,3 +351,51 @@ export function titleCase(phrase) {
})
.join(' ');
}
export async function getIsSetupComplete() {
try {
const { setupComplete } = await frappe.getSingle('AccountingSettings');
return !!setupComplete;
} catch {
return false;
}
}
export async function getCurrency() {
let currency = frappe?.AccountingSettings?.currency ?? undefined;
if (!currency) {
try {
currency = (
await frappe.db.getSingleValues({
fieldname: 'currency',
parent: 'AccountingSettings',
})
)[0].value;
} catch (err) {
currency = undefined;
}
}
return currency;
}
export async function callInitializeMoneyMaker(currency) {
currency ??= await getCurrency();
if (!currency && frappe.pesa) {
return;
}
if (currency && frappe.pesa().options.currency === currency) {
return;
}
await frappe.initializeMoneyMaker(currency);
}
export function convertPesaValuesToFloat(obj) {
Object.keys(obj).forEach((key) => {
if (!isPesa(obj[key])) return;
obj[key] = obj[key].float;
});
}

View File

@ -53,6 +53,9 @@ module.exports = {
lg: '0.5rem', // 8px
xl: '0.75rem', // 12px
},
gridColumn: {
'span-full': '1 / -1',
},
colors: {
brand: '#2490EF',
'brand-100': '#f4f9ff',