pdo = $pdo; $this->companyId = $company_id; } public function getPostableDocuments(string $doc_type, string $date_from = '', string $date_to = '', int $formula_id = 0): array { $doc_type = trim($doc_type); $date_from = $this->parseDate($date_from); $date_to = $this->parseDate($date_to); if (!in_array($doc_type, $this->postingTypes, true)) { throw new Exception('Invalid doc_type.'); } if (in_array($doc_type, $this->invoiceTypes, true)) { $rows = $this->getInvoicePostableDocuments($doc_type, $date_from, $date_to, $formula_id); } elseif ($doc_type === 'receipt') { $rows = $this->getReceiptPostableDocuments($date_from, $date_to); } elseif ($doc_type === 'purchase_order') { $rows = $this->getPurchaseOrderPostableDocuments($date_from, $date_to, $formula_id); } else { $rows = $this->getPaymentPostableDocuments($date_from, $date_to); } if ($formula_id > 0) { $rows = array_values(array_filter($rows, function($row) use ($formula_id) { return $row['gl_status'] == 0 || (int)$row['gl_formula_id'] === $formula_id; })); } return $rows; } public function getJournalListing(string $source_type = '', string $date_from = '', string $date_to = ''): array { $source_type = trim($source_type); $date_from = $this->parseDate($date_from); $date_to = $this->parseDate($date_to); $where = ['g.company_id = :cid', "g.source_type != 'voided'"]; $params = [':cid' => $this->companyId]; if ($source_type && in_array($source_type, $this->journalTypes, true)) { $where[] = 'g.source_type = :source_type'; $params[':source_type'] = $source_type; } if ($date_from) { $where[] = 'g.journal_date >= :date_from'; $params[':date_from'] = $date_from; } if ($date_to) { $where[] = 'g.journal_date <= :date_to'; $params[':date_to'] = $date_to; } $sql = " SELECT g.id, g.source_type, g.source_id, g.period, g.current_version, g.formula_id, COALESCE(f.formula_name, '') AS formula_name, DATE_FORMAT(g.created_at, '%d/%m/%Y %H:%i') AS posted_at, DATE_FORMAT(g.updated_at, '%d/%m/%Y %H:%i') AS updated_at, CASE g.source_type WHEN 'receipt' THEN r.receipt_number WHEN 'payment' THEN p.payment_number WHEN 'manual' THEN IF(g.reference != '', g.reference, CONCAT('MJE-', g.id)) WHEN 'purchase_order' THEN po.po_number ELSE i.invoice_number END AS doc_number, COALESCE( CASE g.source_type WHEN 'receipt' THEN cr.contact_name WHEN 'payment' THEN cp.contact_name WHEN 'manual' THEN g.description WHEN 'purchase_order' THEN cpo.contact_name ELSE ci.contact_name END, '' ) AS contact_name, COALESCE(SUM(gi.debit), 0) AS total_debit, COALESCE(SUM(gi.credit), 0) AS total_credit FROM td_gl g LEFT JOIN md_account_formula f ON f.company_id = g.company_id AND f.id = g.formula_id LEFT JOIN td_gl_item gi ON gi.company_id = g.company_id AND gi.gl_id = g.id LEFT JOIN td_invoice i ON g.source_type NOT IN ('receipt','payment','manual','purchase_order') AND i.company_id = g.company_id AND i.id = g.source_id LEFT JOIN md_contact ci ON ci.company_id = g.company_id AND ci.id = i.contact_id LEFT JOIN td_receipt r ON g.source_type = 'receipt' AND r.company_id = g.company_id AND r.id = g.source_id LEFT JOIN md_contact cr ON cr.company_id = g.company_id AND cr.id = r.contact_id LEFT JOIN td_payment p ON g.source_type = 'payment' AND p.company_id = g.company_id AND p.id = g.source_id LEFT JOIN md_contact cp ON cp.company_id = g.company_id AND cp.id = p.contact_id LEFT JOIN td_purchase_order po ON g.source_type = 'purchase_order' AND po.company_id = g.company_id AND po.id = g.source_id LEFT JOIN md_contact cpo ON cpo.company_id = g.company_id AND cpo.id = po.contact_id WHERE " . implode(' AND ', $where) . " GROUP BY g.id, g.source_type, g.source_id, g.reference, g.description, g.period, g.current_version, g.formula_id, f.formula_name, g.created_at, g.updated_at, i.invoice_number, r.receipt_number, p.payment_number, po.po_number, ci.contact_name, cr.contact_name, cp.contact_name, cpo.contact_name ORDER BY g.created_at DESC, g.id DESC "; $sth = $this->pdo->prepare($sql); $sth->execute($params); return $sth->fetchAll(PDO::FETCH_ASSOC); } public function getJournalDetail(int $gl_id): array { if ($gl_id <= 0) { throw new Exception('gl_id is required.'); } $sth = $this->pdo->prepare( "SELECT g.*, DATE_FORMAT(g.created_at, '%d/%m/%Y %H:%i') AS posted_at, DATE_FORMAT(g.updated_at, '%d/%m/%Y %H:%i') AS updated_at_fmt, DATE_FORMAT(g.journal_date, '%d/%m/%Y') AS journal_date_fmt, COALESCE(f.formula_name, '') AS formula_name FROM td_gl g LEFT JOIN md_account_formula f ON f.company_id = g.company_id AND f.id = g.formula_id WHERE g.company_id = :cid AND g.id = :gl_id LIMIT 1" ); $sth->execute([':cid' => $this->companyId, ':gl_id' => $gl_id]); $header = $sth->fetch(PDO::FETCH_ASSOC); if (!$header) { throw new Exception('GL entry not found.'); } $sth = $this->pdo->prepare( "SELECT gi.account_code, gi.department_id, gi.debit, gi.credit, gi.description, COALESCE(a.account_name, '') AS account_name, COALESCE(d.dept_code, '') AS dept_code, COALESCE(d.dept_name, '') AS dept_name FROM td_gl_item gi LEFT JOIN md_account a ON a.company_id = gi.company_id AND a.account_code = gi.account_code LEFT JOIN md_department d ON d.company_id = gi.company_id AND d.id = gi.department_id WHERE gi.company_id = :cid AND gi.gl_id = :gl_id ORDER BY gi.id ASC" ); $sth->execute([':cid' => $this->companyId, ':gl_id' => $gl_id]); return [ 'header' => $header, 'lines' => $sth->fetchAll(PDO::FETCH_ASSOC), ]; } private function getInvoicePostableDocuments(string $doc_type, string $date_from, string $date_to, int $formula_id): array { $requires_product_mapping = $this->formulaRequiresProductMapping($doc_type, $formula_id); $mapping_column = in_array($doc_type, ['invoice', 'credit_note'], true) ? 'sales_account_code' : 'purchase_account_code'; $mapping_select = $requires_product_mapping ? ", ( SELECT COUNT(*) FROM td_invoice_item ii LEFT JOIN md_product p ON p.company_id = ii.company_id AND p.sku = ii.product_sku WHERE ii.company_id = i.company_id AND ii.invoice_id = i.id AND ABS(ii.total_price) > 0.0000001 AND COALESCE(NULLIF(p.{$mapping_column}, ''), '') = '' ) AS product_mapping_missing" : ", 0 AS product_mapping_missing"; $where = ['i.company_id = :cid', 'i.doc_type = :doc_type', 'i.status != 4']; $params = [':cid' => $this->companyId, ':doc_type' => $doc_type, ':source_type' => $doc_type]; if ($date_from) { $where[] = 'i.issued_date >= :date_from'; $params[':date_from'] = $date_from; } if ($date_to) { $where[] = 'i.issued_date <= :date_to'; $params[':date_to'] = $date_to; } $sth = $this->pdo->prepare( "SELECT i.id, i.invoice_number AS doc_number, COALESCE(c.contact_name, '') AS contact_name, i.grand_total, i.issued_date AS doc_date, IF(g.id IS NULL, 0, 1) AS gl_status, COALESCE(g.formula_id, 0) AS gl_formula_id, " . ($requires_product_mapping ? '1' : '0') . " AS product_mapping_checked {$mapping_select} FROM td_invoice i LEFT JOIN md_contact c ON c.company_id = i.company_id AND c.id = i.contact_id LEFT JOIN td_gl g ON g.company_id = i.company_id AND g.source_type = :source_type AND g.source_id = i.id WHERE " . implode(' AND ', $where) . " ORDER BY i.issued_date DESC, i.id DESC" ); $sth->execute($params); return $sth->fetchAll(PDO::FETCH_ASSOC); } private function getReceiptPostableDocuments(string $date_from, string $date_to): array { $where = ['r.company_id = :cid', 'r.status = 1']; $params = [':cid' => $this->companyId]; if ($date_from) { $where[] = 'r.receipt_date >= :date_from'; $params[':date_from'] = $date_from; } if ($date_to) { $where[] = 'r.receipt_date <= :date_to'; $params[':date_to'] = $date_to; } $sth = $this->pdo->prepare( "SELECT r.id, r.receipt_number AS doc_number, COALESCE(c.contact_name, '') AS contact_name, r.amount AS grand_total, r.receipt_date AS doc_date, IF(g.id IS NULL, 0, 1) AS gl_status, COALESCE(g.formula_id, 0) AS gl_formula_id, 0 AS product_mapping_checked, 0 AS product_mapping_missing FROM td_receipt r LEFT JOIN md_contact c ON c.company_id = r.company_id AND c.id = r.contact_id LEFT JOIN td_gl g ON g.company_id = r.company_id AND g.source_type = 'receipt' AND g.source_id = r.id WHERE " . implode(' AND ', $where) . " ORDER BY r.receipt_date DESC, r.id DESC" ); $sth->execute($params); return $sth->fetchAll(PDO::FETCH_ASSOC); } private function getPaymentPostableDocuments(string $date_from, string $date_to): array { $where = ['r.company_id = :cid', 'r.status = 1']; $params = [':cid' => $this->companyId]; if ($date_from) { $where[] = 'r.payment_date >= :date_from'; $params[':date_from'] = $date_from; } if ($date_to) { $where[] = 'r.payment_date <= :date_to'; $params[':date_to'] = $date_to; } $sth = $this->pdo->prepare( "SELECT r.id, r.payment_number AS doc_number, COALESCE(c.contact_name, '') AS contact_name, r.amount AS grand_total, r.payment_date AS doc_date, IF(g.id IS NULL, 0, 1) AS gl_status, COALESCE(g.formula_id, 0) AS gl_formula_id, 0 AS product_mapping_checked, 0 AS product_mapping_missing FROM td_payment r LEFT JOIN md_contact c ON c.company_id = r.company_id AND c.id = r.contact_id LEFT JOIN td_gl g ON g.company_id = r.company_id AND g.source_type = 'payment' AND g.source_id = r.id WHERE " . implode(' AND ', $where) . " ORDER BY r.payment_date DESC, r.id DESC" ); $sth->execute($params); return $sth->fetchAll(PDO::FETCH_ASSOC); } private function getPurchaseOrderPostableDocuments(string $date_from, string $date_to, int $formula_id): array { $requires_mapping = $this->formulaRequiresProductMapping('purchase_order', $formula_id); $mapping_select = $requires_mapping ? ", ( SELECT COUNT(*) FROM td_purchase_order_item poi LEFT JOIN md_product p ON p.company_id = poi.company_id AND p.sku = poi.product_sku WHERE poi.company_id = po.company_id AND poi.order_id = po.id AND ABS(poi.total_price) > 0.0000001 AND COALESCE(NULLIF(p.purchase_account_code, ''), '') = '' ) AS product_mapping_missing" : ", 0 AS product_mapping_missing"; $where = ['po.company_id = :cid', 'po.status = 1']; $params = [':cid' => $this->companyId]; if ($date_from) { $where[] = 'po.po_date >= :date_from'; $params[':date_from'] = $date_from; } if ($date_to) { $where[] = 'po.po_date <= :date_to'; $params[':date_to'] = $date_to; } $sth = $this->pdo->prepare( "SELECT po.id, po.po_number AS doc_number, COALESCE(c.contact_name, '') AS contact_name, po.grand_total, po.po_date AS doc_date, IF(g.id IS NULL, 0, 1) AS gl_status, COALESCE(g.formula_id, 0) AS gl_formula_id, " . ($requires_mapping ? '1' : '0') . " AS product_mapping_checked {$mapping_select} FROM td_purchase_order po LEFT JOIN md_contact c ON c.company_id = po.company_id AND c.id = po.contact_id LEFT JOIN td_gl g ON g.company_id = po.company_id AND g.source_type = 'purchase_order' AND g.source_id = po.id WHERE " . implode(' AND ', $where) . " ORDER BY po.po_date DESC, po.id DESC" ); $sth->execute($params); return $sth->fetchAll(PDO::FETCH_ASSOC); } private function formulaRequiresProductMapping(string $doc_type, int $formula_id): bool { if ($formula_id <= 0 || !in_array($doc_type, $this->mappingTypes, true)) { return false; } $sth = $this->pdo->prepare( "SELECT COUNT(*) FROM md_account_formula_item fi JOIN md_account_formula f ON f.company_id = fi.company_id AND f.id = fi.formula_id WHERE fi.company_id = :cid AND fi.formula_id = :formula_id AND f.document_type = :doc_type AND fi.amount_key = 'total' AND f.status = 1" ); $sth->execute([ ':cid' => $this->companyId, ':formula_id' => $formula_id, ':doc_type' => $doc_type, ]); return ((int)$sth->fetchColumn()) > 0; } private function parseDate(string $value): string { $value = trim($value); if ($value === '') return ''; if (preg_match('#^(\d{2})/(\d{2})/(\d{4})$#', $value, $m)) { return "{$m[3]}-{$m[2]}-{$m[1]}"; } return $value; } } ?>