GnuCash  5.6-150-g038405b370+
gnc-imp-props-price.cpp
1 /********************************************************************\
2  * gnc-imp-props-price.cpp - encapsulate price properties for use *
3  * in the csv importer *
4  * *
5  * This program is free software; you can redistribute it and/or *
6  * modify it under the terms of the GNU General Public License as *
7  * published by the Free Software Foundation; either version 2 of *
8  * the License, or (at your option) any later version. *
9  * *
10  * This program is distributed in the hope that it will be useful, *
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of *
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
13  * GNU General Public License for more details. *
14  * *
15  * You should have received a copy of the GNU General Public License*
16  * along with this program; if not, contact: *
17  * *
18  * Free Software Foundation Voice: +1-617-542-5942 *
19  * 51 Franklin Street, Fifth Floor Fax: +1-617-542-2652 *
20  * Boston, MA 02110-1301, USA gnu@gnu.org *
21  * *
22 \********************************************************************/
23 
24 #include <glib.h>
25 #include <glib/gi18n.h>
26 
27 #include <platform.h>
28 #if PLATFORM(WINDOWS)
29 #include <windows.h>
30 #endif
31 
32 #include "engine-helpers.h"
33 #include "gnc-ui-util.h"
34 
35 #include <exception>
36 #include <map>
37 #include <optional>
38 #include <string>
39 #include <boost/locale.hpp>
40 #include <boost/regex.hpp>
41 #include <boost/regex/icu.hpp>
42 #include <gnc-locale-utils.hpp>
43 #include "gnc-imp-props-price.hpp"
44 
45 namespace bl = boost::locale;
46 
47 G_GNUC_UNUSED static QofLogModule log_module = GNC_MOD_IMPORT;
48 
49 /* This map contains a set of strings representing the different column types. */
50 std::map<GncPricePropType, const char*> gnc_price_col_type_strs = {
51  { GncPricePropType::NONE, N_("None") },
52  { GncPricePropType::DATE, N_("Date") },
53  { GncPricePropType::AMOUNT, N_("Amount") },
54  { GncPricePropType::FROM_SYMBOL, N_("From Symbol") },
55  { GncPricePropType::FROM_NAMESPACE, N_("From Namespace") },
56  { GncPricePropType::TO_CURRENCY, N_("Currency To") },
57 };
58 
65 GncNumeric parse_amount_price (const std::string &str, int currency_format)
66 {
67  /* If a cell is empty or just spaces return invalid amount */
68  if(!boost::regex_search(str, boost::regex("[0-9]")))
69  throw std::invalid_argument (_("Value doesn't appear to contain a valid number."));
70 
71  auto expr = boost::make_u32regex("[[:Sc:]]");
72  std::string str_no_symbols;
73  boost::u32regex_replace(icu::UnicodeString::fromUTF8(str), expr, "").toUTF8String(str_no_symbols);
74 
75  /* Convert based on user chosen currency format */
76  gnc_numeric val = gnc_numeric_zero();
77  char *endptr;
78  switch (currency_format)
79  {
80  case 0:
81  /* Currency locale */
82  if (!(xaccParseAmountImport (str_no_symbols.c_str(), TRUE, &val, &endptr, TRUE)))
83  throw std::invalid_argument (_("Value can't be parsed into a number using the selected currency format."));
84  break;
85  case 1:
86  /* Currency decimal period */
87  if (!(xaccParseAmountExtImport (str_no_symbols.c_str(), TRUE, '-', '.', ',', "$+", &val, &endptr)))
88  throw std::invalid_argument (_("Value can't be parsed into a number using the selected currency format."));
89  break;
90  case 2:
91  /* Currency decimal comma */
92  if (!(xaccParseAmountExtImport (str_no_symbols.c_str(), TRUE, '-', ',', '.', "$+", &val, &endptr)))
93  throw std::invalid_argument (_("Value can't be parsed into a number using the selected currency format."));
94  break;
95  }
96 
97  return GncNumeric(val);
98 }
99 
106 gnc_commodity* parse_commodity_price_comm (const std::string& symbol_str, const std::string& namespace_str)
107 {
108  if (symbol_str.empty())
109  return nullptr;
110 
111  auto table = gnc_commodity_table_get_table (gnc_get_current_book());
112  gnc_commodity* comm = nullptr;
113 
114  /* First try commodity as a unique name, used in loading settings, returns null if not found */
115  comm = gnc_commodity_table_lookup_unique (table, symbol_str.c_str());
116 
117  /* Now lookup with namespace and symbol */
118  if (!comm)
119  {
120  comm = gnc_commodity_table_lookup (table,
121  namespace_str.c_str(), symbol_str.c_str());
122  }
123 
124  if (!comm)
125  throw std::invalid_argument (_("Value can't be parsed into a valid commodity."));
126  else
127  return comm;
128 }
129 
135 bool parse_namespace (const std::string& namespace_str)
136 {
137  if (namespace_str.empty())
138  return false;
139 
140  auto table = gnc_commodity_table_get_table (gnc_get_current_book());
141 
142  if (gnc_commodity_table_has_namespace (table, namespace_str.c_str()))
143  return true;
144  else
145  throw std::invalid_argument (_("Value can't be parsed into a valid namespace."));
146 
147  return false;
148 }
149 
150 void GncImportPrice::set (GncPricePropType prop_type, const std::string& value, bool enable_test_empty)
151 {
152  try
153  {
154  // Drop any existing error for the prop_type we're about to set
155  m_errors.erase(prop_type);
156 
157  // conditional test for empty values
158  if (value.empty() && enable_test_empty)
159  throw std::invalid_argument (_("Column value can not be empty."));
160 
161  gnc_commodity *comm = nullptr;
162  switch (prop_type)
163  {
164  case GncPricePropType::DATE:
165  m_date.reset();
166  m_date = GncDate(value, GncDate::c_formats[m_date_format].m_fmt); // Throws if parsing fails
167  break;
168 
169  case GncPricePropType::AMOUNT:
170  m_amount.reset();
171  m_amount = parse_amount_price (value, m_currency_format); // Throws if parsing fails
172  break;
173 
174  case GncPricePropType::FROM_SYMBOL:
175  m_from_symbol.reset();
176 
177  if (value.empty())
178  throw std::invalid_argument (_("'From Symbol' can not be empty."));
179  else
180  m_from_symbol = value;
181 
182  if (m_from_namespace)
183  {
184  comm = parse_commodity_price_comm (value, *m_from_namespace); // Throws if parsing fails
185  if (comm)
186  {
187  if (m_to_currency == comm)
188  throw std::invalid_argument (_("'Commodity From' can not be the same as 'Currency To'."));
189  m_from_commodity = comm;
190  }
191  }
192  break;
193 
194  case GncPricePropType::FROM_NAMESPACE:
195  m_from_namespace.reset();
196 
197  if (value.empty())
198  throw std::invalid_argument (_("'From Namespace' can not be empty."));
199 
200  if (parse_namespace (value)) // Throws if parsing fails
201  {
202  m_from_namespace = value;
203 
204  if (m_from_symbol)
205  {
206  comm = parse_commodity_price_comm (*m_from_symbol, *m_from_namespace); // Throws if parsing fails
207  if (comm)
208  {
209  if (m_to_currency == comm)
210  throw std::invalid_argument (_("'Commodity From' can not be the same as 'Currency To'."));
211  m_from_commodity = comm;
212  }
213  }
214  }
215  break;
216 
217  case GncPricePropType::TO_CURRENCY:
218  m_to_currency.reset();
219  comm = parse_commodity_price_comm (value, GNC_COMMODITY_NS_CURRENCY); // Throws if parsing fails
220  if (comm)
221  {
222  if (m_from_commodity == comm)
223  throw std::invalid_argument (_("'Currency To' can not be the same as 'Commodity From'."));
224  if (gnc_commodity_is_currency (comm) != true)
225  throw std::invalid_argument (_("Value parsed into an invalid currency for a currency column type."));
226  m_to_currency = comm;
227  }
228  break;
229 
230  default:
231  /* Issue a warning for all other prop_types. */
232  PWARN ("%d is an invalid property for a Price", static_cast<int>(prop_type));
233  break;
234  }
235  }
236  catch (const std::invalid_argument& e)
237  {
238  auto err_str = (bl::format (std::string{_("{1}: {2}")}) %
239  std::string{_(gnc_price_col_type_strs[prop_type])} %
240  e.what()).str();
241  m_errors.emplace(prop_type, err_str);
242  throw std::invalid_argument (err_str);
243  }
244  catch (const std::out_of_range& e)
245  {
246  auto err_str = (bl::format (std::string{_("{1}: {2}")}) %
247  std::string{_(gnc_price_col_type_strs[prop_type])} %
248  e.what()).str();
249  m_errors.emplace(prop_type, err_str);
250  throw std::invalid_argument (err_str);
251  }
252 }
253 
254 void GncImportPrice::reset (GncPricePropType prop_type)
255 {
256  try
257  {
258  if ((prop_type == GncPricePropType::FROM_NAMESPACE) ||
259  (prop_type == GncPricePropType::FROM_SYMBOL))
260  set_from_commodity (nullptr);
261 
262  if (prop_type == GncPricePropType::TO_CURRENCY)
263  set_to_currency (nullptr);
264 
265  // set enable_test_empty to false to allow empty values
266  set (prop_type, std::string(), false);
267  }
268  catch (...)
269  {
270  // Set with an empty string will effectively clear the property
271  // but can also set an error for the property. Clear that error here.
272  m_errors.erase(prop_type);
273  }
274 }
275 
276 std::string GncImportPrice::verify_essentials (void)
277 {
278  /* Make sure this price has the minimum required set of properties defined */
279  if (!m_date)
280  return _("No date column.");
281  else if (!m_amount)
282  return _("No amount column.");
283  else if (!m_to_currency)
284  return _("No 'Currency to'.");
285  else if (!m_from_commodity)
286  return _("No 'Commodity from'.");
287  else if (gnc_commodity_equal (*m_from_commodity, *m_to_currency))
288  return _("'Commodity From' can not be the same as 'Currency To'.");
289  else
290  return std::string();
291 }
292 
293 Result GncImportPrice::create_price (QofBook* book, GNCPriceDB *pdb, bool over)
294 {
295  /* Gently refuse to create the price if the basics are not set correctly
296  * This should have been tested before calling this function though!
297  */
298  auto check = verify_essentials();
299  if (!check.empty())
300  {
301  PWARN ("Refusing to create price because essentials not set properly: %s", check.c_str());
302  return FAILED;
303  }
304 
305  auto date = static_cast<time64>(GncDateTime(*m_date, DayPart::neutral));
306 
307  auto amount = *m_amount;
308  Result ret_val = ADDED;
309 
310  GNCPrice *old_price = gnc_pricedb_lookup_day_t64 (pdb, *m_from_commodity,
311  *m_to_currency, date);
312 
313  // Should old price be over written
314  if ((old_price != nullptr) && (over == true))
315  {
316  DEBUG("Over write");
317  gnc_pricedb_remove_price (pdb, old_price);
318  gnc_price_unref (old_price);
319  old_price = nullptr;
320  ret_val = REPLACED;
321  }
322 
323  char date_str [MAX_DATE_LENGTH + 1];
324  memset (date_str, 0, sizeof(date_str));
325  qof_print_date_buff (date_str, MAX_DATE_LENGTH, date);
326  DEBUG("Date is %s, Commodity from is '%s', Currency is '%s', "
327  "Amount is %s", date_str,
328  gnc_commodity_get_fullname (*m_from_commodity),
329  gnc_commodity_get_fullname (*m_to_currency),
330  amount.to_string().c_str());
331  // Create the new price
332  if (old_price == nullptr)
333  {
334  DEBUG("Create");
335  GNCPrice *price = gnc_price_create (book);
336  gnc_price_begin_edit (price);
337 
338  gnc_price_set_commodity (price, *m_from_commodity);
339  gnc_price_set_currency (price, *m_to_currency);
340 
341  int scu = gnc_commodity_get_fraction (*m_to_currency);
342  auto amount_conv = amount.convert<RoundType::half_up>(scu * COMMODITY_DENOM_MULT);
343 
344  gnc_price_set_value (price, static_cast<gnc_numeric>(amount_conv));
345 
346  gnc_price_set_time64 (price, date);
347  gnc_price_set_source (price, PRICE_SOURCE_USER_PRICE);
348  gnc_price_set_typestr (price, PRICE_TYPE_LAST);
349  gnc_price_commit_edit (price);
350 
351  bool perr = gnc_pricedb_add_price (pdb, price);
352 
353  gnc_price_unref (price);
354 
355  if (perr == false)
356  throw std::invalid_argument (_("Failed to create price from selected columns."));
357  }
358  else
359  {
360  gnc_price_unref (old_price);
361  ret_val = DUPLICATED;
362  }
363  return ret_val;
364 }
365 
366 static std::string gen_err_str (std::map<GncPricePropType, std::string>& errors)
367 {
368  auto full_error = std::string();
369  for (auto error : errors)
370  {
371  full_error += (full_error.empty() ? "" : "\n") + error.second;
372  }
373  return full_error;
374 }
375 
376 std::string GncImportPrice::errors ()
377 {
378  return gen_err_str (m_errors);
379 }
380 
GNCPrice * gnc_pricedb_lookup_day_t64(GNCPriceDB *db, const gnc_commodity *c, const gnc_commodity *currency, time64 t)
Return the price between the two commodities on the indicated day.
GNCPrice * gnc_price_create(QofBook *book)
gnc_price_create - returns a newly allocated and initialized price with a reference count of 1...
gnc_commodity_table * gnc_commodity_table_get_table(QofBook *book)
Returns the commodity table associated with a book.
gboolean xaccParseAmountImport(const char *in_str, gboolean monetary, gnc_numeric *result, char **endstr, gboolean skip)
Similar to xaccParseAmount, but with two differences.
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...
int gnc_commodity_get_fraction(const gnc_commodity *cm)
Retrieve the fraction for the specified commodity.
GnuCash DateTime class.
utility functions for the GnuCash UI
void gnc_price_unref(GNCPrice *p)
gnc_price_unref - indicate you&#39;re finished with a price (i.e.
#define DEBUG(format, args...)
Print a debugging message.
Definition: qoflog.h:264
gboolean gnc_pricedb_add_price(GNCPriceDB *db, GNCPrice *p)
Add a price to the pricedb.
gboolean gnc_commodity_equal(const gnc_commodity *a, const gnc_commodity *b)
This routine returns TRUE if the two commodities are equal.
The primary numeric class for representing amounts and values.
Definition: gnc-numeric.hpp:60
#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.
int gnc_commodity_table_has_namespace(const gnc_commodity_table *table, const char *name_space)
Test to see if the indicated namespace exits in the commodity table.
const char * gnc_commodity_get_fullname(const gnc_commodity *cm)
Retrieve the full name for the specified commodity.
#define MAX_DATE_LENGTH
The maximum length of a string created by the date printers.
Definition: gnc-date.h:108
gboolean gnc_pricedb_remove_price(GNCPriceDB *db, GNCPrice *p)
Remove a price from the pricedb and unref the price.
gint64 time64
Most systems that are currently maintained, including Microsoft Windows, BSD-derived Unixes and Linux...
Definition: gnc-date.h:87
size_t qof_print_date_buff(char *buff, size_t buflen, time64 secs)
Convenience: calls through to qof_print_date_dmy_buff().
Definition: gnc-date.cpp:574
static const std::vector< GncDateFormat > c_formats
A vector with all the date formats supported by the string constructor.
GnuCash Date class.