mirror of
https://github.com/frappe/books.git
synced 2024-11-10 07:40:55 +00:00
Merge pull request #268 from 18alantom/make-money
refactor: currency handling to use pesa
This commit is contained in:
commit
6a9fd904b5
@ -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
@ -68,7 +68,6 @@ export default {
|
||||
fieldname: 'balance',
|
||||
label: 'Balance',
|
||||
fieldtype: 'Currency',
|
||||
default: '0',
|
||||
readOnly: 1,
|
||||
},
|
||||
{
|
||||
|
@ -32,23 +32,5 @@ export default {
|
||||
fieldname: 'symbol',
|
||||
fieldtype: 'Data',
|
||||
},
|
||||
{
|
||||
fieldname: 'numberFormat',
|
||||
fieldtype: 'Select',
|
||||
label: 'Number Format',
|
||||
placeholder: 'Number Format',
|
||||
options: [
|
||||
'#,###.##',
|
||||
'#.###,##',
|
||||
'# ###.##',
|
||||
'# ###,##',
|
||||
"#'###.##",
|
||||
'#, ###.##',
|
||||
'#,##,###.##',
|
||||
'#,###.###',
|
||||
'#.###',
|
||||
'#,###',
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
@ -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'
|
||||
);
|
||||
|
@ -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();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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',
|
||||
|
@ -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),
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
},
|
||||
{
|
||||
|
@ -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),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
@ -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,
|
||||
},
|
||||
{
|
||||
|
@ -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),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
@ -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,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
43
patches/0.0.4/convertCurrencyToStrings.js
Normal file
43
patches/0.0.4/convertCurrencyToStrings.js
Normal 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);
|
||||
}
|
||||
}
|
@ -3,5 +3,10 @@
|
||||
"version": "0.0.3",
|
||||
"fileName": "makePaymentRefIdNullable",
|
||||
"beforeMigrate": true
|
||||
},
|
||||
{
|
||||
"version": "0.0.4",
|
||||
"fileName": "convertCurrencyToStrings",
|
||||
"beforeMigrate": true
|
||||
}
|
||||
]
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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': {
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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));
|
||||
|
@ -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: '' };
|
||||
}
|
||||
|
||||
|
@ -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]),
|
||||
|
@ -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')
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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');
|
||||
|
@ -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);
|
||||
|
54
src/utils.js
54
src/utils.js
@ -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;
|
||||
});
|
||||
}
|
||||
|
@ -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',
|
||||
|
Loading…
Reference in New Issue
Block a user