A more detailed revision of gncEntry and gncInvoice related rounding

First change is to ensure gncEntry rounding is consistent. Internally
calculated values in the entry are never rounded. Consumers of
gncEntry's calculated values can request them either rounded or not.

Next use a pragmatical approach for calculating values on invoices based on
the entry values: do the rounding such that we never
create an unbalanced transaction while posting
That means
- round each entry's net value before summing them in net total
- accumulate all tax totals on invoice level per tax account before rounding
  and round before before summing them in a global tax total

Hopefully this will catch a few more rounding issues in this area.

A complete solution can only offered if we allow users to manually correct
tax entries. This requires changes to user interface and data format
so that's not going to happen in gnucash 3.x.
This commit is contained in:
Geert Janssens 2018-05-24 18:53:15 +02:00
parent 2e8df1984a
commit fcabf6bb96
7 changed files with 370 additions and 81 deletions

View File

@ -727,6 +727,7 @@ gnc_entry_ledger_compute_value (GncEntryLedger *ledger,
GncTaxTable *table;
GList *taxes = NULL;
int denom = 100;
gnc_numeric value_unrounded, taxes_unrounded;
gnc_entry_ledger_get_numeric (ledger, ENTRY_QTY_CELL, &qty);
gnc_entry_ledger_get_numeric (ledger, ENTRY_PRIC_CELL, &price);
@ -778,12 +779,18 @@ gnc_entry_ledger_compute_value (GncEntryLedger *ledger,
}
gncEntryComputeValue (qty, price, (taxable ? table : NULL), taxincluded,
discount, disc_type, disc_how, denom,
value, NULL, &taxes);
discount, disc_type, disc_how, 0,
&value_unrounded, NULL, &taxes);
if (value)
*value = gnc_numeric_convert (value_unrounded, denom,
GNC_HOW_RND_ROUND_HALF_UP);
/* return the tax value */
taxes_unrounded = gncAccountValueTotal (taxes);
if (tax_value)
*tax_value = gncAccountValueTotal (taxes);
*tax_value = gnc_numeric_convert (taxes_unrounded, denom,
GNC_HOW_RND_ROUND_HALF_UP);
}
gboolean

View File

