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