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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

@ -10,34 +10,35 @@ export default {
target: 'Account', target: 'Account',
required: 1, required: 1,
groupBy: 'rootType', groupBy: 'rootType',
getFilters: () => ({ isGroup: 0 }) getFilters: () => ({ isGroup: 0 }),
}, },
{ {
fieldname: 'debit', fieldname: 'debit',
label: 'Debit', label: 'Debit',
fieldtype: 'Currency', fieldtype: 'Currency',
formula: autoDebitCredit('debit') formula: autoDebitCredit('debit'),
}, },
{ {
fieldname: 'credit', fieldname: 'credit',
label: 'Credit', label: 'Credit',
fieldtype: 'Currency', 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'; 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); return (row, doc) => {
let totalOtherType = doc.getSum('accounts', otherType); if (!row[otherType].isZero()) return frappe.pesa(0);
if (totalType < totalOtherType) {
return totalOtherType - totalType; 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 frappe from 'frappejs';
import BaseDocument from 'frappejs/model/document';
export default class PartyServer extends BaseDocument { export default class PartyServer extends BaseDocument {
beforeInsert() { beforeInsert() {
@ -8,8 +8,8 @@ export default class PartyServer extends BaseDocument {
method: 'show-dialog', method: 'show-dialog',
args: { args: {
title: 'Invalid Entry', title: 'Invalid Entry',
message: 'Select a single party type.' message: 'Select a single party type.',
} },
}); });
throw new Error(); throw new Error();
} }
@ -23,14 +23,18 @@ export default class PartyServer extends BaseDocument {
let isCustomer = this.customer; let isCustomer = this.customer;
let doctype = isCustomer ? 'SalesInvoice' : 'PurchaseInvoice'; let doctype = isCustomer ? 'SalesInvoice' : 'PurchaseInvoice';
let partyField = isCustomer ? 'customer' : 'supplier'; let partyField = isCustomer ? 'customer' : 'supplier';
let { totalOutstanding } = await frappe.db.knex
.sum({ totalOutstanding: 'outstandingAmount' }) const outstandingAmounts = await frappe.db.knex
.select('outstandingAmount')
.from(doctype) .from(doctype)
.where('submitted', 1) .where('submitted', 1)
.andWhere(partyField, this.name) .andWhere(partyField, this.name);
.first();
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(); await this.update();
} }
}; }

View File

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

View File

