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