Skip to content

feat: agregar módulo de contabilidad y actualizar versión a 3.4.3#4529

Open
TribuCibernetica wants to merge 7 commits into
opensourcepos:masterfrom
TribuCibernetica:feature/modulo-contabilidad
Open

feat: agregar módulo de contabilidad y actualizar versión a 3.4.3#4529
TribuCibernetica wants to merge 7 commits into
opensourcepos:masterfrom
TribuCibernetica:feature/modulo-contabilidad

Conversation

@TribuCibernetica
Copy link
Copy Markdown

@TribuCibernetica TribuCibernetica commented May 4, 2026

Este módulo añade la funcionalidad de contabilidad básica (Catálogo de cuentas, diarios y asientos contables). Incluye migraciones automáticas para las nuevas tablas y permisos.

Summary by CodeRabbit

  • New Features

    • Added Accounting module: chart of accounts, journal entries, dashboard (balance sheet & profit/loss), account and entry management views
    • New Top 50 items & categories reports (tabular + graphical) with localized titles and safe percentage handling
    • Automatic accounting entries for sales, expenses, and purchases; database migrations and permissions to enable the module
    • Spanish and English labels for accounting and reports
  • Chores

    • Updated application version to 3.4.3

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 4, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds an Accounting module (schema, models, controller, views), integrates automatic accounting entries into Sale/Expense/Receiving flows, adds Top‑50 reports with permissions/translations, supplies a Docker deploy script, and bumps application version to 3.4.3.

Changes

Accounting Module & Integration

Layer / File(s) Summary
Schema / Data Shape
app/Database/Migrations/20260427000000_AccountingModule.php, app/Database/Migrations/20260428000000_AccountingMexico.php
Create accounts, journals, accounting_entries, accounting_items; seed module/permissions/grants; Mexico migration remaps default codes and conditionally inserts/removes IVA accounts.
Core Models
app/Models/Account.php, app/Models/Journal.php, app/Models/Accounting_entry.php
Add Account and Journal models with lookup helpers; Accounting_entry model with transactional save_entry() that validates debit≈credit and provides get_balance_sheet() / get_profit_loss().
Business Integration
app/Models/Sale.php, app/Models/Expense.php, app/Models/Receiving.php
On completed Sale, new Expense, and Receiving saves, compute subtotal/IVA split and create accounting entries (debits/credits) via Accounting_entry->save_entry() within existing model save flows.
Controller & Views
app/Controllers/Accounting.php, app/Views/accounting/*
Add Accounting controller (dashboard, accounts, entries, reports, account save endpoint) and three view templates: dashboard, manage_accounts, manage_entries.
Localization & Permissions
app/Language/en/Module.php, migration seeds
Add Accounting module language entries; migrations seed accounting permission and grant.
Deployment & Config
deploy-accounting.sh, app/Config/App.php
Add script to copy files into a Docker container, clear cache, run migrations, verify tables, and set icon; bump application version to 3.4.3.

Top 50 Reports

Layer / File(s) Summary
Report Query Models
app/Models/Reports/Summary_categories.php, app/Models/Reports/Summary_items.php
Add getData(array $inputs) implementing top_50 branch: group by category/item, order by quantity desc, limit 50; otherwise use existing grouping/ordering.
Controller Methods
app/Controllers/Reports.php
Add summary_top_categories, summary_top_items, graphical_summary_top_categories, graphical_summary_top_items; also guard graphical pie-chart percentage calculations against division-by-zero.
Permissions & Localization
app/Database/Migrations/20260430000000_AddTopReportsPermissions.php, app/Language/en/Reports.php, app/Language/es-MX/Reports.php
Add reports_top_items and reports_top_categories permissions (and grants) and new translation keys for Top‑50 reports in English and Spanish.
View Integration
app/Views/reports/listing.php
Listing view now shows Top‑50 report links (tabular and graphical) when corresponding permissions exist and includes top capability in gating logic.

Sequence Diagram: Accounting Entry Creation on Sale

sequenceDiagram
    participant Controller as Sale Controller
    participant Sale as Sale Model
    participant Journal as Journal Model
    participant Account as Account Model
    participant AcEntry as Accounting_entry Model
    participant DB as Database

    Controller->>Sale: save_value(sale data)
    activate Sale
    Sale->>Journal: get_by_code('SALES')
    Journal->>DB: query journals
    DB-->>Journal: journal row
    Journal-->>Sale: journal info
    Sale->>Account: get_by_code(cash), get_by_code(revenue)
    Account->>DB: query accounts
    DB-->>Account: account rows
    Account-->>Sale: account info
    Sale->>Sale: compute subtotal and total_tax (IVA)
    Sale->>AcEntry: save_entry(entry_data, items_data)
    activate AcEntry
    AcEntry->>DB: BEGIN transaction
    AcEntry->>DB: INSERT/UPDATE accounting_entries header
    AcEntry->>DB: DELETE existing accounting_items (if update)
    AcEntry->>AcEntry: validate debit ≈ credit (tolerance 0.01)
    alt Balanced
        AcEntry->>DB: INSERT accounting_items (debit/credit lines)
        AcEntry->>DB: COMMIT
        AcEntry-->>Sale: success
    else Unbalanced
        AcEntry->>DB: ROLLBACK
        AcEntry-->>Sale: failure
    end
    deactivate AcEntry
    Sale-->>Controller: return with entry_id/status
    deactivate Sale
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~65 minutes

Suggested labels

enhancement

Poem

🐰 I hopped through ledgers, nibbling numbers bright,

Built journals, accounts, and balanced every byte,
Sales bowed to taxes in a tidy little spin,
Debits met credits — bookkeeping with a grin,
A carrot-cheer for the module, soft and light.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 47.92% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: adding an accounting module and updating the version to 3.4.3, which directly matches the PR's primary objectives.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 18

🧹 Nitpick comments (8)
app/Database/Migrations/20260427000000_AccountingModule.php (2)

78-145: 🏗️ Heavy lift

No foreign key constraints defined — referential integrity is unenforced.

accounting_items.entry_id → accounting_entries.entry_id, accounting_items.account_id → accounts.account_id, accounting_entries.journal_id → journals.journal_id, and accounts.parent_id → accounts.account_id are all logical FK relationships with no database-level enforcement. For financial data this means orphaned line items (e.g., after a journal is deleted) will go undetected.

♻️ Example: add FK constraints via Forge
// After createTable('accounting_entries'):
$this->forge->addForeignKey('journal_id', 'journals', 'journal_id', 'CASCADE', 'RESTRICT');
$this->forge->processIndexes('accounting_entries');

// After createTable('accounting_items'):
$this->forge->addForeignKey('entry_id',   'accounting_entries', 'entry_id',   'CASCADE', 'RESTRICT');
$this->forge->addForeignKey('account_id', 'accounts',           'account_id', 'RESTRICT', 'RESTRICT');
$this->forge->processIndexes('accounting_items');

The down() method would need to call $this->forge->dropForeignKey() before dropping tables.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Database/Migrations/20260427000000_AccountingModule.php` around lines 78
- 145, The migration creates accounting_entries, accounting_items and accounts
relations but never adds FK constraints, so referential integrity
(accounting_items.entry_id → accounting_entries.entry_id,
accounting_items.account_id → accounts.account_id, accounting_entries.journal_id
→ journals.journal_id, accounts.parent_id → accounts.account_id) is unenforced;
fix by calling $this->forge->addForeignKey(...) after creating the respective
tables (use the same column names: entry_id, item_id/account_id, journal_id,
parent_id) with appropriate ON DELETE/ON UPDATE actions (e.g., CASCADE for
entry→items and RESTRICT for account links) and then
$this->forge->processIndexes('<table_name>') for each table, and update the
down() method to call $this->forge->dropForeignKey('<table>', '<column>') before
dropping the tables so rollbacks remove FKs cleanly.

147-173: ⚡ Quick win

Use Query Builder instead of raw SQL string concatenation for seed inserts.

All seed and metadata inserts use $this->db->query() with string concatenation. While values are hardcoded literals (no injection risk here), this violates the project's guideline on parameterized queries and bypasses the query builder's automatic prefix handling. Use $this->db->table()->insert() / insertBatch() instead:

♻️ Proposed refactor for seed inserts
-// Insert into modules
-$this->db->query("INSERT INTO " . $this->db->prefixTable('modules') . " (name_lang_key, desc_lang_key, sort, module_id) VALUES ('module_accounting', 'module_accounting_desc', 90, 'accounting')");
-
-// Insert into permissions
-$this->db->query("INSERT INTO " . $this->db->prefixTable('permissions') . " (permission_id, module_id) VALUES ('accounting', 'accounting')");
+$this->db->table('modules')->insert([
+    'name_lang_key' => 'module_accounting',
+    'desc_lang_key' => 'module_accounting_desc',
+    'sort'          => 90,
+    'module_id'     => 'accounting',
+]);
+$this->db->table('permissions')->insert([
+    'permission_id' => 'accounting',
+    'module_id'     => 'accounting',
+]);

-// Seed some basic data
-$this->db->query("INSERT INTO " . $this->db->prefixTable('journals') . " (name, code, type) VALUES 
-    ('Sales Journal', 'SALES', 'Sale'),
-    ('Purchase Journal', 'PURCH', 'Purchase'),
-    ('Cash Journal', 'CASH', 'Cash'),
-    ('General Journal', 'GEN', 'General')
-");
-
-$this->db->query("INSERT INTO " . $this->db->prefixTable('accounts') . " (code, name, type) VALUES 
-    ('1000', 'Cash and Cash Equivalents', 'Asset'),
-    ...
-");
+$this->db->table('journals')->insertBatch([
+    ['name' => 'Sales Journal',    'code' => 'SALES', 'type' => 'Sale'],
+    ['name' => 'Purchase Journal', 'code' => 'PURCH', 'type' => 'Purchase'],
+    ['name' => 'Cash Journal',     'code' => 'CASH',  'type' => 'Cash'],
+    ['name' => 'General Journal',  'code' => 'GEN',   'type' => 'General'],
+]);
+$this->db->table('accounts')->insertBatch([
+    ['code' => '1000', 'name' => 'Cash and Cash Equivalents', 'type' => 'Asset'],
+    ['code' => '1100', 'name' => 'Accounts Receivable',       'type' => 'Asset'],
+    ['code' => '1200', 'name' => 'Inventory',                 'type' => 'Asset'],
+    ['code' => '2000', 'name' => 'Accounts Payable',          'type' => 'Liability'],
+    ['code' => '3000', 'name' => 'Owner Equity',              'type' => 'Equity'],
+    ['code' => '4000', 'name' => 'Sales Revenue',             'type' => 'Income'],
+    ['code' => '5000', 'name' => 'Cost of Goods Sold',        'type' => 'Expense'],
+    ['code' => '6000', 'name' => 'Operating Expenses',        'type' => 'Expense'],
+]);

Apply the same pattern for the down() DELETE statements (lines 187–189), replacing raw queries with $this->db->table('grants')->where('permission_id', 'accounting')->delete() etc.

As per coding guidelines: "Use parameterized queries to prevent SQL injection" (app/Database/Migrations/**/*.php).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Database/Migrations/20260427000000_AccountingModule.php` around lines 147
- 173, Replace raw SQL queries that call $this->db->query(...) and use
$this->db->prefixTable(...) with Query Builder calls: locate the inserts for
'modules', 'permissions', 'grants', 'journals', and 'accounts' and change them
to $this->db->table('modules')->insert(... ) or ->insertBatch(...) as
appropriate (use insert for single row and insertBatch for multiple rows). Do
the same in down() by replacing raw DELETE queries with
$this->db->table('grants')->where('permission_id','accounting')->delete(),
$this->db->table('permissions')->where('permission_id','accounting')->delete(),
and $this->db->table('modules')->where('module_id','accounting')->delete().
Ensure values are provided as arrays so the query builder handles prefixes and
parameterization.
app/Models/Account.php (1)

21-80: ⚡ Quick win

Missing PHP 8.1+ return type and parameter type declarations on all methods.

Per the coding guidelines, PHP 8.1+ code requires proper type declarations. None of the five public/protected methods in this model declare parameter types or return types.

♻️ Proposed fix
-public function get_info($account_id)
+public function get_info(int $account_id): object

-public function get_all()
+public function get_all(): array

-public function get_by_code($code)
+public function get_by_code(string $code): ?object

-public function exists($account_id)
+public function exists(int $account_id): bool

-public function save_value(&$account_data, $account_id = false)
+public function save_value(array &$account_data, int|false $account_id = false): bool

As per coding guidelines: "Write PHP 8.1+ compatible code with proper type declarations" (app/Models/**/*.php).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Models/Account.php` around lines 21 - 80, Update all public methods in
Account model to include PHP 8.1+ parameter and return type declarations:
annotate get_info($account_id) with a typed $account_id (int|string) and a
return type (object or specific model row class / stdClass), get_all() with
return type (ResultInterface or iterable/array as appropriate),
get_by_code(string $code): ?object, exists(int|string $account_id): bool, and
save_value(array &$account_data, int|string|false $account_id = false): bool;
ensure nullable returns use ? and imported types (or fully-qualified names) are
used consistently, and adjust any callers/tests if types change.
app/Models/Accounting_entry.php (1)

