GnuCash c935c2f+
Loading...
Searching...
No Matches
gnc-imp-props-tx.cpp
1/********************************************************************\
2 * gnc-imp-props-tx.cpp - import transactions from csv files *
3 * *
4 * This program is free software; you can redistribute it and/or *
5 * modify it under the terms of the GNU General Public License as *
6 * published by the Free Software Foundation; either version 2 of *
7 * the License, or (at your option) any later version. *
8 * *
9 * This program is distributed in the hope that it will be useful, *
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of *
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
12 * GNU General Public License for more details. *
13 * *
14 * You should have received a copy of the GNU General Public License*
15 * along with this program; if not, contact: *
16 * *
17 * Free Software Foundation Voice: +1-617-542-5942 *
18 * 51 Franklin Street, Fifth Floor Fax: +1-617-542-2652 *
19 * Boston, MA 02110-1301, USA gnu@gnu.org *
20 * *
21\********************************************************************/
22
23#include <glib.h>
24#include <glib/gi18n.h>
25
26#include <platform.h>
27#if PLATFORM(WINDOWS)
28#include <windows.h>
29#endif
30
31#include "engine-helpers.h"
32#include "gnc-ui-util.h"
33#include "Account.h"
34#include "Transaction.h"
35#include "gnc-pricedb.h"
36#include <gnc-exp-parser.h>
37
38#include <algorithm>
39#include <exception>
40#include <map>
41#include <numeric>
42#include <string>
43#include <vector>
44
45#include <boost/locale.hpp>
46#include <boost/regex.hpp>
47#include <boost/regex/icu.hpp>
48#include <gnc-locale-utils.hpp>
49#include "gnc-imp-props-tx.hpp"
50#include <ctre.hpp>
51
52namespace bl = boost::locale;
53
54G_GNUC_UNUSED static QofLogModule log_module = GNC_MOD_IMPORT;
55
56/* This map contains a set of strings representing the different column types. */
57std::map<GncTransPropType, const char*> gnc_csv_col_type_strs = {
58 { GncTransPropType::NONE, N_("None") },
59 { GncTransPropType::UNIQUE_ID, N_("Transaction ID") },
60 { GncTransPropType::DATE, N_("Date") },
61 { GncTransPropType::NUM, N_("Number") },
62 { GncTransPropType::DESCRIPTION, N_("Description") },
63 { GncTransPropType::NOTES, N_("Notes") },
64 { GncTransPropType::COMMODITY, N_("Transaction Commodity") },
65 { GncTransPropType::VOID_REASON, N_("Void Reason") },
66 { GncTransPropType::ACTION, N_("Action") },
67 { GncTransPropType::ACCOUNT, N_("Account") },
68 { GncTransPropType::AMOUNT, N_("Amount") },
69 { GncTransPropType::AMOUNT_NEG, N_("Amount (Negated)") },
70 { GncTransPropType::VALUE, N_("Value") },
71 { GncTransPropType::VALUE_NEG, N_("Value (Negated)") },
72 { GncTransPropType::PRICE, N_("Price") },
73 { GncTransPropType::MEMO, N_("Memo") },
74 { GncTransPropType::REC_STATE, N_("Reconciled") },
75 { GncTransPropType::REC_DATE, N_("Reconcile Date") },
76 { GncTransPropType::TACTION, N_("Transfer Action") },
77 { GncTransPropType::TACCOUNT, N_("Transfer Account") },
78 { GncTransPropType::TAMOUNT, N_("Transfer Amount") },
79 { GncTransPropType::TAMOUNT_NEG, N_("Transfer Amount (Negated)") },
80 { GncTransPropType::TMEMO, N_("Transfer Memo") },
81 { GncTransPropType::TREC_STATE, N_("Transfer Reconciled") },
82 { GncTransPropType::TREC_DATE, N_("Transfer Reconcile Date") }
83};
84
85/* Below two vectors define which properties the user *can't* select
86 * in two-split or multi-split mode (mostly because they don't make
87 * sense in that context).
88 */
89std::vector<GncTransPropType> twosplit_blacklist = {
90 GncTransPropType::UNIQUE_ID };
91std::vector<GncTransPropType> multisplit_blacklist = {
92 GncTransPropType::TACTION,
93 GncTransPropType::TACCOUNT,
94 GncTransPropType::TAMOUNT,
95 GncTransPropType::TAMOUNT_NEG,
96 GncTransPropType::TMEMO,
97 GncTransPropType::TREC_STATE,
98 GncTransPropType::TREC_DATE
99};
100/* List of properties that can be assigned to multiple columns at once */
101std::vector<GncTransPropType> multi_col_props = {
102 GncTransPropType::AMOUNT,
103 GncTransPropType::AMOUNT_NEG,
104 GncTransPropType::TAMOUNT,
105 GncTransPropType::TAMOUNT_NEG,
106 GncTransPropType::VALUE,
107 GncTransPropType::VALUE_NEG
108};
109
110bool is_multi_col_prop (GncTransPropType prop)
111{
112 return (std::find (multi_col_props.cbegin(),
113 multi_col_props.cend(), prop) != multi_col_props.cend());
114}
115
116GncTransPropType sanitize_trans_prop (GncTransPropType prop, bool multi_split)
117{
118 auto bl = multi_split ? multisplit_blacklist : twosplit_blacklist;
119 if (std::find(bl.begin(), bl.end(), prop) == bl.end())
120 return prop;
121 else
122 return GncTransPropType::NONE;
123}
124
125
132GncNumeric parse_monetary (const std::string &str, int currency_format)
133{
134 /* An empty field is treated as zero */
135 if (str.empty())
136 return GncNumeric{};
137
138 /* Strings otherwise containing no digits will be considered invalid */
139 static constexpr ctll::fixed_string digit_re{"[0-9]"};
140 if(!ctre::search<digit_re>(str))
141 throw std::invalid_argument (_("Value doesn't appear to contain a valid number."));
142
143 static const auto expr = boost::make_u32regex("[[:Sc:][:blank:]]|--");
144 std::string str_no_symbols;
145 boost::u32regex_replace(icu::UnicodeString::fromUTF8(str), expr, "").toUTF8String(str_no_symbols);
146
147 /* Convert based on user chosen currency format */
148 gnc_numeric val = gnc_numeric_zero();
149 char *endptr;
150 switch (currency_format)
151 {
152 case 0:
153 /* Currency locale */
154 if (!(xaccParseAmountImport (str_no_symbols.c_str(), TRUE, &val, &endptr, TRUE)))
155 throw std::invalid_argument (_("Value can't be parsed into a number using the selected currency format."));
156 break;
157 case 1:
158 /* Currency decimal period */
159 if (!(xaccParseAmountExtImport (str_no_symbols.c_str(), TRUE, '-', '.', ',', "$+", &val, &endptr)))
160 throw std::invalid_argument (_("Value can't be parsed into a number using the selected currency format."));
161 break;
162 case 2:
163 /* Currency decimal comma */
164 if (!(xaccParseAmountExtImport (str_no_symbols.c_str(), TRUE, '-', ',', '.', "$+", &val, &endptr)))
165 throw std::invalid_argument (_("Value can't be parsed into a number using the selected currency format."));
166 break;
167 }
168
169 return GncNumeric(val);
170}
171
172static char parse_reconciled (const std::string& reconcile)
173{
174 if (g_strcmp0 (reconcile.c_str(), gnc_get_reconcile_str(NREC)) == 0) // Not reconciled
175 return NREC;
176 else if (g_strcmp0 (reconcile.c_str(), gnc_get_reconcile_str(CREC)) == 0) // Cleared
177 return CREC;
178 else if (g_strcmp0 (reconcile.c_str(), gnc_get_reconcile_str(YREC)) == 0) // Reconciled
179 return YREC;
180 else if (g_strcmp0 (reconcile.c_str(), gnc_get_reconcile_str(FREC)) == 0) // Frozen
181 return FREC;
182 else if (g_strcmp0 (reconcile.c_str(), gnc_get_reconcile_str(VREC)) == 0) // Voided will be handled at the transaction level
183 return NREC; // so return not reconciled here
184 else
185 throw std::invalid_argument (_("Value can't be parsed into a valid reconcile state."));
186}
187
188gnc_commodity* parse_commodity (const std::string& comm_str)
189{
190 if (comm_str.empty())
191 return nullptr;
192
193 auto table = gnc_commodity_table_get_table (gnc_get_current_book());
194 gnc_commodity* comm = nullptr;
195
196 /* First try commodity as a unique name, returns null if not found */
197 comm = gnc_commodity_table_lookup_unique (table, comm_str.c_str());
198
199 /* Then try mnemonic in the currency namespace */
200 if (!comm)
201 comm = gnc_commodity_table_lookup (table,
202 GNC_COMMODITY_NS_CURRENCY, comm_str.c_str());
203
204 if (!comm)
205 {
206 /* If that fails try mnemonic in all other namespaces */
207 auto namespaces = gnc_commodity_table_get_namespaces(table);
208 for (auto ns = namespaces; ns; ns = ns->next)
209 {
210 gchar* ns_str = (gchar*)ns->data;
211 if (g_utf8_collate(ns_str, GNC_COMMODITY_NS_CURRENCY) == 0)
212 continue;
213
214 comm = gnc_commodity_table_lookup (table,
215 ns_str, comm_str.c_str());
216 if (comm)
217 break;
218 }
219 g_list_free (namespaces);
220 }
221
222 if (!comm)
223 throw std::invalid_argument (_("Value can't be parsed into a valid commodity."));
224 else
225 return comm;
226}
227
228void GncPreTrans::set (GncTransPropType prop_type, const std::string& value)
229{
230 try
231 {
232 // Drop any existing error for the prop_type we're about to set
233 m_errors.erase(prop_type);
234
235 switch (prop_type)
236 {
237 case GncTransPropType::UNIQUE_ID:
238 m_differ.reset();
239 if (!value.empty())
240 m_differ = value;
241 break;
242
243 case GncTransPropType::DATE:
244 m_date.reset();
245 if (!value.empty())
246 m_date = GncDate(value, GncDate::c_formats[m_date_format].m_fmt); // Throws if parsing fails
247 else if (!m_multi_split)
248 throw std::invalid_argument (
249 (bl::format (std::string{_("Date field can not be empty if 'Multi-split' option is unset.\n")}) %
250 std::string{_(gnc_csv_col_type_strs[prop_type])}).str());
251 break;
252
253 case GncTransPropType::NUM:
254 m_num.reset();
255 if (!value.empty())
256 m_num = value;
257 break;
258
259 case GncTransPropType::DESCRIPTION:
260 m_desc.reset();
261 if (!value.empty())
262 m_desc = value;
263 else if (!m_multi_split)
264 throw std::invalid_argument (
265 (bl::format (std::string{_("Description field can not be empty if 'Multi-split' option is unset.\n")}) %
266 std::string{_(gnc_csv_col_type_strs[prop_type])}).str());
267 break;
268
269 case GncTransPropType::NOTES:
270 m_notes.reset();
271 if (!value.empty())
272 m_notes = value;
273 break;
274
275 case GncTransPropType::COMMODITY:
276 m_currency = nullptr;
277 m_currency = parse_commodity (value);
278 break;
279
280 case GncTransPropType::VOID_REASON:
281 m_void_reason.reset();
282 if (!value.empty())
283 m_void_reason = value;
284 break;
285
286 default:
287 /* Issue a warning for all other prop_types. */
288 PWARN ("%d is an invalid property for a transaction", static_cast<int>(prop_type));
289 break;
290 }
291 }
292 catch (const std::exception& e)
293 {
294 auto err_str = (bl::format (std::string{_("{1}: {2}")}) %
295 std::string{_(gnc_csv_col_type_strs[prop_type])} %
296 e.what()).str();
297 m_errors.emplace(prop_type, err_str);
298 }
299
300}
301
302void GncPreTrans::reset (GncTransPropType prop_type)
303{
304 set (prop_type, std::string());
305 // Set with an empty string will effectively clear the property
306 // but can also set an error for the property. Clear that error here.
307 m_errors.erase(prop_type);
308}
309
310StrVec GncPreTrans::verify_essentials (void)
311{
312 auto errors = StrVec();
313
314 if (!m_date)
315 errors.emplace_back(_("No valid date."));
316
317 if (!m_desc)
318 errors.emplace_back(_("No valid description."));
319
320 return errors;
321}
322
323std::shared_ptr<DraftTransaction> GncPreTrans::create_trans (QofBook* book, gnc_commodity* currency)
324{
325 if (created)
326 return nullptr;
327
328 /* Gently refuse to create the transaction if the basics are not set correctly
329 * This should have been tested before calling this function though!
330 */
331 auto check = verify_essentials();
332 if (!check.empty())
333 {
334 auto err_msg = std::string("Not creating transaction because essentials not set properly:");
335 auto add_bullet_item = [](std::string& a, std::string& b)->std::string { return std::move(a) + "\n• " + b; };
336 err_msg = std::accumulate (check.begin(), check.end(), std::move (err_msg), add_bullet_item);
337 PWARN ("%s", err_msg.c_str());
338 return nullptr;
339 }
340
341 auto trans = xaccMallocTransaction (book);
342 xaccTransBeginEdit (trans);
343
344 if (gnc_commodity_is_currency(m_currency))
345 xaccTransSetCurrency (trans, m_currency);
346 else
347 xaccTransSetCurrency (trans, currency);
349 static_cast<time64>(GncDateTime(*m_date, DayPart::neutral)));
350
351 if (m_num)
352 xaccTransSetNum (trans, m_num->c_str());
353
354 if (m_desc)
355 xaccTransSetDescription (trans, m_desc->c_str());
356
357 if (m_notes)
358 xaccTransSetNotes (trans, m_notes->c_str());
359
360 created = true;
361 return std::make_shared<DraftTransaction>(trans);
362}
363
364bool GncPreTrans::is_part_of (std::shared_ptr<GncPreTrans> parent)
365{
366 if (!parent)
367 return false;
368
369 return (!m_differ || m_differ == parent->m_differ) &&
370 (!m_date || m_date == parent->m_date) &&
371 (!m_num || m_num == parent->m_num) &&
372 (!m_desc || m_desc == parent->m_desc) &&
373 (!m_notes || m_notes == parent->m_notes) &&
374 (!m_currency || m_currency == parent->m_currency) &&
375 (!m_void_reason || m_void_reason == parent->m_void_reason) &&
376 parent->m_errors.empty(); // A GncPreTrans with errors can never be a parent
377}
378
379ErrMap GncPreTrans::errors ()
380{
381 return m_errors;
382}
383
384void GncPreTrans::reset_cross_split_counters()
385{
386 m_alt_currencies.clear();
387 m_acct_commodities.clear();
388}
389
390
391bool GncPreTrans::is_multi_currency()
392{
393 auto num_comm = m_acct_commodities.size() + m_alt_currencies.size();
394 if (m_currency && (std::find (m_alt_currencies.cbegin(),m_alt_currencies.cend(), m_currency) == m_alt_currencies.cend()))
395 num_comm++;
396 return (num_comm > 1);
397}
398
399
400void GncPreSplit::UpdateCrossSplitCounters ()
401{
402 if (m_account && *m_account)
403 {
404 auto acct = *m_account;
405 auto comm = xaccAccountGetCommodity (acct);
406 auto alt_currs = m_pre_trans->m_alt_currencies;
407 auto acct_comms = m_pre_trans->m_acct_commodities;
408 auto curr = static_cast<gnc_commodity*> (nullptr);
409 if (gnc_commodity_is_currency (comm))
410 {
411 curr = comm;
412 comm = nullptr;
413 }
414 else
416
417 auto has_curr = [curr] (const gnc_commodity *vec_curr) { return gnc_commodity_equiv (curr, vec_curr); };
418 if (curr && std::none_of (alt_currs.cbegin(), alt_currs.cend(), has_curr))
419 m_pre_trans->m_alt_currencies.push_back(curr);
420 auto has_comm = [comm] (const gnc_commodity *vec_comm) { return gnc_commodity_equiv (comm, vec_comm); };
421 if (comm && std::none_of (acct_comms.cbegin(), acct_comms.cend(), has_comm))
422 m_pre_trans->m_acct_commodities.push_back(comm);
423 }
424}
425
426void GncPreSplit::set (GncTransPropType prop_type, const std::string& value)
427{
428 try
429 {
430 // Drop any existing error for the prop_type we're about to set
431 m_errors.erase(prop_type);
432
433 Account *acct = nullptr;
434 switch (prop_type)
435 {
436 case GncTransPropType::ACTION:
437 m_action.reset();
438 if (!value.empty())
439 m_action = value;
440 break;
441
442 case GncTransPropType::TACTION:
443 m_taction.reset();
444 if (!value.empty())
445 m_taction = value;
446 break;
447
448 case GncTransPropType::ACCOUNT:
449 m_account.reset();
450 if (value.empty())
451 throw std::invalid_argument (_("Account value can't be empty."));
452 if ((acct = gnc_account_imap_find_any (gnc_get_current_book(), IMAP_CAT_CSV, value.c_str())) ||
453 (acct = gnc_account_lookup_by_full_name (gnc_get_current_root_account(), value.c_str())))
454 m_account = acct;
455 else
456 throw std::invalid_argument (_("Account value can't be mapped back to an account."));
457 break;
458
459 case GncTransPropType::TACCOUNT:
460 m_taccount.reset();
461 if (value.empty())
462 throw std::invalid_argument (_("Transfer account value can't be empty."));
463
464 if ((acct = gnc_account_imap_find_any (gnc_get_current_book(), IMAP_CAT_CSV,value.c_str())) ||
465 (acct = gnc_account_lookup_by_full_name (gnc_get_current_root_account(), value.c_str())))
466 m_taccount = acct;
467 else
468 throw std::invalid_argument (_("Transfer account value can't be mapped back to an account."));
469 break;
470
471 case GncTransPropType::MEMO:
472 m_memo.reset();
473 if (!value.empty())
474 m_memo = value;
475 break;
476
477 case GncTransPropType::TMEMO:
478 m_tmemo.reset();
479 if (!value.empty())
480 m_tmemo = value;
481 break;
482
483 case GncTransPropType::AMOUNT:
484 m_amount.reset();
485 m_amount = parse_monetary (value, m_currency_format); // Will throw if parsing fails
486 break;
487
488 case GncTransPropType::AMOUNT_NEG:
489 m_amount_neg.reset();
490 m_amount_neg = parse_monetary (value, m_currency_format); // Will throw if parsing fails
491 break;
492
493 case GncTransPropType::VALUE:
494 m_value.reset();
495 m_value = parse_monetary (value, m_currency_format); // Will throw if parsing fails
496 break;
497
498 case GncTransPropType::VALUE_NEG:
499 m_value_neg.reset();
500 m_value_neg = parse_monetary (value, m_currency_format); // Will throw if parsing fails
501 break;
502
503 case GncTransPropType::TAMOUNT:
504 m_tamount.reset();
505 m_tamount = parse_monetary (value, m_currency_format); // Will throw if parsing fails
506 break;
507
508 case GncTransPropType::TAMOUNT_NEG:
509 m_tamount_neg.reset();
510 m_tamount_neg = parse_monetary (value, m_currency_format); // Will throw if parsing fails
511 break;
512
513 case GncTransPropType::PRICE:
514 /* Note while a price is not stricly a currency, it will likely use
515 * the same decimal point as currencies in the csv file, so parse
516 * using the same parser */
517 m_price.reset();
518 m_price = parse_monetary (value, m_currency_format); // Will throw if parsing fails
519 break;
520
521 case GncTransPropType::REC_STATE:
522 m_rec_state.reset();
523 m_rec_state = parse_reconciled (value); // Throws if parsing fails
524 break;
525
526 case GncTransPropType::TREC_STATE:
527 m_trec_state.reset();
528 m_trec_state = parse_reconciled (value); // Throws if parsing fails
529 break;
530
531 case GncTransPropType::REC_DATE:
532 m_rec_date.reset();
533 if (!value.empty())
534 m_rec_date = GncDate (value,
535 GncDate::c_formats[m_date_format].m_fmt); // Throws if parsing fails
536 break;
537
538 case GncTransPropType::TREC_DATE:
539 m_trec_date.reset();
540 if (!value.empty())
541 m_trec_date = GncDate (value,
542 GncDate::c_formats[m_date_format].m_fmt); // Throws if parsing fails
543 break;
544
545 default:
546 /* Issue a warning for all other prop_types. */
547 PWARN ("%d is an invalid property for a split", static_cast<int>(prop_type));
548 break;
549 }
550 }
551 catch (const std::exception& e)
552 {
553 auto err_str = (bl::format (std::string{_("{1}: {2}")}) %
554 std::string{_(gnc_csv_col_type_strs[prop_type])} %
555 e.what()).str();
556 m_errors.emplace(prop_type, err_str);
557 }
558
559 /* Extra currency related postprocessing for account type */
560 if (prop_type == GncTransPropType::ACCOUNT)
561 UpdateCrossSplitCounters();
562}
563
564void GncPreSplit::reset (GncTransPropType prop_type)
565{
566 set (prop_type, std::string());
567 // Set with an empty string will effectively clear the property
568 // but can also set an error for the property. Clear that error here.
569 m_errors.erase(prop_type);
570}
571
572void GncPreSplit::add (GncTransPropType prop_type, const std::string& value)
573{
574 try
575 {
576 /* Don't try to add to a property that has an error already */
577 if (m_errors.find(prop_type) != m_errors.cend())
578 return;
579
580 auto num_val = GncNumeric();
581 switch (prop_type)
582 {
583 case GncTransPropType::AMOUNT:
584 num_val = parse_monetary (value, m_currency_format); // Will throw if parsing fails
585 if (m_amount)
586 num_val += *m_amount;
587 m_amount = num_val;
588 break;
589
590 case GncTransPropType::AMOUNT_NEG:
591 num_val = parse_monetary (value, m_currency_format); // Will throw if parsing fails
592 if (m_amount_neg)
593 num_val += *m_amount_neg;
594 m_amount_neg = num_val;
595 break;
596
597 case GncTransPropType::VALUE:
598 num_val = parse_monetary (value, m_currency_format); // Will throw if parsing fails
599 if (m_value)
600 num_val += *m_value;
601 m_value = num_val;
602 break;
603
604 case GncTransPropType::VALUE_NEG:
605 num_val = parse_monetary (value, m_currency_format); // Will throw if parsing fails
606 if (m_value_neg)
607 num_val += *m_value_neg;
608 m_value_neg = num_val;
609 break;
610
611 case GncTransPropType::TAMOUNT:
612 num_val = parse_monetary (value, m_currency_format); // Will throw if parsing fails
613 if (m_tamount)
614 num_val += *m_tamount;
615 m_tamount = num_val;
616 break;
617
618 case GncTransPropType::TAMOUNT_NEG:
619 num_val = parse_monetary (value, m_currency_format); // Will throw if parsing fails
620 if (m_tamount_neg)
621 num_val += *m_tamount_neg;
622 m_tamount_neg = num_val;
623 break;
624
625 default:
626 /* Issue a warning for all other prop_types. */
627 PWARN ("%d can't be used to add values in a split", static_cast<int>(prop_type));
628 break;
629 }
630 }
631 catch (const std::exception& e)
632 {
633 auto err_str = (bl::format (std::string{_("{1}: {2}")}) %
634 std::string{_(gnc_csv_col_type_strs[prop_type])} %
635 e.what()).str();
636 m_errors.emplace(prop_type, err_str);
637 }
638}
639
640StrVec GncPreSplit::verify_essentials()
641{
642 auto err_msg = StrVec();
643 /* Make sure this split has the minimum required set of properties defined. */
644 if (!m_amount && !m_amount_neg)
645 err_msg.emplace_back (_("No amount or negated amount column."));
646
647 if (m_rec_state && *m_rec_state == YREC && !m_rec_date)
648 err_msg.emplace_back (_("Split is reconciled but reconcile date column is missing or invalid."));
649
650 if (m_trec_state && *m_trec_state == YREC && !m_trec_date)
651 err_msg.emplace_back (_("Transfer split is reconciled but transfer reconcile date column is missing or invalid."));
652
653
654 /* When current account selections imply multi-currency
655 * transactions, we require extra columns to ensure each split is
656 * fully defined.
657 * Note this check only involves splits created by the csv importer
658 * code. The generic import matcher may add a balancing split
659 * optionally using Transfer <something> properties. The generic
660 * import matcher has its own tools to balance that split so
661 * we won't concern ourselves with that one here.
662 */
663 if (m_pre_trans->is_multi_currency())
664 {
665 if (m_pre_trans->m_multi_split && !m_price && !m_value && !m_value_neg)
666 err_msg.emplace_back( _("Choice of accounts makes this a multi-currency transaction but price or (negated) value column is missing or invalid."));
667 else if (!m_pre_trans->m_multi_split &&
668 !m_price && !m_value && !m_value_neg && !m_tamount && !m_tamount_neg )
669 err_msg.emplace_back( _("Choice of accounts makes this a multi-currency transaction but price, (negated) value or (negated) transfer amount column is missing or invalid."));
670 }
671
672 return err_msg;
673}
674
683static void trans_add_split (Transaction* trans, Account* account,
684 GncNumeric amount, GncNumeric value,
685 const std::optional<std::string>& action,
686 const std::optional<std::string>& memo,
687 const std::optional<char>& rec_state,
688 const std::optional<GncDate>& rec_date)
689{
690 QofBook* book = xaccTransGetBook (trans);
691 auto split = xaccMallocSplit (book);
692 xaccSplitSetAccount (split, account);
693 xaccSplitSetParent (split, trans);
694 xaccSplitSetAmount (split, static_cast<gnc_numeric>(amount));
695 xaccSplitSetValue (split, static_cast<gnc_numeric>(value));
696
697 if (memo)
698 xaccSplitSetMemo (split, memo->c_str());
699 /* Note, this function assumes the num/action switch is done at a higher level
700 * if needed by the book option */
701 if (action)
702 xaccSplitSetAction (split, action->c_str());
703
704 if (rec_state && *rec_state != 'n')
705 xaccSplitSetReconcile (split, *rec_state);
706 if (rec_state && *rec_state == YREC && rec_date)
708 static_cast<time64>(GncDateTime(*rec_date, DayPart::neutral)));
709
710}
711
712void GncPreSplit::create_split (std::shared_ptr<DraftTransaction> draft_trans)
713{
714 if (created)
715 return;
716
717 /* Gently refuse to create the split if the basics are not set correctly
718 * This should have been tested before calling this function though!
719 */
720 auto check = verify_essentials();
721 if (!check.empty())
722 {
723 auto err_msg = std::string("Not creating split because essentials not set properly:");
724 auto add_bullet_item = [](std::string& a, std::string& b)->std::string { return std::move(a) + "\n• " + b; };
725 err_msg = std::accumulate (check.begin(), check.end(), std::move (err_msg), add_bullet_item);
726 PWARN ("%s", err_msg.c_str());
727 return;
728 }
729
730 auto splits_created = 0;
731 Account *account = nullptr;
732 Account *taccount = nullptr;
733 auto amount = GncNumeric();
734
735 if (m_account)
736 account = *m_account;
737 if (m_taccount)
738 taccount = *m_taccount;
739 if (m_amount)
740 amount += *m_amount;
741 if (m_amount_neg)
742 amount -= *m_amount_neg;
743
744 std::optional<GncNumeric> tamount;
745 if (m_tamount || m_tamount_neg)
746 {
747 tamount = GncNumeric();
748 if (m_tamount)
749 *tamount += *m_tamount;
750 if (m_tamount_neg)
751 *tamount -= *m_tamount_neg;
752 }
753
754 /* Value can be calculated in several ways, depending on what
755 * data was available in the csv import file.
756 * Below code will prefer the method with the least
757 * risk on rounding errors.
758 * */
759 auto value = GncNumeric();
760 auto trans_curr = xaccTransGetCurrency(draft_trans->trans);
761 auto acct_comm = xaccAccountGetCommodity(account);
762 if (m_value || m_value_neg)
763 {
764 if (m_value)
765 value += *m_value;
766 if (m_value_neg)
767 value -= *m_value_neg;
768 }
769 else if (gnc_commodity_equiv(trans_curr, acct_comm))
770 value = amount;
771 else if (tamount)
772 value = -*tamount;
773 else if (m_price)
774 value = amount * *m_price;
775 else
776 {
777 QofBook* book = xaccTransGetBook (draft_trans->trans);
778 auto time = xaccTransRetDatePosted (draft_trans->trans);
779 /* Import data didn't specify price, let's lookup the nearest in time */
780 auto nprice =
782 acct_comm, trans_curr, time);
783 GncNumeric rate = nprice ? gnc_price_get_value (nprice): gnc_numeric_zero();
784 if (!gnc_numeric_zero_p (rate))
785 {
786 /* Found a usable price. Let's check if the conversion direction is right
787 * Reminder: value = amount * price, or amount = value / price */
788 if (gnc_commodity_equiv(gnc_price_get_currency(nprice), trans_curr))
789 value = amount * rate;
790 else
791 value = amount * rate.inv();
792 }
793 else
794 PERR("No price found, can't create this split.");
795 }
796
797 /* Add a split with the cumulative amount value. */
798 trans_add_split (draft_trans->trans, account, amount, value, m_action, m_memo, m_rec_state, m_rec_date);
799 splits_created++;
800
801 if (taccount)
802 {
803 /* If a taccount is set that forcibly means we're processing a single-line
804 * transaction. The csv importer will assume this can only create a
805 * two-split transaction, so whatever transfer data is available, the
806 * transfer split's value must balance the first split value. Remains
807 * to determine: the transfer amount. As with value above, for single
808 * currency case use transfer value. Otherwise calculate from whatever
809 * is found in the csv data preferring minimal rounding calculations. */
810 auto tvalue = -value;
811 auto trans_curr = xaccTransGetCurrency(draft_trans->trans);
812 auto acct_comm = xaccAccountGetCommodity(taccount);
813 if (gnc_commodity_equiv(trans_curr, acct_comm))
814 tamount = tvalue;
815 else if (tamount)
816 ; // Nothing to do, was already calculated
817 else if (m_price)
818 tamount = tvalue * m_price->inv();
819 else
820 {
821 QofBook* book = xaccTransGetBook (draft_trans->trans);
822 auto time = xaccTransRetDatePosted (draft_trans->trans);
823 /* Import data didn't specify price, let's lookup the nearest in time */
824 auto nprice =
826 acct_comm, trans_curr, time);
827 GncNumeric rate = nprice ? gnc_price_get_value (nprice): gnc_numeric_zero();
828 if (!gnc_numeric_zero_p (rate))
829 {
830 /* Found a usable price. Let's check if the conversion direction is right
831 * Reminder: value = amount * price, or amount = value / price */
832 if (gnc_commodity_equiv(gnc_price_get_currency(nprice), trans_curr))
833 tamount = tvalue * rate.inv();
834 else
835 tamount = tvalue * rate;
836 }
837 }
838 if (tamount)
839 {
840 trans_add_split (draft_trans->trans, taccount, *tamount, tvalue, m_taction, m_tmemo, m_trec_state, m_trec_date);
841 splits_created++;
842 }
843 else
844 PWARN("No price found, defer creation of second split to generic import matcher.");
845 }
846
847 if (splits_created == 1)
848 {
849 /* If we get here, we're either
850 * - in multi-line mode
851 * - or single-line mode but didn't have enough details to create the
852 * transfer split.
853 * For the latter we will pass what we know about the transfer split to
854 * allow the generic import matcher to ask the user for the final
855 * details before creating this split.
856 */
857 draft_trans->m_price = m_price;
858 draft_trans->m_taction = m_taction;
859 draft_trans->m_tmemo = m_tmemo;
860 draft_trans->m_tamount = tamount;
861 draft_trans->m_taccount = m_taccount;
862 draft_trans->m_trec_state = m_trec_state;
863 draft_trans->m_trec_date = m_trec_date;
864 }
865
866 created = true;
867}
868
869ErrMap GncPreSplit::errors (void)
870{
871 return m_errors;
872}
873
874
875void GncPreSplit::set_account (Account* acct)
876{
877 if (acct)
878 m_account = acct;
879 else
880 m_account.reset();
881
882 UpdateCrossSplitCounters();
883}
Account handling public routines.
API for Transactions and Splits (journal entries)
GnuCash DateTime class.
GnuCash Date class.
static const std::vector< GncDateFormat > c_formats
A vector with all the date formats supported by the string constructor.
The primary numeric class for representing amounts and values.
GncNumeric inv() const noexcept
bool is_part_of(std::shared_ptr< GncPreTrans > parent)
Check whether the harvested transaction properties for this instance match those of another one (the ...
a simple price database for gnucash
utility functions for the GnuCash UI
gnc_commodity * gnc_account_get_currency_or_parent(const Account *account)
Returns a gnc_commodity that is a currency, suitable for being a Transaction's currency.
Definition Account.cpp:3415
gnc_commodity * xaccAccountGetCommodity(const Account *acc)
Get the account's commodity
Definition Account.cpp:3408
gboolean gnc_commodity_equiv(const gnc_commodity *a, const gnc_commodity *b)
This routine returns TRUE if the two commodities are equivalent.
gnc_commodity_table * gnc_commodity_table_get_table(QofBook *book)
Returns the commodity table associated with a book.
GList * gnc_commodity_table_get_namespaces(const gnc_commodity_table *table)
Return a list of all namespaces in the commodity table.
gboolean gnc_commodity_is_currency(const gnc_commodity *cm)
Checks to see if the specified commodity is an ISO 4217 recognized currency or a legacy currency.
gint64 time64
Most systems that are currently maintained, including Microsoft Windows, BSD-derived Unixes and Linux...
Definition gnc-date.h:87
void xaccTransSetDescription(Transaction *trans, const char *desc)
Sets the transaction Description.
gnc_commodity * xaccTransGetCurrency(const Transaction *trans)
Returns the valuation commodity of this transaction.
void xaccTransSetNum(Transaction *trans, const char *xnum)
Sets the transaction Number (or ID) field; rather than use this function directly,...
time64 xaccTransRetDatePosted(const Transaction *trans)
Retrieve the posted date of the transaction.
Transaction * xaccMallocTransaction(QofBook *book)
The xaccMallocTransaction() will malloc memory and initialize it.
void xaccTransSetDatePostedSecsNormalized(Transaction *trans, time64 time)
This function sets the posted date of the transaction, specified by a time64 (see ctime(3)).
void xaccTransSetCurrency(Transaction *trans, gnc_commodity *curr)
Set a new currency on a transaction.
#define xaccTransGetBook(X)
Account * gnc_account_lookup_by_full_name(const Account *any_acc, const gchar *name)
The gnc_account_lookup_full_name() subroutine works like gnc_account_lookup_by_name,...
Definition Account.cpp:3163
void xaccTransBeginEdit(Transaction *trans)
The xaccTransBeginEdit() method must be called before any changes are made to a transaction or any of...
void xaccTransSetNotes(Transaction *trans, const char *notes)
Sets the transaction Notes.
gboolean xaccParseAmountImport(const char *in_str, gboolean monetary, gnc_numeric *result, char **endstr, gboolean skip)
Similar to xaccParseAmount, but with two differences.
gboolean xaccParseAmountExtImport(const char *in_str, gboolean monetary, gunichar negative_sign, gunichar decimal_point, gunichar group_separator, const char *ignore_list, gnc_numeric *result, char **endstr)
Similar to xaccParseAmountExtended, but will not automatically set a decimal point,...
#define PERR(format, args...)
Log a serious error.
Definition qoflog.h:244
#define PWARN(format, args...)
Log a warning.
Definition qoflog.h:250
gboolean gnc_numeric_zero_p(gnc_numeric a)
Returns 1 if the given gnc_numeric is 0 (zero), else returns 0.
GNCPrice * gnc_pricedb_lookup_nearest_in_time64(GNCPriceDB *db, const gnc_commodity *c, const gnc_commodity *currency, time64 t)
Return the price between the two commoditiesz nearest to the given time.
GNCPriceDB * gnc_pricedb_get_db(QofBook *book)
Return the pricedb associated with the book.
void xaccSplitSetReconcile(Split *split, char recn)
Set the reconcile flag.
void xaccSplitSetMemo(Split *split, const char *memo)
The memo is an arbitrary string associated with a split.
void xaccSplitSetValue(Split *split, gnc_numeric val)
The xaccSplitSetValue() method sets the value of this split in the transaction's commodity.
Split * xaccMallocSplit(QofBook *book)
Constructor.
void xaccSplitSetAmount(Split *split, gnc_numeric amt)
The xaccSplitSetAmount() method sets the amount in the account's commodity that the split should have...
#define VREC
split is void
Definition Split.h:77
void xaccSplitSetDateReconciledSecs(Split *split, time64 secs)
Set the date on which this split was reconciled by specifying the time as time64.
#define NREC
not reconciled or cleared
Definition Split.h:76
#define FREC
frozen into accounting period
Definition Split.h:75
#define CREC
The Split has been cleared
Definition Split.h:73
#define YREC
The Split has been reconciled.
Definition Split.h:74
void xaccSplitSetAction(Split *split, const char *actn)
The Action is an arbitrary user-assigned string.
Definition Split.cpp:1786
STRUCTS.
QofBook reference.
Definition qofbook-p.hpp:47