db = $db; $this->entity = $entity; // Ensure the config table exists $this->ensureConfigTableExists(); } /** * Ensure the declarationtva_config table exists * * @return bool Success */ private function ensureConfigTableExists() { // Check if table exists $sql = "SHOW TABLES LIKE '" . MAIN_DB_PREFIX . "declarationtva_config'"; $result = $this->db->query($sql); if (!$result || $this->db->num_rows($result) == 0) { // Table doesn't exist, create it $create_sql = "CREATE TABLE IF NOT EXISTS `" . MAIN_DB_PREFIX . "declarationtva_config` ( `rowid` int(11) NOT NULL AUTO_INCREMENT, `entity` int(11) NOT NULL DEFAULT 1, `config_key` varchar(64) NOT NULL, `config_value` text, `created_date` datetime DEFAULT NULL, PRIMARY KEY (`rowid`), UNIQUE KEY `uk_config_entity_key` (`entity`, `config_key`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci"; $create_result = $this->db->query($create_sql); if (!$create_result) { return false; } // Insert default configuration values $this->insertDefaultConfigValues(); } return true; } /** * Insert default configuration values * * @return bool Success */ private function insertDefaultConfigValues() { $default_configs = array( 'module_version' => '2.4.0', 'amount_calc_amount_calculation_method' => 'truncate', 'auto_create_auto_create_accounting' => '0', 'bank_bank_account' => '0', 'journal_vat_to_pay' => '44551', 'journal_vat_to_receive' => '44567', 'journal_other_charges' => '658', 'journal_other_products' => '758' ); foreach ($default_configs as $key => $value) { $sql = "INSERT IGNORE INTO " . MAIN_DB_PREFIX . "declarationtva_config (entity, config_key, config_value, created_date) VALUES (" . $this->entity . ", '" . $this->db->escape($key) . "', '" . $this->db->escape($value) . "', NOW())"; $this->db->query($sql); } return true; } /** * Get configuration value * * @param string $key Configuration key * @param mixed $default Default value * @return mixed Configuration value */ public function get($key, $default = null) { $sql = "SELECT config_value FROM " . MAIN_DB_PREFIX . "declarationtva_config WHERE entity = " . $this->entity . " AND config_key = '" . $this->db->escape($key) . "'"; $result = $this->db->query($sql); if ($result && $this->db->num_rows($result) > 0) { $obj = $this->db->fetch_object($result); return $obj->config_value; } return $default; } /** * Set configuration value * * @param string $key Configuration key * @param mixed $value Configuration value * @return bool Success */ public function set($key, $value) { $sql = "INSERT INTO " . MAIN_DB_PREFIX . "declarationtva_config (entity, config_key, config_value, created_date) VALUES (" . $this->entity . ", '" . $this->db->escape($key) . "', '" . $this->db->escape($value) . "', NOW()) ON DUPLICATE KEY UPDATE config_value = '" . $this->db->escape($value) . "'"; $result = $this->db->query($sql); return $result !== false; } /** * Get account mapping for a CA-3 line * * @param string $ca3_line CA-3 line code * @return array|false Account mapping or false if not found */ public function getAccountMapping($ca3_line) { $sql = "SELECT * FROM " . MAIN_DB_PREFIX . "declarationtva_account_mappings WHERE entity = " . $this->entity . " AND ca3_line = '" . $this->db->escape($ca3_line) . "'"; $result = $this->db->query($sql); if ($result && $this->db->num_rows($result) > 0) { return $this->db->fetch_array($result); } return false; } /** * Update account mapping for multiple accounts * * @param string $ca3_line CA-3 line code * @param array $account_codes Array of account codes * @return bool Success */ public function updateAccountMapping($ca3_line, $account_codes) { // First, deactivate all existing mappings for this CA-3 line $sql = "UPDATE " . MAIN_DB_PREFIX . "declarationtva_account_mappings SET is_active = 0 WHERE entity = " . $this->entity . " AND ca3_line = '" . $this->db->escape($ca3_line) . "'"; $this->db->query($sql); // Then insert/activate new mappings (only if account_codes is not empty) if (!empty($account_codes) && is_array($account_codes)) { foreach ($account_codes as $account_code) { if (!empty($account_code)) { // Check if mapping already exists (active or inactive) $check_sql = "SELECT rowid, is_active FROM " . MAIN_DB_PREFIX . "declarationtva_account_mappings WHERE entity = " . $this->entity . " AND ca3_line = '" . $this->db->escape($ca3_line) . "' AND account_code = '" . $this->db->escape($account_code) . "'"; $check_result = $this->db->query($check_sql); if ($check_result && $this->db->num_rows($check_result) > 0) { // Update existing mapping (reactivate it) $sql = "UPDATE " . MAIN_DB_PREFIX . "declarationtva_account_mappings SET is_active = 1 WHERE entity = " . $this->entity . " AND ca3_line = '" . $this->db->escape($ca3_line) . "' AND account_code = '" . $this->db->escape($account_code) . "'"; $this->db->query($sql); } else { // Insert new mapping $sql = "INSERT INTO " . MAIN_DB_PREFIX . "declarationtva_account_mappings (entity, ca3_line, account_code, account_label, vat_rate, is_active, created_date) VALUES (" . $this->entity . ", '" . $this->db->escape($ca3_line) . "', '" . $this->db->escape($account_code) . "', '', 0, 1, NOW())"; $this->db->query($sql); } } } } // If account_codes is empty, all mappings for this line remain deactivated // This ensures that clearing all selections properly saves the empty state return true; } /** * Get all account mappings * * @return array Account mappings */ public function getAllAccountMappings() { $sql = "SELECT * FROM " . MAIN_DB_PREFIX . "declarationtva_account_mappings WHERE entity = " . $this->entity . " AND is_active = 1 ORDER BY ca3_line"; $result = $this->db->query($sql); $mappings = array(); if ($result) { while ($obj = $this->db->fetch_object($result)) { $mappings[] = array( 'rowid' => $obj->rowid, 'ca3_line' => $obj->ca3_line, 'account_code' => $obj->account_code, 'account_label' => $obj->account_label, 'vat_rate' => $obj->vat_rate, 'is_active' => $obj->is_active ); } } return $mappings; } /** * Get account mappings grouped by CA-3 line * * @return array Account mappings grouped by CA-3 line */ public function getAccountMappingsByLine() { $sql = "SELECT ca3_line, account_code FROM " . MAIN_DB_PREFIX . "declarationtva_account_mappings WHERE entity = " . $this->entity . " AND is_active = 1 ORDER BY ca3_line, account_code"; $result = $this->db->query($sql); $mappings = array(); if ($result) { while ($obj = $this->db->fetch_object($result)) { if (!isset($mappings[$obj->ca3_line])) { $mappings[$obj->ca3_line] = array(); } $mappings[$obj->ca3_line][] = $obj->account_code; } } return $mappings; } /** * Get available accounting accounts from Dolibarr * * @return array Accounting accounts */ public function getAccountingAccounts() { // Use the same approach as in the view page - check how it's done there // Order by rowid DESC to get the most recent account for each account number $sql = "SELECT a.account_number, a.label FROM " . MAIN_DB_PREFIX . "accounting_account a WHERE a.entity = " . $this->entity . " ORDER BY a.account_number, a.rowid DESC"; $result = $this->db->query($sql); $accounts = array(); if ($result) { while ($obj = $this->db->fetch_object($result)) { $accounts[] = array( 'account_number' => $obj->account_number, 'label' => $obj->label ); } } return $accounts; } /** * Get auto-create accounting configuration * * @return array Auto-create accounting configuration */ public function getAutoCreateAccountingConfiguration() { $default_config = array( 'auto_create_accounting' => 0 // Disabled by default ); $config = array(); foreach ($default_config as $key => $default_value) { $config[$key] = $this->get('auto_create_' . $key, $default_value); } return $config; } /** * Get amount calculation configuration * * @return array Amount calculation configuration */ public function getAmountCalculationConfiguration() { $default_config = array( 'amount_calculation_method' => 'truncate' // 'round' or 'truncate' ); $config = array(); foreach ($default_config as $key => $default_value) { $config[$key] = $this->get('amount_calc_' . $key, $default_value); } return $config; } /** * Update amount calculation configuration * * @param string $method Calculation method ('round' or 'truncate') * @return bool Success */ public function updateAmountCalculationConfiguration($method) { if (!in_array($method, array('round', 'truncate'))) { return false; } return $this->set('amount_calc_amount_calculation_method', $method); } /** * Update auto-create accounting configuration * * @param int $auto_create_accounting Auto-create accounting flag (0 or 1) * @return bool Success */ public function updateAutoCreateAccountingConfiguration($auto_create_accounting) { return $this->set('auto_create_auto_create_accounting', (int)$auto_create_accounting); } /** * Get bank account configuration * * @return array Bank account configuration */ public function getBankAccountConfiguration() { $default_config = array( 'bank_account' => 0 // No bank account selected by default ); $config = array(); foreach ($default_config as $key => $default_value) { $value = $this->get('bank_' . $key, $default_value); $config[$key] = $value; } return $config; } /** * Update bank account configuration * * @param int $bank_account_id Bank account ID * @return bool Success */ public function updateBankAccountConfiguration($bank_account_id) { $result = $this->set('bank_bank_account', (int)$bank_account_id); return $result; } /** * Get available bank accounts * * @return array Bank accounts */ public function getBankAccounts() { $sql = "SELECT rowid, label, number, bank FROM " . MAIN_DB_PREFIX . "bank_account WHERE entity = " . $this->entity . " AND clos = 0 ORDER BY label"; $result = $this->db->query($sql); $accounts = array(); if ($result) { while ($obj = $this->db->fetch_object($result)) { $accounts[] = array( 'rowid' => $obj->rowid, 'label' => $obj->label, 'number' => $obj->number, 'bank' => $obj->bank ); } } return $accounts; } /** * Get CA-3 line definitions (Notice 4722 - Latest Official Structure) * * @return array CA-3 line definitions */ public function getCA3LineDefinitions() { return array( // A. Opérations imposables (Taxable Operations) - Notice 4722 'A1' => array( 'label' => 'Ventes, prestations de services', 'type' => 'base', 'section' => 'A', 'description' => 'Ventes, prestations de services', 'pcg_accounts' => 'Sales: 7xxxx; VAT: 44571x / 44572x / 44573x / 44574x' ), 'A2' => array( 'label' => 'Autres opérations imposables', 'type' => 'base', 'section' => 'A', 'description' => 'Autres opérations imposables', 'pcg_accounts' => '775xxx (gains on disposal), 72xxx (production immobilisée), VAT: 44571x' ), 'A3' => array( 'label' => 'Achats de prestations de services réalisés auprès d\'un assujetti non établi en France (article 283-2 du code général des impôts)', 'type' => 'base', 'section' => 'A', 'description' => 'Achats de prestations de services réalisés auprès d\'un assujetti non établi en France (article 283-2 du code général des impôts)', 'pcg_accounts' => '6xxxx (services purchased), 4452xxx (TVA due autoliquidée)' ), 'A4' => array( 'label' => 'Importations (autres que les produits pétroliers)', 'type' => 'base', 'section' => 'A', 'description' => 'Importations (autres que les produits pétroliers)', 'pcg_accounts' => '6xxxx / 2xxxx purchases or assets; VAT: 4452xxx (import VAT)' ), 'A5' => array( 'label' => 'Sorties de régime fiscal suspensif (autres que les produits pétroliers) et sorties de régime particulier douanier uniquement lorsque des livraisons ont eu lieu en cours de régime', 'type' => 'base', 'section' => 'A', 'description' => 'Sorties de régime fiscal suspensif (autres que les produits pétroliers) et sorties de régime particulier douanier uniquement lorsque des livraisons ont eu lieu en cours de régime', 'pcg_accounts' => 'Specific 6xxxx/2xxxx depending on goods; VAT: 4452xxx' ), // B. Décompte de la TVA due (VAT Due Calculation) - Notice 4722 '08' => array( 'label' => 'TVA due au taux de 20% (Base HT + TVA)', 'type' => 'base_vat', 'section' => 'B', 'description' => 'Base HT and VAT amounts due at 20% rate', 'pcg_accounts' => 'Base: 7xxxx (Sales); VAT: 44571x (TVA collectée à 20%)' ), '09' => array( 'label' => 'TVA due au taux de 10% (Base HT + TVA)', 'type' => 'base_vat', 'section' => 'B', 'description' => 'Base HT and VAT amounts due at 10% rate', 'pcg_accounts' => 'Base: 7xxxx (Sales); VAT: 44572x (TVA collectée à 10%)' ), '9B' => array( 'label' => 'TVA due aux taux réduits (Base HT + TVA)', 'type' => 'base_vat', 'section' => 'B', 'description' => 'Base HT and VAT amounts due at reduced rates (5,5% and 2,1%)', 'pcg_accounts' => 'Base: 7xxxx (Sales); VAT: 44573x (5,5%) / 44574x (2,1%)' ), '17' => array( 'label' => 'TVA due au titre des acquisitions intracommunautaires (autoliquidation)', 'type' => 'vat', 'section' => 'B', 'description' => 'VAT due on intra-EU acquisitions (autoliquidation)', 'pcg_accounts' => '4452xxx (TVA due intracommunautaire)' ), '18' => array( 'label' => 'Dont TVA sur opérations à destination de Monaco', 'type' => 'vat', 'section' => 'B', 'description' => 'VAT on operations to Monaco', 'pcg_accounts' => '4452xxx (TVA due Monaco)' ), '19' => array( 'label' => 'Biens constituant des immobilisations', 'type' => 'vat', 'section' => 'B', 'description' => 'Goods constituting fixed assets', 'pcg_accounts' => '44562x (TVA déductible immobilisations)' ), '20' => array( 'label' => 'Autres biens et services', 'type' => 'vat', 'section' => 'B', 'description' => 'Other goods and services', 'pcg_accounts' => '44566x (TVA déductible autres)' ), // Additional deductible VAT lines '21' => array( 'label' => 'Autre TVA à déduire', 'type' => 'vat', 'section' => 'B', 'description' => 'Other deductible VAT', 'pcg_accounts' => '44566x (Autre TVA à déduire)' ), '22' => array( 'label' => 'Report du crédit apparaissant ligne 27 de la précédente déclaration', 'type' => 'vat', 'section' => 'B', 'description' => 'Carry forward of credit appearing on line 27 of the previous declaration', 'pcg_accounts' => '44567x (Report crédit TVA)' ), // E. Export and Non-Taxable Operations (part of Section A) - Notice 4722 'E1' => array( 'label' => 'Exportations hors UE', 'type' => 'base', 'section' => 'A', 'description' => 'Exportations hors UE', 'pcg_accounts' => '7xxxx (exports), 4458xxx (TVA export)' ), 'E2' => array( 'label' => 'Autres opérations non imposables', 'type' => 'base', 'section' => 'A', 'description' => 'Autres opérations non imposables', 'pcg_accounts' => '7xxxx (exempt sales), 4458xxx (TVA exempt)' ), 'E3' => array( 'label' => 'Ventes à distance taxables dans un autre État membre au profit des personnes non assujetties – Ventes B to C', 'type' => 'base', 'section' => 'A', 'description' => 'Ventes à distance taxables dans un autre État membre au profit des personnes non assujetties – Ventes B to C', 'pcg_accounts' => '7xxxx (distance sales), 4458xxx (TVA distance)' ), 'E4' => array( 'label' => 'Importations (autres que les produits pétroliers)', 'type' => 'base', 'section' => 'A', 'description' => 'Importations (autres que les produits pétroliers)', 'pcg_accounts' => '6xxxx (imports), 4452xxx (TVA import)' ), 'E5' => array( 'label' => 'Sorties de régime fiscal suspensif (autres que les produits pétroliers)', 'type' => 'base', 'section' => 'A', 'description' => 'Sorties de régime fiscal suspensif (autres que les produits pétroliers)', 'pcg_accounts' => '6xxxx (suspensive regime exits), 4452xxx (TVA suspensive)' ), 'E6' => array( 'label' => 'Importations placées sous régime fiscal suspensif (autres que les produits pétroliers)', 'type' => 'base', 'section' => 'A', 'description' => 'Importations placées sous régime fiscal suspensif (autres que les produits pétroliers)', 'pcg_accounts' => '6xxxx (suspensive regime imports), 4452xxx (TVA suspensive)' ), // F. Intracom Operations (part of Section A) - Notice 4722 'F1' => array( 'label' => 'Acquisitions intracommunautaires', 'type' => 'base', 'section' => 'A', 'description' => 'Acquisitions intracommunautaires', 'pcg_accounts' => '6xxxx (purchases from EU), 2xxxx (assets from EU)' ), 'F2' => array( 'label' => 'Livraisons intracommunautaires à destination d\'une personne assujettie – Ventes B to B', 'type' => 'vat', 'section' => 'A', 'description' => 'Livraisons intracommunautaires à destination d\'une personne assujettie – Ventes B to B', 'pcg_accounts' => '7xxxx (sales to EU), 4452xxx (TVA due autoliquidée sur livraisons intra-EU)' ), 'F6' => array( 'label' => 'Achats en franchise', 'type' => 'base', 'section' => 'A', 'description' => 'Achats en franchise', 'pcg_accounts' => '6xxxx (franchise purchases), 4458xxx (TVA franchise)' ), 'F7' => array( 'label' => 'Ventes de biens ou prestations de services réalisées par un assujetti non établi en France (article 283-1 du code général des impôts)', 'type' => 'base', 'section' => 'A', 'description' => 'Ventes de biens ou prestations de services réalisées par un assujetti non établi en France (article 283-1 du code général des impôts)', 'pcg_accounts' => '7xxxx (non-established sales), 4452xxx (TVA non-established)' ), 'F8' => array( 'label' => 'Régularisations (important : cf. notice)', 'type' => 'base', 'section' => 'A', 'description' => 'Régularisations (important : cf. notice)', 'pcg_accounts' => 'Various (adjustments), 445xxx (TVA adjustments)' ), // D. Résultat (Result) - Notice 4722 '25' => array( 'label' => 'TVA brute due (Total TVA due)', 'type' => 'vat', 'section' => 'D', 'description' => 'Total VAT due (sum of all VAT due amounts)', 'pcg_accounts' => 'Sum of 08 + 09 + 9B + 17' ), '26' => array( 'label' => 'TVA déductible totale (Total deductible VAT)', 'type' => 'vat', 'section' => 'D', 'description' => 'Total deductible VAT (sum of all deductible VAT amounts)', 'pcg_accounts' => 'Sum of 20 + 21 + 22' ), '28' => array( 'label' => 'TVA nette à payer', 'type' => 'vat', 'section' => 'D', 'description' => 'Net VAT to pay (if total due > total deductible)', 'pcg_accounts' => '445510 (État - TVA à décaisser)' ), '29' => array( 'label' => 'Crédit de TVA à reporter ou remboursement', 'type' => 'vat', 'section' => 'D', 'description' => 'VAT credit to carry forward or refund (if total deductible > total due)', 'pcg_accounts' => '445670 (État - Crédit de TVA à reporter ou rembourser)' ) ); } /** * Get CA-3 section headers (Notice 4722) * * @return array Section headers */ public function getCA3SectionHeaders() { return array( 'A' => array( 'title' => 'A. MONTANT DES OPÉRATIONS RÉALISÉES', 'description' => '', 'notice' => '' ), 'B' => array( 'title' => 'B. DÉCOMPTE DE LA TVA À PAYER', 'description' => '', 'notice' => '' ) ); } /** * Get supported VAT rates * * @return array VAT rates */ public function getVATRates() { return array( '20.00' => '20%', '10.00' => '10%', '5.50' => '5.5%', '2.10' => '2.1%', '0.00' => '0%' ); } /** * Validate account mapping * * @param string $ca3_line CA-3 line code * @param string $account_code Account code * @return bool Valid */ public function validateAccountMapping($ca3_line, $account_code) { // Check if account exists in Dolibarr $sql = "SELECT COUNT(*) as count FROM " . MAIN_DB_PREFIX . "accounting_account WHERE account_number = '" . $this->db->escape($account_code) . "' AND active = 1"; $result = $this->db->query($sql); if ($result && $this->db->num_rows($result) > 0) { $obj = $this->db->fetch_object($result); return $obj->count > 0; } return false; } /** * Get journal configuration * * @return array Journal configuration */ public function getJournalConfiguration() { $default_config = array( 'vat_to_pay' => '44551', // TVA A DECAISSER 'vat_to_receive' => '44567', // TVA A PAYER 'other_charges' => '658', // AUTRES CHARGES DE GESTION COURANTE 'other_products' => '758' // AUTRES PRODUITS DE GESTION COURANT ); $config = array(); foreach ($default_config as $key => $default_value) { $config[$key] = $this->get('journal_' . $key, $default_value); } return $config; } /** * Update journal configuration * * @param array $journal_accounts Journal account configuration * @return bool Success */ public function updateJournalConfiguration($journal_accounts) { $success = true; foreach ($journal_accounts as $key => $account_code) { if (!$this->set('journal_' . $key, $account_code)) { $success = false; } } return $success; } }