73-93: ⚡ Quick win

Items are deleted before balance validation - verify rollback restores them.

On update, existing items are deleted (lines 74-78) before the balance validation (lines 80-93). If validation fails and transRollback() is called, the transaction should restore the deleted items. However, this ordering is risky—if any code path between delete and rollback causes an exception, items could be orphaned.

Consider validating balance before deleting existing items.

Safer ordering: validate before delete
 public function save_entry(&$entry_data, &$items_data, $entry_id = false)
 {
+    // Validate items balance BEFORE starting transaction
+    $total_debit = 0.0;
+    $total_credit = 0.0;
+    
+    foreach ($items_data as $item) {
+        $total_debit += (float) $item['debit'];
+        $total_credit += (float) $item['credit'];
+    }
+    
+    if (abs($total_debit - $total_credit) > 0.01) {
+        return false;
+    }
+
     $this->db->transStart();

     // Save Header
     if (!$entry_id || !$this->exists($entry_id)) {
         // ... insert logic
     } else {
         // ... update logic
     }

     // Delete existing items for update
     if ($entry_id) {
         $builder = $this->db->table('accounting_items');
         $builder->where('entry_id', $entry_id);
         $builder->delete();
     }

-    // Validate items balance
-    $total_debit = 0.0;
-    $total_credit = 0.0;
-    
-    foreach ($items_data as $item) {
-        $total_debit += (float) $item['debit'];
-        $total_credit += (float) $item['credit'];
-    }
-    
-    if (abs($total_debit - $total_credit) > 0.01) {
-        // Unbalanced entry, rollback
-        $this->db->transRollback();
-        return false;
-    }

     // Save Items
     foreach ($items_data as $item) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Models/Accounting_entry.php` around lines 73 - 93, Existing
accounting_items are deleted prior to validating the new items' balance, which
risks orphaning rows if an exception occurs before transRollback; change the
logic in the Accounting_entry model so that the balance check on $items_data
(the loop computing $total_debit/$total_credit and the abs(...) check) runs
before any deletion of existing items for $entry_id, and only after the
validation passes perform the delete via the
$this->db->table('accounting_items')->where('entry_id', $entry_id)->delete();
ensure this delete remains inside the same DB transaction so transRollback can
restore changes if later steps fail.
app/Controllers/Accounting.php (2)

25-30: 💤 Low value

getIndex() and getReports() are nearly identical.

Both methods fetch the same data (balance_sheet, profit_loss) and render different views. Consider extracting a private helper to reduce duplication.

Also applies to: 53-58

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Controllers/Accounting.php` around lines 25 - 30, Extract the duplicated
data-fetching into a private helper inside the Accounting class: create a method
(e.g., private function renderReportView(string $view): string) that calls
$this->accounting_entry->get_balance_sheet() and
$this->accounting_entry->get_profit_loss(), builds the $data array, and returns
view($view, $data); then refactor getIndex() and getReports() to simply call
this helper with 'accounting/dashboard' and the other report view respectively
so both methods are one-liners that delegate to the new helper.

38-51: 💤 Low value

Consider encapsulating query logic in the model.

The getEntries() method contains direct database query building that would be better placed in the Accounting_entry model. This improves testability and follows CodeIgniter 4 patterns where models handle data access.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Controllers/Accounting.php` around lines 38 - 51, The getEntries()
controller currently builds the DB query directly; move that logic into the
Accounting_entry model by adding a method (e.g., getRecentEntries or findRecent)
that constructs the builder with the same select, joins (journals j, people p),
where('e.deleted', 0), orderBy('e.date','DESC') and limit(100) and returns the
result set; then update Accounting::getEntries() to instantiate or load
Accounting_entry (e.g., $this->accountingEntryModel or new Accounting_entry())
and call the new model method to get $entries and pass them to
view('accounting/manage_entries', $data). Ensure the model method uses the
proper table alias/primary key and returns getResult() so controller no longer
contains query building code.
app/Views/reports/listing.php (1)

87-110: 💤 Low value

Panel title doesn't match content scope.

The panel heading uses lang('Reports.top_50_items') but the panel also contains "Top Categories" links. Consider using a more generic title like lang('Reports.top_reports').

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Views/reports/listing.php` around lines 87 - 110, The panel heading
currently calls lang('Reports.top_50_items') but the panel also includes links
for reports_top_categories, so change the title to a more generic key (e.g.,
lang('Reports.top_reports')) to accurately reflect both items and categories;
update the call in the panel-heading h3 (replace lang('Reports.top_50_items')
with lang('Reports.top_reports')) and ensure the new language key exists in your
language files or add it if missing.
app/Database/Migrations/20260430000000_AddTopReportsPermissions.php (1)

12-21: 💤 Low value

Query builder approach would be more idiomatic.

While the static analysis SQL injection warnings are false positives (no user input), using CodeIgniter's query builder would be more consistent with the codebase patterns seen in other migrations.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Database/Migrations/20260430000000_AddTopReportsPermissions.php` around
lines 12 - 21, Replace raw SQL in AddTopReportsPermissions::up() with
CodeIgniter's query builder: use $this->db->table(...)->insert(...) (or
insertBatch for multiple rows) for the 'permissions' and 'grants' inserts
instead of $this->db->query(...). Keep using
$this->db->prefixTable('permissions') / prefixTable('grants') to resolve table
names, and perform two inserts (or batch) into the permissions table for
'reports_top_items' and 'reports_top_categories' and corresponding inserts into
the grants table for person_id 1 and menu_group 'home'.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/Controllers/Accounting.php`:
- Around line 60-74: postSaveAccount: replace deprecated
FILTER_SANITIZE_FULL_SPECIAL_CHARS with a PHP 8.1+ safe sanitizer (use esc() or
htmlspecialchars()) when building $account_data, change the $account_id ternary
to null coalescing, and add explicit validation for required fields 'code',
'name', and 'type' plus allowed enum values for 'type' before calling
$this->account->save_value; if validation fails return a JSON error with
details, otherwise proceed to call save_value and return the existing
success/failure response.

In `@app/Controllers/Reports.php`:
- Around line 1067-1070: The loop building $labels and $series can divide by
zero when $summary['total'] is zero or missing; update the foreach over
$report_data to compute a safe denominator (e.g. $denom =
!empty($summary['total']) ? (float)$summary['total'] : 0.0) and use a
conditional to set the percentage to 0 when $denom === 0, otherwise compute
round($row['total'] / $denom * 100, 2); replace the inline division in the
'meta' string construction to use this safe percentage to avoid division-by-zero
in Reports.php where $labels and $series are populated.

In `@app/Database/Migrations/20260427000000_AccountingModule.php`:
- Around line 153-154: The migration currently inserts a grant using a hardcoded
person_id of 1 which is unsafe; update the up() migration logic in
AccountingModule (the block using $this->db->query and
$this->db->prefixTable('grants')) to instead look up the real admin person_id
(for example by querying the people table for a record with the admin
username/role or highest-privilege flag) and only insert the grant for that
returned id, or if no admin is found skip the insert and emit a warning/log
entry; alternatively remove the automatic insert entirely and leave a
comment/instruction for manual setup—ensure you reference and replace the
existing INSERT statement that uses person_id = 1.

In `@app/Database/Migrations/20260428000000_AccountingMexico.php`:
- Around line 47-60: The down() migration in AccountingMexico.php reverts
account code '101.01' to code '1000' but restores the name as 'Cash' which
mismatches the original seed; update the down() method's update for code
'101.01' (the call that sets code => '1000') to set name => 'Cash and Cash
Equivalents' (matching the original seed in AccountingModule.php) so rollbacks
restore the exact seeded data for ospos_accounts.
- Around line 11-44: The migration uses $db->table('ospos_accounts') which
causes CI4 to double-prefix to ospos_ospos_accounts; replace every
$db->table('ospos_accounts') call in this migration (both up and down methods)
with $db->table('accounts') so the framework's DB prefix is applied once,
ensuring updates and inserts target the correct table; update all occurrences
including the foreach check and insert/update calls referenced in this diff.

In `@app/Database/Migrations/20260430000000_AddTopReportsPermissions.php`:
- Around line 18-20: The migration AddTopReportsPermissions currently hardcodes
person_id = 1 when inserting into the grants table; replace that brittle
approach by resolving admin user(s) dynamically before inserting: in the
migration's up() use a SELECT against the persons (or users) table to find admin
accounts (e.g. WHERE is_admin = 1 or role = 'admin'), collect their ids, and
then run the INSERT INTO ... VALUES (...) for each resolved person_id (using the
existing $this->db->query call or looping inserts). If no admin is found, skip
the inserts and write a clear log notice or throw a descriptive migration error
so the deployer can take action. Ensure you update the same
AddTopReportsPermissions migration method that performs the current INSERTs and
remove the hardcoded literal 1.

In `@app/Models/Account.php`:
- Around line 41-47: Add a return type declaration of ResultInterface to the
get_all() method: update the method signature for get_all() to declare it
returns ResultInterface, leaving the implementation (using $builder =
$this->db->table($this->table); ->where('deleted', 0); ->orderBy('code', 'asc');
return $builder->get();) unchanged so callers that call ->getResult() still
behave the same.

In `@app/Models/Expense.php`:
- Around line 285-334: create_accounting_entry currently hardcodes a 16% IVA and
ignores the expense's tax_amount and doesn't check save_entry result or declare
a return type; update create_accounting_entry to use $expense_data['tax_amount']
(treat null/zero as no tax), compute subtotal = $expense_data['amount'] -
$expense_data['tax_amount'], and compute iva_amount from that field instead of
dividing by 1.16, ensure you handle negative or missing tax_amount defensively,
check the boolean/result returned by $accounting_entry->save_entry($entry_data,
$items_data) and return that result, and add an explicit return type (e.g., :
bool) to the create_accounting_entry method signature so callers can rely on
success/failure.

In `@app/Models/Journal.php`:
- Around line 47-56: The get_by_code method can return soft-deleted records;
update get_by_code to include the same deleted filter used in get_all by adding
a condition for 'deleted = 0' on the query builder before executing it (i.e.,
when building the query on $this->table inside get_by_code), so it only returns
non-deleted rows and keeps the existing single-row return/null behavior.

In `@app/Models/Receiving.php`:
- Around line 187-246: The code currently assumes save_entry(...) always
succeeds and uses a hardcoded 16% IVA; to fix, after computing per-item totals
(in the foreach over $items) compute IVA per item (or skip IVA if item rate is
0) instead of a flat /1.16, build accurate subtotal and iva_amount from
item-level rates, then call $accounting_entry->save_entry($entry_data,
$items_data) and check its boolean return; if it returns false, throw an
exception or return false to abort the receiving flow (ensuring the outer
transaction does not commit) and log the failure including $receiving_id and the
$items_data for debugging. Ensure references: adjust the logic around
total_amount, the subtotal/iva_amount calculation, the
iva_account/payable_account entries, and the call to
$accounting_entry->save_entry to implement the check and error handling.

In `@app/Models/Sale.php`:
- Around line 674-733: The code calls $accounting_entry->save_entry(...) but
ignores its result, risking committed sales with failed accounting entries;
update the Sale save flow to capture the return value from save_entry (in
Sale.php where the ACCOUNTING MODULE INTEGRATION block runs) and handle failures
by aborting/rolling back the sale (e.g., throw an exception, return false, or
rollback DB transaction), log the error with details (include sale_id and
save_entry error), and ensure you propagate the failure up so the sale commit
does not proceed when save_entry fails; optionally adjust/save_item rounding
only after confirming the entry saved.

In `@app/Views/accounting/manage_accounts.php`:
- Around line 6-20: Replace hardcoded UI strings with language helper calls so
headers and button label are localised: change the New Account button text
(element id new_account_btn) and the table header labels "Account ID", "Code",
"Name", "Type" to use lang('...') keys (create/choose appropriate keys like
accounts_new_account, accounts_account_id, accounts_code, accounts_name,
accounts_type if missing). Ensure the view uses the same lang() helper as other
OSPOS views and update any corresponding language files to include the new keys
and translations.

In `@app/Views/reports/listing.php`:
- Around line 97-106: Replace the hardcoded Spanish label "(Gráfico)" with a
translatable string using the lang() helper where the graphical report links are
rendered (look for $graphical_link usage in listing.php and the two anchor tags
that use esc($graphical_link['label']) followed by the hardcoded "(Gráfico)").
Change those appended literal labels to use lang('...') (e.g.,
lang('Reports.graphical') or another appropriate key) and add the matching
key/value to the language files so the label is internationalized; keep the
existing esc($summary_link['label']) and esc($graphical_link['label']) usage
intact and only replace the literal parenthetical text.

In `@deploy-accounting.sh`:
- Line 62: The echo uses unquoted command substitutions causing word-splitting
for filenames with spaces; update the echo to use quoted substitutions like
"$(basename "$SRC")" and also fix the other occurrence to use "$(basename
"$file")" (and generally quote any direct variable expansions used in these echo
commands) so basename and variable values are wrapped in double quotes to
prevent splitting.
- Line 27: Update the step counter strings in the echo status messages so they
use a consistent six-step sequence ([1/6] … [6/6]) instead of the current mix of
[1/5]…[4/5] then [5/6]/[6/6]; specifically edit the echo lines that print
"${YELLOW}[1/5] Verificando..." and the other similar echo statements (the ones
at the same locations as lines printing step counters) to use [1/6], [2/6],
[3/6], [4/6], [5/6], and [6/6] respectively so the sequence is consistent across
the script.
- Around line 83-88: The deployment script is missing the copy step for the new
Top Reports migration; add a copy_file invocation similar to the others to copy
"20260430000000_AddTopReportsPermissions.php" from
"$OSPOS_DIR/app/Database/Migrations/20260430000000_AddTopReportsPermissions.php"
to
"$APP_PATH/app/Database/Migrations/20260430000000_AddTopReportsPermissions.php"
so the migration is deployed alongside the existing AccountingModule and
AccountingMexico migrations (use the same copy_file pattern and variable names
OSPOS_DIR and APP_PATH).
- Around line 177-178: Replace the hardcoded MySQL credentials in the TABLES
command with secure variables/secrets: stop using the literal "admin" and
"pointofsale" in the docker exec mysql mysql -u ... -p... invocation and instead
read credentials from environment variables or Docker secrets (e.g. MYSQL_USER
and MYSQL_PASSWORD) and pass them safely (use -u "$MYSQL_USER" and
-p"$MYSQL_PASSWORD" or source a .env prior to use); update the TABLES assignment
line that currently calls docker exec mysql mysql -u admin -ppointofsale ospos
-e ... to reference those variables and ensure proper quoting/escaping so
passwords with special characters are handled correctly.

In `@docker-compose.yml`:
- Line 18: The CI_ENVIRONMENT variable in the docker-compose service is set to
development which causes CodeIgniter to run in dev mode and may leak secrets;
change the value of CI_ENVIRONMENT back to production (i.e., set
CI_ENVIRONMENT=production) before merging to master and verify no other commit
in this compose environment block contains development or debug values (check
the environment entries for MYSQL_PASSWORD, MYSQL_USERNAME, MYSQL_DB_NAME and
ensure they remain secret/configured via secure CI or secrets store).

---

Nitpick comments:
In `@app/Controllers/Accounting.php`:
- Around line 25-30: Extract the duplicated data-fetching into a private helper
inside the Accounting class: create a method (e.g., private function
renderReportView(string $view): string) that calls
$this->accounting_entry->get_balance_sheet() and
$this->accounting_entry->get_profit_loss(), builds the $data array, and returns
view($view, $data); then refactor getIndex() and getReports() to simply call
this helper with 'accounting/dashboard' and the other report view respectively
so both methods are one-liners that delegate to the new helper.
- Around line 38-51: The getEntries() controller currently builds the DB query
directly; move that logic into the Accounting_entry model by adding a method
(e.g., getRecentEntries or findRecent) that constructs the builder with the same
select, joins (journals j, people p), where('e.deleted', 0),
orderBy('e.date','DESC') and limit(100) and returns the result set; then update
Accounting::getEntries() to instantiate or load Accounting_entry (e.g.,
$this->accountingEntryModel or new Accounting_entry()) and call the new model
method to get $entries and pass them to view('accounting/manage_entries',
$data). Ensure the model method uses the proper table alias/primary key and
returns getResult() so controller no longer contains query building code.

In `@app/Database/Migrations/20260427000000_AccountingModule.php`:
- Around line 78-145: The migration creates accounting_entries, accounting_items
and accounts relations but never adds FK constraints, so referential integrity
(accounting_items.entry_id → accounting_entries.entry_id,
accounting_items.account_id → accounts.account_id, accounting_entries.journal_id
→ journals.journal_id, accounts.parent_id → accounts.account_id) is unenforced;
fix by calling $this->forge->addForeignKey(...) after creating the respective
tables (use the same column names: entry_id, item_id/account_id, journal_id,
parent_id) with appropriate ON DELETE/ON UPDATE actions (e.g., CASCADE for
entry→items and RESTRICT for account links) and then
$this->forge->processIndexes('<table_name>') for each table, and update the
down() method to call $this->forge->dropForeignKey('<table>', '<column>') before
dropping the tables so rollbacks remove FKs cleanly.
- Around line 147-173: Replace raw SQL queries that call $this->db->query(...)
and use $this->db->prefixTable(...) with Query Builder calls: locate the inserts
for 'modules', 'permissions', 'grants', 'journals', and 'accounts' and change
them to $this->db->table('modules')->insert(... ) or ->insertBatch(...) as
appropriate (use insert for single row and insertBatch for multiple rows). Do
the same in down() by replacing raw DELETE queries with
$this->db->table('grants')->where('permission_id','accounting')->delete(),
$this->db->table('permissions')->where('permission_id','accounting')->delete(),
and $this->db->table('modules')->where('module_id','accounting')->delete().
Ensure values are provided as arrays so the query builder handles prefixes and
parameterization.

In `@app/Database/Migrations/20260430000000_AddTopReportsPermissions.php`:
- Around line 12-21: Replace raw SQL in AddTopReportsPermissions::up() with
CodeIgniter's query builder: use $this->db->table(...)->insert(...) (or
insertBatch for multiple rows) for the 'permissions' and 'grants' inserts
instead of $this->db->query(...). Keep using
$this->db->prefixTable('permissions') / prefixTable('grants') to resolve table
names, and perform two inserts (or batch) into the permissions table for
'reports_top_items' and 'reports_top_categories' and corresponding inserts into
the grants table for person_id 1 and menu_group 'home'.

In `@app/Models/Account.php`:
- Around line 21-80: Update all public methods in Account model to include PHP
8.1+ parameter and return type declarations: annotate get_info($account_id) with
a typed $account_id (int|string) and a return type (object or specific model row
class / stdClass), get_all() with return type (ResultInterface or iterable/array
as appropriate), get_by_code(string $code): ?object, exists(int|string
$account_id): bool, and save_value(array &$account_data, int|string|false
$account_id = false): bool; ensure nullable returns use ? and imported types (or
fully-qualified names) are used consistently, and adjust any callers/tests if
types change.

In `@app/Models/Accounting_entry.php`:
- Around line 73-93: Existing accounting_items are deleted prior to validating
the new items' balance, which risks orphaning rows if an exception occurs before
transRollback; change the logic in the Accounting_entry model so that the
balance check on $items_data (the loop computing $total_debit/$total_credit and
the abs(...) check) runs before any deletion of existing items for $entry_id,
and only after the validation passes perform the delete via the
$this->db->table('accounting_items')->where('entry_id', $entry_id)->delete();
ensure this delete remains inside the same DB transaction so transRollback can
restore changes if later steps fail.

In `@app/Views/reports/listing.php`:
- Around line 87-110: The panel heading currently calls
lang('Reports.top_50_items') but the panel also includes links for
reports_top_categories, so change the title to a more generic key (e.g.,
lang('Reports.top_reports')) to accurately reflect both items and categories;
update the call in the panel-heading h3 (replace lang('Reports.top_50_items')
with lang('Reports.top_reports')) and ensure the new language key exists in your
language files or add it if missing.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 53e98843-1cd3-43c1-947c-93145dfbe508

📥 Commits

Reviewing files that changed from the base of the PR and between 7edefe8 and 97b35b0.

📒 Files selected for processing (35)
  • .htaccess
  • app/Config/App.php
  • app/Controllers/Accounting.php
  • app/Controllers/Reports.php
  • app/Database/Migrations/20260427000000_AccountingModule.php
  • app/Database/Migrations/20260428000000_AccountingMexico.php
  • app/Database/Migrations/20260430000000_AddTopReportsPermissions.php
  • app/Language/en/Module.php
  • app/Language/en/Reports.php
  • app/Language/es-MX/Reports.php
  • app/Models/Account.php
  • app/Models/Accounting_entry.php
  • app/Models/Expense.php
  • app/Models/Journal.php
  • app/Models/Receiving.php
  • app/Models/Reports/Summary_categories.php
  • app/Models/Reports/Summary_items.php
  • app/Models/Sale.php
  • app/Views/accounting/dashboard.php
  • app/Views/accounting/manage_accounts.php
  • app/Views/accounting/manage_entries.php
  • app/Views/reports/listing.php
  • deploy-accounting.sh
  • docker-compose.yml
  • tools/ci4-lang-migration/convert.sh
  • tools/ci4-upgrade-language-interpolation/run_sed_scripts.sh
  • tools/ci4-upgrade-language-interpolation/sed_Attributes.sh
  • tools/ci4-upgrade-language-interpolation/sed_Bootstrap_tables.sh
  • tools/ci4-upgrade-language-interpolation/sed_Common.sh
  • tools/ci4-upgrade-language-interpolation/sed_Giftcards.sh
  • tools/ci4-upgrade-language-interpolation/sed_Items.sh
  • tools/ci4-upgrade-language-interpolation/sed_Login.sh
  • tools/ci4-upgrade-language-interpolation/sed_Sales.sh
  • tools/ci4-upgrade-language-interpolation/sed_Taxes.sh
  • tools/ci4-upgrade-language-interpolation/sed_sv_Common.sh

Comment thread app/Controllers/Accounting.php
Comment thread app/Controllers/Reports.php
Comment thread app/Database/Migrations/20260427000000_AccountingModule.php Outdated
Comment thread app/Database/Migrations/20260428000000_AccountingMexico.php Outdated
Comment thread app/Database/Migrations/20260428000000_AccountingMexico.php Outdated
Comment thread deploy-accounting.sh Outdated
Comment thread deploy-accounting.sh Outdated
Comment thread deploy-accounting.sh
Comment thread deploy-accounting.sh Outdated
Comment thread docker-compose.yml Outdated
@TribuCibernetica
Copy link
Copy Markdown
Author

I have addressed the feedback from CodeRabbit. Security issues (hardcoded credentials), migration double-prefixing, and code standards (sanitization/validation) have been fixed. Ready for a new review.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
app/Database/Migrations/20260428000000_AccountingMexico.php (1)

11-11: ⚡ Quick win

Prefer $this->db over \Config\Database::connect().

The database connection is available to you directly through $this->db in CI4 migrations. Using \Config\Database::connect() bypasses the migration's own pre-wired connection property and is non-idiomatic. Both resolve to the same default group in practice, but $this->db is the idiomatic approach and ensures consistency if a $DBGroup is ever set on this migration.

♻️ Proposed fix (applies to both `up()` and `down()`)
-        $db = \Config\Database::connect();
-
-        // 1. Update existing default accounts to SAT codes
-        $db->table('accounts')->...
+        // 1. Update existing default accounts to SAT codes
+        $this->db->table('accounts')->...

Remove the $db local variable entirely and replace all $db-> calls with $this->db->.

Also applies to: 50-50

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Database/Migrations/20260428000000_AccountingMexico.php` at line 11,
Replace the non-idiomatic manual connection in the migration by removing the
local $db = \Config\Database::connect() and use the migration's pre-wired
connection ($this->db) instead; update all occurrences of $db->... inside the
AccountingMexico class (both up() and down() methods) to $this->db->...,
ensuring you do not introduce a new local variable and that any $db references
are fully replaced.
app/Database/Migrations/20260427000000_AccountingModule.php (1)

