Commit c6e31c96 authored by Łukasz Wojniłowicz's avatar Łukasz Wojniłowicz
Browse files

Fix rounding problem with investments

Patch introduces rounding rules per security for fixing bug #372163. It
seems that this broker always rounds amounts down while my broker rounds
amounts depending on the outlying digit, so it couldn't work for both of
us without rules.

Rounding is done in InvestTransactionEditor because it has all needed
informations at hand.

No rounding of shares is done in InvestTransactionEditor::setupPrice.
Transaction from bug #372163 looks as follows:
brokerage:
shares = 1,009 ; value = 1,009
investment:
shares = -1 ; value = 1,009

InvestTransactionEditor::setupPrice causes brokerage to look as follows:
shares = 1,01 ; value = 1,009
As we can see shares and value diverge, which is unacceptable here.

Patch makes assumption that transaction has only single split of
stock/mutual fund/bond.

BUG:345655
BUG:357784
BUG:365177
BUG:372163
FIXED-IN:5.0

Differential Revision: https://phabricator.kde.org/D5187

Signed-off-by: Łukasz Wojniłowicz's avatarŁukasz Wojniłowicz <lukasz.wojnilowicz@gmail.com>
parent c8b12d84
......@@ -803,7 +803,8 @@ void InvestTransactionEditor::slotUpdateTotalAmount()
if (total && total->isVisible()) {
MyMoneyMoney amount;
totalAmount(amount);
total->setText(amount.formatMoney(m_currency.tradingSymbol(), MyMoneyMoney::denomToPrec(m_security.smallestAccountFraction())));
total->setText(amount.convert(m_currency.smallestAccountFraction(), static_cast<MyMoneyMoney::roundingMethod>(m_security.roundingMethod()))
.formatMoney(m_currency.tradingSymbol(), MyMoneyMoney::denomToPrec(m_currency.smallestAccountFraction())));
}
}
......@@ -935,9 +936,9 @@ bool InvestTransactionEditor::setupPrice(const MyMoneyTransaction& t, MyMoneySpl
// update shares if the transaction commodity is the currency
// of the current selected account
split.setShares((split.value() * price).convert(fract));
split.setShares(split.value() * price);
} else {
split.setShares(split.value().convert(fract));
split.setShares(split.value());
}
return true;
......@@ -1056,21 +1057,37 @@ bool InvestTransactionEditor::createTransaction(MyMoneyTransaction& t, const MyM
// add the splits to the transaction
if (rc) {
if (!assetAccountSplit.accountId().isEmpty()) {
assetAccountSplit.clearId();
t.addSplit(assetAccountSplit);
}
if (security.name().isEmpty()) // new transaction has no security filled...
security = file->security(file->account(s0.accountId()).currencyId()); // ...so fetch it from s0 split
QList<MyMoneySplit>::iterator it_s;
for (it_s = feeSplits.begin(); it_s != feeSplits.end(); ++it_s) {
(*it_s).clearId();
t.addSplit(*it_s);
}
QList<MyMoneySplit> resultSplits; // concatenates splits for easy processing
if (!assetAccountSplit.accountId().isEmpty())
resultSplits.append(assetAccountSplit);
if (!feeSplits.isEmpty())
resultSplits.append(feeSplits);
for (it_s = interestSplits.begin(); it_s != interestSplits.end(); ++it_s) {
(*it_s).clearId();
t.addSplit(*it_s);
if (!interestSplits.isEmpty())
resultSplits.append(interestSplits);
AlkValue::RoundingMethod roundingMethod = AlkValue::RoundRound;
if (security.roundingMethod() != AlkValue::RoundNever)
roundingMethod = security.roundingMethod();
int currencyFraction = currency.smallestAccountFraction();
int securityFraction = security.smallestAccountFraction();
// assuming that all non-stock splits are monetary
foreach (auto split, resultSplits) {
split.clearId();
split.setShares(split.shares().convertDenominator(currencyFraction, roundingMethod));
split.setValue(split.value().convertDenominator(currencyFraction, roundingMethod));
t.addSplit(split);
}
s0.setShares(s0.shares().convertDenominator(securityFraction, roundingMethod)); // only shares variable from stock split isn't evaluated in currency
s0.setValue(s0.value().convertDenominator(currencyFraction, roundingMethod));
t.addSplit(s0);
}
......
......@@ -9,6 +9,7 @@
John C <thetacoturtle@users.sourceforge.net>
Thomas Baumgart <ipwizard@users.sourceforge.net>
Kevin Tambascio <ktambascio@users.sourceforge.net>
(C) 2017 by Łukasz Wojniłowicz <lukasz.wojnilowicz@gmail.com>
***************************************************************************/
/***************************************************************************
......@@ -126,6 +127,36 @@ const QString KMyMoneyUtils::securityTypeToString(const MyMoneySecurity::eSECURI
return i18n(MyMoneySecurity::securityTypeToString(securityType).toLatin1());
}
AlkValue::RoundingMethod KMyMoneyUtils::stringToRoundingMethod(const QString& txt)
{
AlkValue::RoundingMethod rc = AlkValue::RoundNever;
QString tmp = txt.toLower();
if (tmp == i18n("Never").toLower())
rc = AlkValue::RoundNever;
else if (tmp == i18n("Floor").toLower())
rc = AlkValue::RoundFloor;
else if (tmp == i18n("Ceil").toLower())
rc = AlkValue::RoundCeil;
else if (tmp == i18n("Truncate").toLower())
rc = AlkValue::RoundTruncate;
else if (tmp == i18n("Promote").toLower())
rc = AlkValue::RoundPromote;
else if (tmp == i18n("HalfDown").toLower())
rc = AlkValue::RoundHalfDown;
else if (tmp == i18n("HalfUp").toLower())
rc = AlkValue::RoundHalfUp;
else if (tmp == i18n("Round").toLower())
rc = AlkValue::RoundRound;
return rc;
}
const QString KMyMoneyUtils::roundingMethodToString(const AlkValue::RoundingMethod roundingMethod)
{
return i18n(MyMoneySecurity::roundingMethodToString(roundingMethod).toLatin1());
}
const QString KMyMoneyUtils::occurrenceToString(const MyMoneySchedule::occurrenceE occurrence)
{
return i18nc("Frequency of schedule", MyMoneySchedule::occurrenceToString(occurrence).toLatin1());
......
......@@ -180,6 +180,28 @@ public:
*/
static const QString securityTypeToString(const MyMoneySecurity::eSECURITYTYPE securityType);
/**
* This method is used to convert a rounding method from its
* string form to the internal used numeric value.
*
* @param txt reference to a QString containing the string to convert
* @return RoundingMethod containing the internal used numeric value. For possible
* values see AlkValue::RoundingMethod
*/
static AlkValue::RoundingMethod stringToRoundingMethod(const QString& txt);
/**
* This method is used to convert the internal representation of
* an rounding method into a human readable format
*
* @param roundingMethod enumerated representation of the rounding method.
* For possible values, see AlkValue::RoundingMethod
* @return QString representing the human readable form translated according to the language cataglogue
*
* @sa MyMoneySecurity::roundingMethodToString()
*/
static const QString roundingMethodToString(const AlkValue::RoundingMethod roundingMethod);
/**
* This method is used to convert the occurrence type from its
* internal representation into a human readable format.
......
......@@ -9,6 +9,7 @@
John C <thetacoturtle@users.sourceforge.net>
Thomas Baumgart <ipwizard@users.sourceforge.net>
Kevin Tambascio <ktambascio@users.sourceforge.net>
(C) 2017 by Łukasz Wojniłowicz <lukasz.wojnilowicz@gmail.com>
***************************************************************************/
/***************************************************************************
......@@ -82,6 +83,7 @@ MyMoneySecurity::MyMoneySecurity(const QDomElement& node) :
setName(QStringEmpty(node.attribute("name")));
setTradingSymbol(QStringEmpty(node.attribute("symbol")));
setSecurityType(static_cast<eSECURITYTYPE>(node.attribute("type").toInt()));
setRoundingMethod(static_cast<AlkValue::RoundingMethod>(node.attribute("rounding-method").toInt()));
setSmallestAccountFraction(node.attribute("saf").toInt());
if (isCurrency()) {
......@@ -103,6 +105,7 @@ bool MyMoneySecurity::operator == (const MyMoneySecurity& r) const
&& (m_name == r.m_name)
&& (m_tradingSymbol == r.m_tradingSymbol)
&& (m_tradingMarket == r.m_tradingMarket)
&& (m_roundingMethod == r.m_roundingMethod)
&& (m_tradingSymbol == r.m_tradingSymbol)
&& (m_tradingCurrency == r.m_tradingCurrency)
&& (m_securityType == r.m_securityType)
......@@ -139,6 +142,7 @@ void MyMoneySecurity::writeXML(QDomDocument& document, QDomElement& parent) cons
el.setAttribute("name", m_name);
el.setAttribute("symbol", m_tradingSymbol);
el.setAttribute("type", static_cast<int>(m_securityType));
el.setAttribute("rounding-method", static_cast<int>(m_roundingMethod));
el.setAttribute("saf", m_smallestAccountFraction);
if (isCurrency()) {
el.setAttribute("ppu", m_partsPerUnit);
......@@ -181,3 +185,38 @@ QString MyMoneySecurity::securityTypeToString(const eSECURITYTYPE securityType)
return returnString;
}
QString MyMoneySecurity::roundingMethodToString(const AlkValue::RoundingMethod roundingMethod)
{
QString returnString;
switch (roundingMethod) {
case AlkValue::RoundNever:
returnString = I18N_NOOP("Never");
break;
case AlkValue::RoundFloor:
returnString = I18N_NOOP("Floor");
break;
case AlkValue::RoundCeil:
returnString = I18N_NOOP("Ceil");
break;
case AlkValue::RoundTruncate:
returnString = I18N_NOOP("Truncate");
break;
case AlkValue::RoundPromote:
returnString = I18N_NOOP("Promote");
break;
case AlkValue::RoundHalfDown:
returnString = I18N_NOOP("HalfDown");
break;
case AlkValue::RoundHalfUp:
returnString = I18N_NOOP("HalfUp");
break;
case AlkValue::RoundRound:
returnString = I18N_NOOP("Round");
break;
default:
returnString = I18N_NOOP("Unknown");
}
return returnString;
}
......@@ -108,6 +108,13 @@ public:
return m_securityType == SECURITY_CURRENCY;
};
AlkValue::RoundingMethod roundingMethod() const {
return m_roundingMethod;
}
void setRoundingMethod(const AlkValue::RoundingMethod& rnd) {
m_roundingMethod = rnd;
}
const QString& tradingMarket() const {
return m_tradingMarket;
}
......@@ -167,16 +174,28 @@ public:
*/
static QString securityTypeToString(const MyMoneySecurity::eSECURITYTYPE securityType);
/**
* This method is used to convert the internal representation of
* an rounding method into a human readable format
*
* @param roundingMethod enumerated representation of the rouding method.
* For possible values, see AlkValue::RoundingMethod
*
* @return QString representing the human readable form
*/
static QString roundingMethodToString(const AlkValue::RoundingMethod roundingMethod);
protected:
QString m_name;
QString m_tradingSymbol;
QString m_tradingMarket;
QString m_tradingCurrency;
eSECURITYTYPE m_securityType;
int m_smallestAccountFraction;
int m_smallestCashFraction;
int m_partsPerUnit;
QString m_name;
QString m_tradingSymbol;
QString m_tradingMarket;
QString m_tradingCurrency;
eSECURITYTYPE m_securityType;
AlkValue::RoundingMethod m_roundingMethod;
int m_smallestAccountFraction;
int m_smallestCashFraction;
int m_partsPerUnit;
};
/**
......
......@@ -332,6 +332,7 @@ void MyMoneyDbDef::Securities()
appendField(MyMoneyDbColumn("name", "text", false, NOTNULL));
appendField(MyMoneyDbTextColumn("symbol"));
appendField(MyMoneyDbIntColumn("type", MyMoneyDbIntColumn::SMALL, UNSIGNED, false, NOTNULL));
appendField(MyMoneyDbIntColumn("roundingMethod", MyMoneyDbIntColumn::SMALL, UNSIGNED, false, NOTNULL));
appendField(MyMoneyDbTextColumn("typeString"));
appendField(MyMoneyDbColumn("smallestAccountFraction", "varchar(24)"));
appendField(MyMoneyDbTextColumn("tradingMarket"));
......
......@@ -2245,6 +2245,7 @@ void MyMoneyStorageSql::writeSecurity(const MyMoneySecurity& security, QSqlQuery
q.bindValue(":symbol", security.tradingSymbol());
q.bindValue(":type", static_cast<int>(security.securityType()));
q.bindValue(":typeString", MyMoneySecurity::securityTypeToString(security.securityType()));
q.bindValue(":roundingMethod", static_cast<int>(security.roundingMethod()));
q.bindValue(":smallestAccountFraction", security.smallestAccountFraction());
q.bindValue(":tradingCurrency", security.tradingCurrency());
q.bindValue(":tradingMarket", security.tradingMarket());
......@@ -4277,6 +4278,7 @@ const QMap<QString, MyMoneySecurity> MyMoneyStorageSql::fetchSecurities(const QS
int nameCol = t.fieldNumber("name");
int symbolCol = t.fieldNumber("symbol");
int typeCol = t.fieldNumber("type");
int roundingMethodCol = t.fieldNumber("roundingMethod");
int smallestAccountFractionCol = t.fieldNumber("smallestAccountFraction");
int tradingCurrencyCol = t.fieldNumber("tradingCurrency");
int tradingMarketCol = t.fieldNumber("tradingMarket");
......@@ -4289,6 +4291,7 @@ const QMap<QString, MyMoneySecurity> MyMoneyStorageSql::fetchSecurities(const QS
e.setName(GETSTRING(nameCol));
e.setTradingSymbol(GETSTRING(symbolCol));
e.setSecurityType(static_cast<MyMoneySecurity::eSECURITYTYPE>(GETINT(typeCol)));
e.setRoundingMethod(static_cast<AlkValue::RoundingMethod>(GETINT(roundingMethodCol)));
saf = GETINT(smallestAccountFractionCol);
e.setTradingCurrency(GETSTRING(tradingCurrencyCol));
e.setTradingMarket(GETSTRING(tradingMarketCol));
......
......@@ -2,8 +2,8 @@
transaction.cpp - description
-------------------
begin : Tue Jun 13 2006
copyright : (C) 2000-2006 by Thomas Baumgart
email : Thomas Baumgart <ipwizard@users.sourceforge.net>
copyright : (C) 2000-2006 by Thomas Baumgart <ipwizard@users.sourceforge.net>
(C) 2017 by Łukasz Wojniłowicz <lukasz.wojnilowicz@gmail.com>
***************************************************************************/
/***************************************************************************
......@@ -1656,7 +1656,8 @@ bool InvestTransaction::formCellText(QString& txt, Qt::Alignment& align, int row
case ValueColumn2:
align |= Qt::AlignRight;
if ((fieldEditable = haveAmount()) == true) {
txt = MyMoneyUtils::formatMoney(m_assetAccountSplit.value().abs(), m_currency);
txt = m_assetAccountSplit.value().abs()
.formatMoney(m_currency.tradingSymbol(), MyMoneyMoney::denomToPrec(m_currency.smallestAccountFraction()));
}
}
break;
......
......@@ -3,6 +3,7 @@
-------------------
begin : Sun Jun 27 2010
copyright : (C) 2010 by Fernando Vilas
(C) 2017 by Łukasz Wojniłowicz
email : kmymoney-devel@kde.org
***************************************************************************/
......@@ -65,6 +66,12 @@ KInvestmentDetailsWizardPage::KInvestmentDetailsWizardPage(QWidget *parent)
registerField("tradingMarket", m_tradingMarket, "currentText", SIGNAL(currentIndexChanged(QString)));
m_roundingMethod->addItem(i18nc("Rounding method", "Round"), AlkValue::RoundRound);
m_roundingMethod->addItem(i18nc("Rounding method", "Ceil"), AlkValue::RoundCeil);
m_roundingMethod->addItem(i18nc("Rounding method", "Floor"), AlkValue::RoundFloor);
m_roundingMethod->addItem(i18nc("Rounding method", "Truncate"), AlkValue::RoundTruncate);
registerField("roundingMethod", m_roundingMethod, "currentData", SIGNAL(currentIndexChanged(int)));
registerField("fraction", m_fraction, "value", SIGNAL(textChanged()));
connect(m_fraction, SIGNAL(textChanged(QString)),
this, SIGNAL(completeChanged()));
......@@ -78,6 +85,10 @@ void KInvestmentDetailsWizardPage::init2(const MyMoneySecurity& security)
MyMoneySecurity tradingCurrency = MyMoneyFile::instance()->currency(security.tradingCurrency());
m_investmentSymbol->setText(security.tradingSymbol());
m_tradingMarket->setCurrentIndex(m_tradingMarket->findText(security.tradingMarket(), Qt::MatchExactly));
if (security.roundingMethod() == AlkValue::RoundNever)
m_roundingMethod->setCurrentIndex(0);
else
m_roundingMethod->setCurrentIndex(m_roundingMethod->findData(security.roundingMethod()));
m_fraction->setValue(MyMoneyMoney(security.smallestAccountFraction(), 1));
m_tradingCurrencyEdit->setSecurity(tradingCurrency);
......
......@@ -6,12 +6,12 @@
<rect>
<x>0</x>
<y>0</y>
<width>198</width>
<height>228</height>
<width>216</width>
<height>266</height>
</rect>
</property>
<property name="title">
<string>Investment details</string>
<string>In&amp;vestment details</string>
</property>
<layout class="QVBoxLayout">
<item>
......@@ -45,35 +45,91 @@
</item>
<item>
<layout class="QGridLayout">
<item row="0" column="0">
<widget class="QLabel" name="textLabel2_2">
<item row="3" column="0">
<widget class="QLabel" name="textLabel1_3">
<property name="text">
<string>Trading symbol</string>
<string>Trading market</string>
</property>
<property name="wordWrap">
<bool>false</bool>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="kMyMoneyLineEdit" name="m_investmentSymbol" native="true">
<property name="toolTip">
<string>Enter the ticker symbol (e.g. RHAT).</string>
<item row="4" column="0">
<widget class="QLabel" name="textLabel1_2">
<property name="text">
<string>Identification</string>
</property>
<property name="wordWrap">
<bool>false</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="textLabel3">
<item row="3" column="1">
<widget class="KComboBox" name="m_tradingMarket">
<property name="editable">
<bool>true</bool>
</property>
<item>
<property name="text">
<string>AMEX</string>
</property>
</item>
<item>
<property name="text">
<string>EUREX</string>
</property>
</item>
<item>
<property name="text">
<string>FUND</string>
</property>
</item>
<item>
<property name="text">
<string>NASDAQ</string>
</property>
</item>
<item>
<property name="text">
<string>NYSE</string>
</property>
</item>
<item>
<property name="text">
<string>XETRA</string>
</property>
</item>
</widget>
</item>
<item row="5" column="1">
<widget class="KMyMoneySecuritySelector" name="m_tradingCurrencyEdit" native="true"/>
</item>
<item row="1" column="1">
<widget class="kMyMoneyLineEdit" name="m_investmentName" native="true"/>
</item>
<item row="5" column="0">
<widget class="QLabel" name="textLabel1_4">
<property name="text">
<string>Full name</string>
<string>Trading Currency</string>
</property>
<property name="wordWrap">
<bool>false</bool>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="kMyMoneyLineEdit" name="m_investmentName" native="true"/>
<item row="6" column="0">
<widget class="QLabel" name="textLabel2_5">
<property name="text">
<string>Price entry</string>
</property>
<property name="wordWrap">
<bool>false</bool>
</property>
</widget>
</item>
<item row="6" column="1">
<widget class="KMyMoneyGeneralCombo" name="m_priceMode" native="true"/>
</item>
<item row="2" column="0">
<widget class="QLabel" name="textLabel2_2_2">
......@@ -111,60 +167,20 @@
</item>
</layout>
</item>
<item row="3" column="0">
<widget class="QLabel" name="textLabel1_3">
<item row="0" column="0">
<widget class="QLabel" name="textLabel2_2">
<property name="text">
<string>Trading market</string>
<string>Trading symbol</string>
</property>
<property name="wordWrap">
<bool>false</bool>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="KComboBox" name="m_tradingMarket">
<property name="editable">
<bool>true</bool>
</property>
<item>
<property name="text">
<string>AMEX</string>
</property>
</item>
<item>
<property name="text">
<string>EUREX</string>
</property>
</item>
<item>
<property name="text">
<string>FUND</string>
</property>
</item>
<item>
<property name="text">
<string>NASDAQ</string>
</property>
</item>
<item>
<property name="text">
<string>NYSE</string>
</property>
</item>
<item>
<property name="text">
<string>XETRA</string>
</property>
</item>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="textLabel1_2">
<property name="text">
<string>Identification</string>
</property>
<property name="wordWrap">
<bool>false</bool>
<item row="0" column="1">
<widget class="kMyMoneyLineEdit" name="m_investmentSymbol" native="true">
<property name="toolTip">
<string>Enter the ticker symbol (e.g. RHAT).</string>
</property>
</widget>
</item>
......@@ -175,31 +191,29 @@
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="textLabel1_4">
<item row="1" column="0">
<widget class="QLabel" name="textLabel3">
<property name="text">
<string>Trading Currency</string>
<string>Full name</string>
</property>
<property name="wordWrap">
<bool>false</bool>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="KMyMoneySecuritySelector" name="m_tradingCurrencyEdit" native="true"/>
</item>
<item row="6" column="0">
<widget class="QLabel" name="textLabel2_5">
<item row="7" column="0">
<widget class="QLabel" name="textLabel4">
<property name="text">
<string>Price entry</string>
</property>
<property name="wordWrap">
<bool>false</bool>
<string>Remainder</string>
</property>
</widget>
</item>
<item row="6" column="1">
<widget class="KMyMoneyGeneralCombo" name="m_priceMode" native="true"/>
<item row="7" column="1">
<widget class="QComboBox" name="m_roundingMethod">
<property name="toolTip">
<string>This controls what to do with digit situated after precision digits in amount values.</string>
</property>
</widget>
</item>
</layout>
</item>
......@@ -227,6 +241,11 @@
<extends>QComboBox</extends>
<header>kcombobox.h</header>
</customwidget>
<customwidget>
<class>kMyMoneyEdit</class>
<extends>QWidget</extends>
<header>kmymoneyedit.h</header>
</customwidget>
<customwidget>
<class>kMyMoneyLineEdit</class>
<extends>QWidget</extends>
......@@ -244,21 +263,16 @@