aboutsummaryrefslogtreecommitdiff
path: root/cashflow.cpp
diff options
context:
space:
mode:
authorCalvin Morrison <calvin@pobox.com>2025-12-27 16:04:48 -0500
committerCalvin Morrison <calvin@pobox.com>2025-12-27 16:04:48 -0500
commit5e9b299dfe95a7f99f029802089c047a392eee3a (patch)
tree164c14e8f4e5a8161338f94463ef4df7b63519bd /cashflow.cpp
parent5305661f325ea84bbbc9c8fc7b6f2a4813a9147d (diff)
WIP: occurrence_key model before refactoring to persistent projections
Diffstat (limited to 'cashflow.cpp')
-rw-r--r--cashflow.cpp206
1 files changed, 192 insertions, 14 deletions
diff --git a/cashflow.cpp b/cashflow.cpp
index 8eb46a4..f302110 100644
--- a/cashflow.cpp
+++ b/cashflow.cpp
@@ -71,6 +71,10 @@ void CashFlow::setupConnections() {
connect(ui->newBtn, &QPushButton::clicked, this, &CashFlow::onNewTransaction);
connect(ui->deleteBtn, &QPushButton::clicked, this, &CashFlow::onDeleteTransaction);
+ // Transaction entry recurring rule linking
+ connect(ui->entryRecurringCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &CashFlow::onRecurringRuleChanged);
+ connect(ui->entryDateEdit, &QDateEdit::dateChanged, this, &CashFlow::onTransactionDateChanged);
+
// Set up Delete key shortcut for transaction table
ui->deleteBtn->setShortcut(Qt::Key_Delete);
@@ -131,6 +135,9 @@ void CashFlow::refreshView() {
ui->entryCategoryCombo->setCurrentText(currentCategory);
ui->entryCategoryCombo->blockSignals(false);
+ // Populate recurring rules combo
+ populateRecurringRulesCombo();
+
// Populate recurring rule account and category combos
ui->recurringAccountCombo->blockSignals(true);
ui->recurringAccountCombo->clear();
@@ -200,6 +207,10 @@ void CashFlow::refreshTransactionTable() {
// Store ID in first column's data for retrieval later
QTableWidgetItem *dateItem = new QTableWidgetItem(t.date.toString("MM/dd/yy"));
dateItem->setData(Qt::UserRole, t.id);
+ // Store full transaction for projected items (id == -1)
+ if (t.id == -1) {
+ dateItem->setData(Qt::UserRole + 1, QVariant::fromValue(t));
+ }
dateItem->setFlags(dateItem->flags() & ~Qt::ItemIsEditable);
ui->transactionTable->setItem(row, 0, dateItem);
@@ -484,6 +495,15 @@ QList<Transaction> CashFlow::generateProjectedTransactions() {
QDate startDate = ui->dateFromEdit->date();
QDate endDate = ui->dateToEdit->date();
+ // Load all actual transactions with recurring_id (converted projections)
+ QList<Transaction> actualTransactions = database->getAllTransactions();
+ QSet<QString> existingOccurrences; // recurring_id:occurrence_key pairs
+ for (const Transaction &t : actualTransactions) {
+ if (t.recurringId != -1 && !t.occurrenceKey.isEmpty()) {
+ existingOccurrences.insert(QString("%1:%2").arg(t.recurringId).arg(t.occurrenceKey));
+ }
+ }
+
for (const RecurringRule &rule : rules) {
QDate currentDate = rule.startDate > startDate ? rule.startDate : startDate;
@@ -514,17 +534,37 @@ QList<Transaction> CashFlow::generateProjectedTransactions() {
break;
}
- Transaction t;
- t.id = -1; // Projected transactions have no ID
- t.date = currentDate;
- t.amount = rule.amount;
- t.account = rule.account;
- t.category = rule.category;
- t.description = rule.description + " (projected)";
- t.type = TransactionType::Estimated;
- t.recurringId = rule.id;
+ // Generate occurrence key based on frequency
+ QString occurrenceKey;
+ if (rule.frequency == RecurrenceFrequency::Daily) {
+ occurrenceKey = currentDate.toString("yyyy-MM-dd");
+ } else if (rule.frequency == RecurrenceFrequency::Weekly || rule.frequency == RecurrenceFrequency::BiWeekly) {
+ occurrenceKey = QString("%1-W%2").arg(currentDate.year()).arg(currentDate.weekNumber(), 2, 10, QChar('0'));
+ } else if (rule.frequency == RecurrenceFrequency::Monthly) {
+ occurrenceKey = currentDate.toString("yyyy-MM");
+ } else if (rule.frequency == RecurrenceFrequency::Yearly) {
+ occurrenceKey = QString::number(currentDate.year());
+ }
- projections.append(t);
+ // Skip if actual already exists for this occurrence
+ QString occurrenceCheck = QString("%1:%2").arg(rule.id).arg(occurrenceKey);
+ if (existingOccurrences.contains(occurrenceCheck)) {
+ // Actual exists, skip this projection
+ } else {
+ Transaction t;
+ t.id = -1; // Projected transactions have no ID
+ t.date = currentDate;
+ t.amount = rule.amount;
+ t.account = rule.account;
+ t.category = rule.category;
+ t.description = rule.description + " (projected)";
+ t.type = TransactionType::Estimated;
+ t.recurringId = rule.id;
+ t.occurrenceKey = occurrenceKey;
+ t.reconciled = false;
+
+ projections.append(t);
+ }
count++;
// Calculate next occurrence
@@ -574,10 +614,16 @@ void CashFlow::onTransactionSelected() {
int row = selected[0]->row();
int id = ui->transactionTable->item(row, 0)->data(Qt::UserRole).toInt();
- // If it's a projected transaction (id = -1), don't load it for editing
+ // If it's a projected transaction (id = -1), load it for editing
if (id == -1) {
- ui->entryStatusLabel->setText("(Projected - cannot edit)");
- currentTransactionId = -1;
+ QVariant projectedData = ui->transactionTable->item(row, 0)->data(Qt::UserRole + 1);
+ if (projectedData.canConvert<Transaction>()) {
+ Transaction t = projectedData.value<Transaction>();
+ currentTransactionId = -1; // Will create new actual when saved
+ currentProjectedTransaction = t; // Store for conversion
+ loadTransactionToEntry(t);
+ ui->entryStatusLabel->setText("(Converting Projection to Actual)");
+ }
return;
}
@@ -619,7 +665,51 @@ void CashFlow::onSaveTransaction() {
t.category = ui->entryCategoryCombo->currentText();
t.description = ui->entryDescriptionEdit->text();
t.type = ui->entryTypeCombo->currentText() == "Actual" ? TransactionType::Actual : TransactionType::Estimated;
- t.recurringId = -1; // Manual entries don't have recurring ID
+
+ // Check if user manually linked to a recurring rule
+ int manualRuleId = ui->entryRecurringCombo->currentData().toInt();
+ QString manualOccurrenceKey = ui->entryOccurrenceEdit->text().trimmed();
+
+ // Check if we're converting a projection to actual
+ if (currentTransactionId == -1 && currentProjectedTransaction.recurringId != -1) {
+ // Converting projection - keep recurring link and store expected values
+ t.recurringId = currentProjectedTransaction.recurringId;
+ t.occurrenceKey = currentProjectedTransaction.occurrenceKey;
+ t.expectedAmount = currentProjectedTransaction.amount;
+ t.expectedDate = currentProjectedTransaction.date;
+ t.reconciled = true;
+ t.type = TransactionType::Actual; // Force to actual when converting
+ } else if (manualRuleId != -1 && !manualOccurrenceKey.isEmpty()) {
+ // User manually linked to recurring rule
+ t.recurringId = manualRuleId;
+ t.occurrenceKey = manualOccurrenceKey;
+ t.reconciled = true;
+ // Try to get expected amount from the rule
+ QList<RecurringRule> rules = database->getAllRecurringRules();
+ for (const RecurringRule &rule : rules) {
+ if (rule.id == manualRuleId) {
+ t.expectedAmount = rule.amount;
+ break;
+ }
+ }
+ } else if (currentTransactionId != -1) {
+ // Editing existing transaction - load from database to preserve reconciliation fields
+ QList<Transaction> allTrans = database->getAllTransactions();
+ for (const Transaction &existing : allTrans) {
+ if (existing.id == currentTransactionId) {
+ t.recurringId = existing.recurringId;
+ t.occurrenceKey = existing.occurrenceKey;
+ t.expectedAmount = existing.expectedAmount;
+ t.expectedDate = existing.expectedDate;
+ t.reconciled = existing.reconciled;
+ break;
+ }
+ }
+ } else {
+ // New manual transaction with no recurring link
+ t.recurringId = -1;
+ t.reconciled = false;
+ }
bool success;
if (currentTransactionId == -1) {
@@ -647,6 +737,7 @@ void CashFlow::onSaveTransaction() {
void CashFlow::onNewTransaction() {
clearTransactionEntry();
+ currentProjectedTransaction = Transaction(); // Reset projected transaction
ui->entryDateEdit->setDate(QDate::currentDate());
ui->entryDateEdit->setFocus();
}
@@ -771,6 +862,9 @@ void CashFlow::clearTransactionEntry() {
ui->entryCategoryCombo->setCurrentText("");
ui->entryDescriptionEdit->clear();
ui->entryTypeCombo->setCurrentIndex(0);
+ ui->entryRecurringCombo->setCurrentIndex(0); // (None)
+ ui->entryOccurrenceEdit->clear();
+ ui->entryOccurrenceEdit->setEnabled(false);
ui->entryStatusLabel->setText("(New transaction)");
updateAmountColors();
}
@@ -783,6 +877,8 @@ void CashFlow::loadTransactionToEntry(const Transaction &t) {
ui->entryCategoryCombo->blockSignals(true);
ui->entryDescriptionEdit->blockSignals(true);
ui->entryTypeCombo->blockSignals(true);
+ ui->entryRecurringCombo->blockSignals(true);
+ ui->entryOccurrenceEdit->blockSignals(true);
ui->entryDateEdit->setDate(t.date);
ui->entryAmountSpin->setValue(t.amount);
@@ -791,12 +887,31 @@ void CashFlow::loadTransactionToEntry(const Transaction &t) {
ui->entryDescriptionEdit->setText(t.description);
ui->entryTypeCombo->setCurrentIndex(t.type == TransactionType::Actual ? 1 : 0);
+ // Set recurring rule link if present
+ if (t.recurringId != -1) {
+ // Find and select the rule in combo
+ for (int i = 0; i < ui->entryRecurringCombo->count(); i++) {
+ if (ui->entryRecurringCombo->itemData(i).toInt() == t.recurringId) {
+ ui->entryRecurringCombo->setCurrentIndex(i);
+ break;
+ }
+ }
+ ui->entryOccurrenceEdit->setText(t.occurrenceKey);
+ ui->entryOccurrenceEdit->setEnabled(true);
+ } else {
+ ui->entryRecurringCombo->setCurrentIndex(0); // (None)
+ ui->entryOccurrenceEdit->clear();
+ ui->entryOccurrenceEdit->setEnabled(false);
+ }
+
ui->entryDateEdit->blockSignals(false);
ui->entryAmountSpin->blockSignals(false);
ui->entryAccountCombo->blockSignals(false);
ui->entryCategoryCombo->blockSignals(false);
ui->entryDescriptionEdit->blockSignals(false);
ui->entryTypeCombo->blockSignals(false);
+ ui->entryRecurringCombo->blockSignals(false);
+ ui->entryOccurrenceEdit->blockSignals(false);
updateAmountColors();
}
@@ -966,3 +1081,66 @@ void CashFlow::onPreferences() {
refreshView();
}
}
+
+void CashFlow::populateRecurringRulesCombo() {
+ ui->entryRecurringCombo->clear();
+ ui->entryRecurringCombo->addItem("(None)", -1);
+
+ QList<RecurringRule> rules = database->getAllRecurringRules();
+ for (const RecurringRule &rule : rules) {
+ QString label = QString("%1 (%2)").arg(rule.name).arg(
+ rule.frequency == RecurrenceFrequency::Daily ? "Daily" :
+ rule.frequency == RecurrenceFrequency::Weekly ? "Weekly" :
+ rule.frequency == RecurrenceFrequency::BiWeekly ? "Bi-weekly" :
+ rule.frequency == RecurrenceFrequency::Monthly ? "Monthly" :
+ rule.frequency == RecurrenceFrequency::Yearly ? "Yearly" : "Unknown"
+ );
+ ui->entryRecurringCombo->addItem(label, rule.id);
+ }
+}
+
+QString CashFlow::generateOccurrenceKey(const QDate &date, RecurrenceFrequency frequency) const {
+ if (frequency == RecurrenceFrequency::Daily) {
+ return date.toString("yyyy-MM-dd");
+ } else if (frequency == RecurrenceFrequency::Weekly || frequency == RecurrenceFrequency::BiWeekly) {
+ return QString("%1-W%2").arg(date.year()).arg(date.weekNumber(), 2, 10, QChar('0'));
+ } else if (frequency == RecurrenceFrequency::Monthly) {
+ return date.toString("yyyy-MM");
+ } else if (frequency == RecurrenceFrequency::Yearly) {
+ return QString::number(date.year());
+ }
+ return QString();
+}
+
+void CashFlow::updateOccurrenceKey() {
+ int ruleId = ui->entryRecurringCombo->currentData().toInt();
+ if (ruleId == -1) {
+ ui->entryOccurrenceEdit->clear();
+ ui->entryOccurrenceEdit->setEnabled(false);
+ return;
+ }
+
+ ui->entryOccurrenceEdit->setEnabled(true);
+
+ // Find the rule to get its frequency
+ QList<RecurringRule> rules = database->getAllRecurringRules();
+ for (const RecurringRule &rule : rules) {
+ if (rule.id == ruleId) {
+ QString occurrenceKey = generateOccurrenceKey(ui->entryDateEdit->date(), rule.frequency);
+ ui->entryOccurrenceEdit->setText(occurrenceKey);
+ break;
+ }
+ }
+}
+
+void CashFlow::onRecurringRuleChanged() {
+ updateOccurrenceKey();
+}
+
+void CashFlow::onTransactionDateChanged() {
+ // Update occurrence key if a recurring rule is selected
+ if (ui->entryRecurringCombo->currentData().toInt() != -1) {
+ updateOccurrenceKey();
+ }
+}
+