31-32: ⚡ Quick win

Use the proper CI4 Forge ENUM syntax with a constraint array.

The CI4 Forge API requires ENUM to be declared with 'type' => 'ENUM' and 'constraint' => [...] as an array of allowed values. The current form — 'type' => 'ENUM("Asset", ...)' — embeds raw MySQL syntax in the type string, bypassing CI4's type-normalization layer. This happens to work on MySQL via raw passthrough but will break on non-MySQL databases and is non-idiomatic.

♻️ Proposed fix for both ENUM fields
-            'type' => [
-                'type' => 'ENUM("Asset", "Liability", "Equity", "Income", "Expense")',
-                'default' => 'Asset',
-            ],
+            'type' => [
+                'type'       => 'ENUM',
+                'constraint' => ['Asset', 'Liability', 'Equity', 'Income', 'Expense'],
+                'default'    => 'Asset',
+            ],
-            'type' => [
-                'type' => 'ENUM("Sale", "Purchase", "Cash", "Bank", "General")',
-                'default' => 'General',
-            ],
+            'type' => [
+                'type'       => 'ENUM',
+                'constraint' => ['Sale', 'Purchase', 'Cash', 'Bank', 'General'],
+                'default'    => 'General',
+            ],

Also applies to: 65-66

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Database/Migrations/20260427000000_AccountingModule.php` around lines 31
- 32, The migration uses raw MySQL ENUM syntax ('type' => 'ENUM("Asset",
"Liability", "Equity", "Income", "Expense")') which must be converted to CI4
Forge's ENUM form; in the AccountingModule migration replace those entries with
'type' => 'ENUM' and add 'constraint' =>
['Asset','Liability','Equity','Income','Expense'] (and do the same for the
second ENUM at the other location), so update the field definitions in the
AccountingModule class to use 'type' => 'ENUM' plus the corresponding
'constraint' array.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/Database/Migrations/20260427000000_AccountingModule.php`:
- Around line 155-156: The migration currently interpolates $admin_id into a raw
SQL string via $this->db->query; replace that with the DB query builder to use
parameterized insertion: locate the AccountingModule migration where $admin_id
is set and the subsequent $this->db->query(...) call, and change it to use the
query builder (e.g., $this->db->table('grants')->insert(...) or an equivalent
insert with bound parameters) to insert permission_id = 'accounting', person_id
= $admin_id, menu_group = 'office' so the value is passed as a parameter rather
than concatenated into SQL.