@ -46,9 +46,9 @@ export default class PaymentServer extends BaseDocument {
} }
updateAmountOnReferenceUpdate() { updateAmountOnReferenceUpdate() {
this.amount = 0; this.amount = frappe.pesa(0);
for (let paymentReference of this.for) { 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; if (!this.for?.length) return;
const referenceAmountTotal = this.for const referenceAmountTotal = this.for
.map(({ amount }) => amount) .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 writeoff = frappe.format(this.writeoff, 'Currency');
const payment = frappe.format(this.amount, 'Currency'); const payment = frappe.format(this.amount, 'Currency');
const refAmount = frappe.format(referenceAmountTotal, 'Currency'); const refAmount = frappe.format(referenceAmountTotal, 'Currency');
const writeoffString = const writeoffString = this.writeoff.gt(0)
this.writeoff > 0 ? `and writeoff: ${writeoff} ` : ''; ? `and writeoff: ${writeoff} `
: '';
throw new Error( throw new Error(
frappe._( 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) { if (outstandingAmount == null) {
outstandingAmount = baseGrandTotal; outstandingAmount = baseGrandTotal;
} }
if (this.amount <= 0 || this.amount > outstandingAmount) { if (this.amount.lte(0) || this.amount.gt(outstandingAmount)) {
let message = frappe._( 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})` : ''; if (this.amount.lte(0)) {
message = frappe._(`Payment amount${amt} should be greater than 0.`); const amt = frappe.format(this.amount, 'Currency');
message = frappe._(
`Payment amount: ${amt} should be greater than 0.`
);
} }
throw new frappe.errors.ValidationError(message); throw new frappe.errors.ValidationError(message);
} else { } else {
// update outstanding amounts in invoice and party // 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.set('outstandingAmount', newOutstanding);
await referenceDoc.update(); await referenceDoc.update();
let party = await frappe.getDoc('Party', this.party); let party = await frappe.getDoc('Party', this.party);
@ -147,7 +158,9 @@ export default class PaymentServer extends BaseDocument {
async updateReferenceOutstandingAmount() { async updateReferenceOutstandingAmount() {
await this.for.forEach(async ({ amount, referenceType, referenceName }) => { await this.for.forEach(async ({ amount, referenceType, referenceName }) => {
const refDoc = await frappe.getDoc(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'; import { _ } from 'frappejs/utils';
const referenceTypeMap = { const referenceTypeMap = {
SalesInvoice: _('Invoice'), SalesInvoice: _('Invoice'),
PurchaseInvoice: _('Bill'), PurchaseInvoice: _('Bill'),
@ -38,12 +40,13 @@ export default {
fieldname: 'amount', fieldname: 'amount',
label: 'Amount', label: 'Amount',
fieldtype: 'Currency', fieldtype: 'Currency',
placeholder: '0.00',
formula: (row, doc) => { formula: (row, doc) => {
return doc.getFrom( return (
row.referenceType, doc.getFrom(
row.referenceName, row.referenceType,
'outstandingAmount' row.referenceName,
'outstandingAmount'
) || frappe.pesa(0)
); );
}, },
required: 1, required: 1,

View File

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

View File

@ -32,7 +32,7 @@ export default {
label: 'Quantity', label: 'Quantity',
fieldtype: 'Float', fieldtype: 'Float',
required: 1, required: 1,
formula: () => 1, default: 1,
}, },
{ {
fieldname: 'rate', fieldname: 'rate',
@ -40,9 +40,9 @@ export default {
fieldtype: 'Currency', fieldtype: 'Currency',
required: 1, required: 1,
formula: async (row, doc) => { formula: async (row, doc) => {
const baseRate = (await doc.getFrom('Item', row.item, 'rate')) || 0; const baseRate =
const exchangeRate = doc.exchangeRate ?? 1; (await doc.getFrom('Item', row.item, 'rate')) || frappe.pesa(0);
return baseRate / exchangeRate; return baseRate.div(doc.exchangeRate);
}, },
getCurrency: (row, doc) => doc.currency, getCurrency: (row, doc) => doc.currency,
}, },
@ -50,7 +50,7 @@ export default {
fieldname: 'baseRate', fieldname: 'baseRate',
label: 'Rate (Company Currency)', label: 'Rate (Company Currency)',
fieldtype: 'Currency', fieldtype: 'Currency',
formula: (row, doc) => row.rate * doc.exchangeRate, formula: (row, doc) => row.rate.mul(doc.exchangeRate),
readOnly: 1, readOnly: 1,
}, },
{ {
@ -76,7 +76,7 @@ export default {
label: 'Amount', label: 'Amount',
fieldtype: 'Currency', fieldtype: 'Currency',
readOnly: 1, readOnly: 1,
formula: (row) => row.quantity * row.rate, formula: (row) => row.rate.mul(row.quantity),
getCurrency: (row, doc) => doc.currency, getCurrency: (row, doc) => doc.currency,
}, },
{ {
@ -84,7 +84,7 @@ export default {
label: 'Amount (Company Currency)', label: 'Amount (Company Currency)',
fieldtype: 'Currency', fieldtype: 'Currency',
readOnly: 1, 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', label: 'Customer Currency',
fieldtype: 'Link', fieldtype: 'Link',
target: 'Currency', target: 'Currency',
formula: (doc) => doc.getFrom('Party', doc.customer, 'currency'), formula: (doc) =>
doc.getFrom('Party', doc.customer, 'currency') ||
frappe.AccountingSettings.currency,
formulaDependsOn: ['customer'], formulaDependsOn: ['customer'],
}, },
{ {
fieldname: 'exchangeRate', fieldname: 'exchangeRate',
label: 'Exchange Rate', label: 'Exchange Rate',
fieldtype: 'Float', fieldtype: 'Float',
default: 1,
formula: (doc) => doc.getExchangeRate(), formula: (doc) => doc.getExchangeRate(),
readOnly: true, readOnly: true,
}, },
@ -74,7 +77,7 @@ export default {
fieldname: 'netTotal', fieldname: 'netTotal',
label: 'Net Total', label: 'Net Total',
fieldtype: 'Currency', fieldtype: 'Currency',
formula: (doc) => doc.getSum('items', 'amount'), formula: (doc) => doc.getSum('items', 'amount', false),
readOnly: 1, readOnly: 1,
getCurrency: (doc) => doc.currency, getCurrency: (doc) => doc.currency,
}, },
@ -82,7 +85,7 @@ export default {
fieldname: 'baseNetTotal', fieldname: 'baseNetTotal',
label: 'Net Total (Company Currency)', label: 'Net Total (Company Currency)',
fieldtype: 'Currency', fieldtype: 'Currency',
formula: (doc) => doc.netTotal * doc.exchangeRate, formula: (doc) => doc.netTotal.mul(doc.exchangeRate),
readOnly: 1, readOnly: 1,
}, },
{ {
@ -105,7 +108,7 @@ export default {
fieldname: 'baseGrandTotal', fieldname: 'baseGrandTotal',
label: 'Grand Total (Company Currency)', label: 'Grand Total (Company Currency)',
fieldtype: 'Currency', fieldtype: 'Currency',
formula: (doc) => doc.grandTotal * doc.exchangeRate, formula: (doc) => doc.grandTotal.mul(doc.exchangeRate),
readOnly: 1, readOnly: 1,
}, },
{ {

View File

@ -34,7 +34,16 @@ export default {
label: 'Quantity', label: 'Quantity',
fieldtype: 'Float', fieldtype: 'Float',
required: 1, 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', fieldname: 'rate',
@ -42,18 +51,29 @@ export default {
fieldtype: 'Currency', fieldtype: 'Currency',
required: 1, required: 1,
formula: async (row, doc) => { formula: async (row, doc) => {
const baseRate = (await doc.getFrom('Item', row.item, 'rate')) || 0; const baseRate =
const exchangeRate = doc.exchangeRate ?? 1; (await doc.getFrom('Item', row.item, 'rate')) || frappe.pesa(0);
return baseRate / exchangeRate; return baseRate.div(doc.exchangeRate);
}, },
getCurrency: (row, doc) => doc.currency, getCurrency: (row, doc) => doc.currency,
formulaDependsOn: ['item'], 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', fieldname: 'baseRate',
label: 'Rate (Company Currency)', label: 'Rate (Company Currency)',
fieldtype: 'Currency', fieldtype: 'Currency',
formula: (row, doc) => row.rate * doc.exchangeRate, formula: (row, doc) => row.rate.mul(doc.exchangeRate),
readOnly: 1, readOnly: 1,
}, },
{ {
@ -78,7 +98,7 @@ export default {
label: 'Amount', label: 'Amount',
fieldtype: 'Currency', fieldtype: 'Currency',
readOnly: 1, readOnly: 1,
formula: (row) => row.quantity * row.rate, formula: (row) => row.rate.mul(row.quantity),
getCurrency: (row, doc) => doc.currency, getCurrency: (row, doc) => doc.currency,
}, },
{ {
@ -86,7 +106,7 @@ export default {
label: 'Amount (Company Currency)', label: 'Amount (Company Currency)',
fieldtype: 'Currency', fieldtype: 'Currency',
readOnly: 1, 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', label: 'Tax Account',
fieldtype: 'Link', fieldtype: 'Link',
target: 'Account', target: 'Account',
required: 1 required: 1,
}, },
{ {
fieldname: 'rate', fieldname: 'rate',
label: 'Rate', label: 'Rate',
fieldtype: 'Float', fieldtype: 'Float',
required: 1 required: 1,
}, },
{ {
fieldname: 'amount', fieldname: 'amount',
label: 'Amount', label: 'Amount',
fieldtype: 'Currency', fieldtype: 'Currency',
required: 1 required: 1,
}, },
{ {
fieldname: 'baseAmount', fieldname: 'baseAmount',
label: 'Amount (Company Currency)', label: 'Amount (Company Currency)',
fieldtype: 'Currency', fieldtype: 'Currency',
formula: (row, doc) => row.amount * doc.exchangeRate, formula: (row, doc) => row.amount.mul(doc.exchangeRate),
readOnly: 1 readOnly: 1,
} },
] ],
}; };

View File

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

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", "version": "0.0.3",
"fileName": "makePaymentRefIdNullable", "fileName": "makePaymentRefIdNullable",
"beforeMigrate": true "beforeMigrate": true
},
{
"version": "0.0.4",
"fileName": "convertCurrencyToStrings",
"beforeMigrate": true
} }
] ]

View File

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

View File

@ -29,7 +29,14 @@ class GeneralLedger {
], ],
filters: filters, 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); return this.appendOpeningEntry(data);
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -20,22 +20,29 @@
v-for="df in tableFields" v-for="df in tableFields"
:df="df" :df="df"
:value="row[df.fieldname]" :value="row[df.fieldname]"
@change="value => row.set(df.fieldname, value)" @change="(value) => onChange(df, value)"
@new-doc="doc => row.set(df.fieldname, doc.name)" @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> </Row>
</template> </template>
<script> <script>
import FormControl from './FormControl'; import FormControl from './FormControl';
import { getErrorMessage } from '../../utils';
import Row from '@/components/Row'; import Row from '@/components/Row';
export default { export default {
name: 'TableRow', name: 'TableRow',
props: ['row', 'tableFields', 'size', 'ratio', 'isNumeric'], props: ['row', 'tableFields', 'size', 'ratio', 'isNumeric'],
components: { components: {
Row Row,
}, },
data: () => ({ hovering: false }), data: () => ({ hovering: false, errors: {} }),
beforeCreate() { beforeCreate() {
this.$options.components.FormControl = FormControl; this.$options.components.FormControl = FormControl;
}, },
@ -43,8 +50,28 @@ export default {
return { return {
doctype: this.row.doctype, doctype: this.row.doctype,
name: this.row.name, 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> </script>

View File

@ -162,6 +162,7 @@ let TwoColumnForm = {
let oldValue = this.doc.get(df.fieldname); let oldValue = this.doc.get(df.fieldname);
this.$set(this.errors, df.fieldname, null);
if (oldValue === value) { if (oldValue === value) {
return; return;
} }
@ -171,9 +172,6 @@ let TwoColumnForm = {
return this.doc.rename(value); return this.doc.rename(value);
} }
// reset error messages
this.$set(this.errors, df.fieldname, null);
this.doc.set(df.fieldname, value).catch((e) => { this.doc.set(df.fieldname, value).catch((e) => {
// set error message for this field // set error message for this field
this.$set(this.errors, df.fieldname, getErrorMessage(e, this.doc)); 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 postStart from '../server/postStart';
import { DB_CONN_FAILURE } from './messages'; import { DB_CONN_FAILURE } from './messages';
import migrate from './migrate'; import migrate from './migrate';
import { getSavePath } from './utils'; import { callInitializeMoneyMaker, getSavePath } from './utils';
export async function createNewDatabase() { export async function createNewDatabase() {
const { canceled, filePath } = await getSavePath('books', 'db'); 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 }; return { connectionSuccess: false, reason: DB_CONN_FAILURE.CANT_CONNECT };
} }
// first init no currency, for migratory needs
await callInitializeMoneyMaker();
try { try {
await runRegionalModelUpdates(); await runRegionalModelUpdates();
} catch (error) { } catch (error) {
@ -87,6 +90,9 @@ export async function connectToLocalDatabase(filePath) {
// set last selected file // set last selected file
config.set('lastSelectedFilePath', filePath); config.set('lastSelectedFilePath', filePath);
// second init with currency, normal usage
await callInitializeMoneyMaker();
return { connectionSuccess: true, reason: '' }; return { connectionSuccess: true, reason: '' };
} }

View File

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

View File

@ -84,9 +84,10 @@ export default {
.select('name') .select('name')
.from('Account') .from('Account')
.where('rootType', 'Expense'); .where('rootType', 'Expense');
let topExpenses = await frappe.db.knex let topExpenses = await frappe.db.knex
.select({ .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') .select('account')
.from('AccountingLedgerEntry') .from('AccountingLedgerEntry')

View File

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

View File

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

View File

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

View File

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

View File

@ -3,7 +3,7 @@ import Toast from '@/components/Toast';
import router from '@/router'; import router from '@/router';
import { ipcRenderer } from 'electron'; import { ipcRenderer } from 'electron';
import frappe from 'frappejs'; import frappe from 'frappejs';
import { _ } from 'frappejs/utils'; import { isPesa, _ } from 'frappejs/utils';
import lodash from 'lodash'; import lodash from 'lodash';
import Vue from 'vue'; import Vue from 'vue';
import { IPC_ACTIONS, IPC_MESSAGES } from './messages'; import { IPC_ACTIONS, IPC_MESSAGES } from './messages';
@ -152,7 +152,7 @@ export function openQuickEdit({ doctype, name, hideFields, defaults = {} }) {
} }
export function getErrorMessage(e, doc) { export function getErrorMessage(e, doc) {
let errorMessage = e.message || _('An error occurred'); let errorMessage = e.message || _('An error occurred.');
const { doctype, name } = doc; const { doctype, name } = doc;
const canElaborate = doctype && name; const canElaborate = doctype && name;
if (e.type === frappe.errors.LinkValidationError && canElaborate) { if (e.type === frappe.errors.LinkValidationError && canElaborate) {
@ -258,7 +258,7 @@ export function getInvoiceStatus(doc) {
if (!doc.submitted) { if (!doc.submitted) {
status = 'Draft'; status = 'Draft';
} }
if (doc.submitted === 1 && doc.outstandingAmount === 0.0) { if (doc.submitted === 1 && doc.outstandingAmount.isZero()) {
status = 'Paid'; status = 'Paid';
} }
if (doc.cancelled === 1) { if (doc.cancelled === 1) {
@ -351,3 +351,51 @@ export function titleCase(phrase) {
}) })
.join(' '); .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 lg: '0.5rem', // 8px
xl: '0.75rem', // 12px xl: '0.75rem', // 12px
}, },
gridColumn: {
'span-full': '1 / -1',
},
colors: { colors: {
brand: '#2490EF', brand: '#2490EF',
'brand-100': '#f4f9ff', 'brand-100': '#f4f9ff',