GnuCash  5.6-150-g038405b370+
assistant-stock-transaction.cpp
1 /********************************************************************\
2  * assistant-stock-transaction.cpp -- stock assistant for GnuCash *
3  * Copyright (C) 2022 Christopher Lam *
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 #include <config.h>
24 
25 #include <cstddef>
26 #include <exception>
27 #include <gtk/gtk.h>
28 #include <glib/gi18n.h>
29 #include <cinttypes>
30 #include <memory>
31 #include <vector>
32 #include <string>
33 #include <numeric>
34 #include <algorithm>
35 #include <optional>
36 #include <stdexcept>
37 #include <sstream>
38 
39 #include "Account.h"
40 #include "Account.hpp"
41 #include "Transaction.h"
42 #include "engine-helpers.h"
43 #include "dialog-utils.h"
44 #include "assistant-stock-transaction.h"
45 #include "gnc-account-sel.h"
46 #include "gnc-amount-edit.h"
47 #include "gnc-date.h"
48 #include <gnc-date-edit.h>
49 #include "gnc-engine.h"
50 #include "gnc-numeric.h"
51 #include "gnc-numeric.hpp"
52 #include "gnc-prefs.h"
53 #include "gnc-component-manager.h"
54 #include "gnc-date-edit.h"
55 #include "gnc-tree-view-account.h"
56 #include "gnc-ui-util.h"
57 
58 static QofLogModule log_module = GNC_MOD_ASSISTANT;
59 
78 extern "C"
79 {
80 // These functions are the GtkAssistant primary button callbacks. They're
81 // connected to their signals in assistant-stock-transaction.glade so they
82 // mustn't be name-mangled.
83 void stock_assistant_prepare_cb (GtkAssistant *assistant, GtkWidget *page,
84  gpointer user_data);
85 void stock_assistant_finish_cb (GtkAssistant *assistant, gpointer user_data);
86 void stock_assistant_cancel_cb (GtkAssistant *gtkassistant, gpointer user_data);
87 }
88 
89 static const char* GNC_PREFS_GROUP = "dialogs.stock-assistant";
90 static const char* ASSISTANT_STOCK_TRANSACTION_CM_CLASS = "assistant-stock-transaction";
91 
92 static const char* DIVIDEND_KVP_TAG = "stock-dividends";
93 static const char* CAPGAINS_KVP_TAG = "stock-capgains";
94 static const char* PROCEEDS_KVP_TAG = "stock-cash-proceeds";
95 static const char* FEES_KVP_TAG = "stock-broker-fees";
96 
99 enum class FieldMask : unsigned
100 {
101  DISABLED = 0,
102  ENABLED_DEBIT = 1,
103  ENABLED_CREDIT = 1 << 1,
104  AMOUNT_DEBIT = 1 << 2, // stock only
105  AMOUNT_CREDIT = 1 << 3, // stock only
106  INPUT_NEW_BALANCE = 1 << 4, // stock_amt only: instead of amount, get new balance
107  ALLOW_ZERO = 1 << 5,
108  ALLOW_NEGATIVE = 1 << 6,
109  CAPITALIZE_DEFAULT = 1 << 7, // fees only: capitalize by default into stock acct
110  CAPGAINS_IN_STOCK = 1 << 8, // capg only: add a balancing split in stock acct
111  MARKER_SPLIT = 1 << 9, // stock only, place a no-amount, no-value split in the
112  // stock account to associate the income.
113 };
114 
115 static FieldMask
116 operator |(FieldMask lhs, FieldMask rhs)
117 {
118  return static_cast<FieldMask> (static_cast<unsigned>(lhs) |
119  static_cast<unsigned>(rhs));
120 };
121 
122 static bool
123 operator &(FieldMask lhs, FieldMask rhs)
124 {
125  return (static_cast<unsigned>(lhs) & static_cast<unsigned>(rhs));
126 };
127 
135 {
136  FieldMask stock_amount;
137  FieldMask cash_value;
138  FieldMask fees_value;
139  FieldMask dividend_value;
140  FieldMask capgains_value;
141  const char* friendly_name;
142  const char* explanation;
143 };
144 
145 using StringVec = std::vector<std::string>;
146 using TxnTypeVec = std::vector<TxnTypeInfo>;
147 using AccountVec = std::vector<Account*>;
148 
149 static const TxnTypeVec starting_types
150 {
151 
152  {
153  FieldMask::ENABLED_DEBIT | FieldMask::AMOUNT_DEBIT, // stock_amt
154  FieldMask::ENABLED_CREDIT, // cash_amt
155  FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO | FieldMask::CAPITALIZE_DEFAULT, // fees_amt
156  FieldMask::DISABLED, // dividend_amt
157  FieldMask::DISABLED, // capg_amt
158  // Translators: this is a stock transaction describing an
159  // Initial stock long purchase
160  N_("Open buy"),
161  N_("Initial stock long purchase.")
162  },
163  {
164  FieldMask::ENABLED_CREDIT | FieldMask::AMOUNT_CREDIT, // stock_amt
165  FieldMask::ENABLED_DEBIT, // cash_amt
166  FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO | FieldMask::CAPITALIZE_DEFAULT, // fees_amt
167  FieldMask::DISABLED, // dividend_amt
168  FieldMask::DISABLED, // capg_amt
169  // Translators: this is a stock transaction describing an
170  // initial stock short sale
171  N_("Open short"),
172  N_("Initial stock short sale.")
173  }
174 };
175 
176 static const TxnTypeVec long_types
177 {
178  {
179  FieldMask::ENABLED_DEBIT | FieldMask::AMOUNT_DEBIT, // stock_amt
180  FieldMask::ENABLED_CREDIT, // cash_amt
181  FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO | FieldMask::CAPITALIZE_DEFAULT, // fees_amt
182  FieldMask::DISABLED, // dividend_amt
183  FieldMask::DISABLED, // capg_amt
184  // Translators: this is a stock transaction describing
185  // new purchase of stock.
186  N_("Buy"),
187  N_("Buying stock long.")
188  },
189  {
190  FieldMask::ENABLED_CREDIT | FieldMask::AMOUNT_CREDIT, // stock_amt
191  FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO, // cash_amt
192  FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO, // fees_amt
193  FieldMask::DISABLED, // dividend_amt
194  FieldMask::ENABLED_CREDIT | FieldMask::ALLOW_ZERO | FieldMask::ALLOW_NEGATIVE | FieldMask::CAPGAINS_IN_STOCK, // capgains_amt
195  // Translators: this is a stock transaction describing new
196  // sale of stock, and recording capital gain/loss
197  N_("Sell"),
198  N_("Selling stock long, and record capital gain/loss."
199  "\n\nIf you are unable to calculate capital gains you can enter a "
200  "placeholder amount and correct it in the transaction later.")
201  },
202  {
203  FieldMask::MARKER_SPLIT, // stock_amt
204  FieldMask::ENABLED_DEBIT, // cash_amt
205  FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO, // fees_amt
206  FieldMask::ENABLED_CREDIT, // dividend_amt
207  FieldMask::DISABLED, // capg_amt
208  // Translators: this is a stock transaction describing
209  // dividends issued to holder
210  N_("Dividend"),
211  N_("Company issues cash dividends to holder.\n\nAny dividend being "
212  "reinvested must be subsequently recorded as a regular stock purchase.")
213  },
214  {
215  FieldMask::ENABLED_CREDIT, // stock_amt
216  FieldMask::ENABLED_DEBIT, // cash_amt
217  FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO | FieldMask::CAPITALIZE_DEFAULT, // fees_amt
218  FieldMask::DISABLED, // dividend_amt
219  FieldMask::DISABLED, // capg_amt
220  // Translators: this is a stock transaction describing return
221  // of capital
222  N_("Return of capital"),
223  N_("Company returns capital, reducing the cost basis without affecting # units.")
224  },
225  {
226  FieldMask::ENABLED_CREDIT, // stock_amt
227  FieldMask::DISABLED, // cash_amt
228  FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO | FieldMask::CAPITALIZE_DEFAULT, // fees_amt
229  FieldMask::ENABLED_DEBIT, // dividend_amt
230  FieldMask::DISABLED, // capg_amt
231  // Translators: this is a stock transaction describing return
232  // of capital, reclassifying a dividend into return of capital
233  N_("Return of capital (reclassification)"),
234  N_("Company returns capital, reducing the cost basis without affecting # units. "
235  "A distribution previously recorded as a dividend is reclassified to return "
236  "of capital, often due to end-of-year tax information.")
237  },
238  {
239  FieldMask::ENABLED_DEBIT, // stock_amt
240  FieldMask::DISABLED, // cash_amt
241  FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO, // fees_amt
242  FieldMask::ENABLED_CREDIT, // dividend_amt
243  FieldMask::DISABLED, // capg_amt
244  // Translators: this is a stock transaction describing a
245  // notional distribution recorded as dividend
246  N_("Notional distribution (dividend)"),
247  N_("Company issues a notional distribution, which is recorded as dividend "
248  "income and increases the cost basis without affecting # units.")
249  },
250  {
251  FieldMask::ENABLED_DEBIT, // stock_amt
252  FieldMask::DISABLED, // cash_amt
253  FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO, // fees_amt
254  FieldMask::DISABLED, // dividend_amt
255  FieldMask::ENABLED_CREDIT, // capg_amt
256  // Translators: this is a stock transaction describing a
257  // notional distribution recorded as capital gain
258  N_("Notional distribution (capital gain)"),
259  N_("Company issues a notional distribution, which is recorded as capital gain "
260  "and increases the cost basis without affecting # units.")
261  },
262  {
263  FieldMask::AMOUNT_DEBIT | FieldMask::INPUT_NEW_BALANCE, // stock_amt
264  FieldMask::ENABLED_CREDIT | FieldMask::ALLOW_ZERO, // cash_amt
265  FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO | FieldMask::CAPITALIZE_DEFAULT, // fees_amt
266  FieldMask::DISABLED, // dividend_amt
267  FieldMask::DISABLED, // capg_amt
268  // Translators: this is a stock transaction describing a stock
269  // split
270  N_("Stock split"),
271  N_("Company issues additional units, thereby reducing the stock price by a divisor "
272  ", while keeping the total monetary value of the overall investment constant. "
273  "\n\nIf the split results in a cash in lieu for remainder units, please "
274  "record the sale using the Stock Transaction Assistant first, then record the split.")
275  },
276  {
277  FieldMask::AMOUNT_CREDIT | FieldMask::INPUT_NEW_BALANCE, // stock_amt
278  FieldMask::ENABLED_CREDIT | FieldMask::ALLOW_ZERO, // cash_amt
279  FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO | FieldMask::CAPITALIZE_DEFAULT, // fees_amt
280  FieldMask::DISABLED, // dividend_amt
281  FieldMask::DISABLED, // capg_amt
282  // Translators: this is a stock transaction describing a reverse split
283  N_("Reverse split"),
284  N_("Company redeems units, thereby increasing the stock price by a multiple, while "
285  "keeping the total monetary value of the overall investment constant.\n\nIf the "
286  "reverse split results in a cash in lieu for remainder units, please record the "
287  "sale using the Stock Transaction Assistant first, then record the reverse split.")
288  }
289 };
290 
291 static const TxnTypeVec short_types
292 {
293  {
294  FieldMask::ENABLED_CREDIT | FieldMask::AMOUNT_CREDIT, // stock_amt
295  FieldMask::ENABLED_DEBIT, // cash_amt
296  FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO | FieldMask::CAPITALIZE_DEFAULT, // fees_amt
297  FieldMask::DISABLED, // dividend_amt
298  FieldMask::DISABLED, // capg_amt
299  // Translators: this is a stock transaction describing
300  // shorting of stock.
301  N_("Short sell"),
302  N_("Selling stock short.")
303  },
304  {
305  FieldMask::ENABLED_DEBIT | FieldMask::AMOUNT_DEBIT, // stock_amt
306  FieldMask::ENABLED_CREDIT | FieldMask::ALLOW_ZERO, // cash_amt
307  FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO, // fees_amt
308  FieldMask::DISABLED, // dividend_amt
309  FieldMask::ENABLED_CREDIT | FieldMask::ALLOW_ZERO | FieldMask::ALLOW_NEGATIVE | FieldMask::CAPGAINS_IN_STOCK, // capg_amt
310  // Translators: this is a stock transaction describing cover
311  // buying stock, and recording capital gain/loss
312  N_("Buy to cover short"),
313  N_("Buy back stock to cover short position, and record capital gain/loss. "
314  "\n\nIf you are unable to calculate capital gains you can enter a placeholder "
315  "amount and correct it in the transaction later.")
316  },
317  {
318  FieldMask::MARKER_SPLIT, // stock_amt
319  FieldMask::ENABLED_CREDIT, // cash_amt
320  FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO, // fees_amt
321  FieldMask::ENABLED_DEBIT, // dividend_amt
322  FieldMask::DISABLED, // capg_amt
323  // Translators: this is a stock transaction describing
324  // dividends retrieved from holder when shorting stock
325  N_("Compensatory dividend"),
326  N_("Company issues dividends, and the short stock holder must make a compensatory "
327  "payment for the dividend.")
328  },
329  {
330  FieldMask::ENABLED_DEBIT, // stock_amt
331  FieldMask::ENABLED_CREDIT, // cash_amt
332  FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO | FieldMask::CAPITALIZE_DEFAULT, // fees_amt
333  FieldMask::DISABLED, // dividend_amt
334  FieldMask::DISABLED, // capg_amt
335  // Translators: this is a stock transaction describing return
336  // of capital retrieved from holder when shorting stock
337  N_("Compensatory return of capital"),
338  N_("Company returns capital, and the short stock holder must make a compensatory "
339  "payment for the returned capital. This reduces the cost basis (less negative, "
340  "towards 0.00 value) without affecting # units.")
341  },
342  {
343  FieldMask::ENABLED_DEBIT, // stock_amt
344  FieldMask::DISABLED, // cash_amt
345  FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO | FieldMask::CAPITALIZE_DEFAULT, // fees_amt
346  FieldMask::ENABLED_CREDIT, // dividend_amt
347  FieldMask::DISABLED, // capg_amt
348  // Translators: this is a stock transaction describing
349  // reclassifying a compensatory dividend into compensatory
350  // return of capital when shorting stock
351  N_("Compensatory return of capital (reclassification)"),
352  N_("Company returns capital, and the short stock holder must make a compensatory "
353  "payment for the returned capital. This reduces the cost basis (less negative, "
354  "towards 0.00 value) without affecting # units. A distribution previously recorded "
355  "as a compensatory dividend is reclassified to compensatory return of capital,"
356  "often due to end-of-year tax information.")
357  },
358  {
359  FieldMask::ENABLED_CREDIT, // stock_amt
360  FieldMask::DISABLED, // cash_amt
361  FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO, // fees_amt
362  FieldMask::ENABLED_DEBIT, // dividend_amt
363  FieldMask::DISABLED, // capg_amt
364  // Translators: this is a stock transaction describing a
365  // notional distribution recorded as dividend when shorting
366  // stock
367  N_("Compensatory notional distribution (dividend)"),
368  N_("Company issues a notional distribution, and the short stock holder must make a "
369  "compensatory payment for the notional distribution. This is recorded as a "
370  "loss/negative dividend income amount, and increases the cost basis (more "
371  "negative, away from 0.00 value) without affecting # units.")
372  },
373  {
374  FieldMask::ENABLED_CREDIT, // stock_amt
375  FieldMask::DISABLED, // cash_amt
376  FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO, // fees_amt
377  FieldMask::DISABLED, // dividend_amt
378  FieldMask::ENABLED_DEBIT, // capg_amt
379  // Translators: this is a stock transaction describing a
380  // notional distribution recorded as capital gain when
381  // shorting stock
382  N_("Compensatory notional distribution (capital gain)"),
383  N_("Company issues a notional distribution, and the short stock holder must make "
384  "a compensatory payment for the notional distribution. This is recorded as a "
385  "capital loss amount, and increases the cost basis (more negative, away from "
386  "0.00 value) without affecting # units.")
387  },
388  {
389  FieldMask::AMOUNT_CREDIT | FieldMask::INPUT_NEW_BALANCE, // stock_amt
390  FieldMask::ENABLED_CREDIT | FieldMask::ALLOW_ZERO, // cash_amt
391  FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO | FieldMask::CAPITALIZE_DEFAULT, // fees_amt
392  FieldMask::DISABLED, // dividend_amt
393  FieldMask::DISABLED, // capg_amt
394  // Translators: this is a stock transaction describing a stock
395  // split when shorting stock
396  N_("Stock split"),
397  N_("Company issues additional units, thereby reducing the stock price by a divisor, "
398  "while keeping the total monetary value of the overall investment constant. "
399  "\n\nIf the split results in a cash in lieu for remainder units, please "
400  "record the cover buy using the Stock Transaction Assistant first, then record the split.")
401  },
402  {
403  FieldMask::AMOUNT_DEBIT | FieldMask::INPUT_NEW_BALANCE, // stock_amt
404  FieldMask::ENABLED_CREDIT | FieldMask::ALLOW_ZERO, // cash_amt
405  FieldMask::ENABLED_DEBIT | FieldMask::ALLOW_ZERO | FieldMask::CAPITALIZE_DEFAULT, // fees_amt
406  FieldMask::DISABLED, // dividend_amt
407  FieldMask::DISABLED, // capg_amt
408  // Translators: this is a stock transaction describing a
409  // reverse split when shorting stock.
410  N_("Reverse split"),
411  N_("Company redeems units, thereby increasing the stock price by a multiple, while "
412  "keeping the total monetary value of the overall investment constant.\n\nIf the "
413  "reverse split results in a cash in lieu for remainder units, please record the "
414  "cover buy using the Stock Transaction Assistant first, then record the reverse split.")
415  }
416 };
417 
418 enum class LogMsgType
419 {
420  info,
421  warning,
422  error
423 };
424 
426 {
427  LogMsgType m_type;
428  const std::string m_message;
429 public:
430  LogMessage(LogMsgType type, std::string&& message) :
431  m_type{type}, m_message(std::move(message)) {}
432  LogMessage(LogMsgType type, const char* message) :
433  m_type{type}, m_message(message) {}
434  LogMessage(const LogMessage&) = default;
435  LogMessage(LogMessage&&) = default;
436  ~LogMessage() = default;
437  LogMsgType type() { return m_type; }
438  const std::string& message() { return m_message; }
439 };
440 
447 using Log = std::vector<LogMessage>;
448 
449 class Logger
450 {
451  Log m_log;
452 public: // compiler generated ctors & dtor are fine
453  void info(const char* message) { m_log.emplace_back(LogMsgType::info, message); }
454  void warn(const char* message) { m_log.emplace_back(LogMsgType::warning, message); }
455  void error(const char* message) { m_log.emplace_back(LogMsgType::error, message); }
456  void clear() { m_log.clear(); }
457  bool has_errors();
458  bool has_warnings();
459  void write_log(std::stringstream& stream, LogMsgType type);
460  void infos(std::stringstream& stream) { return write_log(stream, LogMsgType::info); }
461  void warnings(std::stringstream& stream) { return write_log(stream, LogMsgType::warning); }
462  void errors(std::stringstream& stream) { return write_log(stream, LogMsgType::error); }
468  std::string report();
469 };
470 
471 void
472 Logger::write_log(std::stringstream& stream, LogMsgType type)
473 {
474  std::for_each(m_log.begin(), m_log.end(),
475  [&](auto& msg){
476  if (msg.type() == type)
477  stream << "\n * " << msg.message();
478  });
479 }
480 
481 bool
482 Logger::has_warnings()
483 {
484  return std::any_of(m_log.begin(), m_log.end(),
485  [](auto& msg){ return msg.type() == LogMsgType::warning;
486  });
487 }
488 
489 bool
490 Logger::has_errors()
491 {
492  return std::any_of(m_log.begin(), m_log.end(),
493  [](auto& msg){ return msg.type() == LogMsgType::error;
494  });
495 }
496 
497 std::string
499 {
500  std::stringstream summary;
501  if (!has_errors())
502  {
503  summary << _("No errors found. Click Apply to create transaction.");
504  infos(summary);
505  }
506  else
507  {
508  summary << _("The following errors must be fixed:");
509  errors(summary);
510  }
511  if (has_warnings())
512  {
513  summary << "\n\n" << _("The following warnings exist:");
514  warnings(summary);
515  }
516  return summary.str();
517 }
518 
519 /* StockTransactionEntry QofEventHandler for accounts.
520  * Nulls the tranaction entry's account member if the account gets destroyed on
521  * us.
522  */
523 static void account_destroyed_handler(QofInstance *inst, QofEventId event,
524  void* handler_data, [[maybe_unused]]void* event_data);
525 
535 {
536 protected:
537  bool m_enabled;
538  bool m_debit_side;
539  bool m_allow_zero;
540  bool m_allow_negative;
541  bool m_input_new_balance = false;
542  Account *m_account;
543  gnc_numeric m_value;
544  const char* m_memo;
545  const char* m_action;
546  gnc_numeric m_balance = gnc_numeric_zero();
547  const char* m_kvp_tag;
548  int m_qof_event_handler;
549 public:
551  m_enabled{false}, m_debit_side{false}, m_allow_zero{false}, m_account{nullptr},
552  m_value{gnc_numeric_error(GNC_ERROR_ARG)}, m_memo{nullptr}, m_action{nullptr},
553  m_kvp_tag{nullptr}, m_qof_event_handler{qof_event_register_handler(account_destroyed_handler, this)} {}
554  StockTransactionEntry(const char* action, const char* kvp_tag) :
555  m_enabled{false}, m_debit_side{false}, m_allow_zero{false}, m_account{nullptr},
556  m_value{gnc_numeric_error(GNC_ERROR_ARG)}, m_memo{nullptr}, m_action{action},
557  m_kvp_tag{kvp_tag}, m_qof_event_handler{qof_event_register_handler(account_destroyed_handler, this)} {}
559  virtual ~StockTransactionEntry() { qof_event_unregister_handler(m_qof_event_handler); }
564  virtual void set_fieldmask(FieldMask mask);
565  virtual bool enabled() const { return m_enabled; }
566  virtual bool debit_side() const { return m_debit_side; }
567  virtual void set_capitalize(bool capitalize) {}
568  virtual bool input_new_balance() const { return m_input_new_balance; }
569  virtual bool do_capitalize() const { return false; }
570  virtual void set_account(Account* account) { m_account = account; }
571  virtual Account* account() const { return m_account; }
572  virtual const char* print_account() const;
573  virtual void set_memo(const char* memo) { m_memo = memo; }
574  virtual const char* get_kvp_tag () { return m_kvp_tag; }
575  virtual const char* memo() const { return m_memo; }
576  virtual void set_value(gnc_numeric amount);
577  virtual GncNumeric value() { return (gnc_numeric_check(m_value) ? GncNumeric{} : GncNumeric(m_value)); }
578  virtual void set_amount(gnc_numeric) {}
579  virtual gnc_numeric amount() const { return m_value; }
580  virtual bool has_amount() const { return false; }
581  virtual bool marker_split() const { return false; }
582  /* Validates that the value and for stock entry the amount meet
583  * the criteria set for the entry by the field mask.
584  *
585  * @param logger collects any emitted diagnostics.
586  */
587  virtual void validate_amount(Logger&) const;
588  virtual void set_balance(gnc_numeric balance) { m_balance = balance; }
589  virtual gnc_numeric get_balance() const { return m_balance; }
590  /* Creates a GnuCash split from the data in the entry and adds it
591  * to the transaction, adding the account to the account vector so
592  * that it can be committed when the transaction is completed.
593  *
594  * @param trans the transaction to which to add the split
595  * @param commits the list of accounts to have edits committed later.
596  */
597  virtual void create_split(Transaction* trans, AccountVec& commits) const;
601  virtual const char* print_value() const;
605  virtual const char* print_amount(gnc_numeric amt) const;
613  virtual std::string amount_str_for_display() const { return ""; }
620  virtual gnc_numeric calculate_price() const { return gnc_numeric_error(GNC_ERROR_ARG); }
624  virtual const char* print_price() const;
625 };
626 
627 static void
628 account_destroyed_handler(QofInstance *inst, QofEventId event,
629  void* handler_data, [[maybe_unused]]void* event_data)
630 {
631  auto entry{static_cast<StockTransactionEntry*>(handler_data)};
632  if ((inst && inst != QOF_INSTANCE(entry->account())) || (event & QOF_EVENT_DESTROY) == 0)
633  return;
634  entry->set_account(nullptr);
635 }
636 
637 using StockTransactionEntryPtr = std::unique_ptr<StockTransactionEntry>;
638 
639 void
641 {
642  m_enabled = mask != FieldMask::DISABLED;
643  m_debit_side = mask & FieldMask::ENABLED_DEBIT;
644  m_allow_zero = mask & FieldMask::ALLOW_ZERO;
645  m_allow_negative = mask & FieldMask::ALLOW_NEGATIVE;
646 }
647 
648 const char *
649 StockTransactionEntry::print_account() const
650 {
651  auto acct_required = m_enabled &&
652  !(m_allow_zero && (gnc_numeric_zero_p(m_value) ||
653  gnc_numeric_check(m_value)));
654  return m_account ? xaccAccountGetName(m_account) :
655  acct_required ? _("missing") : "";
656 }
657 
658 void
659 StockTransactionEntry::set_value(gnc_numeric amount)
660 {
661  if (gnc_numeric_check (amount))
662  {
663  m_value = gnc_numeric_error(GNC_ERROR_ARG);
664  return;
665  }
666 
667  if (gnc_numeric_negative_p (amount))
668  {
669  m_value = gnc_numeric_neg(amount);
670  m_debit_side = !m_debit_side;
671  }
672  else
673  {
674  m_value = amount;
675  }
676  PINFO("Set %s value to %" PRId64 "/%" PRId64, m_action, m_value.num, m_value.denom);
677 }
678 
679 void
680 StockTransactionEntry::validate_amount(Logger& logger) const
681 {
682  auto add_error = [&logger](const char* format_str, const char* arg)
683  {
684  char *buf = g_strdup_printf (_(format_str),
685  g_dpgettext2 (nullptr, "Stock Assistant: Page name", arg));
686  logger.error(buf);
687  g_free (buf);
688  };
689 
690 
691  if (gnc_numeric_check (m_value))
692  {
693  if (!m_allow_zero)
694  add_error (N_("Amount for %s is missing."), m_action);
695  return;
696  }
697 
698  if (gnc_numeric_negative_p (m_value) && !m_allow_negative && m_allow_zero)
699  add_error (N_("Amount for %s must not be negative."), m_action);
700 
701  if (!m_allow_zero && !gnc_numeric_positive_p (m_value))
702  add_error (N_("Amount for %s must be positive."), m_action);
703 
704  if (!gnc_numeric_zero_p(m_value) && !m_account)
705  add_error(N_("The %s amount has no associated account."), m_action);
706 }
707 
708 const char *
710 {
711  if (!m_enabled || (gnc_numeric_check(m_value) && m_allow_zero))
712  return nullptr;
713 
714  if ((gnc_numeric_check(m_value) || gnc_numeric_zero_p(m_value))
715  && !m_allow_zero)
716  return _("missing");
717 
718  /* Don't combine this with the first if, it would prevent showing
719  * "missing" when the value is required.
720  */
721  if (!m_account)
722  return nullptr;
723 
724  auto currency{gnc_account_get_currency_or_parent(m_account)};
725  auto pinfo{gnc_commodity_print_info(currency, TRUE)};
726  return xaccPrintAmount(m_value, pinfo);
727 }
728 
729 const char *
731 {
732  if (!m_account || gnc_numeric_check(amt))
733  return nullptr;
734  auto commodity{xaccAccountGetCommodity(m_account)};
735  auto pinfo{gnc_commodity_print_info(commodity, TRUE)};
736  return xaccPrintAmount(amt, pinfo);
737 }
738 
739 void
740 StockTransactionEntry::create_split(Transaction *trans, AccountVec &account_commits) const
741 {
742  g_return_if_fail(trans);
743  if (!m_account || gnc_numeric_check(m_value))
744  return;
745  auto split = xaccMallocSplit(qof_instance_get_book(trans));
746  xaccSplitSetParent(split, trans);
747  xaccAccountBeginEdit(m_account);
748  account_commits.push_back(m_account);
749  xaccSplitSetAccount(split, m_account);
750  xaccSplitSetMemo(split, m_memo);
751  if (m_enabled)
752  xaccSplitSetValue(split, m_debit_side ? m_value : gnc_numeric_neg(m_value));
753  xaccSplitSetAmount(split, amount());
754  PINFO("creating %s split in Acct(%s): Val(%s), Amt(%s) => Val(%s), Amt(%s)",
755  m_action, m_account ? xaccAccountGetName (m_account) : "Empty!",
756  gnc_num_dbg_to_string(m_value),
757  gnc_num_dbg_to_string(amount()),
760  gnc_set_num_action(nullptr, split, nullptr,
761  g_dpgettext2(nullptr, "Stock Assistant: Action field",
762  m_action));
763 }
764 
765 const char *
767 {
768  auto price{calculate_price()};
769  if (gnc_numeric_check(price))
770 //Translators: "N/A" here means that a commodity doesn't have a valid price.
771  return _("N/A");
772  auto currency{gnc_account_get_currency_or_parent(m_account)};
773  auto pinfo{gnc_price_print_info(currency, TRUE)};
774  return xaccPrintAmount(price, pinfo);
775 }
776 
786 {
787  bool m_amount_enabled;
788  gnc_numeric m_amount;
789  bool m_marker = false;
790 public:
793  {
794  PINFO("Stock Entry");
795  }
796  StockTransactionStockEntry(const char* action) :
797  StockTransactionEntry{action, nullptr}, m_amount{gnc_numeric_error(GNC_ERROR_ARG)}
798  {
799  PINFO("Stock Entry");
800  }
801  void set_fieldmask(FieldMask mask) override;
802  void set_amount(gnc_numeric amount) override;
803  gnc_numeric amount() const override { return m_amount; }
804  bool has_amount() const override { return m_amount_enabled; }
805  void validate_amount(Logger& logger) const override;
806  void create_split(Transaction *trans, AccountVec &account_commits) const override;
807  std::string amount_str_for_display() const override;
808  gnc_numeric calculate_price() const override;
809  bool marker_split() const override { return m_marker; }
810 };
811 
812 void
814 {
816  m_enabled = mask & (FieldMask::ENABLED_CREDIT | FieldMask::ENABLED_DEBIT);
817  m_amount_enabled = mask & (FieldMask::AMOUNT_CREDIT | FieldMask::AMOUNT_DEBIT);
818  m_debit_side = mask & (FieldMask::ENABLED_DEBIT | FieldMask::AMOUNT_DEBIT);
819  m_input_new_balance = mask & FieldMask::INPUT_NEW_BALANCE;
820  m_marker = mask & FieldMask::MARKER_SPLIT;
821 }
822 
823 
824 void
825 StockTransactionStockEntry::set_amount(gnc_numeric amount)
826 {
827  if (!m_amount_enabled || gnc_numeric_check(amount))
828  {
829  m_amount = gnc_numeric_error(GNC_ERROR_ARG);
830  return;
831  }
832 
833  if (m_input_new_balance)
834  {
835  if (m_debit_side)
836  m_amount = gnc_numeric_sub_fixed(amount, m_balance);
837  else
838  m_amount = gnc_numeric_sub_fixed(m_balance, amount);
839 
840  PINFO("%s set amount for new balance %s", m_memo, print_amount(m_amount));
841  }
842  else
843  {
844  m_amount = amount;
845  PINFO("%s set amount %s", m_memo, print_amount(m_amount));
846  }
847 }
848 
849 void
850 StockTransactionStockEntry::validate_amount(Logger& logger) const
851 {
852  if (m_enabled)
853  StockTransactionEntry::validate_amount(logger);
854 
855  if (!m_amount_enabled)
856  return;
857 
858  auto add_error_str = [&logger]
859  (const char* str) { logger.error (_(str)); };
860 
861  if (gnc_numeric_check(m_amount) || gnc_numeric_zero_p(m_amount))
862  {
863  add_error_str(_("Amount for stock value is missing."));
864  return;
865  }
866 
867  if (m_input_new_balance)
868  {
869  auto amount = gnc_numeric_add_fixed(m_debit_side ? m_amount : gnc_numeric_neg(m_amount), m_balance);
870  auto delta = gnc_numeric_sub_fixed(amount, m_balance);
871  auto ratio = gnc_numeric_div(amount, m_balance,
873 
874  if (gnc_numeric_check(ratio) || !gnc_numeric_positive_p(ratio))
875  add_error_str(N_("Invalid stock new balance."));
876  else if (gnc_numeric_negative_p(delta) && m_debit_side)
877  add_error_str(N_("New balance must be higher than old balance."));
878  else if (gnc_numeric_positive_p(delta) && !m_debit_side)
879  add_error_str(N_("New balance must be lower than old balance."));
880 
881  PINFO("Delta %" PRId64 "/%" PRId64 ", Ratio %" PRId64 "/%" PRId64, delta.num, delta.denom, ratio.num, ratio.denom);
882  return;
883  }
884 
885  if (!gnc_numeric_positive_p(m_amount))
886  add_error_str(N_("Stock amount must be positive."));
887 
888  auto new_bal = gnc_numeric_add_fixed(m_balance, m_amount);
889  if (gnc_numeric_positive_p(m_balance) && gnc_numeric_negative_p(new_bal))
890  add_error_str(N_("Cannot sell more units than owned."));
891  else if (gnc_numeric_negative_p(m_balance) && gnc_numeric_positive_p(new_bal))
892  add_error_str(N_("Cannot cover buy more units than owed."));
893 }
894 
895 std::string
897 {
898  std::string rv{""};
899 
900  if (gnc_numeric_check (m_amount))
901  return rv;
902 
903  if (m_input_new_balance)
904  {
905  auto amount = gnc_numeric_add(m_debit_side ? m_amount : gnc_numeric_neg(m_amount), m_balance,
907  auto ratio = gnc_numeric_div (amount, m_balance,
909  PINFO("Computed ratio %" PRId64 "/%" PRId64 "; amount %" PRId64
910  "/%" PRId64 " and balance %" PRId64 "/%" PRId64,
911  ratio.num, ratio.denom, amount.num, amount.denom, m_balance.num, m_balance.denom);
912  if (gnc_numeric_check (ratio) || !gnc_numeric_positive_p (ratio))
913  return rv;
914 
915  std::ostringstream ret;
916  ret << ratio.num << ':' << ratio.denom;
917  rv = ret.str();
918  }
919  else
920  {
921  auto amount = m_debit_side ? m_amount : gnc_numeric_neg (m_amount);
922  amount = gnc_numeric_add_fixed (amount, m_balance);
923  rv = print_amount(amount);
924  }
925 
926  return rv;
927 };
928 
929 
930 void
931 StockTransactionStockEntry::create_split(Transaction *trans, AccountVec &account_commits) const
932 {
933  g_return_if_fail(trans);
934  if (!m_account)
935  return;
936  auto split = xaccMallocSplit(qof_instance_get_book(trans));
937  xaccSplitSetParent(split, trans);
938  xaccAccountBeginEdit(m_account);
939  account_commits.push_back(m_account);
940  xaccSplitSetAccount(split, m_account);
941  xaccSplitSetMemo(split, m_memo);
942  if (m_enabled)
943  xaccSplitSetValue(split, m_debit_side ? m_value : gnc_numeric_neg(m_value));
944  if (m_amount_enabled)
945  xaccSplitSetAmount(split, m_debit_side ? m_amount : gnc_numeric_neg(m_amount));
946  if (m_amount_enabled && !m_enabled) // It's a stock split
948  PINFO("creating %s split in Acct(%s): Val(%s), Amt(%s) => Val(%s), Amt(%s)",
949  m_action, m_account ? xaccAccountGetName (m_account) : "Empty!",
950  gnc_num_dbg_to_string(m_value),
951  gnc_num_dbg_to_string(amount()),
954  gnc_set_num_action(nullptr, split, nullptr,
955  g_dpgettext2(nullptr, "Stock Assistant: Action field",
956  m_action));
957 }
958 
959 gnc_numeric
961 {
962  if (m_input_new_balance ||
963  !m_amount_enabled || gnc_numeric_check(m_amount) ||
964  !m_enabled || gnc_numeric_check(m_value) ||
965  gnc_numeric_zero_p(m_amount) || gnc_numeric_zero_p(m_value))
967 
968  auto price = gnc_numeric_div(m_value, m_amount,
970 
971  auto comm{xaccAccountGetCommodity(m_account)};
972  auto curr{gnc_account_get_currency_or_parent(m_account)};
973  auto ainfo{gnc_commodity_print_info (comm, true)};
974  auto pinfo{gnc_price_print_info (curr, true)};
975  auto vinfo{gnc_commodity_print_info (curr, true)};
976 
977  PINFO("Calculated price %s from value %s and amount %s",
978  xaccPrintAmount(price, pinfo), xaccPrintAmount(m_value, vinfo),
979  xaccPrintAmount(m_amount, ainfo));
980  return price;
981 }
982 
990 {
991 public:
993  const StockTransactionEntry* stk_entry);
994  gnc_numeric amount() const { return gnc_numeric_zero(); }
995 };
996 
997 StockTransactionStockCapGainsEntry::StockTransactionStockCapGainsEntry(const StockTransactionEntry *cg_entry,
998  const StockTransactionEntry *stk_entry) :
999  StockTransactionEntry(*cg_entry)
1000 {
1001  m_debit_side = !m_debit_side;
1002  m_account = stk_entry->account();
1003 }
1004 
1010 {
1011  bool m_capitalize;
1012 public:
1013  StockTransactionFeesEntry() : StockTransactionEntry{}, m_capitalize{false} {}
1014  StockTransactionFeesEntry(const char* action, const char *tag) : StockTransactionEntry{action, tag}, m_capitalize{false} {}
1015  void set_fieldmask(FieldMask mask) override;
1016  void set_capitalize(bool capitalize) override { m_capitalize = capitalize; }
1017  bool do_capitalize() const override { return m_capitalize; }
1018  void validate_amount(Logger &logger) const override;
1019  void create_split(Transaction *trans, AccountVec &commits) const override;
1020 };
1021 
1022 void
1024 {
1026  m_capitalize = mask & FieldMask::CAPITALIZE_DEFAULT;
1027 }
1028 
1029 void
1030 StockTransactionFeesEntry::validate_amount(Logger& logger) const
1031 {
1032  auto add_error = [&logger](const char* format_str, const char* arg)
1033  {
1034  char *buf = g_strdup_printf (_(format_str),
1035  g_dpgettext2 (nullptr, "Stock Assistant: Page name", arg));
1036  logger.error(buf);
1037  g_free (buf);
1038  };
1039 
1040 
1041  if (gnc_numeric_check (m_value))
1042  {
1043  if (!m_allow_zero)
1044  add_error (N_("Amount for %s is missing."), m_action);
1045  return;
1046  }
1047 
1048  if (gnc_numeric_negative_p (m_value) && !m_allow_negative && m_allow_zero)
1049  add_error (N_("Amount for %s must not be negative."), m_action);
1050 
1051  if (!m_allow_zero && !gnc_numeric_positive_p (m_value))
1052  add_error (N_("Amount for %s must be positive."), m_action);
1053 
1054  if (!gnc_numeric_zero_p(m_value) && !m_account && !m_capitalize)
1055  add_error(N_("The %s amount has no associated account."), m_action);
1056 }
1057 
1058 void
1059 StockTransactionFeesEntry::create_split(Transaction* trans, AccountVec& commits) const
1060 {
1061  g_return_if_fail(trans);
1062  if ((!m_account && !m_capitalize) || gnc_numeric_check(m_value))
1063  return;
1064  auto split = xaccMallocSplit(qof_instance_get_book(trans));
1065  xaccSplitSetParent(split, trans);
1066  if (m_capitalize)
1067  {
1068  xaccSplitSetAccount(split, commits[0]); // Should be the stock account
1069  }
1070  else
1071  {
1072  xaccAccountBeginEdit(m_account);
1073  commits.push_back(m_account);
1074  xaccSplitSetAccount(split, m_account);
1075  xaccSplitSetAmount(split, amount());
1076  }
1077  xaccSplitSetMemo(split, m_memo);
1078  xaccSplitSetValue(split, m_debit_side ? m_value : gnc_numeric_neg(m_value));
1079  PINFO("creating %s split in Acct(%s): Val(%s), Amt(%s) => Val(%s), Amt(%s)",
1080  m_action, m_account ? xaccAccountGetName (m_account) : "Empty!",
1081  gnc_num_dbg_to_string(m_value),
1082  gnc_num_dbg_to_string(amount()),
1085  gnc_set_num_action(nullptr, split, nullptr,
1086  g_dpgettext2(nullptr, "Stock Assistant: Action field",
1087  m_action));
1088 }
1089 
1090 using EntryVec = std::vector<StockTransactionEntry*>;
1091 
1092 static void stock_assistant_model_date_changed_cb(GtkWidget*, void*);
1093 static void stock_assistant_model_description_changed_cb(GtkWidget *, void *);
1094 
1102 {
1103  Account* m_acct;
1104  gnc_commodity* m_currency;
1105  time64 m_transaction_date;
1106  const char* m_transaction_description;
1107  std::optional<TxnTypeVec> m_txn_types;
1108 
1109  std::optional<TxnTypeInfo> m_txn_type;
1110 
1111  StockTransactionEntryPtr m_stock_entry;
1112  StockTransactionEntryPtr m_cash_entry;
1113  StockTransactionEntryPtr m_fees_entry;
1114  StockTransactionEntryPtr m_dividend_entry;
1115  StockTransactionEntryPtr m_capgains_entry;
1116  StockTransactionEntryPtr m_stock_cg_entry; // Required at this level for lifetime management
1117  Logger m_logger;
1118 
1119  std::optional<time64> m_txn_types_date;
1120  bool m_ready_to_create = false;
1121 
1122  EntryVec m_list_of_splits;
1123 
1124 public:
1125  StockAssistantModel(Account *account)
1126  : m_acct{account}, m_currency{gnc_account_get_currency_or_parent(
1127  account)},
1128  m_stock_entry{std::make_unique<StockTransactionStockEntry>(
1129  NC_("Stock Assistant: Page name", "Stock"))},
1130  m_cash_entry{std::make_unique<StockTransactionEntry>(
1131  NC_("Stock Assistant: Page name", "Cash"), PROCEEDS_KVP_TAG)},
1132  m_fees_entry{std::make_unique<StockTransactionFeesEntry>(
1133  NC_("Stock Assistant: Page name", "Fees"), FEES_KVP_TAG)},
1134  m_dividend_entry{std::make_unique<StockTransactionEntry>(
1135  NC_("Stock Assistant: Page name", "Dividend"), DIVIDEND_KVP_TAG)},
1136  m_capgains_entry{std::make_unique<StockTransactionEntry>(
1137  NC_("Stock Assistant: Page name", "Capital Gains"),
1138  CAPGAINS_KVP_TAG)} {
1139  DEBUG ("StockAssistantModel constructor\n");
1140  m_stock_entry->set_account(m_acct);
1141  };
1142 
1144  {
1145  DEBUG ("StockAssistantModel destructor\n");
1146  };
1147 
1154  bool maybe_reset_txn_types ();
1159  const std::optional<TxnTypeVec>& get_txn_types() { return m_txn_types; }
1165  bool set_txn_type (guint type_idx);
1170  bool txn_type_valid() { return m_txn_type.has_value(); }
1175  void set_transaction_date(time64 date) { m_transaction_date = date;}
1180  void set_transaction_desc(const char* desc) { m_transaction_description = desc; }
1185  const std::optional<TxnTypeInfo>& txn_type() { return m_txn_type; }
1190  std::string get_new_amount_str () const;
1195  StockTransactionEntry* stock_entry() { return m_stock_entry.get(); }
1200  StockTransactionEntry* cash_entry() { return m_cash_entry.get(); }
1205  StockTransactionEntry* fees_entry() { return m_fees_entry.get(); }
1210  StockTransactionEntry* dividend_entry() { return m_dividend_entry.get(); }
1215  StockTransactionEntry* capgains_entry() { return m_capgains_entry.get(); }
1220  Logger& logger() { return m_logger; }
1231  std::tuple<bool, std::string, EntryVec> generate_list_of_splits ();
1237  std::tuple<bool, Transaction*> create_transaction ();
1238  Account* account() { return m_acct; }
1239 private:
1243  void add_price (QofBook *book);
1244 };
1245 
1246 bool
1248 {
1249  auto old_bal = m_stock_entry->get_balance();
1250  auto new_bal = xaccAccountGetBalanceAsOfDate
1251  (m_acct, gnc_time64_get_day_end (m_transaction_date));
1252  if (m_txn_types_date && m_txn_types_date == m_transaction_date &&
1253  gnc_numeric_equal (old_bal, new_bal))
1254  return false;
1255  m_stock_entry->set_balance(new_bal);
1256  m_txn_types_date = m_transaction_date;
1257  m_txn_types = gnc_numeric_zero_p (new_bal) ? starting_types
1258  : gnc_numeric_positive_p (new_bal) ? long_types
1259  : short_types;
1260  return true;
1261 };
1262 
1263 bool
1265 {
1266  if (!m_txn_types_date || m_txn_types_date != m_transaction_date)
1267  {
1268  PERR ("transaction_date has changed. rerun maybe_reset_txn_types!");
1269  return false;
1270  }
1271  try
1272  {
1273  m_txn_type = m_txn_types->at (type_idx);
1274  }
1275  catch (const std::out_of_range&)
1276  {
1277  PERR ("out of range type_idx=%d", type_idx);
1278  return false;
1279  }
1280 
1281  m_stock_entry->set_fieldmask(m_txn_type->stock_amount);
1282  m_fees_entry->set_fieldmask(m_txn_type->fees_value);
1283  m_capgains_entry->set_fieldmask(m_txn_type->capgains_value);
1284  m_dividend_entry->set_fieldmask(m_txn_type->dividend_value);
1285  m_cash_entry->set_fieldmask(m_txn_type->cash_value);
1286  return true;
1287 };
1288 
1289 static void
1290 check_txn_date(const Split* last_split, time64 txn_date, Logger& logger)
1291 {
1292  auto last_split_date = xaccTransGetDate(xaccSplitGetParent(last_split));
1293  if (txn_date <= last_split_date) {
1294  auto last_split_date_str = qof_print_date(last_split_date);
1295  auto new_date_str = qof_print_date(txn_date);
1296  // Translators: the first %s is the new transaction date;
1297  // the second %s is the current stock account's latest
1298  // transaction date.
1299  auto warn_txt = g_strdup_printf(
1300  _("You will enter a transaction "
1301  "with date %s which is earlier than the latest transaction in this account, "
1302  "dated %s. Doing so may affect the cost basis, and therefore capital gains, "
1303  "of transactions dated after the new entry. Please review all transactions "
1304  "to ensure proper recording."),
1305  new_date_str, last_split_date_str);
1306  logger.warn(warn_txt);
1307  g_free(warn_txt);
1308  g_free(new_date_str);
1309  g_free(last_split_date_str);
1310  }
1311 }
1312 
1313 std::tuple<bool, std::string, EntryVec>
1315  if (!m_txn_types || !m_txn_type)
1316  return { false, "Error: txn_type not initialized", {} };
1317 
1318  m_logger.clear();
1319  m_list_of_splits.clear();
1320 
1321  GncNumeric debit{};
1322  GncNumeric credit{};
1323 
1324  // check the stock transaction date. If there are existing stock
1325  // transactions dated after the date specified, it is very likely
1326  // the later stock transactions will be invalidated. warn the user
1327  // to review them.
1328  if (const auto& splits = xaccAccountGetSplits (m_acct); !splits.empty())
1329  check_txn_date(splits.back(), m_transaction_date, m_logger);
1330 
1331  if (m_stock_entry->enabled() || m_stock_entry->has_amount())
1332  {
1333  m_stock_entry->validate_amount(m_logger);
1334  m_list_of_splits.push_back(m_stock_entry.get());
1335 
1336  auto price{m_stock_entry->calculate_price()};
1337  if (!gnc_numeric_check(price))
1338  {
1339  // Translators: %s refer to: stock mnemonic, broker currency,
1340  // date of transaction.
1341  auto tmpl = N_("A price of 1 %s = %s on %s will be recorded.");
1342  auto date_str = qof_print_date (m_transaction_date);
1343  auto price_msg = g_strdup_printf
1344  (_(tmpl),
1346  m_stock_entry->print_price(), date_str);
1347  m_logger.info(price_msg);
1348  g_free (date_str);
1349  g_free (price_msg);
1350  }
1351  }
1352 
1353  if (m_stock_entry->marker_split())
1354  m_list_of_splits.push_back(m_stock_entry.get());
1355 
1356  if (m_cash_entry->enabled())
1357  {
1358  m_cash_entry->validate_amount(m_logger);
1359  m_list_of_splits.push_back (m_cash_entry.get());
1360  }
1361 
1362  if (m_fees_entry->enabled())
1363  {
1364  m_fees_entry->validate_amount(m_logger);
1365  if (m_fees_entry->do_capitalize())
1366  m_fees_entry->set_account(m_acct);
1367  m_list_of_splits.push_back (m_fees_entry.get());
1368  }
1369 
1370  if (m_dividend_entry->enabled())
1371  {
1372  m_dividend_entry->validate_amount(m_logger);
1373  m_list_of_splits.push_back (m_dividend_entry.get());
1374  }
1375 
1376  if (m_capgains_entry->enabled())
1377  {
1378  m_stock_cg_entry =
1379  std::make_unique<StockTransactionStockCapGainsEntry>(m_capgains_entry.get(),
1380  m_stock_entry.get());
1381  m_stock_cg_entry->validate_amount(m_logger);
1382  m_capgains_entry->validate_amount(m_logger);
1383  m_list_of_splits.push_back(m_stock_cg_entry.get());
1384  m_list_of_splits.push_back (m_capgains_entry.get());
1385  }
1386 
1387  std::for_each(m_list_of_splits.begin(), m_list_of_splits.end(),
1388  [&debit, &credit](auto& entry) {
1389  if (entry->debit_side())
1390  debit += entry->value();
1391  else
1392  credit += entry->value();
1393  });
1394 
1395  if (gnc_numeric_check(debit) || gnc_numeric_check(credit) ||!gnc_numeric_equal (debit, credit))
1396  {
1397  const char *err_act = NULL, *err_reason = NULL;
1398  if (gnc_numeric_check(debit))
1399  {
1400  err_act = "debit";
1402  }
1403  else if (gnc_numeric_check(credit))
1404  {
1405  err_act = "credit";
1406  err_reason = gnc_numeric_errorCode_to_string(gnc_numeric_check(credit));
1407  }
1408 
1409  if (err_act)
1410  {
1411  auto err_str = g_strdup_printf (N_("Transaction can't balance, %s is error value %s"), err_act, err_reason);
1412  m_logger.error(err_str);
1413  g_free (err_str);
1414  }
1415  else
1416  {
1417  auto imbalance_str = N_("Total Debits of %s does not balance with total Credits of %s.");
1418  auto pinfo{gnc_commodity_print_info (m_currency, true)};
1419  auto debit_str = g_strdup (xaccPrintAmount (debit, pinfo));
1420  auto credit_str = g_strdup (xaccPrintAmount (credit, pinfo));
1421  auto error_str = g_strdup_printf (_(imbalance_str), debit_str, credit_str);
1422  m_logger.error (error_str);
1423  g_free (error_str);
1424  g_free (credit_str);
1425  g_free (debit_str);
1426  }
1427  }
1428 
1429  // generate final summary message. Collates a header, the errors
1430  // and warnings. Then allow completion if errors is empty.
1431  m_ready_to_create = !m_logger.has_errors();
1432  return { m_ready_to_create, m_logger.report(), m_list_of_splits };
1433 }
1434 
1435 std::tuple<bool, Transaction*>
1437 {
1438  if (!m_ready_to_create)
1439  {
1440  PERR ("errors exist. cannot create transaction.");
1441  m_list_of_splits.clear();
1442  return {false, nullptr};
1443  }
1444  auto book = qof_instance_get_book (m_acct);
1445  auto trans = xaccMallocTransaction (book);
1446  xaccTransBeginEdit (trans);
1447  xaccTransSetCurrency (trans, m_currency);
1448  xaccTransSetDescription (trans, m_transaction_description);
1449  xaccTransSetDatePostedSecsNormalized (trans, m_transaction_date);
1450  AccountVec accounts;
1451  std::for_each (m_list_of_splits.begin(), m_list_of_splits.end(),
1452  [&](auto& entry)
1453  {
1454  entry->create_split (trans, accounts);
1455  if (entry->get_kvp_tag() && entry->account())
1456  xaccAccountSetAssociatedAccount (m_acct, entry->get_kvp_tag(), entry->account());
1457  });
1458  add_price (book);
1459  xaccTransCommitEdit (trans);
1460  std::for_each (accounts.begin(), accounts.end(), xaccAccountCommitEdit);
1461  m_list_of_splits.clear();
1462  m_ready_to_create = false;
1463  return {true, trans};
1464 }
1465 
1466 void
1467 StockAssistantModel::add_price (QofBook *book)
1468 {
1469  auto stock_price{m_stock_entry->calculate_price()};
1470  if (gnc_numeric_check(stock_price))
1471  return;
1472 
1473  auto price = gnc_price_create (book);
1474  gnc_price_begin_edit (price);
1475  gnc_price_set_commodity (price, xaccAccountGetCommodity (m_acct));
1476  gnc_price_set_currency (price, m_currency);
1477  gnc_price_set_time64 (price, m_transaction_date);
1478  gnc_price_set_source (price, PRICE_SOURCE_STOCK_TRANSACTION);
1479  gnc_price_set_typestr (price, PRICE_TYPE_UNK);
1480  gnc_price_set_value (price, stock_price);
1481  gnc_price_commit_edit (price);
1482 
1483  auto pdb = gnc_pricedb_get_db (book);
1484  if (!gnc_pricedb_add_price (pdb, price))
1485  PWARN ("error adding price");
1486 
1487  gnc_price_unref (price);
1488 }
1489 
1490 static void
1491 stock_assistant_model_date_changed_cb(GtkWidget* widget, void* data)
1492 {
1493  auto model{static_cast<StockAssistantModel*>(data)};
1494  model->set_transaction_date(gnc_date_edit_get_date_end(GNC_DATE_EDIT(widget)));
1495 }
1496 
1497 static void
1498 stock_assistant_model_description_changed_cb(GtkWidget* widget, void* data)
1499 {
1500  auto model{static_cast<StockAssistantModel*>(data)};
1501  model->set_transaction_desc(gtk_entry_get_text(GTK_ENTRY(widget)));
1502 }
1503 
1504 /* ********************* View Classes ************************/
1505 
1506 /* ***************** Generic Event Callbacks ****************/
1507 static void
1508 text_entry_changed_cb (GtkWidget *widget, StockTransactionEntry* entry)
1509 {
1510  entry->set_memo(gtk_entry_get_text (GTK_ENTRY (widget)));
1511 }
1512 
1513 
1514 static inline GtkWidget*
1515 get_widget (GtkBuilder *builder, const gchar * ID)
1516 {
1517  g_return_val_if_fail (builder && ID, nullptr);
1518  auto obj = gtk_builder_get_object (builder, ID);
1519  if (!obj)
1520  PWARN ("get_widget ID '%s' not found. it may be a typo?", ID);
1521  return GTK_WIDGET (obj);
1522 }
1523 
1529 {
1530  GtkWidget *m_edit;
1531 public:
1532  GncDateEdit(GtkBuilder *builder) :
1533  m_edit{gnc_date_edit_new(gnc_time(nullptr), FALSE, FALSE)} {}
1534  void attach(GtkBuilder *builder, const char *table_ID, const char *label_ID,
1535  int row);
1536  time64 get_date_time() { return gnc_date_edit_get_date_end(GNC_DATE_EDIT(m_edit)); }
1537  void connect(GCallback, gpointer);
1538 };
1539 
1540 void
1541 GncDateEdit::attach(GtkBuilder *builder, const char *table_ID,
1542  const char *label_ID, int row)
1543 {
1544  auto table = get_widget(builder, table_ID);
1545  auto label = get_widget (builder, label_ID);
1546  gtk_grid_attach(GTK_GRID(table), m_edit, 1, row, 1, 1);
1547  gtk_widget_show(m_edit);
1548  gnc_date_make_mnemonic_target (GNC_DATE_EDIT(m_edit), label);
1549 }
1550 
1551 void
1552 GncDateEdit::connect(GCallback cb, gpointer data)
1553 {
1554  g_signal_connect(m_edit, "date_changed", cb, data);
1555 }
1556 
1561 {
1562  GtkWidget *m_edit;
1563 public:
1564  GncAmountEdit (GtkBuilder *builder, gnc_commodity *commodity);
1565  void attach (GtkBuilder *builder, const char *table_id,
1566  const char *label_ID, int row);
1567  GtkWidget* widget() {
1568  return gnc_amount_edit_gtk_entry(GNC_AMOUNT_EDIT(m_edit));
1569  }
1570  gnc_numeric get ();
1571  void connect (GCallback cb, gpointer data);
1572  void set_owner (gpointer obj);
1573 };
1574 
1575 static void
1576 value_changed_cb (GtkWidget* widget, StockTransactionEntry* entry)
1577 {
1578  g_return_if_fail(GNC_IS_AMOUNT_EDIT(widget));
1579  gnc_numeric value;
1580  auto invalid{gnc_amount_edit_expr_is_valid(GNC_AMOUNT_EDIT(widget),
1581  &value, true, nullptr)};
1582  entry->set_value(invalid ? gnc_numeric_error(GNC_ERROR_ARG) : value);
1583 }
1584 
1585 GncAmountEdit::GncAmountEdit (GtkBuilder *builder, gnc_commodity *commodity) :
1586  m_edit{gnc_amount_edit_new()}
1587 {
1588  // shares amount
1589  auto info = gnc_commodity_print_info(commodity, true);
1590  gnc_amount_edit_set_evaluate_on_enter(GNC_AMOUNT_EDIT(m_edit), TRUE);
1591  gnc_amount_edit_set_print_info(GNC_AMOUNT_EDIT(m_edit), info);
1592 }
1593 
1594 void
1595 GncAmountEdit::attach (GtkBuilder *builder, const char *table_ID,
1596  const char* label_ID, int row)
1597 {
1598  auto table = get_widget(builder, table_ID);
1599  auto label = get_widget(builder, label_ID);
1600  gtk_grid_attach(GTK_GRID(table), m_edit, 1, row, 1, 1);
1601  gtk_widget_show(m_edit);
1602  gnc_amount_edit_make_mnemonic_target(GNC_AMOUNT_EDIT(m_edit), label);
1603 }
1604 
1605 gnc_numeric
1606 GncAmountEdit::get ()
1607 {
1608  gnc_numeric amt;
1609  if (!gnc_amount_edit_expr_is_valid (GNC_AMOUNT_EDIT(m_edit), &amt, true, nullptr))
1610  return amt;
1612 }
1613 
1614 void
1615 GncAmountEdit::connect (GCallback cb, gpointer data)
1616 {
1617  g_signal_connect(m_edit, "changed", cb, data);
1618 }
1619 
1620 void
1621 GncAmountEdit::set_owner(gpointer obj)
1622 {
1623  g_object_set_data(G_OBJECT (m_edit), "owner", obj);
1624 }
1625 
1626 using AccountTypeList = std::vector<GNCAccountType>;
1627 
1632 {
1633  GtkWidget* m_selector;
1634 public:
1635  GncAccountSelector (GtkBuilder *builder, AccountTypeList types,
1636  gnc_commodity *currency, Account *default_acct);
1637  void attach (GtkBuilder *builder, const char *table_id,
1638  const char *label_ID, int row);
1639  void connect (StockTransactionEntry*);
1640  void set (Account *acct) { gnc_account_sel_set_account (GNC_ACCOUNT_SEL (m_selector), acct, TRUE); }
1641  void set_sensitive(bool sensitive);
1642  Account *get () { return gnc_account_sel_get_account (GNC_ACCOUNT_SEL (m_selector)); }
1643 };
1644 
1645 static void
1646 gnc_account_sel_changed_cb (GtkWidget* widget, StockTransactionEntry* entry)
1647 {
1648  g_return_if_fail (GNC_IS_ACCOUNT_SEL (widget));
1649  entry->set_account(gnc_account_sel_get_account (GNC_ACCOUNT_SEL (widget)));
1650 }
1651 
1652 GncAccountSelector::GncAccountSelector (GtkBuilder *builder, AccountTypeList types,
1653  gnc_commodity *currency, Account *default_acct) :
1654  m_selector{gnc_account_sel_new ()}
1655 {
1656  auto accum = [](auto a, auto b) { return g_list_prepend(a, (gpointer)b); };
1657  auto null_glist = static_cast<GList *>(nullptr);
1658  auto acct_list = std::accumulate(types.begin(), types.end(), null_glist, accum);
1659  auto curr_list = accum(null_glist, currency);
1660  gnc_account_sel_set_new_account_ability(GNC_ACCOUNT_SEL(m_selector), true);
1661  gnc_account_sel_set_acct_filters(GNC_ACCOUNT_SEL(m_selector), acct_list, curr_list);
1662  gnc_account_sel_set_default_new_commodity(GNC_ACCOUNT_SEL(m_selector), currency);
1663  gnc_account_sel_set_new_account_modal (GNC_ACCOUNT_SEL(m_selector), true);
1664  if (default_acct)
1665  gnc_account_sel_set_account (GNC_ACCOUNT_SEL(m_selector), default_acct, true);
1666  g_list_free(acct_list);
1667  g_list_free(curr_list);
1668 }
1669 
1670 void
1671 GncAccountSelector::attach (GtkBuilder *builder, const char *table_ID,
1672  const char *label_ID, int row)
1673 {
1674  auto table = get_widget(builder, table_ID);
1675  auto label = get_widget(builder, label_ID);
1676  gtk_grid_attach(GTK_GRID(table), m_selector, 1, row, 1, 1);
1677  gtk_widget_show(m_selector);
1678  gtk_label_set_mnemonic_widget(GTK_LABEL(label), m_selector);
1679 }
1680 
1681 void
1682 GncAccountSelector::connect (StockTransactionEntry* entry)
1683 {
1684  g_signal_connect(m_selector, "account_sel_changed", G_CALLBACK (gnc_account_sel_changed_cb), entry);
1685 }
1686 
1687 void
1688 GncAccountSelector::set_sensitive(bool sensitive)
1689 {
1690  gtk_widget_set_sensitive(m_selector, sensitive);
1691 }
1692 
1693 
1705 static void
1706 assistant_page_set_focus(GtkWidget* page, [[maybe_unused]]GtkDirectionType type, GtkWidget* entry)
1707 {
1708  gtk_widget_grab_focus(entry);
1709  g_signal_handlers_disconnect_by_data(page, entry);
1710 }
1742  // transaction type page
1743  GtkWidget * m_page;
1744  GtkWidget * m_type;
1745  GtkWidget * m_explanation;
1746 public:
1747  PageTransType(GtkBuilder *builder);
1748  void prepare(StockAssistantModel* model);
1749  int get_transaction_type_index ();
1750  void set_transaction_types (const TxnTypeVec& txn_types);
1757  void set_txn_type_explanation (const gchar *txt);
1758  void connect (StockAssistantModel *model);
1759  void change_txn_type (StockAssistantModel *model);
1760 };
1761 
1762 PageTransType::PageTransType(GtkBuilder *builder)
1763  : m_page(get_widget(builder, "transaction_type_page")),
1764  m_type(get_widget(builder, "transaction_type_page_combobox")),
1765  m_explanation(get_widget(builder, "transaction_type_page_explanation"))
1766 {
1767  g_object_set_data(G_OBJECT(m_type), "owner", this);
1768 }
1769 
1770 static void
1771 page_trans_type_changed_cb (GtkWidget* widget, StockAssistantModel *model)
1772 {
1773  auto me = static_cast<PageTransType *>(g_object_get_data (G_OBJECT (widget), "owner"));
1774  g_return_if_fail (me);
1775  me->change_txn_type (model);
1776 }
1777 
1778 void
1779 PageTransType::prepare(StockAssistantModel *model)
1780 {
1781  const auto& txn_types{model->get_txn_types()};
1782  if (!txn_types)
1783  return;
1784 
1785  set_transaction_types(txn_types.value());
1786  change_txn_type (model);
1787  g_signal_connect(m_page, "focus", G_CALLBACK(assistant_page_set_focus), m_type);
1788 }
1789 
1790 int
1791 PageTransType::get_transaction_type_index ()
1792 {
1793  return gtk_combo_box_get_active (GTK_COMBO_BOX (m_type));
1794 }
1795 
1796 void
1797 PageTransType::set_transaction_types (const TxnTypeVec& txn_types)
1798 {
1799  auto combo = GTK_COMBO_BOX_TEXT (m_type);
1800  gtk_combo_box_text_remove_all (combo);
1801  std::for_each (txn_types.begin(), txn_types.end(),
1802  [&combo](const auto& it)
1803  { gtk_combo_box_text_append_text (combo, _(it.friendly_name)); });
1804  gtk_combo_box_set_active (GTK_COMBO_BOX (combo), 0);
1805 }
1806 
1807 void
1809 {
1810  gtk_label_set_text (GTK_LABEL (this->m_explanation), txt);
1811 }
1812 
1813 void
1814 PageTransType::change_txn_type (StockAssistantModel *model)
1815 {
1816  auto type_idx = get_transaction_type_index();
1817  if (type_idx < 0) // combo isn't initialized yet.
1818  return;
1819 
1820  if (!model->set_txn_type(type_idx))
1821  return;
1822  auto txn_type{model->txn_type()};
1823  set_txn_type_explanation (_(txn_type->explanation));
1824 }
1825 
1826 void
1827 PageTransType::connect(StockAssistantModel *model)
1828 {
1829  g_signal_connect(m_type, "changed",
1830  G_CALLBACK (page_trans_type_changed_cb), model);
1831 }
1832 
1839 {
1840  // transaction details page
1841  GtkWidget *m_page;
1842  GncDateEdit m_date;
1843  GtkWidget *m_description;
1844 public:
1845  PageTransDeets (GtkBuilder *builder);
1846  time64 get_date_time () { return m_date.get_date_time(); }
1847  const char* get_description () { return gtk_entry_get_text (GTK_ENTRY (m_description)); }
1848  void connect (StockAssistantModel*);
1849  void prepare(StockAssistantModel*);
1850 };
1851 
1852 PageTransDeets::PageTransDeets (GtkBuilder *builder) :
1853  m_page (get_widget (builder, "transaction_details_page")),
1854  m_date (builder),
1855  m_description (get_widget (builder, "transaction_description_entry"))
1856 {
1857  m_date.attach(builder, "transaction_details_table", "transaction_date_label", 0);
1858 }
1859 
1860 void
1861 PageTransDeets::connect(StockAssistantModel* model)
1862 {
1863  m_date.connect(G_CALLBACK (stock_assistant_model_date_changed_cb),
1864  static_cast<void*>(model));
1865  g_signal_connect(m_description, "changed",
1866  G_CALLBACK (stock_assistant_model_description_changed_cb),
1867  static_cast<void*>(model));
1868 }
1869 
1870 void
1871 PageTransDeets::prepare(StockAssistantModel* model)
1872 {
1873  model->set_transaction_date(get_date_time());
1874  model->set_transaction_desc(get_description());
1875  g_signal_connect(m_page, "focus", G_CALLBACK(assistant_page_set_focus), m_description);
1876 }
1877 
1888 {
1889  // stock amount page
1890  GtkWidget * m_page;
1891  GtkWidget * m_title;
1892  GtkWidget * m_prev_amount;
1893  GtkWidget * m_next_amount;
1894  GtkWidget * m_next_amount_label;
1895  GncAmountEdit m_amount;
1896  GtkWidget * m_amount_label;
1897 public:
1898  PageStockAmount (GtkBuilder *builder, Account* account);
1899  void prepare (StockTransactionEntry*);
1900  gnc_numeric get_stock_amount () { return m_amount.get(); }
1901  void set_stock_amount (std::string new_amount_str);
1902  void connect(StockTransactionEntry* entry);
1903 };
1904 
1905 PageStockAmount::PageStockAmount (GtkBuilder *builder, Account* account) :
1906  m_page (get_widget (builder, "stock_amount_page")),
1907  m_title (get_widget (builder, "stock_amount_title")),
1908  m_prev_amount (get_widget (builder, "prev_balance_amount")),
1909  m_next_amount (get_widget (builder, "next_balance_amount")),
1910  m_next_amount_label (get_widget (builder, "next_balance_label")),
1911  m_amount (builder, xaccAccountGetCommodity(account)),
1912  m_amount_label (get_widget (builder, "stock_amount_label"))
1913 {
1914  m_amount.attach (builder, "stock_amount_table", "stock_amount_label", 1);
1915 }
1916 
1917 void
1918 PageStockAmount::prepare (StockTransactionEntry* entry)
1919 {
1920  gtk_label_set_text_with_mnemonic
1921  (GTK_LABEL (m_amount_label),
1922  entry->input_new_balance() ? _("Ne_w Balance") : _("_Shares"));
1923  gtk_label_set_text
1924  (GTK_LABEL (m_next_amount_label),
1925  entry->input_new_balance() ? _("Ratio") : _("Next Balance"));
1926  gtk_label_set_text
1927  (GTK_LABEL (m_title),
1928  entry->input_new_balance() ?
1929  _("Enter the new balance of shares after the stock split.") :
1930  _("Enter the number of shares you gained or lost in the transaction."));
1931  gtk_label_set_text (GTK_LABEL (m_prev_amount), entry->print_amount(entry->get_balance()));
1932  if (!gnc_numeric_check(get_stock_amount()))
1933  entry->set_amount(get_stock_amount());
1934  set_stock_amount(entry->amount_str_for_display());
1935  g_signal_connect(m_page, "focus", G_CALLBACK(assistant_page_set_focus), m_amount.widget());
1936 }
1937 
1938 static void
1939 page_stock_amount_changed_cb(GtkWidget *widget, StockTransactionEntry* entry)
1940 {
1941  auto me = static_cast<PageStockAmount*>(g_object_get_data (G_OBJECT (widget), "owner"));
1942  entry->set_amount(me->get_stock_amount());
1943  me->set_stock_amount(entry->amount_str_for_display());
1944 }
1945 
1946 void
1947 PageStockAmount::connect(StockTransactionEntry* entry)
1948 {
1949  m_amount.connect(G_CALLBACK (page_stock_amount_changed_cb), entry);
1950  m_amount.set_owner(static_cast<gpointer>(this));
1951 }
1952 
1953 void
1954 PageStockAmount::set_stock_amount (std::string new_amount_str)
1955 {
1956  gtk_label_set_text (GTK_LABEL(m_next_amount), new_amount_str.c_str());
1957 }
1958 
1964 {
1965  // stock value page
1966  GtkWidget * m_page;
1967  GncAmountEdit m_value;
1968  GtkWidget * m_price;
1969  GtkWidget * m_memo;
1970 public:
1971  PageStockValue (GtkBuilder *builder, Account* account);
1972  const char* get_memo ();
1973  void connect(StockTransactionEntry* entry);
1974  void prepare(StockTransactionEntry* entry);
1975  GncAmountEdit& value_edit() { return m_value; }
1976  void set_price(const gchar *val);
1977 };
1978 
1979 static void
1980 page_stock_value_changed_cb(GtkWidget *widget, StockTransactionEntry* entry)
1981 {
1982  auto me = static_cast<PageStockValue*>(g_object_get_data (G_OBJECT (widget), "owner"));
1983  entry->set_value (me->value_edit().get());
1984  me->set_price(entry->print_price());
1985 }
1986 
1987 PageStockValue::PageStockValue(GtkBuilder *builder, Account* account)
1988  : m_page(get_widget(builder, "stock_value_page")),
1989  m_value(builder, gnc_account_get_currency_or_parent(account)),
1990  m_price(get_widget(builder, "stock_price_amount")),
1991  m_memo(get_widget(builder, "stock_memo_entry"))
1992 {
1993  m_value.attach(builder, "stock_value_table", "stock_value_label", 0);
1994 }
1995 
1996 void
1997 PageStockValue::connect(StockTransactionEntry* entry)
1998 {
1999  m_value.connect(G_CALLBACK (page_stock_value_changed_cb), entry);
2000  m_value.set_owner (static_cast<gpointer>(this));
2001  g_signal_connect (m_memo, "changed", G_CALLBACK(text_entry_changed_cb), entry);
2002 }
2003 
2004 void
2005 PageStockValue::prepare(StockTransactionEntry* entry)
2006 {
2007  entry->set_memo(get_memo());
2008  if (!gnc_numeric_check(m_value.get()))
2009  entry->set_value(m_value.get());
2010  set_price(entry->print_price());
2011  g_signal_connect(m_page, "focus", G_CALLBACK(assistant_page_set_focus), m_value.widget());
2012 }
2013 
2014 const char *
2015 PageStockValue::get_memo()
2016 {
2017  return gtk_entry_get_text(GTK_ENTRY (m_memo));
2018 }
2019 
2020 void
2021 PageStockValue::set_price (const gchar *val)
2022 {
2023  gtk_label_set_text(GTK_LABEL(this->m_price), val);
2024 };
2025 
2032 {
2033  // cash page
2034  GtkWidget * m_page;
2035  GncAccountSelector m_account;
2036  GtkWidget * m_memo;
2037  GncAmountEdit m_value;
2038 public:
2039  PageCash (GtkBuilder *builder, Account* account);
2040  void connect(StockTransactionEntry* entry);
2041  void prepare(StockTransactionEntry* entry);
2042  const char* get_memo();
2043 };
2044 
2045 PageCash::PageCash(GtkBuilder *builder, Account* account)
2046  : m_page(get_widget(builder, "cash_details_page")),
2047  m_account(builder, {ACCT_TYPE_ASSET, ACCT_TYPE_BANK},
2049  xaccAccountGetAssociatedAccount (account, PROCEEDS_KVP_TAG)),
2050  m_memo(get_widget(builder, "cash_memo_entry")),
2051  m_value(builder, gnc_account_get_currency_or_parent(account))
2052 {
2053  m_account.attach (builder, "cash_table", "cash_account_label", 0);
2054  m_value.attach (builder, "cash_table", "cash_label", 1);
2055 }
2056 
2057 void
2058 PageCash::connect(StockTransactionEntry* entry)
2059 {
2060  m_account.connect(entry);
2061  g_signal_connect(m_memo, "changed", G_CALLBACK(text_entry_changed_cb), entry);
2062  m_value.connect(G_CALLBACK(value_changed_cb), entry);
2063 }
2064 
2065 void
2066 PageCash::prepare(StockTransactionEntry* entry)
2067 {
2068  entry->set_memo(get_memo());
2069  if (!gnc_numeric_check(m_value.get()))
2070  entry->set_value(m_value.get());
2071  entry->set_account(m_account.get());
2072  g_signal_connect(m_page, "focus", G_CALLBACK(assistant_page_set_focus), m_value.widget());
2073 }
2074 
2075 const char *
2076 PageCash::get_memo()
2077 {
2078  return gtk_entry_get_text(GTK_ENTRY (m_memo));
2079 }
2080 
2087 {
2088  // fees page
2089  GtkWidget * m_page;
2090  GtkWidget * m_capitalize;
2091  GncAccountSelector m_account;
2092  GtkWidget * m_memo;
2093  GncAmountEdit m_value;
2094  Account* m_stock_account;
2095 public:
2096  PageFees (GtkBuilder *builder, Account* account);
2097  void connect(StockTransactionEntry*);
2098  bool get_capitalize_fees ();
2099  const char* get_memo();
2100  void set_capitalize_fees (bool state);
2101  void set_account (Account *acct) { m_account.set(acct); }
2102  Account* stock_account() { return m_stock_account; }
2103  void update_fees_acct_sensitive (bool sensitive);
2104  void prepare(StockTransactionEntry*);
2105 };
2106 
2107 PageFees::PageFees(GtkBuilder *builder, Account* account)
2108  : m_page(get_widget(builder, "fees_details_page")),
2109  m_capitalize(
2110  get_widget(builder, "capitalize_fees_checkbutton")),
2111  m_account(builder, {ACCT_TYPE_EXPENSE}, gnc_account_get_currency_or_parent(account),
2112  xaccAccountGetAssociatedAccount (account, FEES_KVP_TAG)),
2113  m_memo(get_widget(builder, "fees_memo_entry")),
2114  m_value(builder, gnc_account_get_currency_or_parent(account)),
2115  m_stock_account(account)
2116 {
2117  m_account.attach (builder, "fees_table", "fees_account_label", 1);
2118  m_value.attach(builder, "fees_table", "fees_label", 2);
2119 }
2120 
2121 bool
2122 PageFees::get_capitalize_fees()
2123 {
2124  return gtk_toggle_button_get_active(
2125  GTK_TOGGLE_BUTTON(m_capitalize));
2126 }
2127 
2128 const char *
2129 PageFees::get_memo()
2130 {
2131  return gtk_entry_get_text(GTK_ENTRY (m_memo));
2132 }
2133 
2134 void
2135 PageFees::set_capitalize_fees(bool state)
2136 {
2137  gtk_toggle_button_set_active(
2138  GTK_TOGGLE_BUTTON(m_capitalize), state);
2139 }
2140 
2141 void
2142 PageFees::update_fees_acct_sensitive(bool sensitive)
2143 {
2144  m_account.set_sensitive(sensitive);
2145 }
2146 
2147 static void
2148 capitalize_fees_toggled_cb (GtkWidget *widget, StockTransactionEntry *entry)
2149 {
2150  g_return_if_fail (entry);
2151  auto me = static_cast<PageFees *>(g_object_get_data (G_OBJECT (widget), "owner"));
2152  g_return_if_fail (me);
2153  bool cap = me->get_capitalize_fees();
2154  entry->set_capitalize(cap);
2155  if (cap)
2156  entry->set_account(me->stock_account());
2157  me->update_fees_acct_sensitive(!cap);
2158 }
2159 
2160 void
2161 PageFees::connect(StockTransactionEntry* entry)
2162 {
2163  m_account.connect(entry);
2164  g_signal_connect(m_memo, "changed", G_CALLBACK(text_entry_changed_cb), entry);
2165  m_value.connect(G_CALLBACK(value_changed_cb), entry);
2166  g_object_set_data(G_OBJECT (m_capitalize), "owner", this);
2167  g_signal_connect (m_capitalize, "toggled", G_CALLBACK (capitalize_fees_toggled_cb), entry);
2168 }
2169 
2170 void
2171 PageFees::prepare(StockTransactionEntry* entry)
2172 {
2173  set_capitalize_fees (entry->do_capitalize());
2174  entry->set_memo(get_memo());
2175  if (!gnc_numeric_check(m_value.get()))
2176  entry->set_value (m_value.get());
2177  entry->set_account(m_account.get());
2178  g_signal_connect(m_page, "focus", G_CALLBACK(assistant_page_set_focus), m_value.widget());
2179 }
2180 
2184 {
2185  // dividend page
2186  GtkWidget *m_page;
2187  GncAccountSelector m_account;
2188  GtkWidget *m_memo;
2189  GncAmountEdit m_value;
2190 public:
2191  PageDividend (GtkBuilder *builder, Account* account);
2192  void connect(StockTransactionEntry*);
2193  void prepare(StockTransactionEntry*);
2194  const char* get_memo();
2195 };
2196 
2197 PageDividend::PageDividend(GtkBuilder *builder, Account* account)
2198  : m_page(get_widget(builder, "dividend_details_page")),
2199  m_account(builder, {ACCT_TYPE_INCOME}, gnc_account_get_currency_or_parent(account),
2200  xaccAccountGetAssociatedAccount (account, DIVIDEND_KVP_TAG)),
2201  m_memo(get_widget(builder, "dividend_memo_entry")),
2202  m_value(builder, gnc_account_get_currency_or_parent(account))
2203 {
2204  m_account.attach(builder, "dividend_table", "dividend_account_label", 0);
2205  m_value.attach(builder, "dividend_table", "dividend_label", 1);
2206 }
2207 
2208 
2209 void
2210 PageDividend::connect(StockTransactionEntry* entry)
2211 {
2212  m_account.connect(entry);
2213  g_signal_connect(m_memo, "changed", G_CALLBACK(text_entry_changed_cb), entry);
2214  m_value.connect(G_CALLBACK(value_changed_cb), entry);
2215 }
2216 
2217 void
2218 PageDividend::prepare(StockTransactionEntry* entry)
2219 {
2220  entry->set_memo(get_memo());
2221  if (!gnc_numeric_check(m_value.get()))
2222  entry->set_value(m_value.get());
2223  entry->set_account(m_account.get());
2224  g_signal_connect(m_page, "focus", G_CALLBACK(assistant_page_set_focus), m_value.widget());
2225 }
2226 
2227 const char *
2228 PageDividend::get_memo()
2229 {
2230  return gtk_entry_get_text(GTK_ENTRY (m_memo));
2231 }
2232 
2234 {
2235  // capgains page
2236  GtkWidget * m_page;
2237  GncAccountSelector m_account;
2238  GtkWidget * m_memo;
2239  GncAmountEdit m_value;
2240 public:
2241  PageCapGain (GtkBuilder *builder, Account* account);
2242  void connect(StockTransactionEntry* entry);
2243  void prepare(StockTransactionEntry* entry);
2244  const char* get_memo();
2245 };
2246 
2247 PageCapGain::PageCapGain (GtkBuilder *builder, Account* account) :
2248  m_page (get_widget (builder, "capgains_details_page")),
2249  m_account (builder, { ACCT_TYPE_INCOME }, gnc_account_get_currency_or_parent(account),
2250  xaccAccountGetAssociatedAccount (account, CAPGAINS_KVP_TAG)),
2251  m_memo (get_widget (builder, "capgains_memo_entry")),
2252  m_value (builder, gnc_account_get_currency_or_parent(account))
2253 {
2254  m_account.attach(builder, "capgains_table", "capgains_account_label", 0);
2255  m_value.attach(builder, "capgains_table", "capgains_label", 1);
2256 }
2257 
2258 const char *
2259 PageCapGain::get_memo()
2260 {
2261  return gtk_entry_get_text(GTK_ENTRY (m_memo));
2262 }
2263 
2264 
2265 void
2266 PageCapGain::connect(StockTransactionEntry*entry)
2267 {
2268  m_account.connect(entry);
2269  g_signal_connect(m_memo, "changed", G_CALLBACK(text_entry_changed_cb), entry);
2270  m_value.connect(G_CALLBACK(value_changed_cb), entry);
2271 }
2272 
2273 void
2274 PageCapGain::prepare(StockTransactionEntry* entry)
2275 {
2276  entry->set_memo(get_memo());
2277  if (gnc_numeric_check(m_value.get()))
2278  entry->set_value(m_value.get());
2279  entry->set_account(m_account.get());
2280  g_signal_connect(m_page, "focus", G_CALLBACK(assistant_page_set_focus), m_value.widget());
2281 }
2282 
2283 
2284 enum split_cols
2285 {
2286  SPLIT_COL_ACCOUNT = 0,
2287  SPLIT_COL_MEMO,
2288  SPLIT_COL_TOOLTIP,
2289  SPLIT_COL_DEBIT,
2290  SPLIT_COL_CREDIT,
2291  SPLIT_COL_UNITS,
2292  SPLIT_COL_UNITS_COLOR,
2293  NUM_SPLIT_COLS
2294 };
2295 
2296 /* Displays a summary of the transactions as a list. */
2298 {
2299  GtkWidget *m_treeview;
2300 public:
2301  GncFinishTreeview(GtkBuilder *builder);
2306  void load(const EntryVec& list_of_splits);
2307 };
2308 
2309 GncFinishTreeview::GncFinishTreeview (GtkBuilder *builder) :
2310  m_treeview{get_widget (builder, "transaction_view")}
2311 {
2312  auto view = GTK_TREE_VIEW (m_treeview);
2313  gtk_tree_view_set_grid_lines (GTK_TREE_VIEW(view), gnc_tree_view_get_grid_lines_pref ());
2314 
2315  auto store = gtk_list_store_new (NUM_SPLIT_COLS, G_TYPE_STRING, G_TYPE_STRING,
2316  G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING,
2317  G_TYPE_STRING, G_TYPE_STRING);
2318  gtk_tree_view_set_model(view, GTK_TREE_MODEL(store));
2319  gtk_tree_selection_set_mode (gtk_tree_view_get_selection (view),
2320  GTK_SELECTION_NONE);
2321  g_object_unref(store);
2322 
2323  auto renderer = gtk_cell_renderer_text_new();
2324  auto column = gtk_tree_view_column_new_with_attributes
2325  (_("Account"), renderer, "text", SPLIT_COL_ACCOUNT, nullptr);
2326  gtk_tree_view_append_column(view, column);
2327 
2328  renderer = gtk_cell_renderer_text_new();
2329  g_object_set (renderer, "ellipsize", PANGO_ELLIPSIZE_END, nullptr);
2330  column = gtk_tree_view_column_new_with_attributes
2331  (_("Memo"), renderer, "text", SPLIT_COL_MEMO, nullptr);
2332  gtk_tree_view_column_set_expand (column, true);
2333  gtk_tree_view_append_column(view, column);
2334 
2335  renderer = gtk_cell_renderer_text_new();
2336  gtk_cell_renderer_set_alignment (renderer, 1.0, 0.5);
2337  gtk_cell_renderer_set_padding (renderer, 5, 0);
2338  column = gtk_tree_view_column_new_with_attributes
2339  (_("Debit"), renderer, "text", SPLIT_COL_DEBIT, nullptr);
2340  gtk_tree_view_append_column(view, column);
2341 
2342  renderer = gtk_cell_renderer_text_new();
2343  gtk_cell_renderer_set_alignment (renderer, 1.0, 0.5);
2344  gtk_cell_renderer_set_padding (renderer, 5, 0);
2345  column = gtk_tree_view_column_new_with_attributes
2346  (_("Credit"), renderer, "text", SPLIT_COL_CREDIT, nullptr);
2347  gtk_tree_view_append_column(view, column);
2348 
2349  renderer = gtk_cell_renderer_text_new();
2350  gtk_cell_renderer_set_alignment (renderer, 1.0, 0.5);
2351  gtk_cell_renderer_set_padding (renderer, 5, 0);
2352  column = gtk_tree_view_column_new_with_attributes
2353  (_("Units"), renderer,
2354  "text", SPLIT_COL_UNITS,
2355  "foreground", SPLIT_COL_UNITS_COLOR,
2356  nullptr);
2357  gtk_tree_view_append_column(view, column);
2358  gtk_tree_view_set_tooltip_column(GTK_TREE_VIEW(m_treeview),
2359  SPLIT_COL_TOOLTIP);}
2360 
2361 void
2362 GncFinishTreeview::load(const EntryVec& list_of_splits)
2363 {
2364  auto gtv = GTK_TREE_VIEW(m_treeview);
2365  bool negative_in_red = gnc_prefs_get_bool (GNC_PREFS_GROUP_GENERAL,
2366  GNC_PREF_NEGATIVE_IN_RED);
2367  auto list = GTK_LIST_STORE(gtk_tree_view_get_model(gtv));
2368  gtk_list_store_clear(list);
2369  for (const auto &entry : list_of_splits) {
2370  GtkTreeIter iter;
2371  auto memo{entry->memo()};
2372  auto tooltip = (memo && *memo ?
2373  g_markup_escape_text(memo, -1) : strdup(""));
2374  /* print_value and print_amount rely on xaccPrintAmount that
2375  * uses static memory so the result needs to be copied
2376  * immediately or the second call overwrites the results of
2377  * the first one.
2378  */
2379  auto char2str{[](const char* str) -> std::string {
2380  return std::string{ str ? str : "" }; }};
2381  auto amount{char2str(entry->print_value())};
2382  auto units{char2str(entry->has_amount() ?
2383  entry->print_amount(entry->debit_side() ? entry->amount() :
2384  gnc_numeric_neg(entry->amount())) : "")};
2385  auto units_in_red{negative_in_red && !entry->debit_side()};
2386  gtk_list_store_append(list, &iter);
2387  gtk_list_store_set(
2388  list, &iter,
2389  SPLIT_COL_ACCOUNT,
2390  entry->print_account(), SPLIT_COL_MEMO,
2391  entry->memo(), SPLIT_COL_TOOLTIP, tooltip, SPLIT_COL_DEBIT,
2392  entry->debit_side() ? amount.c_str() : nullptr,
2393  SPLIT_COL_CREDIT,
2394  entry->debit_side() ? nullptr : amount.c_str(),
2395  SPLIT_COL_UNITS, units.c_str(),
2396  SPLIT_COL_UNITS_COLOR, units_in_red ? "red" : nullptr, -1);
2397  g_free(tooltip);
2398  }
2399 }
2400 
2405 {
2406  // finish page
2407  GtkWidget * m_page;
2408  GncFinishTreeview m_view;
2409  GtkWidget * m_summary;
2410 public:
2411  PageFinish (GtkBuilder *builder);
2412  void prepare (GtkWidget *window, StockAssistantModel *model);
2413 };
2414 
2415 PageFinish::PageFinish (GtkBuilder *builder) :
2416  m_page (get_widget (builder, "finish_page")), m_view (builder),
2417  m_summary (get_widget (builder, "finish_summary")) {}
2418 
2419 
2420 void
2421 PageFinish::prepare (GtkWidget *window, StockAssistantModel *model)
2422 {
2423  auto [success, summary, list_of_splits] = model->generate_list_of_splits ();
2424  m_view.load(list_of_splits);
2425  gtk_label_set_text(GTK_LABEL(m_summary), summary.c_str());
2426  gtk_assistant_set_page_complete(GTK_ASSISTANT(window), m_page, success);
2427 }
2428 
2429 enum assistant_pages
2430 {
2431  PAGE_INTRO = 0,
2432  PAGE_TRANSACTION_DETAILS,
2433  PAGE_TRANSACTION_TYPE,
2434  PAGE_STOCK_AMOUNT,
2435  PAGE_STOCK_VALUE,
2436  PAGE_CASH,
2437  PAGE_FEES,
2438  PAGE_DIVIDEND,
2439  PAGE_CAPGAINS,
2440  PAGE_FINISH
2441 };
2442 
2445  GtkWidget * m_window;
2446 
2447  PageTransType m_type_page;
2448  PageTransDeets m_deets_page;
2449  PageStockAmount m_stock_amount_page;
2450  PageStockValue m_stock_value_page;
2451  PageCash m_cash_page;
2452  PageFees m_fees_page;
2453  PageDividend m_dividend_page;
2454  PageCapGain m_capgain_page;
2455  PageFinish m_finish_page;
2456 public:
2457  StockAssistantView(GtkBuilder *builder, Account* account, GtkWidget *parent);
2458  ~StockAssistantView();
2471  void prepare(int page, StockAssistantModel*);
2472  GtkWidget* window() { return m_window; }
2473 };
2474 
2475 StockAssistantView::StockAssistantView (GtkBuilder *builder, Account* account, GtkWidget *parent) :
2476  m_window (get_widget (builder, "stock_transaction_assistant")), m_type_page(builder), m_deets_page(builder),
2477  m_stock_amount_page (builder, account), m_stock_value_page (builder, account), m_cash_page (builder, account),
2478  m_fees_page (builder, account), m_dividend_page (builder, account), m_capgain_page (builder, account),
2479  m_finish_page (builder)
2480 {
2481  // Set the name for this assistant so it can be easily manipulated with css
2482  gtk_widget_set_name (GTK_WIDGET(m_window), "gnc-id-assistant-stock-transaction");
2483  gtk_window_set_transient_for (GTK_WINDOW (m_window), GTK_WINDOW(parent));
2484  gnc_window_adjust_for_screen (GTK_WINDOW(m_window));
2485  gnc_restore_window_size (GNC_PREFS_GROUP, GTK_WINDOW(m_window),
2486  GTK_WINDOW(parent));
2487  gtk_widget_show_all (m_window);
2488  DEBUG ("StockAssistantView constructor\n");
2489 };
2490 
2491 StockAssistantView::~StockAssistantView()
2492 {
2493  gnc_save_window_size (GNC_PREFS_GROUP, GTK_WINDOW(m_window));
2494  gtk_widget_destroy (m_window);
2495  DEBUG ("StockAssistantView destructor\n");
2496 };
2497 
2504 static gint
2505 forward_page_func (gint current_page, void* data)
2506 {
2507  auto model{static_cast<StockAssistantModel*>(data)};
2508  current_page++;
2509  if (!model->txn_type_valid())
2510  return current_page;
2511 
2512  if (!model->stock_entry()->has_amount() && current_page == PAGE_STOCK_AMOUNT)
2513  current_page++;
2514  if (!model->stock_entry()->enabled() && current_page == PAGE_STOCK_VALUE)
2515  current_page++;
2516  if (!model->cash_entry()->enabled() && current_page == PAGE_CASH)
2517  current_page++;
2518  if (!model->fees_entry()->enabled() && current_page == PAGE_FEES)
2519  current_page++;
2520  if (!model->dividend_entry()->enabled() && current_page == PAGE_DIVIDEND)
2521  current_page++;
2522  if (!model->capgains_entry()->enabled() && current_page == PAGE_CAPGAINS)
2523  current_page++;
2524 
2525  return current_page;
2526 }
2527 
2528 void
2530 {
2531  m_type_page.connect(model);
2532  m_deets_page.connect(model);
2533  m_stock_amount_page.connect(model->stock_entry());
2534  m_stock_value_page.connect(model->stock_entry());
2535  m_cash_page.connect(model->cash_entry());
2536  m_fees_page.connect(model->fees_entry());
2537  m_dividend_page.connect(model->dividend_entry());
2538  m_capgain_page.connect(model->capgains_entry());
2539 
2540  gtk_assistant_set_forward_page_func (GTK_ASSISTANT(m_window),
2541  (GtkAssistantPageFunc)forward_page_func,
2542  model, nullptr);
2543 }
2544 
2545 void
2547 {
2548  g_return_if_fail (page < PAGE_STOCK_AMOUNT || model->txn_type_valid());
2549  switch (page)
2550  {
2551  case PAGE_TRANSACTION_TYPE:
2552  if (!model->maybe_reset_txn_types())
2553  break;
2554  m_type_page.prepare(model);
2555  break;
2556  case PAGE_TRANSACTION_DETAILS:
2557  m_deets_page.prepare(model);
2558  break;
2559  case PAGE_STOCK_AMOUNT:
2560  {
2561  m_stock_amount_page.prepare(model->stock_entry());
2562  break;
2563  }
2564  case PAGE_STOCK_VALUE:
2565  m_stock_value_page.prepare(model->stock_entry());
2566  break;
2567  case PAGE_CASH:
2568  m_cash_page.prepare(model->cash_entry());
2569  break;
2570  case PAGE_FEES:
2571  {
2572  m_fees_page.prepare(model->fees_entry());
2573  break;
2574  }
2575  case PAGE_DIVIDEND:
2576  m_dividend_page.prepare(model->dividend_entry());
2577  break;
2578  case PAGE_CAPGAINS:
2579  {
2580  m_capgain_page.prepare(model->capgains_entry());
2581  break;
2582  }
2583  case PAGE_FINISH:
2584  {
2585  m_finish_page.prepare (m_window, model);
2586  break;
2587  }
2588  default:
2589  break;
2590  }
2591 }
2592 
2598 static void stock_account_destroyed_handler(QofInstance *inst, QofEventId event,
2599  void* handler_data, [[maybe_unused]]void* event_data);
2600 
2602 {
2603  std::unique_ptr<StockAssistantModel> m_model;
2604  StockAssistantView m_view;
2605  bool m_destroying = false;
2606  int m_qof_event_handler;
2607 public:
2608  StockAssistantController (GtkWidget *parent, GtkBuilder* builder, Account* acct)
2609  : m_model{std::make_unique<StockAssistantModel>(acct)},
2610  m_view{builder, acct, parent},
2611  m_qof_event_handler{qof_event_register_handler(stock_account_destroyed_handler, this)}
2612  {
2613  connect_signals (builder);
2614  DEBUG ("StockAssistantController constructor\n");
2615  };
2617  void connect_signals(GtkBuilder *builder);
2618  void prepare(GtkAssistant* assistant, GtkWidget *page);
2619  void finish();
2620  bool destroying() { return m_destroying; }
2621  Account* model_account() { return m_model->account(); }
2622 };
2623 
2624 static void stock_assistant_window_destroy_cb(GtkWidget *object, gpointer user_data);
2625 static void close_handler (gpointer user_data);
2626 
2627 StockAssistantController::~StockAssistantController()
2628 {
2629  m_destroying = true;
2630  gnc_unregister_gui_component_by_data (ASSISTANT_STOCK_TRANSACTION_CM_CLASS, this);
2631  qof_event_unregister_handler(m_qof_event_handler);
2632 }
2633 
2634 void
2635 StockAssistantController::connect_signals (GtkBuilder *builder)
2636 {
2637  m_view.connect(m_model.get());
2638  gtk_builder_connect_signals (builder, this); //Stock Assistant View: cancel, close, prepare
2639  g_signal_connect (m_view.window(), "destroy",
2640  G_CALLBACK (stock_assistant_window_destroy_cb), this);
2641 
2642 
2643  auto component_id = gnc_register_gui_component
2644  (ASSISTANT_STOCK_TRANSACTION_CM_CLASS, nullptr, close_handler, this);
2645  gnc_gui_component_watch_entity_type (component_id, GNC_ID_ACCOUNT,
2646  QOF_EVENT_MODIFY | QOF_EVENT_DESTROY);
2647 }
2648 
2649 void
2650 StockAssistantController::prepare(GtkAssistant* assistant, GtkWidget* page)
2651 {
2652  auto currentpage = gtk_assistant_get_current_page(assistant);
2653  m_view.prepare(currentpage, m_model.get());
2654 }
2655 
2656 void
2657 StockAssistantController::finish()
2658 {
2659  g_return_if_fail (m_model->txn_type_valid());
2660 
2661  gnc_suspend_gui_refresh ();
2662  [[maybe_unused]] auto [success, trans] = m_model->create_transaction();
2663  gnc_resume_gui_refresh ();
2664 
2665  gnc_close_gui_component_by_data (ASSISTANT_STOCK_TRANSACTION_CM_CLASS, this);
2666 }
2667 
2668 static void
2669 stock_account_destroyed_handler(QofInstance *inst, QofEventId event,
2670  void* handler_data, [[maybe_unused]]void* event_data)
2671 {
2672  auto controller{static_cast<StockAssistantController*>(handler_data)};
2673  if ((inst && inst != QOF_INSTANCE(controller->model_account())) || (event & QOF_EVENT_DESTROY) == 0 ||
2674  controller->destroying())
2675  return;
2676  delete controller;
2677 }
2678 
2679 // These callbacks must be registered with the GtkAssistant so they can't be member functions.
2680 /* The StockAssistantController manages the event handlers and user input. */
2681 void
2682 stock_assistant_prepare_cb (GtkAssistant *assistant, GtkWidget *page,
2683  gpointer user_data)
2684 {
2685  auto info = static_cast<StockAssistantController*>(user_data);
2686  info->prepare(assistant, page);
2687 }
2688 
2689 
2690 static void
2691 stock_assistant_window_destroy_cb (GtkWidget *object, gpointer user_data) //crashes before this gets called.
2692 {
2693  auto controller = static_cast<StockAssistantController*>(user_data);
2694  if (controller->destroying())
2695  return;
2696 
2697  gnc_close_gui_component_by_data (ASSISTANT_STOCK_TRANSACTION_CM_CLASS, controller);
2698 }
2699 
2700 
2701 void
2702 stock_assistant_finish_cb (GtkAssistant *assistant, gpointer user_data)
2703 {
2704  auto controller = static_cast<StockAssistantController*>(user_data);
2705  controller->finish();
2706 }
2707 
2708 
2709 void
2710 stock_assistant_cancel_cb (GtkAssistant *assistant, gpointer user_data)
2711 {
2712  auto controller = static_cast<StockAssistantController*>(user_data);
2713  if (controller->destroying())
2714  return;
2715  gnc_close_gui_component_by_data (ASSISTANT_STOCK_TRANSACTION_CM_CLASS, controller);
2716 }
2717 
2718 
2719 static void
2720 close_handler (gpointer user_data)
2721 {
2722  auto controller = static_cast<StockAssistantController*>(user_data);
2723  if (controller->destroying())
2724  return;
2725  delete controller;
2726 }
2727 
2728 /********************************************************************\
2729  * gnc_stock_transaction_assistant *
2730  * opens up a assistant to record a stock transaction *
2731  * *
2732  * Args: parent - the parent ofthis window *
2733  * initial - the initial account to use *
2734  * Return: nothing *
2735 \********************************************************************/
2736 void
2737 gnc_stock_transaction_assistant (GtkWidget *parent, Account *account)
2738 {
2739  auto builder = gtk_builder_new();
2740  gnc_builder_add_from_file(builder, "assistant-stock-transaction.glade",
2741  "stock_transaction_assistant");
2742 
2743  [[maybe_unused]] auto info = new StockAssistantController(parent, builder, account);
2744  g_object_unref(builder);
2745 }
messages for later display to the user.
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
GNCPrice * gnc_price_create(QofBook *book)
gnc_price_create - returns a newly allocated and initialized price with a reference count of 1...
Transaction * xaccMallocTransaction(QofBook *book)
The xaccMallocTransaction() will malloc memory and initialize it.
gboolean gnc_numeric_equal(gnc_numeric a, gnc_numeric b)
Equivalence predicate: Returns TRUE (1) if a and b represent the same number.
std::string report()
Compose all of the logged messages into a bullet list, errors first, then warnings, infos last.
void xaccTransSetDatePostedSecsNormalized(Transaction *trans, time64 time)
This function sets the posted date of the transaction, specified by a time64 (see ctime(3))...
Contains the pages and manages displaying them one at a time.
gchar * gnc_num_dbg_to_string(gnc_numeric n)
Convert to string.
void xaccSplitMakeStockSplit(Split *s)
Mark a split to be of type stock split - after this, you shouldn&#39;t modify the value anymore...
Definition: Split.cpp:2001
Page classes generate the several pages of the assistant.
time64 xaccTransGetDate(const Transaction *trans)
Retrieve the posted date of the transaction.
Date and Time handling routines.
const char * gnc_commodity_get_mnemonic(const gnc_commodity *cm)
Retrieve the mnemonic for the specified commodity.
void xaccAccountSetAssociatedAccount(Account *acc, const char *tag, const Account *assoc_acct)
Set the account&#39;s associated account e.g.
Definition: Account.cpp:2620
virtual const char * print_value() const
QofBook * qof_instance_get_book(gconstpointer inst)
Return the book pointer.
utility functions for the GnuCash UI
Expense accounts are used to denote expenses.
Definition: Account.h:143
StockTransactionEntry * fees_entry()
Accessor.
#define PINFO(format, args...)
Print an informational note.
Definition: qoflog.h:256
gnc_numeric gnc_numeric_neg(gnc_numeric a)
Returns a newly created gnc_numeric that is the negative of the given gnc_numeric value...
void set_txn_type_explanation(const gchar *txt)
Sets the explanation text for the selected transaction type, allowing the user to make sure that the ...
std::tuple< bool, Transaction * > create_transaction()
Generate a GnuCash transaction from the active entries.
An exact-rational-number library for gnucash.
STRUCTS.
gnc_numeric calculate_price() const override
Calculate the price (amount/value) for non-currency accounts.
void gnc_price_unref(GNCPrice *p)
gnc_price_unref - indicate you&#39;re finished with a price (i.e.
The Cash page collects the cash account (usually corresponds the broker&#39;s cash management account)...
const std::optional< TxnTypeInfo > & txn_type()
Accessor.
#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.
const char * xaccPrintAmount(gnc_numeric val, GNCPrintAmountInfo info)
Make a string representation of a gnc_numeric.
void xaccTransSetDescription(Transaction *trans, const char *desc)
Sets the transaction Description.
gnc_numeric gnc_numeric_add(gnc_numeric a, gnc_numeric b, gint64 denom, gint how)
Return a+b.
C++ wrapper for GncAmountEdit, see gnucash/gnome-utils/gnc-amount-edit.h.
gboolean gnc_numeric_zero_p(gnc_numeric a)
Returns 1 if the given gnc_numeric is 0 (zero), else returns 0.
Specialized Entry for the stock account&#39;s capital gains split.
The primary numeric class for representing amounts and values.
Definition: gnc-numeric.hpp:60
Transaction * xaccSplitGetParent(const Split *split)
Returns the parent transaction of the split.
Use any denominator which gives an exactly correct ratio of numerator to denominator.
Definition: gnc-numeric.h:188
virtual gnc_numeric calculate_price() const
Calculate the price (amount/value) for non-currency accounts.
#define PERR(format, args...)
Log a serious error.
Definition: qoflog.h:244
C++ wrapper for the GncDateEdit control (see gnucash/gnome-utils/gnc-date-edit.h).
void set_transaction_date(time64 date)
Setter.
StockTransactionEntry * dividend_entry()
Accessor.
GNCPriceDB * gnc_pricedb_get_db(QofBook *book)
Return the pricedb associated with the book.
gboolean gnc_numeric_negative_p(gnc_numeric a)
Returns 1 if a < 0, otherwise returns 0.
void xaccTransSetCurrency(Transaction *trans, gnc_commodity *curr)
Set a new currency on a transaction.
gint qof_event_register_handler(QofEventHandler handler, gpointer user_data)
Register a handler for events.
Definition: qofevent.cpp:73
#define PWARN(format, args...)
Log a warning.
Definition: qoflog.h:250
char * qof_print_date(time64 secs)
Convenience; calls through to qof_print_date_dmy_buff().
Definition: gnc-date.cpp:609
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.
gint QofEventId
Define the type of events allowed.
Definition: qofevent.h:45
Reduce the result value by common factor elimination, using the smallest possible value for the denom...
Definition: gnc-numeric.h:195
GtkTreeView implementation for gnucash account tree.
Income accounts are used to denote income.
Definition: Account.h:140
Account public routines (C++ api)
Transaction Details page.
class TxnTypeinfo has no functions.
const char * gnc_numeric_errorCode_to_string(GNCNumericErrorCode error_code)
Returns a string representation of the given GNCNumericErrorCode.
void xaccSplitSetMemo(Split *split, const char *memo)
The memo is an arbitrary string associated with a split.
void connect(StockAssistantModel *)
Calls each page&#39;s connect function.
gnc_numeric gnc_numeric_error(GNCNumericErrorCode error_code)
Create a gnc_numeric object that signals the error condition noted by error_code, rather than a numbe...
bool maybe_reset_txn_types()
Selects a TxnTypevec for the user to pick from depending on whether the account has a positive...
Holds the configuration information from the fieldmask and the data to create a single split...
void prepare(int page, StockAssistantModel *)
Calls the specified page&#39;s prepare function.
The bank account type denotes a savings or checking account held at a bank.
Definition: Account.h:107
void qof_event_unregister_handler(gint handler_id)
Unregister an event handler.
Definition: qofevent.cpp:103
StockTransactionEntry * capgains_entry()
Accessor.
Argument is not a valid number.
Definition: gnc-numeric.h:224
available transaction types based on the state of the account, the collection and validation of input...
void xaccTransCommitEdit(Transaction *trans)
The xaccTransCommitEdit() method indicates that the changes to the transaction and its splits are com...
gnc_numeric gnc_numeric_div(gnc_numeric a, gnc_numeric b, gint64 denom, gint how)
Division.
void load(const EntryVec &list_of_splits)
Extract the information from the StockTransactionEntries in the vector created by the model&#39;s make_li...
virtual const char * print_price() const
void set_fieldmask(FieldMask mask) override
Set up the state variables from the FieldMask.
StockTransactionEntry * cash_entry()
Accessor.
void xaccTransBeginEdit(Transaction *trans)
The xaccTransBeginEdit() method must be called before any changes are made to a transaction or any of...
asset (and liability) accounts indicate generic, generalized accounts that are none of the above...
Definition: Account.h:116
std::tuple< bool, std::string, EntryVec > generate_list_of_splits()
Generate the proposed list of splits.
StockTransactionEntry * stock_entry()
Accessor.
void set_transaction_desc(const char *desc)
Setter.
gnc_numeric xaccAccountGetBalanceAsOfDate(Account *acc, time64 date)
Get the balance of the account at the end of the day before the date specified.
Definition: Account.cpp:3469
All type declarations for the whole Gnucash engine.
gboolean gnc_numeric_positive_p(gnc_numeric a)
Returns 1 if a > 0, otherwise returns 0.
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:3358
Account * xaccAccountGetAssociatedAccount(const Account *acc, const char *tag)
Get the account&#39;s associated account e.g.
Definition: Account.cpp:3330
virtual const char * print_amount(gnc_numeric amt) const
Split * xaccMallocSplit(QofBook *book)
Constructor.
Definition: gmock-Split.cpp:37
bool set_txn_type(guint type_idx)
Setter.
Generic api to store and retrieve preferences.
Specialized StockTransactionEntry for the stock split.
gnc_numeric xaccSplitGetValue(const Split *split)
Returns the value of this split in the transaction&#39;s commodity.
Definition: gmock-Split.cpp:84
void xaccAccountBeginEdit(Account *acc)
The xaccAccountBeginEdit() subroutine is the first phase of a two-phase-commit wrapper for account up...
Definition: Account.cpp:1475
const std::optional< TxnTypeVec > & get_txn_types()
Accessor function.
gnc_commodity * xaccAccountGetCommodity(const Account *acc)
Get the account&#39;s commodity.
Definition: Account.cpp:3351
gboolean gnc_prefs_get_bool(const gchar *group, const gchar *pref_name)
Get a boolean value from the preferences backend.
virtual void set_fieldmask(FieldMask mask)
Set up the state variables from the FieldMask.
std::string amount_str_for_display() const override
Generate a string representation of the value.
time64 gnc_time(time64 *tbuf)
get the current time
Definition: gnc-date.cpp:261
GNCNumericErrorCode gnc_numeric_check(gnc_numeric in)
Check for error signal in value.
gint64 time64
Most systems that are currently maintained, including Microsoft Windows, BSD-derived Unixes and Linux...
Definition: gnc-date.h:87
time64 gnc_time64_get_day_end(time64 time_val)
The gnc_time64_get_day_end() routine will take the given time in seconds and adjust it to the last se...
Definition: gnc-date.cpp:1316
const char * xaccAccountGetName(const Account *acc)
Get the account&#39;s name.
Definition: Account.cpp:3239
#define GNC_DENOM_AUTO
Values that can be passed as the &#39;denom&#39; argument.
Definition: gnc-numeric.h:245
API for Transactions and Splits (journal entries)
C++ wrapper for GncAccounSel, see gnucash/gnome-utils/gnc-account-sel.h.
void xaccAccountCommitEdit(Account *acc)
ThexaccAccountCommitEdit() subroutine is the second phase of a two-phase-commit wrapper for account u...
Definition: Account.cpp:1516
virtual std::string amount_str_for_display() const
Generate a string representation of the value.
Specialized Entry for fees, taxes, commissions, and so on.
void set_fieldmask(FieldMask mask) override
Set up the state variables from the FieldMask.
std::string get_new_amount_str() const
Accessor.
gnc_numeric xaccSplitGetAmount(const Split *split)
Returns the amount of the split in the account&#39;s commodity.
Definition: gmock-Split.cpp:69
Dividend page, collects an amount, an INCOME account, and a memo.