In `@app/Database/Migrations/20260428000000_AccountingMexico.php`:
- Line 9: The Migration class methods up() and down() lack PHP 8.1+ return type
hints; update the method signatures for both up() and down() in AccountingMexico
(which extends Migration) to include the : void return type so they match the
parent Migration and coding guidelines.

---

Nitpick comments:
In `@app/Database/Migrations/20260427000000_AccountingModule.php`:
- Around line 31-32: The migration uses raw MySQL ENUM syntax ('type' =>
'ENUM("Asset", "Liability", "Equity", "Income", "Expense")') which must be
converted to CI4 Forge's ENUM form; in the AccountingModule migration replace
those entries with 'type' => 'ENUM' and add 'constraint' =>
['Asset','Liability','Equity','Income','Expense'] (and do the same for the
second ENUM at the other location), so update the field definitions in the
AccountingModule class to use 'type' => 'ENUM' plus the corresponding
'constraint' array.

In `@app/Database/Migrations/20260428000000_AccountingMexico.php`:
- Line 11: Replace the non-idiomatic manual connection in the migration by
removing the local $db = \Config\Database::connect() and use the migration's
pre-wired connection ($this->db) instead; update all occurrences of $db->...
inside the AccountingMexico class (both up() and down() methods) to
$this->db->..., ensuring you do not introduce a new local variable and that any
$db references are fully replaced.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6613fc5b-1acb-4679-b45f-74fd89ac1aa0