@ -1084,15 +1084,17 @@ GncOrder * gncEntryGetOrder (const GncEntry *entry)
* the amount the merchant gets; the taxes are the amount the gov't
* gets, and the customer pays the sum or value + taxes.
*
* The SCU is the denominator to convert the value.
*
* The discount return value is just for entertainment -- you may want
* to let a consumer know how much they saved.
*
* Note this function will not do any rounding unless forced to prevent overflow.
* It's the caller's responsability to round to the proper commodity
* denominator if needed.
*/
static void gncEntryComputeValueInt (gnc_numeric qty, gnc_numeric price,
const GncTaxTable *tax_table, gboolean tax_included,
gnc_numeric discount, GncAmountType discount_type,
GncDiscountHow discount_how, int SCU,
GncDiscountHow discount_how,
gnc_numeric *value, gnc_numeric *discount_value,
GList **tax_value, gnc_numeric *net_price)
{
@ -1110,7 +1112,7 @@ static void gncEntryComputeValueInt (gnc_numeric qty, gnc_numeric price,
/* Step 1: compute the aggregate price */
aggregate = gnc_numeric_mul (qty, price, GNC_DENOM_AUTO, GNC_HOW_DENOM_LCD | GNC_HOW_RND_ROUND);
aggregate = gnc_numeric_mul (qty, price, GNC_DENOM_AUTO, GNC_HOW_DENOM_REDUCE | GNC_HOW_RND_ROUND);
/* Step 2: compute the pre-tax aggregate */
@ -1154,10 +1156,10 @@ static void gncEntryComputeValueInt (gnc_numeric qty, gnc_numeric price,
gnc_numeric_add (tpercent,
gnc_numeric_create (1, 1),
GNC_DENOM_AUTO, GNC_HOW_DENOM_LCD),
GNC_DENOM_AUTO, GNC_HOW_DENOM_LCD);
GNC_DENOM_AUTO, GNC_HOW_DENOM_REDUCE | GNC_HOW_RND_ROUND);
if (!gnc_numeric_zero_p(qty))
{
i_net_price = gnc_numeric_div (pretax, qty, GNC_DENOM_AUTO, GNC_HOW_DENOM_LCD);
i_net_price = gnc_numeric_div (pretax, qty, GNC_DENOM_AUTO, GNC_HOW_DENOM_REDUCE | GNC_HOW_RND_ROUND);
}
}
else
@ -1195,7 +1197,7 @@ static void gncEntryComputeValueInt (gnc_numeric qty, gnc_numeric price,
discount = gnc_numeric_div (discount, percent, GNC_DENOM_AUTO,
GNC_HOW_DENOM_EXACT | GNC_HOW_RND_NEVER);
discount = gnc_numeric_mul (pretax, discount, GNC_DENOM_AUTO,
GNC_HOW_DENOM_LCD);
GNC_HOW_DENOM_REDUCE | GNC_HOW_RND_ROUND);
}
result = gnc_numeric_sub (pretax, discount, GNC_DENOM_AUTO, GNC_HOW_DENOM_LCD);
@ -1212,14 +1214,14 @@ static void gncEntryComputeValueInt (gnc_numeric qty, gnc_numeric price,
{
gnc_numeric after_tax;
tax = gnc_numeric_mul (pretax, tpercent, GNC_DENOM_AUTO, GNC_HOW_DENOM_LCD);
tax = gnc_numeric_mul (pretax, tpercent, GNC_DENOM_AUTO, GNC_HOW_DENOM_REDUCE | GNC_HOW_RND_ROUND);
after_tax = gnc_numeric_add (pretax, tax, GNC_DENOM_AUTO, GNC_HOW_DENOM_LCD);
after_tax = gnc_numeric_add (after_tax, tvalue, GNC_DENOM_AUTO,
GNC_HOW_DENOM_LCD);
discount = gnc_numeric_div (discount, percent, GNC_DENOM_AUTO,
GNC_HOW_DENOM_EXACT | GNC_HOW_RND_NEVER);
discount = gnc_numeric_mul (after_tax, discount, GNC_DENOM_AUTO,
GNC_HOW_DENOM_LCD);
GNC_HOW_DENOM_REDUCE | GNC_HOW_RND_ROUND);
}
result = gnc_numeric_sub (pretax, discount, GNC_DENOM_AUTO, GNC_HOW_DENOM_LCD);
@ -1238,16 +1240,10 @@ static void gncEntryComputeValueInt (gnc_numeric qty, gnc_numeric price,
*/
if (discount_value != NULL)
{
if (SCU) discount = gnc_numeric_convert(discount, SCU, GNC_HOW_RND_ROUND_HALF_UP);
*discount_value = discount;
}
if (value != NULL)
{
if (SCU) result = gnc_numeric_convert(result, SCU, GNC_HOW_RND_ROUND_HALF_UP);
*value = result;
}
/* Now... Compute the list of tax values (if the caller wants it) */
@ -1266,14 +1262,12 @@ static void gncEntryComputeValueInt (gnc_numeric qty, gnc_numeric price,
switch (gncTaxTableEntryGetType (entry))
{
case GNC_AMT_TYPE_VALUE:
if (SCU) amount = gnc_numeric_convert(amount, SCU, GNC_HOW_RND_ROUND_HALF_UP);
taxes = gncAccountValueAdd (taxes, acc, amount);
break;
case GNC_AMT_TYPE_PERCENT:
amount = gnc_numeric_div (amount, percent, GNC_DENOM_AUTO,
GNC_HOW_DENOM_EXACT | GNC_HOW_RND_NEVER);
tax = gnc_numeric_mul (pretax, amount, GNC_DENOM_AUTO, GNC_HOW_DENOM_LCD);
if (SCU) tax = gnc_numeric_convert(tax, SCU, GNC_HOW_RND_ROUND_HALF_UP);
tax = gnc_numeric_mul (pretax, amount, GNC_DENOM_AUTO, GNC_HOW_DENOM_REDUCE | GNC_HOW_RND_ROUND);
taxes = gncAccountValueAdd (taxes, acc, tax);
break;
default:
@ -1284,10 +1278,7 @@ static void gncEntryComputeValueInt (gnc_numeric qty, gnc_numeric price,
}
if (net_price != NULL)
{
if (SCU) i_net_price = gnc_numeric_convert(i_net_price, SCU, GNC_HOW_RND_ROUND_HALF_UP);
*net_price = i_net_price;
}
return;
}
@ -1295,12 +1286,12 @@ static void gncEntryComputeValueInt (gnc_numeric qty, gnc_numeric price,
void gncEntryComputeValue (gnc_numeric qty, gnc_numeric price,
const GncTaxTable *tax_table, gboolean tax_included,
gnc_numeric discount, GncAmountType discount_type,
GncDiscountHow discount_how, int SCU,
GncDiscountHow discount_how, G_GNUC_UNUSED int SCU,
gnc_numeric *value, gnc_numeric *discount_value,
GList **tax_value)
{
gncEntryComputeValueInt (qty, price, tax_table, tax_included, discount, discount_type,
discount_how, SCU, value, discount_value, tax_value, NULL);
discount_how, value, discount_value, tax_value, NULL);
}
static int
@ -1328,6 +1319,7 @@ static void
gncEntryRecomputeValues (GncEntry *entry)
{
int denom;
GList *tv_iter;
/* See if either tax table changed since we last computed values */
if (entry->i_tax_table)
@ -1386,18 +1378,28 @@ gncEntryRecomputeValues (GncEntry *entry)
&(entry->b_value), NULL, &(entry->b_tax_values));
entry->i_value_rounded = gnc_numeric_convert (entry->i_value, denom,
GNC_HOW_RND_ROUND_HALF_UP);
GNC_HOW_DENOM_EXACT | GNC_HOW_RND_ROUND_HALF_UP);
entry->i_disc_value_rounded = gnc_numeric_convert (entry->i_disc_value, denom,
GNC_HOW_RND_ROUND_HALF_UP);
GNC_HOW_DENOM_EXACT | GNC_HOW_RND_ROUND_HALF_UP);
entry->i_tax_value = gncAccountValueTotal (entry->i_tax_values);
entry->i_tax_value_rounded = gnc_numeric_convert (entry->i_tax_value, denom,
GNC_HOW_RND_ROUND_HALF_UP);
entry->i_tax_value_rounded = gnc_numeric_zero();
for (tv_iter = entry->i_tax_values; tv_iter; tv_iter=tv_iter->next)
{
GncAccountValue *acc_val = tv_iter->data;
entry->i_tax_value_rounded = gnc_numeric_add (entry->i_tax_value_rounded, acc_val->value,
denom, GNC_HOW_DENOM_EXACT | GNC_HOW_RND_ROUND_HALF_UP);
}
entry->b_value_rounded = gnc_numeric_convert (entry->b_value, denom,
GNC_HOW_RND_ROUND_HALF_UP);
GNC_HOW_DENOM_EXACT | GNC_HOW_RND_ROUND_HALF_UP);
entry->b_tax_value = gncAccountValueTotal (entry->b_tax_values);
entry->b_tax_value_rounded = gnc_numeric_convert (entry->b_tax_value, denom,
GNC_HOW_RND_ROUND_HALF_UP);
entry->b_tax_value_rounded = gnc_numeric_zero();
for (tv_iter = entry->b_tax_values; tv_iter; tv_iter=tv_iter->next)
{
GncAccountValue *acc_val = tv_iter->data;
entry->b_tax_value_rounded = gnc_numeric_add (entry->b_tax_value_rounded, acc_val->value,
denom, GNC_HOW_DENOM_EXACT | GNC_HOW_RND_ROUND_HALF_UP);
}
entry->values_dirty = FALSE;
}
@ -1441,6 +1443,10 @@ static gnc_numeric gncEntryGetIntDiscountValue (GncEntry *entry, gboolean round,
return (is_cust_doc ? entry->i_disc_value : gnc_numeric_zero());
}
/* Note contrary to the GetDoc*Value and GetBal*Value functions below
* this function will always round the net price to the entry's
* currency denominator (being the invoice/bill denom or 100000 if not
* included in a bill or invoice) */
gnc_numeric gncEntryGetPrice (const GncEntry *entry, gboolean cust_doc, gboolean net)
{
gnc_numeric result;
@ -1448,10 +1454,8 @@ gnc_numeric gncEntryGetPrice (const GncEntry *entry, gboolean cust_doc, gboolean
if (!entry) return gnc_numeric_zero();
if (!net) return (cust_doc ? entry->i_price : entry->b_price);
/* Determine the commodity denominator */
denom = get_entry_commodity_denom (entry);
/* Compute the net price */
if (cust_doc)
gncEntryComputeValueInt (entry->quantity, entry->i_price,
@ -1459,16 +1463,20 @@ gnc_numeric gncEntryGetPrice (const GncEntry *entry, gboolean cust_doc, gboolean
entry->i_taxincluded,
entry->i_discount, entry->i_disc_type,
entry->i_disc_how,
denom,
NULL, NULL, NULL, &result);
else
gncEntryComputeValueInt (entry->quantity, entry->b_price,
(entry->b_taxable ? entry->b_tax_table : NULL),
entry->b_taxincluded,
gnc_numeric_zero(), GNC_AMT_TYPE_VALUE, GNC_DISC_PRETAX,
denom,
NULL, NULL, NULL, &result);
/* Determine the commodity denominator */
denom = get_entry_commodity_denom (entry);
result = gnc_numeric_convert (result, denom,
GNC_HOW_DENOM_EXACT | GNC_HOW_RND_ROUND_HALF_UP);
return result;
}

View File

@ -47,6 +47,8 @@ typedef enum
GNC_DISC_POSTTAX
} GncDiscountHow;
typedef GList AccountValueList;
#ifdef GNUCASH_MAJOR_VERSION
#include "gncBusiness.h"
#endif
@ -243,7 +245,6 @@ void gncEntryCopy (const GncEntry *src, GncEntry *dest, gboolean add_entry);
* these functions.
@{
*/
typedef GList AccountValueList;
gnc_numeric gncEntryGetDocValue (GncEntry *entry, gboolean round, gboolean is_cust_doc, gboolean is_cn);
gnc_numeric gncEntryGetDocTaxValue (GncEntry *entry, gboolean round, gboolean is_cust_doc, gboolean is_cn);
/** Careful: the returned list is NOT owned by the entry and should be freed by the caller */

View File

@ -857,15 +857,39 @@ GncOwnerType gncInvoiceGetOwnerType (const GncInvoice *invoice)
}
static gnc_numeric
gncInvoiceGetTotalInternal (GncInvoice *invoice, gboolean use_value,
gboolean use_tax,
gboolean use_payment_type, GncEntryPaymentType type)
gncInvoiceSumTaxesInternal (AccountValueList *taxes)
{
gnc_numeric tt = gnc_numeric_zero();
if (taxes)
{
GList *node;
// Note we can use GNC_DENOM_AUTO below for rounding because
// the values passed to this function should already have been rounded
// to the desired denom and addition will just preserve it in that case.
for (node = taxes; node; node=node->next)
{
GncAccountValue *acc_val = node->data;
tt = gnc_numeric_add (tt, acc_val->value, GNC_DENOM_AUTO,
GNC_HOW_DENOM_EXACT | GNC_HOW_RND_ROUND_HALF_UP);
}
}
return tt;
}
static gnc_numeric
gncInvoiceGetNetAndTaxesInternal (GncInvoice *invoice, gboolean use_value,
AccountValueList **taxes,
gboolean use_payment_type, GncEntryPaymentType type
)
{
GList *node;
gnc_numeric total = gnc_numeric_zero();
gnc_numeric net_total = gnc_numeric_zero();
gboolean is_cust_doc, is_cn;
AccountValueList *tv_list = NULL;
int denom = gnc_commodity_get_fraction(gncInvoiceGetCurrency(invoice));
g_return_val_if_fail (invoice, total);
g_return_val_if_fail (invoice, net_total);
/* Is the current document an invoice/credit note related to a customer or a vendor/employee ?
* The GncEntry code needs to know to return the proper entry amounts
@ -873,6 +897,7 @@ gncInvoiceGetTotalInternal (GncInvoice *invoice, gboolean use_value,
is_cust_doc = (gncInvoiceGetOwnerType (invoice) == GNC_OWNER_CUSTOMER);
is_cn = gncInvoiceGetIsCreditNote (invoice);
for (node = gncInvoiceGetEntries(invoice); node; node = node->next)
{
GncEntry *entry = node->data;
@ -881,23 +906,64 @@ gncInvoiceGetTotalInternal (GncInvoice *invoice, gboolean use_value,
if (use_payment_type && gncEntryGetBillPayment (entry) != type)
continue;
value = gncEntryGetDocValue (entry, FALSE, is_cust_doc, is_cn);
if (gnc_numeric_check (value) == GNC_ERROR_OK)
if (use_value)
{
if (use_value)
total = gnc_numeric_add (total, value, GNC_DENOM_AUTO, GNC_HOW_DENOM_LCD);
}
else
g_warning ("bad value in our entry");
if (use_tax)
{
tax = gncEntryGetDocTaxValue (entry, FALSE, is_cust_doc, is_cn);
if (gnc_numeric_check (tax) == GNC_ERROR_OK)
total = gnc_numeric_add (total, tax, GNC_DENOM_AUTO, GNC_HOW_DENOM_LCD);
// Always use rounded net values to prevent creating imbalanced transactions on posting
// https://bugzilla.gnome.org/show_bug.cgi?id=628903
value = gncEntryGetDocValue (entry, TRUE, is_cust_doc, is_cn);
if (gnc_numeric_check (value) == GNC_ERROR_OK)
net_total = gnc_numeric_add (net_total, value, GNC_DENOM_AUTO, GNC_HOW_DENOM_LCD);
else
g_warning ("bad tax-value in our entry");
g_warning ("bad value in our entry");
}
if (taxes)
{
AccountValueList *entrytaxes = gncEntryGetDocTaxValues (entry, is_cust_doc, is_cn);
tv_list = gncAccountValueAddList (tv_list, entrytaxes);
gncAccountValueDestroy (entrytaxes);
}
}
if (taxes)
{
GList *node;
// Round tax totals (accumulated per tax account) to prevent creating imbalanced transactions on posting
// which could otherwise happen when using a tax table with multiple tax rates
for (node = tv_list; node; node=node->next)
{
GncAccountValue *acc_val = node->data;
acc_val->value = gnc_numeric_convert (acc_val->value,
denom, GNC_HOW_DENOM_EXACT | GNC_HOW_RND_ROUND_HALF_UP);
}
*taxes = tv_list;
}
return net_total;
}
static gnc_numeric
gncInvoiceGetTotalInternal(GncInvoice *invoice, gboolean use_value,
gboolean use_tax,
gboolean use_payment_type, GncEntryPaymentType type)
{
AccountValueList *taxes;
gnc_numeric total;
int denom;
if (!invoice) return gnc_numeric_zero();
denom = gnc_commodity_get_fraction(gncInvoiceGetCurrency(invoice));
total = gncInvoiceGetNetAndTaxesInternal(invoice, use_value, use_tax? &taxes : NULL, use_payment_type, type);
if (use_tax)
{
// Note we can use GNC_DENOM_AUTO below for rounding because
// the values passed to this function should already have been rounded
// to the desired denom and addition will just preserve it in that case.
total = gnc_numeric_add (total, gncInvoiceSumTaxesInternal (taxes),
GNC_DENOM_AUTO, GNC_HOW_DENOM_EXACT | GNC_HOW_RND_ROUND_HALF_UP);
gncAccountValueDestroy (taxes);
}
return total;
}
@ -926,6 +992,16 @@ gnc_numeric gncInvoiceGetTotalOf (GncInvoice *invoice, GncEntryPaymentType type)
return gncInvoiceGetTotalInternal(invoice, TRUE, TRUE, TRUE, type);
}
AccountValueList *gncInvoiceGetTotalTaxList (GncInvoice *invoice)
{
gnc_numeric unused;
AccountValueList *taxes;
if (!invoice) return NULL;
unused = gncInvoiceGetNetAndTaxesInternal(invoice, FALSE, &taxes, FALSE, 0);
return taxes;
}
GList * gncInvoiceGetTypeListForOwnerType (GncOwnerType type)
{
GList *type_list = NULL;
@ -1376,6 +1452,8 @@ Transaction * gncInvoicePostToAccount (GncInvoice *invoice, Account *acc,
char *lot_title;
Account *ccard_acct = NULL;
const GncOwner *owner;
int denom = xaccAccountGetCommoditySCU(acc);
AccountValueList *taxes;
if (!invoice || !acc) return NULL;
if (gncInvoiceIsPosted (invoice)) return NULL;
@ -1427,14 +1505,29 @@ Transaction * gncInvoicePostToAccount (GncInvoice *invoice, Account *acc,
xaccTransSetDateDue (txn, due_date);
/* Get invoice total and taxes. */
total = gncInvoiceGetTotal (invoice);
taxes = gncInvoiceGetTotalTaxList (invoice);
/* The two functions above return signs relative to the document
* We need to convert them to balance values before we can use them here */
if (is_cust_doc)
{
GList *node;
total = gnc_numeric_neg (total);
for (node = taxes; node; node = node->next)
{
GncAccountValue *acc_val = node->data;
acc_val->value = gnc_numeric_neg (acc_val->value);
}
}
/* Iterate through the entries; sum up everything for each account.
* then create the appropriate splits in this txn.
*/
total = gnc_numeric_zero();
for (iter = gncInvoiceGetEntries(invoice); iter; iter = iter->next)
{
gnc_numeric value, tax;
GList *taxes;
GncEntry * entry = iter->data;
Account *this_acc;
@ -1458,10 +1551,10 @@ Transaction * gncInvoicePostToAccount (GncInvoice *invoice, Account *acc,
}
gncEntryCommitEdit (entry);
/* Obtain the Entry's Value and TaxValues */
value = gncEntryGetBalValue (entry, FALSE, is_cust_doc);
tax = gncEntryGetBalTaxValue (entry, FALSE, is_cust_doc);
taxes = gncEntryGetBalTaxValues (entry, is_cust_doc);
/* Obtain the Entry's Value and TaxValues
Note we use rounded values here and below to prevent creating an imbalanced transaction */
value = gncEntryGetBalValue (entry, TRUE, is_cust_doc);
tax = gncEntryGetBalTaxValue (entry, TRUE, is_cust_doc);
/* add the value for the account split */
this_acc = (is_cust_doc ? gncEntryGetInvAccount (entry) :
@ -1472,6 +1565,7 @@ Transaction * gncInvoicePostToAccount (GncInvoice *invoice, Account *acc,
{
if (accumulatesplits)
splitinfo = gncAccountValueAdd (splitinfo, this_acc, value);
/* Adding to total in case of accumulatesplits will be deferred to later when each split is effectively added */
else if (!gncInvoicePostAddSplit (book, this_acc, txn, value,
gncEntryGetDescription (entry),
type, invoice))
@ -1484,7 +1578,7 @@ Transaction * gncInvoicePostToAccount (GncInvoice *invoice, Account *acc,
}
/* If there is a credit-card account, and this is a CCard
* payment type, the don't add it to the total, and instead
* payment type, subtract it from the total, and instead
* create a split to the CC Acct with a memo of the entry
* description instead of the provided memo. Note that the
* value reversal is the same as the post account.
@ -1496,6 +1590,9 @@ Transaction * gncInvoicePostToAccount (GncInvoice *invoice, Account *acc,
{
Split *split;
total = gnc_numeric_sub (total, value, denom,
GNC_HOW_DENOM_EXACT | GNC_HOW_RND_ROUND_HALF_UP);
split = xaccMallocSplit (book);
xaccSplitSetMemo (split, gncEntryGetDescription (entry));
/* set action based on book option */
@ -1508,30 +1605,30 @@ Transaction * gncInvoicePostToAccount (GncInvoice *invoice, Account *acc,
invoice->currency);
}
else
total = gnc_numeric_add (total, value, GNC_DENOM_AUTO, GNC_HOW_DENOM_LCD);
}
else
g_warning ("bad value in our entry");
}
/* now merge in the TaxValues */
splitinfo = gncAccountValueAddList (splitinfo, taxes);
/* ... and add the tax total */
if (gnc_numeric_check (tax) == GNC_ERROR_OK)
total = gnc_numeric_add (total, tax, GNC_DENOM_AUTO, GNC_HOW_DENOM_LCD);
else
/* check the taxes */
if (gnc_numeric_check (tax) != GNC_ERROR_OK)
g_warning ("bad tax in our entry");
gncAccountValueDestroy (taxes);
} /* for */
/* now merge in the TaxValues */
splitinfo = gncAccountValueAddList (splitinfo, taxes);
gncAccountValueDestroy (taxes);
/* Iterate through the splitinfo list and generate the splits */
for (iter = splitinfo; iter; iter = iter->next)
{
GncAccountValue *acc_val = iter->data;
//gnc_numeric amt_rounded = gnc_numeric_convert(acc_val->value,
// denom, GNC_HOW_DENOM_EXACT | GNC_HOW_RND_ROUND_HALF_UP);
if (!gncInvoicePostAddSplit (book, acc_val->account, txn, acc_val->value,
memo, type, invoice))
{
@ -1567,8 +1664,8 @@ Transaction * gncInvoicePostToAccount (GncInvoice *invoice, Account *acc,
xaccSplitSetBaseValue (split, gnc_numeric_neg (to_charge_bal_amount),
invoice->currency);
total = gnc_numeric_sub (total, to_charge_bal_amount,
GNC_DENOM_AUTO, GNC_HOW_DENOM_LCD);
total = gnc_numeric_sub (total, to_charge_bal_amount, denom,
GNC_HOW_DENOM_EXACT | GNC_HOW_RND_ROUND_HALF_UP);
}
/* Now create the Posted split (which is the opposite sign of the above splits) */

View File

@ -164,6 +164,9 @@ gnc_numeric gncInvoiceGetTotal (GncInvoice *invoice);
gnc_numeric gncInvoiceGetTotalOf (GncInvoice *invoice, GncEntryPaymentType type);
gnc_numeric gncInvoiceGetTotalSubtotal (GncInvoice *invoice);
gnc_numeric gncInvoiceGetTotalTax (GncInvoice *invoice);
/** Return a list of tax totals accumulated per tax account.
*/
AccountValueList *gncInvoiceGetTotalTaxList (GncInvoice *invoice);
typedef GList EntryList;
EntryList * gncInvoiceGetEntries (GncInvoice *invoice);

View File

@ -936,7 +936,7 @@ GList *gncAccountValueAdd (GList *list, Account *acc, gnc_numeric value)
if (res->account == acc)
{
res->value = gnc_numeric_add (res->value, value, GNC_DENOM_AUTO,
GNC_HOW_DENOM_LCD);
GNC_HOW_DENOM_REDUCE | GNC_HOW_RND_ROUND_HALF_UP);
return list;
}
}
@ -970,7 +970,7 @@ gnc_numeric gncAccountValueTotal (GList *list)
for ( ; list ; list = list->next)
{
GncAccountValue *val = list->data;
total = gnc_numeric_add (total, val->value, GNC_DENOM_AUTO, GNC_HOW_DENOM_LCD);
total = gnc_numeric_add (total, val->value, GNC_DENOM_AUTO, GNC_HOW_DENOM_REDUCE | GNC_HOW_RND_ROUND_HALF_UP);
}
return total;
}

View File

@ -25,6 +25,7 @@
#include <qof.h>
#include <unittest-support.h>
#include "../gncEntry.h"
#include "../gncTaxTableP.h"
static const gchar *suitename = "/engine/gncEntry";
void test_suite_gncEntry ( void );
@ -33,9 +34,11 @@ typedef struct
{
QofBook *book;
Account *account;
Account *vatacct;
GncOwner owner;
GncCustomer *customer;
gnc_commodity *commodity;
GncInvoice *invoice;
} Fixture;
static void
@ -47,6 +50,11 @@ setup( Fixture *fixture, gconstpointer pData )
fixture->commodity = gnc_commodity_new(fixture->book, "foo", "bar", "xy", "xy", 100);
xaccAccountSetCommodity(fixture->account, fixture->commodity);
fixture->vatacct = xaccMallocAccount(fixture->book);
xaccAccountSetCommodity(fixture->vatacct, fixture->commodity);
fixture->invoice = gncInvoiceCreate(fixture->book);
gncInvoiceSetCurrency(fixture->invoice, fixture->commodity);
}
static void
@ -54,8 +62,13 @@ teardown( Fixture *fixture, gconstpointer pData )
{
xaccAccountBeginEdit(fixture->account);
xaccAccountDestroy(fixture->account);
gnc_commodity_destroy(fixture->commodity);
xaccAccountBeginEdit(fixture->vatacct);
xaccAccountDestroy(fixture->vatacct);
gncInvoiceBeginEdit(fixture->invoice);
gncInvoiceDestroy(fixture->invoice);
gnc_commodity_destroy(fixture->commodity);
qof_book_destroy( fixture->book );
}
@ -109,8 +122,168 @@ test_entry_basics ( Fixture *fixture, gconstpointer pData )
}
static void
test_entry_rounding ( Fixture *fixture, gconstpointer pData )
{
GncEntry *entry = gncEntryCreate(fixture->book);
GncTaxTable *taxtable;
GncTaxTableEntry *tt_entry;
gncTaxTableRegister();
taxtable = gncTaxTableCreate(fixture->book);
tt_entry = gncTaxTableEntryCreate();
gncTaxTableSetName(taxtable, "Percent tax");
gncTaxTableEntrySetAccount(tt_entry, fixture->vatacct);
gncTaxTableEntrySetType(tt_entry, GNC_AMT_TYPE_PERCENT);
gncTaxTableEntrySetAmount(tt_entry, gnc_numeric_create(1000000, 100000));
gncTaxTableAddEntry(taxtable, tt_entry);
// 1. Freestanding entry - a default denominator of 100000 is expected during rounding
// Test with numbers that don't require rounding
/* Tax 10% (high precision GncNumeric), tax not included */
gncEntrySetInvTaxable(entry, TRUE);
gncEntrySetInvTaxIncluded(entry, FALSE);
gncEntrySetInvTaxTable(entry, taxtable);
gncEntrySetQuantity(entry, gnc_numeric_create (2, 1));
gncEntrySetInvPrice(entry, gnc_numeric_create (3, 1));
/* Check unrounded result */
g_assert(gnc_numeric_equal (gncEntryGetDocValue(entry, FALSE, TRUE, FALSE), gnc_numeric_create (6, 1)));
g_assert(gnc_numeric_equal (gncEntryGetDocTaxValue(entry, FALSE, TRUE, FALSE), gnc_numeric_create (6, 10)));
/* Check rounded result */
g_assert(gnc_numeric_equal (gncEntryGetDocValue(entry, TRUE, TRUE, FALSE), gnc_numeric_create (6, 1)));
g_assert(gnc_numeric_equal (gncEntryGetDocTaxValue(entry, TRUE, TRUE, FALSE), gnc_numeric_create (6, 10)));
// Test with numbers that do require rounding
/* Tax 10% (high precision GncNumeric), tax included */
gncEntrySetInvTaxIncluded(entry, TRUE);
/* Check unrounded result */
g_assert(gnc_numeric_equal (gncEntryGetDocValue(entry, FALSE, TRUE, FALSE), gnc_numeric_create (60, 11)));
g_assert(gnc_numeric_equal (gncEntryGetDocTaxValue(entry, FALSE, TRUE, FALSE), gnc_numeric_create (60, 110)));
/* Check rounded result */
g_assert(gnc_numeric_equal (gncEntryGetDocValue(entry, TRUE, TRUE, FALSE), gnc_numeric_create (545455, 100000)));
g_assert(gnc_numeric_equal (gncEntryGetDocTaxValue(entry, TRUE, TRUE, FALSE), gnc_numeric_create (54545, 100000)));
// Use different taxtable percentage precision
/* Tax 10% (low precision GncNumeric), tax included */
gncTaxTableEntrySetAmount(tt_entry, gnc_numeric_create(10, 1));
gncEntrySetInvTaxTable(entry, NULL);
gncEntrySetInvTaxTable(entry, taxtable);
/* Check unrounded result */
g_assert(gnc_numeric_equal (gncEntryGetDocValue(entry, FALSE, TRUE, FALSE), gnc_numeric_create (60, 11)));
g_assert(gnc_numeric_equal (gncEntryGetDocTaxValue(entry, FALSE, TRUE, FALSE), gnc_numeric_create (60, 110)));
/* Check rounded result */
g_assert(gnc_numeric_equal (gncEntryGetDocValue(entry, TRUE, TRUE, FALSE), gnc_numeric_create (545455, 100000)));
g_assert(gnc_numeric_equal (gncEntryGetDocTaxValue(entry, TRUE, TRUE, FALSE), gnc_numeric_create (54545, 100000)));
// Test with odd tax percentage (Taken from a mailing list example)
/* Tax 13% (high precision GncNumeric), tax not included */
gncEntrySetInvTaxIncluded(entry, FALSE);
gncTaxTableEntrySetAmount(tt_entry, gnc_numeric_create(1300000, 100000));
gncEntrySetInvTaxTable(entry, NULL);
gncEntrySetInvTaxTable(entry, taxtable);
gncEntrySetQuantity(entry, gnc_numeric_create (1, 1));
gncEntrySetInvPrice(entry, gnc_numeric_create (27750, 100));
/* Check unrounded result */
g_assert(gnc_numeric_equal (gncEntryGetDocValue(entry, FALSE, TRUE, FALSE), gnc_numeric_create (27750, 100)));
g_assert(gnc_numeric_equal (gncEntryGetDocTaxValue(entry, FALSE, TRUE, FALSE), gnc_numeric_create (36075, 1000)));
/* Check rounded result */
g_assert(gnc_numeric_equal (gncEntryGetDocValue(entry, TRUE, TRUE, FALSE), gnc_numeric_create (27750, 100)));
g_assert(gnc_numeric_equal (gncEntryGetDocTaxValue(entry, TRUE, TRUE, FALSE), gnc_numeric_create (36075, 1000)));
/* Note on the above expected result: the standard gncEntry denom is 100000 if the entry has no invoice or
* bill set. So with the example above no rounding is required yet */
// Test with odd tax percentage (Taken from a mailing list example)
/* Tax 13% (low precision GncNumeric), tax not included */
gncEntrySetInvTaxIncluded(entry, FALSE);
gncTaxTableEntrySetAmount(tt_entry, gnc_numeric_create(13, 1));
gncEntrySetInvTaxTable(entry, NULL);
gncEntrySetInvTaxTable(entry, taxtable);
/* Check unrounded result */
g_assert(gnc_numeric_equal (gncEntryGetDocValue(entry, FALSE, TRUE, FALSE), gnc_numeric_create (27750, 100)));
g_assert(gnc_numeric_equal (gncEntryGetDocTaxValue(entry, FALSE, TRUE, FALSE), gnc_numeric_create (36075, 1000)));
/* Check rounded result */
g_assert(gnc_numeric_equal (gncEntryGetDocValue(entry, TRUE, TRUE, FALSE), gnc_numeric_create (27750, 100)));
g_assert(gnc_numeric_equal (gncEntryGetDocTaxValue(entry, TRUE, TRUE, FALSE), gnc_numeric_create (36075, 1000)));
/* Note on the above expected result: the standard gncEntry denom is 100000 if the entry has no invoice or
* bill set. So with the example above no rounding is required yet */
// 2. gncEntry as part of a gncInvoice - the invoice currency's denominator is expected during rounding
gncInvoiceAddEntry(fixture->invoice, entry);
// Test with numbers that don't require rounding
/* Tax 10% (high precision GncNumeric), tax not included */
gncEntrySetInvTaxIncluded(entry, FALSE);
gncTaxTableEntrySetAmount(tt_entry, gnc_numeric_create(1000000, 100000));
gncEntrySetQuantity(entry, gnc_numeric_create (2, 1));
gncEntrySetInvPrice(entry, gnc_numeric_create (3, 1));
/* Check unrounded result */
g_assert(gnc_numeric_equal (gncEntryGetDocValue(entry, FALSE, TRUE, FALSE), gnc_numeric_create (6, 1)));
g_assert(gnc_numeric_equal (gncEntryGetDocTaxValue(entry, FALSE, TRUE, FALSE), gnc_numeric_create (6, 10)));
/* Check rounded result */
g_assert(gnc_numeric_equal (gncEntryGetDocValue(entry, TRUE, TRUE, FALSE), gnc_numeric_create (6, 1)));
g_assert(gnc_numeric_equal (gncEntryGetDocTaxValue(entry, TRUE, TRUE, FALSE), gnc_numeric_create (6, 10)));
// Test with numbers that do require rounding
/* Tax 10% (high precision GncNumeric), tax included */
gncEntrySetInvTaxIncluded(entry, TRUE);
/* Check unrounded result */
g_assert(gnc_numeric_equal (gncEntryGetDocValue(entry, FALSE, TRUE, FALSE), gnc_numeric_create (60, 11)));
g_assert(gnc_numeric_equal (gncEntryGetDocTaxValue(entry, FALSE, TRUE, FALSE), gnc_numeric_create (60, 110)));
/* Check rounded result */
g_assert(gnc_numeric_equal (gncEntryGetDocValue(entry, TRUE, TRUE, FALSE), gnc_numeric_create (545, 100)));
g_assert(gnc_numeric_equal (gncEntryGetDocTaxValue(entry, TRUE, TRUE, FALSE), gnc_numeric_create (55, 100)));
// Use different taxtable percentage precision
/* Tax 10% (low precision GncNumeric), tax included */
gncTaxTableEntrySetAmount(tt_entry, gnc_numeric_create(10, 1));
gncEntrySetInvTaxTable(entry, NULL);
gncEntrySetInvTaxTable(entry, taxtable);
/* Check unrounded result */
g_assert(gnc_numeric_equal (gncEntryGetDocValue(entry, FALSE, TRUE, FALSE), gnc_numeric_create (60, 11)));
g_assert(gnc_numeric_equal (gncEntryGetDocTaxValue(entry, FALSE, TRUE, FALSE), gnc_numeric_create (60, 110)));
/* Check rounded result */
g_assert(gnc_numeric_equal (gncEntryGetDocValue(entry, TRUE, TRUE, FALSE), gnc_numeric_create (545, 100)));
g_assert(gnc_numeric_equal (gncEntryGetDocTaxValue(entry, TRUE, TRUE, FALSE), gnc_numeric_create (55, 100)));
// Test with odd tax percentage (Taken from a mailing list example)
/* Tax 13% (high precision GncNumeric), tax not included */
gncEntrySetInvTaxIncluded(entry, FALSE);
gncTaxTableEntrySetAmount(tt_entry, gnc_numeric_create(1300000, 100000));
gncEntrySetInvTaxTable(entry, NULL);
gncEntrySetInvTaxTable(entry, taxtable);
gncEntrySetQuantity(entry, gnc_numeric_create (1, 1));
gncEntrySetInvPrice(entry, gnc_numeric_create (27750, 100));
/* Check unrounded result */
g_assert(gnc_numeric_equal (gncEntryGetDocValue(entry, FALSE, TRUE, FALSE), gnc_numeric_create (27750, 100)));
g_assert(gnc_numeric_equal (gncEntryGetDocTaxValue(entry, FALSE, TRUE, FALSE), gnc_numeric_create (36075, 1000)));
/* Check rounded result */
g_assert(gnc_numeric_equal (gncEntryGetDocValue(entry, TRUE, TRUE, FALSE), gnc_numeric_create (27750, 100)));
g_assert(gnc_numeric_equal (gncEntryGetDocTaxValue(entry, TRUE, TRUE, FALSE), gnc_numeric_create (3608, 100)));
/* Note on the above expected result: the standard gncEntry denom is 100000 if the entry has no invoice or
* bill set. So with the example above no rounding is required yet */
// Test with odd tax percentage (Taken from a mailing list example)
/* Tax 13% (low precision GncNumeric), tax not included */
gncEntrySetInvTaxIncluded(entry, FALSE);
gncTaxTableEntrySetAmount(tt_entry, gnc_numeric_create(13, 1));
gncEntrySetInvTaxTable(entry, NULL);
gncEntrySetInvTaxTable(entry, taxtable);
/* Check unrounded result */
g_assert(gnc_numeric_equal (gncEntryGetDocValue(entry, FALSE, TRUE, FALSE), gnc_numeric_create (27750, 100)));
g_assert(gnc_numeric_equal (gncEntryGetDocTaxValue(entry, FALSE, TRUE, FALSE), gnc_numeric_create (36075, 1000)));
/* Check rounded result */
g_assert(gnc_numeric_equal (gncEntryGetDocValue(entry, TRUE, TRUE, FALSE), gnc_numeric_create (27750, 100)));
g_assert(gnc_numeric_equal (gncEntryGetDocTaxValue(entry, TRUE, TRUE, FALSE), gnc_numeric_create (3608, 100)));
/* Note on the above expected result: the standard gncEntry denom is 100000 if the entry has no invoice or
* bill set. So with the example above no rounding is required yet */
gncTaxTableBeginEdit(taxtable);
gncTaxTableDestroy(taxtable);
}
void
test_suite_gncEntry ( void )
{
GNC_TEST_ADD( suitename, "basics", Fixture, NULL, setup, test_entry_basics, teardown );
GNC_TEST_ADD( suitename, "value rounding", Fixture, NULL, setup, test_entry_rounding, teardown );
}