GnuCash  5.6-150-g038405b370+
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 
52 namespace bl = boost::locale;
53 
54 G_GNUC_UNUSED static QofLogModule log_module = GNC_MOD_IMPORT;
55 
56 /* This map contains a set of strings representing the different column types. */
57 std::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  */
89 std::vector<GncTransPropType> twosplit_blacklist = {
90  GncTransPropType::UNIQUE_ID };
91 std::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 */
101 std::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 
110 bool 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 
116 GncTransPropType 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 
132 GncNumeric 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 
172 static 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 
188 gnc_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 
228 void 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 
302 void 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 
310 StrVec 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 
323 std::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 
364 bool 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 
379 ErrMap GncPreTrans::errors ()
380 {
381  return m_errors;
382 }
383 
384 void GncPreTrans::reset_cross_split_counters()
385 {
386  m_alt_currencies.clear();
387  m_acct_commodities.clear();
388 }
389 
390 
391 bool 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 
400 void 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 
426 void 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 
564 void 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 
572 void 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 
640 StrVec 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 
683 static 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 
712 void 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 
869 ErrMap GncPreSplit::errors (void)
870 {
871  return m_errors;
872 }
873 
874 
875 void GncPreSplit::set_account (Account* acct)
876 {
877  if (acct)
878  m_account = acct;
879  else
880  m_account.reset();
881 
882  UpdateCrossSplitCounters();
883 }
void xaccSplitSetValue(Split *split, gnc_numeric val)
The xaccSplitSetValue() method sets the value of this split in the transaction&#39;s commodity.
Definition: gmock-Split.cpp:92
gnc_commodity_table * gnc_commodity_table_get_table(QofBook *book)
Returns the commodity table associated with a book.
Transaction * xaccMallocTransaction(QofBook *book)
The xaccMallocTransaction() will malloc memory and initialize it.
gboolean xaccParseAmountImport(const char *in_str, gboolean monetary, gnc_numeric *result, char **endstr, gboolean skip)
Similar to xaccParseAmount, but with two differences.
void xaccTransSetDatePostedSecsNormalized(Transaction *trans, time64 time)
This function sets the posted date of the transaction, specified by a time64 (see ctime(3))...
void xaccSplitSetAction(Split *split, const char *actn)
The Action is an arbitrary user-assigned string.
Definition: Split.cpp:1750
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...
GnuCash DateTime class.
a simple price database for gnucash
utility functions for the GnuCash UI
void xaccTransSetNotes(Transaction *trans, const char *notes)
Sets the transaction Notes.
STRUCTS.
void xaccTransSetDescription(Transaction *trans, const char *desc)
Sets the transaction Description.
void xaccTransSetNum(Transaction *trans, const char *xnum)
Sets the transaction Number (or ID) field; rather than use this function directly, see &#39;gnc_set_num_action&#39; in engine/engine-helpers.c & .h which takes a user-set book option for selecting the source for the num-cell (the transaction-number or the split-action field) in registers/reports into account automatically.
gboolean gnc_numeric_zero_p(gnc_numeric a)
Returns 1 if the given gnc_numeric is 0 (zero), else returns 0.
The primary numeric class for representing amounts and values.
Definition: gnc-numeric.hpp:60
void xaccSplitSetReconcile(Split *split, char recn)
Set the reconcile flag.
#define PERR(format, args...)
Log a serious error.
Definition: qoflog.h:244
GNCPriceDB * gnc_pricedb_get_db(QofBook *book)
Return the pricedb associated with the book.
#define VREC
split is void
Definition: Split.h:77
void xaccTransSetCurrency(Transaction *trans, gnc_commodity *curr)
Set a new currency on a transaction.
#define PWARN(format, args...)
Log a warning.
Definition: qoflog.h:250
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, regardless of what the user has set for this option.
void xaccSplitSetAmount(Split *split, gnc_numeric amt)
The xaccSplitSetAmount() method sets the amount in the account&#39;s commodity that the split should have...
Definition: gmock-Split.cpp:77
Account handling public routines.
#define YREC
The Split has been reconciled.
Definition: Split.h:74
void xaccSplitSetMemo(Split *split, const char *memo)
The memo is an arbitrary string associated with a split.
GList * gnc_commodity_table_get_namespaces(const gnc_commodity_table *table)
Return a list of all namespaces in the commodity table.
#define FREC
frozen into accounting period
Definition: Split.h:75
time64 xaccTransRetDatePosted(const Transaction *trans)
Retrieve the posted date of the transaction.
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, but uses fully-qualified names using the given separator.
Definition: Account.cpp:3137
#define xaccTransGetBook(X)
Definition: Transaction.h:786
void xaccTransBeginEdit(Transaction *trans)
The xaccTransBeginEdit() method must be called before any changes are made to a transaction or any of...
#define CREC
The Split has been cleared.
Definition: Split.h:73
gnc_commodity * gnc_account_get_currency_or_parent(const Account *account)
Returns a gnc_commodity that is a currency, suitable for being a Transaction&#39;s currency.
Definition: Account.cpp:3382
Split * xaccMallocSplit(QofBook *book)
Constructor.
Definition: gmock-Split.cpp:37
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.
void xaccSplitSetDateReconciledSecs(Split *split, time64 secs)
Set the date on which this split was reconciled by specifying the time as time64. ...
bool is_part_of(std::shared_ptr< GncPreTrans > parent)
Check whether the harvested transaction properties for this instance match those of another one (the ...
gnc_commodity * xaccAccountGetCommodity(const Account *acc)
Get the account&#39;s commodity.
Definition: Account.cpp:3375
gnc_commodity * xaccTransGetCurrency(const Transaction *trans)
Returns the valuation commodity of this transaction.
GncNumeric inv() const noexcept
API for Transactions and Splits (journal entries)
gboolean gnc_commodity_equiv(const gnc_commodity *a, const gnc_commodity *b)
This routine returns TRUE if the two commodities are equivalent.
static const std::vector< GncDateFormat > c_formats
A vector with all the date formats supported by the string constructor.
GnuCash Date class.
#define NREC
not reconciled or cleared
Definition: Split.h:76