📥 Commits

Reviewing files that changed from the base of the PR and between 97b35b0 and 2143d3c.

📒 Files selected for processing (4)
  • app/Controllers/Accounting.php
  • app/Database/Migrations/20260427000000_AccountingModule.php
  • app/Database/Migrations/20260428000000_AccountingMexico.php
  • deploy-accounting.sh
✅ Files skipped from review due to trivial changes (1)
  • app/Controllers/Accounting.php
🚧 Files skipped from review as they are similar to previous changes (1)
  • deploy-accounting.sh

Comment thread app/Database/Migrations/20260427000000_AccountingModule.php Outdated
Comment thread app/Database/Migrations/20260428000000_AccountingMexico.php Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (6)
app/Database/Migrations/20260427000000_AccountingModule.php (6)

80-113: Add secondary indexes on foreign-key/filter columns in accounting_entries and accounting_items.

accounting_entries.journal_id and accounting_items.entry_id/account_id are the primary join and filter columns for every ledger query (trial balance, per-journal reports, account drill-downs) but have no secondary indexes. These tables will grow proportionally with transaction volume; full-table scans will be the norm without them.

♻️ Proposed addition
         $this->forge->addKey('entry_id', true);
+        $this->forge->addKey('journal_id');
         $this->forge->createTable('accounting_entries');
         $this->forge->addKey('item_id', true);
+        $this->forge->addKey('entry_id');
+        $this->forge->addKey('account_id');
         $this->forge->createTable('accounting_items');

Also applies to: 116-146

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Database/Migrations/20260427000000_AccountingModule.php` around lines 80
- 113, Add non-unique secondary indexes for the join/filter columns so queries
don't trigger full-table scans: in the migration that defines the
accounting_entries table add a secondary index on journal_id (use
$this->forge->addKey('journal_id', false) before
createTable('accounting_entries')), and in the migration that defines
accounting_items add secondary indexes on entry_id and account_id (use
$this->forge->addKey('entry_id', false) and $this->forge->addKey('account_id',
false) before createTable('accounting_items')). Ensure you add these using the
same Forge API used for the primary key so the indexes are created alongside the
tables.

149-152: ⚡ Quick win

Replace remaining $this->db->query() calls with the query builder.

Lines 149, 152, 164–169, 171–180 (up()) and 194–196 (down()) still use raw SQL via $this->db->query() with prefixTable() string concatenation. While the concatenated values are all hardcoded literals (no actual injection risk), this pattern is flagged by the static analysis tool and is inconsistent with the already-fixed grant insert (lines 157–161) and with the coding guideline to use parameterized queries for migrations.

♻️ Example refactor for modules/permissions inserts and down() deletes
-        $this->db->query("INSERT INTO " . $this->db->prefixTable('modules') . " (name_lang_key, desc_lang_key, sort, module_id) VALUES ('module_accounting', 'module_accounting_desc', 90, 'accounting')");
+        $this->db->table('modules')->insert([
+            'name_lang_key' => 'module_accounting',
+            'desc_lang_key' => 'module_accounting_desc',
+            'sort'          => 90,
+            'module_id'     => 'accounting',
+        ]);

-        $this->db->query("INSERT INTO " . $this->db->prefixTable('permissions') . " (permission_id, module_id) VALUES ('accounting', 'accounting')");
+        $this->db->table('permissions')->insert([
+            'permission_id' => 'accounting',
+            'module_id'     => 'accounting',
+        ]);

For the multi-row journal and account seeds, use insertBatch():

-        $this->db->query("INSERT INTO " . $this->db->prefixTable('journals') . " (name, code, type) VALUES 
-            ('Sales Journal', 'SALES', 'Sale'),
-            ...
-        ");
+        $this->db->table('journals')->insertBatch([
+            ['name' => 'Sales Journal',    'code' => 'SALES', 'type' => 'Sale'],
+            ['name' => 'Purchase Journal', 'code' => 'PURCH', 'type' => 'Purchase'],
+            ['name' => 'Cash Journal',     'code' => 'CASH',  'type' => 'Cash'],
+            ['name' => 'General Journal',  'code' => 'GEN',   'type' => 'General'],
+        ]);
-        $this->db->query("DELETE FROM " . $this->db->prefixTable('grants') . " WHERE permission_id = 'accounting'");
-        $this->db->query("DELETE FROM " . $this->db->prefixTable('permissions') . " WHERE module_id = 'accounting'");
-        $this->db->query("DELETE FROM " . $this->db->prefixTable('modules') . " WHERE module_id = 'accounting'");
+        $this->db->table('grants')->where('permission_id', 'accounting')->delete();
+        $this->db->table('permissions')->where('module_id', 'accounting')->delete();
+        $this->db->table('modules')->where('module_id', 'accounting')->delete();

As per coding guidelines, migrations must use parameterized queries / query builder to prevent SQL injection.

Also applies to: 163-180, 194-196

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Database/Migrations/20260427000000_AccountingModule.php` around lines 149
- 152, Replace the remaining raw SQL calls using $this->db->query(...) and
prefixTable(...) inside the AccountingModule migration's up() and down() with
the query builder: use $this->db->table('modules')->insert([...]) for single-row
inserts (replace the modules insert),
$this->db->table('permissions')->insert([...]) for the permissions insert, use
insertBatch([...]) for the multi-row journal/account seed blocks, and replace
DELETE queries in down() with $this->db->table(...)->where(...)->delete() (use
parameter arrays for values). Locate usages of $this->db->query, prefixTable,
and the up()/down() methods in this migration and convert each raw SQL statement
to the corresponding query builder call with parameterized data structures.

80-146: Add indexes on foreign-key/filter columns in accounting_entries and accounting_items.

accounting_entries.journal_id and accounting_items.entry_id/account_id are high-cardinality join and filter columns with no secondary indexes. Ledger queries (e.g., trial balance, per-journal reports) will perform full-table scans as transaction volume grows.

♻️ Proposed addition
         $this->forge->addKey('entry_id', true);
+        $this->forge->addKey('journal_id');     // index for journal-filtered queries
         $this->forge->createTable('accounting_entries');
         $this->forge->addKey('item_id', true);
+        $this->forge->addKey('entry_id');       // index for entry→items joins
+        $this->forge->addKey('account_id');     // index for account-filtered reports
         $this->forge->createTable('accounting_items');
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Database/Migrations/20260427000000_AccountingModule.php` around lines 80
- 146, Add non-primary indexes on high-cardinality FK/filter columns to avoid
full-table scans: in the migration where createTable('accounting_entries') is
defined, call $this->forge->addKey('journal_id') (non-unique) before
createTable; and in the accounting_items section, call
$this->forge->addKey('entry_id') and $this->forge->addKey('account_id') (both
non-unique) before createTable('accounting_items'); locate these changes next to
the existing addKey('entry_id', true) and addKey('item_id', true) calls so the
new indexes are applied during table creation.

65-68: ⚡ Quick win

journals.type ENUM uses a raw SQL string — use the documented constraint array pattern.

The CI4 Forge documentation consistently documents ENUM fields as:

'type' => 'ENUM', 'constraint' => ['value1', 'value2', ...]

The canonical Forge example defines ENUM as 'type' => 'ENUM', 'constraint' => ['publish', 'pending', 'draft'], 'default' => 'pending' — i.e. the values go in constraint, not embedded in the type string. Additionally, starting with v4.5.0, SQLSRV Forge converts ENUM data types to VARCHAR(n), meaning ENUM has special cross-driver handling that the raw string form bypasses. The accounts.type field in the same migration (lines 31-33) uses the correct array pattern; the journals.type field (line 66) does not.

♻️ Proposed fix
-            'type' => [
-                'type' => 'ENUM("Sale", "Purchase", "Cash", "Bank", "General")',
-                'default' => 'General',
-            ],
+            'type' => [
+                'type'       => 'ENUM',
+                'constraint' => ['Sale', 'Purchase', 'Cash', 'Bank', 'General'],
+                'default'    => 'General',
+            ],
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Database/Migrations/20260427000000_AccountingModule.php` around lines 65
- 68, The journals.type field in the AccountingModule migration uses a raw SQL
ENUM string; change it to the documented Forge array pattern by replacing the
raw type string with 'type' => 'ENUM', add a 'constraint' =>
['Sale','Purchase','Cash','Bank','General'], and keep 'default' => 'General' so
it matches how accounts.type is defined and lets Forge/SQLSRV handle ENUM
conversion correctly.

149-152: ⚡ Quick win

Replace remaining $this->db->query() calls with the query builder.

Lines 149, 152, 164–169, 171–180 (up()) and 194–196 (down()) use $this->db->query() with prefixTable() string concatenation. While all concatenated values are hardcoded literals (no live injection vector), this pattern is flagged by the static analyser and is inconsistent with the already-fixed grant insert at lines 157–161 which correctly uses the query builder.

♻️ Example refactor (modules/permissions inserts and down() deletes)
-        $this->db->query("INSERT INTO " . $this->db->prefixTable('modules') . " (name_lang_key, desc_lang_key, sort, module_id) VALUES ('module_accounting', 'module_accounting_desc', 90, 'accounting')");
+        $this->db->table('modules')->insert([
+            'name_lang_key' => 'module_accounting',
+            'desc_lang_key' => 'module_accounting_desc',
+            'sort'          => 90,
+            'module_id'     => 'accounting',
+        ]);

-        $this->db->query("INSERT INTO " . $this->db->prefixTable('permissions') . " (permission_id, module_id) VALUES ('accounting', 'accounting')");
+        $this->db->table('permissions')->insert([
+            'permission_id' => 'accounting',
+            'module_id'     => 'accounting',
+        ]);

For the multi-row seeds, use insertBatch():

-        $this->db->query("INSERT INTO " . $this->db->prefixTable('journals') . " (name, code, type) VALUES 
-            ('Sales Journal', 'SALES', 'Sale'),
-            ('Purchase Journal', 'PURCH', 'Purchase'),
-            ('Cash Journal', 'CASH', 'Cash'),
-            ('General Journal', 'GEN', 'General')
-        ");
+        $this->db->table('journals')->insertBatch([
+            ['name' => 'Sales Journal',    'code' => 'SALES', 'type' => 'Sale'],
+            ['name' => 'Purchase Journal', 'code' => 'PURCH', 'type' => 'Purchase'],
+            ['name' => 'Cash Journal',     'code' => 'CASH',  'type' => 'Cash'],
+            ['name' => 'General Journal',  'code' => 'GEN',   'type' => 'General'],
+        ]);
-        $this->db->query("DELETE FROM " . $this->db->prefixTable('grants') . " WHERE permission_id = 'accounting'");
-        $this->db->query("DELETE FROM " . $this->db->prefixTable('permissions') . " WHERE module_id = 'accounting'");
-        $this->db->query("DELETE FROM " . $this->db->prefixTable('modules') . " WHERE module_id = 'accounting'");
+        $this->db->table('grants')->where('permission_id', 'accounting')->delete();
+        $this->db->table('permissions')->where('module_id', 'accounting')->delete();
+        $this->db->table('modules')->where('module_id', 'accounting')->delete();

As per coding guidelines, migrations must use parameterized queries to prevent SQL injection.

Also applies to: 163-180, 194-196

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Database/Migrations/20260427000000_AccountingModule.php` around lines 149
- 152, Replace raw $this->db->query(...) calls in up() and down() with the query
builder: use $this->db->table($this->db->prefixTable('modules'))->insert([...])
for the single-row modules insert (replace the query that inserts
'module_accounting'), use
$this->db->table($this->db->prefixTable('permissions'))->insertBatch([...]) for
the multi-row permissions seed (replace the existing $this->db->query inserts),
and in down() use
$this->db->table($this->db->prefixTable('modules'))->where('module_id',
'accounting')->delete() and similarly
$this->db->table($this->db->prefixTable('permissions'))->where(...)->delete()
for cleanup; keep hardcoded values as array elements passed to
insert/insertBatch to ensure parameterized queries.

65-68: ⚡ Quick win

ENUM field definition uses a raw SQL string — use the CI4 Forge constraint array instead.

The accounts table in the same migration (lines 30-34) correctly uses the constraint array pattern for ENUM. The journals.type field uses a raw type string with the ENUM values embedded, which bypasses CI4 Forge's internal type handling and may behave unexpectedly on non-MySQL databases or future CI4 versions.

♻️ Proposed fix
-            'type' => [
-                'type' => 'ENUM("Sale", "Purchase", "Cash", "Bank", "General")',
-                'default' => 'General',
-            ],
+            'type' => [
+                'type'       => 'ENUM',
+                'constraint' => ['Sale', 'Purchase', 'Cash', 'Bank', 'General'],
+                'default'    => 'General',
+            ],
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Database/Migrations/20260427000000_AccountingModule.php` around lines 65
- 68, The ENUM definition for the journals.type field in the AccountingModule
migration uses a raw SQL string; update it to use the Forge-style constraint
array instead: change the field definition for 'type' to use 'type' => 'ENUM',
'constraint' => ['Sale','Purchase','Cash','Bank','General'], and keep the
'default' => 'General' so CI4 Forge handles platform-specific SQL correctly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/Database/Migrations/20260428000000_AccountingMexico.php`:
- Around line 47-60: The down() currently unconditionally deletes codes '118.01'
and '208.01' even if they pre-existed; change down() to mirror the safe insert
guard in up() by only deleting those rows if they appear to have been created by
this migration (e.g., check that the account's name matches the exact name/value
used in up() or that created_at is >= the migration timestamp) before calling
delete on the accounts table; update the down() logic that references '118.01'
and '208.01' to perform that conditional check (use the same identifying values
that up() inserts) and add a brief comment explaining the assumption.

---

Nitpick comments:
In `@app/Database/Migrations/20260427000000_AccountingModule.php`:
- Around line 80-113: Add non-unique secondary indexes for the join/filter
columns so queries don't trigger full-table scans: in the migration that defines
the accounting_entries table add a secondary index on journal_id (use
$this->forge->addKey('journal_id', false) before
createTable('accounting_entries')), and in the migration that defines
accounting_items add secondary indexes on entry_id and account_id (use
$this->forge->addKey('entry_id', false) and $this->forge->addKey('account_id',
false) before createTable('accounting_items')). Ensure you add these using the
same Forge API used for the primary key so the indexes are created alongside the
tables.
- Around line 149-152: Replace the remaining raw SQL calls using
$this->db->query(...) and prefixTable(...) inside the AccountingModule
migration's up() and down() with the query builder: use
$this->db->table('modules')->insert([...]) for single-row inserts (replace the
modules insert), $this->db->table('permissions')->insert([...]) for the
permissions insert, use insertBatch([...]) for the multi-row journal/account
seed blocks, and replace DELETE queries in down() with
$this->db->table(...)->where(...)->delete() (use parameter arrays for values).
Locate usages of $this->db->query, prefixTable, and the up()/down() methods in
this migration and convert each raw SQL statement to the corresponding query
builder call with parameterized data structures.
- Around line 80-146: Add non-primary indexes on high-cardinality FK/filter
columns to avoid full-table scans: in the migration where
createTable('accounting_entries') is defined, call
$this->forge->addKey('journal_id') (non-unique) before createTable; and in the
accounting_items section, call $this->forge->addKey('entry_id') and
$this->forge->addKey('account_id') (both non-unique) before
createTable('accounting_items'); locate these changes next to the existing
addKey('entry_id', true) and addKey('item_id', true) calls so the new indexes
are applied during table creation.
- Around line 65-68: The journals.type field in the AccountingModule migration
uses a raw SQL ENUM string; change it to the documented Forge array pattern by
replacing the raw type string with 'type' => 'ENUM', add a 'constraint' =>
['Sale','Purchase','Cash','Bank','General'], and keep 'default' => 'General' so
it matches how accounts.type is defined and lets Forge/SQLSRV handle ENUM
conversion correctly.
- Around line 149-152: Replace raw $this->db->query(...) calls in up() and
down() with the query builder: use
$this->db->table($this->db->prefixTable('modules'))->insert([...]) for the
single-row modules insert (replace the query that inserts 'module_accounting'),
use $this->db->table($this->db->prefixTable('permissions'))->insertBatch([...])
for the multi-row permissions seed (replace the existing $this->db->query
inserts), and in down() use
$this->db->table($this->db->prefixTable('modules'))->where('module_id',
'accounting')->delete() and similarly
$this->db->table($this->db->prefixTable('permissions'))->where(...)->delete()
for cleanup; keep hardcoded values as array elements passed to
insert/insertBatch to ensure parameterized queries.
- Around line 65-68: The ENUM definition for the journals.type field in the
AccountingModule migration uses a raw SQL string; update it to use the
Forge-style constraint array instead: change the field definition for 'type' to
use 'type' => 'ENUM', 'constraint' =>
['Sale','Purchase','Cash','Bank','General'], and keep the 'default' => 'General'
so CI4 Forge handles platform-specific SQL correctly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6cf581ed-2c02-4a8a-91f6-6b319cbce398

📥 Commits

Reviewing files that changed from the base of the PR and between 2143d3c and 430a54b.

📒 Files selected for processing (4)
  • app/Controllers/Reports.php
  • app/Database/Migrations/20260427000000_AccountingModule.php
  • app/Database/Migrations/20260428000000_AccountingMexico.php
  • deploy-accounting.sh
🚧 Files skipped from review as they are similar to previous changes (2)
  • app/Controllers/Reports.php
  • deploy-accounting.sh

Comment thread app/Database/Migrations/20260428000000_AccountingMexico.php
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
app/Database/Migrations/20260427000000_AccountingModule.php (1)

80-150: ⚡ Quick win

Foreign key constraints missing on the two core financial tables.

accounting_entries and accounting_items declare indexes on their FK columns but don't register actual database-level FK constraints. This means:

  • accounting_items can reference non-existent entries or accounts.
  • Deleting an accounting_entries row leaves orphaned accounting_items rows silently.

The rest of the OSPOS schema enforces referential integrity via FK constraints (e.g., ospos_grants_ibfk_2 on employees.person_id). The accounting tables should be consistent with that pattern.

CI4 forge's addForeignKey() must be called before createTable():

♻️ Proposed addition for `accounting_entries` and `accounting_items`
 $this->forge->addKey('entry_id', true);
 $this->forge->addKey('journal_id');
+$this->forge->addForeignKey('journal_id', 'journals', 'journal_id', 'RESTRICT', 'RESTRICT');
 $this->forge->createTable('accounting_entries');

 // ...

 $this->forge->addKey('item_id', true);
 $this->forge->addKey('entry_id');
 $this->forge->addKey('account_id');
+$this->forge->addForeignKey('entry_id', 'accounting_entries', 'entry_id', 'CASCADE', 'CASCADE');
+$this->forge->addForeignKey('account_id', 'accounts', 'account_id', 'RESTRICT', 'RESTRICT');
 $this->forge->createTable('accounting_items');

The CASCADE on entry_id means deleting a header automatically removes its lines; RESTRICT on account_id prevents deleting an account while it has posted items.

Note: accounts.parent_id is self-referential and would require a separate ALTER TABLE after createTable('accounts') — adding it as a follow-up is acceptable.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Database/Migrations/20260427000000_AccountingModule.php` around lines 80
- 150, The migration omits DB-level foreign keys for accounting_entries and
accounting_items: add calls to $this->forge->addForeignKey before each
createTable to enforce referential integrity (e.g., on accounting_entries
addForeignKey('journal_id','journals','journal_id','RESTRICT','CASCADE') and
addForeignKey('employee_id','employees','employee_id','RESTRICT','SET NULL' or
appropriate); for accounting_items
addForeignKey('entry_id','accounting_entries','entry_id','CASCADE','CASCADE')
and addForeignKey('account_id','accounts','account_id','RESTRICT','RESTRICT') so
items cannot reference missing accounts and entries are cascaded on delete;
ensure addForeignKey calls are placed prior to
createTable('accounting_entries')/createTable('accounting_items'); note
self-referential accounts.parent_id should be added later via ALTER TABLE if
needed.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/Database/Migrations/20260427000000_AccountingModule.php`:
- Around line 206-208: In the down() method replace the three raw SQL DELETE
queries that use $this->db->query("DELETE FROM " . $this->db->prefixTable(...))
with parameterized query-builder calls on $this->db (e.g., use
$this->db->table($this->db->prefixTable('grants'))->where('permission_id',
'accounting')->delete() and similar for the 'permissions' table filtering on
module_id and the 'modules' table filtering on module_id) so the deletes use the
query builder's where binding instead of string concatenation.

---

Nitpick comments:
In `@app/Database/Migrations/20260427000000_AccountingModule.php`:
- Around line 80-150: The migration omits DB-level foreign keys for
accounting_entries and accounting_items: add calls to
$this->forge->addForeignKey before each createTable to enforce referential
integrity (e.g., on accounting_entries
addForeignKey('journal_id','journals','journal_id','RESTRICT','CASCADE') and
addForeignKey('employee_id','employees','employee_id','RESTRICT','SET NULL' or
appropriate); for accounting_items
addForeignKey('entry_id','accounting_entries','entry_id','CASCADE','CASCADE')
and addForeignKey('account_id','accounts','account_id','RESTRICT','RESTRICT') so
items cannot reference missing accounts and entries are cascaded on delete;
ensure addForeignKey calls are placed prior to
createTable('accounting_entries')/createTable('accounting_items'); note
self-referential accounts.parent_id should be added later via ALTER TABLE if
needed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: df463ecb-75b3-43ff-847c-071ba42e6c9f

📥 Commits

Reviewing files that changed from the base of the PR and between 430a54b and abd4759.

📒 Files selected for processing (2)
  • app/Database/Migrations/20260427000000_AccountingModule.php
  • app/Database/Migrations/20260428000000_AccountingMexico.php

Comment thread app/Database/Migrations/20260427000000_AccountingModule.php Outdated
@TribuCibernetica
Copy link
Copy Markdown
Author

I have addressed the feedback from CodeRabbit

@objecttothis
Copy link
Copy Markdown
Member

@TribuCibernetica can you include screenshots of the UI changes in this PR? Also, please translate the Spanish to English in the Title and Description as not all the devs speak Spanish.

Comment thread app/Config/App.php
* @var string
*/
public string $application_version = '3.4.2';
public string $application_version = '3.4.3';
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jekkos what do you think about us replacing this in the master with public string $application_version = '[DEV]'; Then as part of the release process we replace the placeholder with the version tag created during the release?


public function getEntries(): string
{
$db = \Config\Database::connect();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replace \Config\ with an import.

public function getEntries(): string
{
$db = \Config\Database::connect();
$builder = $db->table('accounting_entries e');
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

single letter variable names are more difficult to follow in the code. Let's use full word variables. entries, journals, people and employees are all much more readable in code.


public function getReports(): string
{
$data['balance_sheet'] = $this->accounting_entry->get_balance_sheet();
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All Class names must be PascalCase. All function and variable names must be camelCase. Please refactor all of them in any code you touch or create in this PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants