load->database(); $this->load->model('DynamicModel', 'dm'); if (!$this->session->userdata('logged_in')) { redirect('auth'); } } // ========================= // VIEW // ========================= public function paid() { $data = ["active_menu" => "invoice_paid"]; $this->load->view('partials/header', $data); $this->load->view('invoices/layout'); $this->load->view('partials/footer'); } public function draft() { $data = ["active_menu" => "invoice_draft"]; $this->load->view('partials/header', $data); $this->load->view('invoices/layout'); $this->load->view('partials/footer'); } // ========================= // GET DATA (FILTER STATUS) // ========================= public function get_data($status = null) { $this->db->select(' invoices.id, invoices.customer_id, invoices.no_invoice, invoices.tanggal, invoices.jatuh_tempo, invoices.total, invoices.total_bayar, invoices.sisa_piutang, invoices.status, customers.nama '); $this->db->from('invoices'); $this->db->join('customers','customers.id = invoices.customer_id'); // ========================= // FILTER STATUS // ========================= if ($status) { if($status == 'draft'){ $this->db->where_in('invoices.status', ['unpaid','partial','draft']); } else { $this->db->where('invoices.status', $status); } } $this->db->where('invoices.deleted_at IS NULL', null, false); $this->db->order_by('invoices.tanggal','DESC'); $this->db->order_by('customers.nama','ASC'); $data = $this->db->get()->result(); $result = []; $no = 1; foreach ($data as $row) { // ========================= // CEK TUNGGAKAN SEBELUMNYA // ========================= $this->db->select_sum('sisa_piutang'); $this->db->where('customer_id', $row->customer_id); $this->db->where('status !=', 'paid'); $this->db->where('id <', $row->id); $tunggakan = $this->db->get('invoices')->row()->sisa_piutang ?? 0; // ========================= // STATUS BADGE // ========================= switch ($row->status) { case 'paid': $status_badge = 'PAID'; break; case 'partial': $status_badge = 'PARTIAL'; break; case 'unpaid': $status_badge = 'UNPAID'; break; default: $status_badge = 'DRAFT'; } // ========================= // AKSI // ========================= $aksi = ''; // tombol detail $aksi .= ' '; // ========================= // LOGIC BAYAR // ========================= $bolehBayar = ($tunggakan <= 0); // DRAFT if($row->status == 'draft'){ if ($row->total > 0 && $bolehBayar) { $aksi .= ' '; } $aksi .= ''; } // UNPAID + PARTIAL if(in_array($row->status, ['unpaid','partial'])){ if($bolehBayar){ $aksi .= ' '; } else { // indikator tunggakan $aksi .= ' Ada tunggakan '; } } // ========================= // FORMAT OUTPUT // ========================= $result[] = [ $no++, ''.$row->no_invoice.'', tanggal_indo($row->tanggal), tanggal_indo($row->jatuh_tempo), $row->nama, 'Rp '.number_format($row->total,0,',','.').'', 'Rp '.number_format($row->total_bayar,0,',','.').'', 'Rp '.number_format($row->sisa_piutang,0,',','.').'', $status_badge, $aksi ]; } echo json_encode(["data"=>$result]); } // ========================= // GET CUSTOMER // ========================= public function get_customers() { echo json_encode($this->db->get('customers')->result()); } // ========================= // SAVE INVOICE // ========================= public function save() { $this->output->set_content_type('application/json'); $customer_id = $this->input->post('customer_id'); $tanggal = $this->input->post('tanggal'); $jatuh_tempo = $this->input->post('jatuh_tempo'); // ========================= // VALIDASI WAJIB FIELD // ========================= if (empty($customer_id) || empty($tanggal) || empty($jatuh_tempo)) { echo json_encode([ 'status' => false, 'message' => 'Customer, tanggal, dan jatuh tempo wajib diisi' ]); return; } // ========================= // NORMALISASI TANGGAL // ========================= $today = date('Y-m-d'); $tanggal = date('Y-m-d', strtotime($tanggal)); $jatuh_tempo = date('Y-m-d', strtotime($jatuh_tempo)); // ❌ jatuh tempo tidak boleh kurang dari tanggal invoice if ($jatuh_tempo < $tanggal) { echo json_encode([ 'status' => false, 'message' => 'Jatuh tempo tidak boleh lebih kecil dari tanggal invoice' ]); return; } // ========================= // GENERATE NO INVOICE // ========================= $tanggal_now = date('Ymd'); $this->db->like('no_invoice', 'INV-'.$tanggal_now, 'after'); $this->db->from('invoices'); $count = $this->db->count_all_results(); $urutan = str_pad($count + 1, 4, '0', STR_PAD_LEFT); $no_invoice = 'INV-' . $tanggal_now . '-' . $urutan; // ========================= // HANDEL Adb->query(" SELECT * FROM invoice_installments WHERE customer_id = ? AND status IN ('unpaid','partial') AND (nominal - paid_amount) > 0 AND is_billed = 0 ORDER BY invoice_detail_id, angsuran_ke ASC ", [$customer_id])->result(); $cicilan = []; $group = []; foreach ($cicilan_raw as $c) { $group[$c->invoice_detail_id][] = $c; } foreach ($group as $detail_id => $rows) { $picked_status = []; $min_row = null; foreach ($rows as $r) { // ambil angsuran terkecil if ($min_row === null) { $min_row = $r; $picked_status[$r->status] = true; $cicilan[] = $r; continue; } // ambil jika status beda dan belum pernah diambil if (!isset($picked_status[$r->status])) { $picked_status[$r->status] = true; $cicilan[] = $r; } } } // ========================= // TRANSACTION // ========================= $this->db->trans_start(); $this->db->insert('invoices', [ 'no_invoice' => $no_invoice, 'customer_id' => $customer_id, 'tanggal' => $tanggal, 'jatuh_tempo' => $jatuh_tempo, 'total' => 0, 'status' => 'draft' ]); $invoice_id = $this->db->insert_id(); $total_invoice = 0; foreach ($cicilan as $c) { // ambil data item asli $inv_items = $this->db ->select('account_id, items_id, nama_item, keterangan, subtotal_asli, is_cicilan') ->where('id', $c->invoice_detail_id) ->get('invoice_details') ->row(); if(!$inv_items) continue; // ========================= // FORMAT NAMA // ========================= if($c->paid_amount > 0){ $priodecicilan = '( Sisa Cicilan ke ' . $c->angsuran_ke . '/' . $c->total_angsuran . ' )'; }else{ $priodecicilan = '( Cicilan ke ' . $c->angsuran_ke . '/' . $c->total_angsuran . ' )'; } $nama_item = preg_replace( '/\s*\(\s*Cicilan.*$/i', '', $inv_items->nama_item ) . ' ' . $priodecicilan; // ========================= // NOMINAL // ========================= $harga_cicilan = max(0, $c->nominal - $c->paid_amount); if($harga_cicilan <= 0) continue; // ========================= // INSERT DETAIL // ========================= $insert_data = [ 'invoice_id' => $invoice_id, 'tanggal' => $tanggal, 'account_id' => $inv_items->account_id, 'items_id' => $inv_items->items_id ?: null, 'nama_item' => $nama_item, 'qty' => 1, 'harga' => $harga_cicilan, 'subtotal' => $harga_cicilan, 'warehouse_id' => null, 'keterangan' => $inv_items->keterangan, 'subtotal_asli' => $inv_items->subtotal_asli, 'is_cicilan' => $inv_items->is_cicilan, 'source_cicilan_id' => $c->id, 'total_hpp_barang' => $c->cicilan_hpp_barang ]; $this->db->insert('invoice_details', $insert_data); // ========================= // AKUMULASI TOTAL // ========================= $total_invoice += $harga_cicilan; // ========================= // MARK SUDAH DITAGIHKAN // ========================= $this->db->where('id', $c->id)->update('invoice_installments', [ 'is_billed' => 1 ]); } // ========================= // UPDATE TOTAL INVOICE // ========================= $this->update_total($invoice_id); $this->db->trans_complete(); // ========================= // RESPONSE // ========================= if (!$this->db->trans_status()) { echo json_encode([ 'status' => false, 'message' => 'Gagal membuat invoice' ]); return; } log_activity( 'invoice', 'create', 'Membuat invoice ' . $no_invoice . ' untuk customer ID ' . $customer_id, 'success' ); echo json_encode([ 'status' => true, 'invoice_id' => $invoice_id, 'no_invoice' => $no_invoice ]); } // ========================= // DELETE // ========================= public function delete($id) { // Ambil invoice $invoice = $this->db->get_where('invoices', ['id' => $id])->row(); if (!$invoice) { echo json_encode(['status'=>false,'message'=>'Invoice tidak ditemukan']); return; } // ❌ Cek detail $cekDetail = $this->db->where('invoice_id', $id) ->count_all_results('invoice_details'); if ($cekDetail > 0) { echo json_encode([ 'status'=>false, 'message'=>'Invoice tidak bisa dihapus karena masih memiliki detail' ]); return; } if ($invoice->posted == 1) { echo json_encode([ 'status'=>false, 'message'=>'Invoice sudah diposting, tidak bisa dihapus' ]); return; } // ❌ Cek sudah dibayar if (in_array($invoice->status, ['paid','partial'])) { echo json_encode([ 'status'=>false, 'message'=>'Invoice sudah ada pembayaran, tidak bisa dihapus' ]); return; } // ✅ Hapus $this->db->delete('invoices', ['id'=>$id]); log_activity( 'invoice', 'delete', 'Menghapus invoice ID ' . $id . ' - ' . $invoice->no_invoice, 'success' ); echo json_encode(['status'=>true]); } public function detail($id) { // GET customer_id $row = $this->db->select('customer_id, no_invoice') ->where('id',$id) ->get('invoices') ->row(); $data['active_menu'] = "invoice_draft"; $data['invoice_id'] = $id; $data['no_invoice'] = $row ? $row->no_invoice : null; $data['customer_id'] = $row ? $row->customer_id : null; $this->load->view('partials/header', $data); $this->load->view('invoices/detail', $data); $this->load->view('partials/footer'); } public function get_invoice($id) { // ================= AMBIL INVOICE ================= $this->db->select('invoices.*, customers.nama, customers.alamat, customers.telp, customers.email'); $this->db->from('invoices'); $this->db->join('customers', 'customers.id = invoices.customer_id'); $this->db->where('invoices.id', $id); $data = $this->db->get()->row(); if(!$data){ echo json_encode(null); return; } // ================= AMBIL TUNGGAKAN ================= $this->db->select_sum('sisa_piutang'); $this->db->where('customer_id', $data->customer_id); $this->db->where('status !=', 'paid'); $this->db->where('id !=', $id); // exclude invoice sekarang $tunggakan = $this->db->get('invoices')->row()->sisa_piutang ?? 0; // ================= MASUKKAN KE DATA ================= $data->tunggakan = (int)$tunggakan; // ================= OUTPUT ================= echo json_encode($data); } // ========================= // GET ITEMS // ========================= public function get_items($id) { $this->db->select('invoice_details.*, accounts.kode_akun, accounts.nama_akun, invoices.status, item_barcodes.barcode'); $this->db->from('invoice_details'); $this->db->join('accounts', 'accounts.id = invoice_details.account_id', 'left'); $this->db->join('invoices', 'invoices.id = invoice_details.invoice_id', 'left'); $this->db->join('item_barcodes', 'item_barcodes.id = invoice_details.barcode_id', 'left'); $this->db->where('invoice_details.invoice_id', $id); $this->db->order_by('tanggal','ASC'); $this->db->order_by('id','ASC'); $data = $this->db->get()->result(); echo json_encode($data); } // ========================= // ADD ITEM - FIXED RESPONSE // ========================= public function add_item() { $this->db->trans_begin(); $invoice_id = $this->input->post('invoice_id'); $tanggal = $this->input->post('tanggal'); $item_id = $this->input->post('item_id'); $nama_item = $this->input->post('nama_item'); $qty = (float)$this->input->post('qty'); $harga = (float)$this->input->post('harga'); $account_id = $this->input->post('account_id'); $warehouse_id = $this->input->post('warehouse_id'); $barcode_id = $this->input->post('barcode_id'); $total_harga = $qty * $harga; $total_hpp = 0; // jika di bayar cicilan $is_cicilan = $this->input->post('is_cicilan') ?? null; $tenor = (int)$this->input->post('tenor') ?? 0; // ambil invoice Detail $invoice = $this->db->get_where('invoices', ['id' => $invoice_id])->row(); // ========================= // VALIDASI & AMBIL ITEM // ========================= if($item_id){ $item = $this->db->get_where('items', ['id'=>$item_id])->row(); if(!$item){ echo json_encode(['status'=>false, 'message'=>'Item tidak ditemukan']); $this->db->trans_rollback(); return; } $nama_item = $item->nama_barang; // cek stok // $stok = $this->get_stok($item_id); // if($stok < $qty){ // echo json_encode(['status'=>false, 'message'=>'Stok tidak cukup. Sisa: '.$stok]); // $this->db->trans_rollback(); // return; // } // START - Item Movement & item barcodes update status $this->db->where('item_id', $item_id); $this->db->order_by('created_at', 'DESC'); $this->db->limit(1); $item_move = $this->db->get('item_movements')->row(); // get item_barcode $itemBar = $this->db->get_where('item_barcodes', ['id'=>$barcode_id])->row(); if($itemBar->qty_sisa < $qty) { echo json_encode(['status'=>false, 'message'=>'Stok tidak cukup. Sisa: '.$itemBar->qty_sisa]); $this->db->trans_rollback(); return; } $total_sisa = $itemBar->qty_sisa - $qty; $status_bar = ($total_sisa == 0) ? 'sold_out' : 'available'; $this->db->insert('item_movements',[ 'item_id' => $item_id, 'barcode_id' => $barcode_id, 'qty' => $qty, 'from_type' => $item_move->to_type, //enum('supplier','warehouse','technician','customer','invoice') 'to_type' => 'invoice', //enum('supplier','warehouse','technician','customer','invoice') 'movement_type' => 'sold_out', //enum('purchase','transfer','installation','uninstallation','supplier_return','customer_return','adjustment','sold_out') 'notes' => 'Barang terjual ke INV : ' . $invoice->no_invoice, 'created_at' => date('Y-m-d H:i:s') ]); $this->db->where('id', $barcode_id)->update('item_barcodes', ['status' => $status_bar, 'qty_sisa' => $total_sisa]); if ($status_bar == 'sold_out') { $this->db->where('barcode', $itemBar->barcode)->where('status', 'active')->update('item_technician', ['status' => 'inactive', 'deleted_at' => date('Y-m-d H:i:s')]); } // END - Item Movement & item barcodes update status $total_hpp = $qty * $item->harga_beli; } // ========================= // HANDLE CICILAN // ========================= $harga_simpan = $harga; $subtotal_simpan= $total_harga; $harga_asli = $total_harga; $ket_cicilan_inv = ''; $ket_cicilan_jr = ''; if ($is_cicilan && $tenor > 0) { // harga per cicilan $harga_cicilan = round($total_harga / $tenor); //ngatur HPP Cicilan $total_hpp = round($total_hpp / $tenor); $harga_simpan = $harga_cicilan; $subtotal_simpan = $harga_cicilan; $ket_cicilan_inv = ' ( Cicilan ke 1/' . $tenor . ' )'; $ket_cicilan_jr = ' ( Cicilan ' . $tenor . ' Periode )'; } // ========================= // INSERT DETAIL // ========================= $insert_data = [ 'invoice_id' => $invoice_id, 'tanggal' => $tanggal, 'account_id' => $account_id, 'items_id' => $item_id ?: null, 'nama_item' => $nama_item . $ket_cicilan_inv, 'qty' => $qty, 'harga' => $harga_simpan, 'subtotal' => $subtotal_simpan, 'warehouse_id' => $warehouse_id, 'keterangan' => $this->input->post('keterangan'), 'subtotal_asli' => $harga_asli, 'is_cicilan' => $is_cicilan ? 1 : 0, 'total_hpp_barang' => $total_hpp, 'barcode_id' => $barcode_id ]; $this->db->insert('invoice_details', $insert_data); $detail_id = $this->db->insert_id(); // ========================= // JURNAL + STOK // ========================= if($this->input->post('account_id') != 70) { $this->generate_jurnal_item($detail_id, $ket_cicilan_jr); } $this->update_total($invoice_id); if ($is_cicilan && $tenor > 0) { // generate cicilan $this->generate_installments( $invoice_id, $detail_id, $invoice->customer_id, $total_harga, $tenor, $tanggal, $account_id, $total_hpp, $item_id ?: null ); } // ✅ RESPONSE KONSISTEN DENGAN JS if ($this->db->trans_status() === FALSE){ $this->db->trans_rollback(); echo json_encode(['status'=>false, 'message'=>'Transaksi gagal']); } else { $this->db->trans_commit(); log_activity( 'invoice', 'add_item', 'Tambah item invoice ID ' . $invoice_id . ' - ' . $nama_item, 'success' ); echo json_encode(['status'=>true, 'message'=>'Item berhasil ditambahkan']); } } // // ========================= // // UPDATE ITEM - RESPONSE KONSISTEN // // ========================= // public function update_item() // { // $this->db->trans_begin(); // $id = $this->input->post('id'); // $old = $this->db->get_where('invoice_details', ['id'=>$id])->row(); // if(!$old){ // echo json_encode(['status'=>false, 'message'=>'Item tidak ditemukan']); // return; // } // $cek = $this->db->get_where('invoice_installments', [ // 'invoice_detail_id' => $id // ])->num_rows(); // if ($cek > 0) { // echo json_encode([ // 'status' => false, // 'message' => 'Item cicilan tidak bisa diubah. Hapus dan buat ulang.' // ]); // return; // } // // 🔥 REVERSE DATA LAMA // $this->delete_jurnal_item($id); // $this->delete_stok_item($id); // // DATA BARU // $item_id = $this->input->post('item_id'); // $nama_item = $this->input->post('nama_item'); // $tanggal = $this->input->post('tanggal'); // $total_hpp = 0; // if($item_id){ // $item = $this->db->get_where('items', ['id'=>$item_id])->row(); // if($item){ // $nama_item = $item->nama_barang; // $stok = $this->get_stok($item_id); // $qty = (float)$this->input->post('qty'); // $total_hpp = $this->input->post('qty') * $item->harga_beli; // if($stok < $qty){ // echo json_encode(['status'=>false, 'message'=>'Stok tidak cukup']); // return; // } // } // } // $qty = (float)$this->input->post('qty'); // $harga = (float)$this->input->post('harga'); // // UPDATE // $update_data = [ // 'tanggal' => $tanggal, // 'account_id' => $this->input->post('account_id'), // 'items_id' => $item_id ?: null, // 'nama_item' => $nama_item, // 'qty' => $qty, // 'harga' => $harga, // 'subtotal' => $qty * $harga, // 'warehouse_id' => $this->input->post('warehouse_id'), // 'keterangan' => $this->input->post('keterangan'), // 'total_hpp_barang' => $total_hpp // ]; // $this->db->where('id', $id)->update('invoice_details', $update_data); // // GENERATE ULANG // if($this->input->post('account_id') != 70) { // $this->generate_jurnal_item($id); // } // $this->update_total($old->invoice_id); // // ✅ RESPONSE KONSISTEN DENGAN JS // if ($this->db->trans_status() === FALSE){ // $this->db->trans_rollback(); // echo json_encode(['status'=>false, 'message'=>'Update gagal']); // } else { // $this->db->trans_commit(); // log_activity( // 'invoice', // 'update_item', // 'Update item invoice detail ID ' . $id, // 'success' // ); // echo json_encode(['status'=>true, 'message'=>'Item berhasil diupdate']); // } // } // ========================= // DELETE ITEM - RESPONSE KONSISTEN // ========================= public function delete_item($id, $no_invoice) { $this->db->trans_begin(); $d = $this->db->get_where('invoice_details', ['id'=>$id])->row(); if(!$d){ echo json_encode(['status'=>false, 'message'=>'Item tidak ditemukan']); return; } if($d->is_cicilan == 1) { $this->delete_invoice_installments($id, $d->source_cicilan_id); } // reverse stok & hapus $this->delete_jurnal_item($id); $this->delete_stok_item($id); $this->db->delete('invoice_details', ['id'=>$id]); $this->update_total($d->invoice_id); if ($d->items_id AND $d->is_cicilan != 1) { // START - Item Movement & item barcodes update status $this->db->where('item_id', $d->items_id); $this->db->order_by('created_at', 'DESC'); $this->db->limit(1); $item_move = $this->db->get('item_movements')->row(); // get item_barcode $itemBar = $this->db->get_where('item_barcodes', ['id'=>$d->barcode_id])->row(); $total_sisa = $itemBar->qty_sisa + $d->qty; $status_bar = ($total_sisa == 0) ? 'sold_out' : 'available'; $this->db->insert('item_movements',[ 'item_id' => $d->items_id, 'barcode_id' => $d->barcode_id, 'qty' => $d->qty, 'from_type' => $item_move->to_type, //enum('supplier','warehouse','technician','customer','invoice') 'to_type' => 'warehouse', //enum('supplier','warehouse','technician','customer','invoice') 'movement_type' => 'purchase', //enum('purchase','transfer','installation','uninstallation','supplier_return','customer_return','adjustment','sold_out') 'notes' => 'Barang di kembalikan ke gudang dari : ' . $no_invoice, 'created_at' => date('Y-m-d H:i:s') ]); // $this->db->where('id', $d->barcode_id)->update('item_barcodes', ['status' => 'available']); $this->db->where('id', $d->barcode_id)->update('item_barcodes', ['status' => $status_bar, 'qty_sisa' => $total_sisa]); // END - Item Movement & item barcodes update status } // ✅ RESPONSE KONSISTEN DENGAN JS if ($this->db->trans_status() === FALSE){ $this->db->trans_rollback(); echo json_encode(['status'=>false, 'message'=>'Hapus gagal']); } else { $this->db->trans_commit(); log_activity( 'invoice', 'delete_item', 'Hapus item invoice detail ID ' . $id, 'success' ); echo json_encode(['status'=>true, 'message'=>'Item berhasil dihapus']); } } // ========================= // AMBIL DATA CICILAN // ========================= public function get_cicilan($id) { $cicilan_raw = $this->db->query(" SELECT ii.*, it.nama_barang FROM invoice_installments ii LEFT JOIN items it ON ii.item_id = it.id WHERE ii.customer_id = ? AND ii.status IN ('unpaid','partial') AND (ii.nominal - ii.paid_amount) > 0 AND ii.is_billed = 0 ORDER BY ii.invoice_detail_id, ii.angsuran_ke ASC ", [$id])->result(); echo json_encode($cicilan_raw); } private function generate_jurnal_item($id, $ket_cicilan_jr = null) { // $d = $this->db->get_where('invoice_details', ['id'=>$id])->row(); // if(!$d) return; $d = $this->db ->select(' invoice_details.*, customers.nama, invoices.no_invoice, item_barcodes.barcode ') ->from('invoice_details') ->join('invoices', 'invoices.id = invoice_details.invoice_id') ->join('customers', 'customers.id = invoices.customer_id') ->join('item_barcodes', 'item_barcodes.id = invoice_details.barcode_id', 'left') ->where('invoice_details.id', $id) ->get() ->row(); if(!$d) return; $total_hpp = 0; $barcode = ''; // ========================= // 🔥 TENTUKAN NILAI JURNAL // ========================= $nilai_jurnal = $d->subtotal; if (isset($d->is_cicilan) && $d->is_cicilan == 1) { $nilai_jurnal = $d->subtotal_asli ?? $d->subtotal; } // ========================= // 🔥 STOK & HPP // ========================= if($d->items_id){ $item = $this->db->get_where('items',['id'=>$d->items_id])->row(); if($item){ $total_hpp = $item->harga_beli * $d->qty; } // stok keluar $this->db->insert('stock_logs', [ 'item_id' => $d->items_id, 'qty' => $d->qty, 'tipe' => 'keluar', 'ref_type' => 'invoice_details', 'ref_id' => $id, 'warehouse_id' => $d->warehouse_id, 'keterangan' => 'Penjualan ' . $item->nama_barang . ' kepada ' . $d->nama . ' - Invoice ' . $d->no_invoice . ' - Tgl ' . $d->tanggal . ' - Kode ' . $d->barcode ]); $barcode = ' - Kode: ' . $d->barcode; $this->update_item_stok($d->items_id); } // ========================= // 🔥 generate no_ref // ========================= $no_ref = $this->generate_no_ref(); // ========================= // 🔥 JOURNAL HEADER // ========================= $nama_item_jr = preg_replace('/\s*\(\s*Cicilan.*$/i', '', $d->nama_item) . $ket_cicilan_jr; $this->db->insert('journals', [ 'tanggal' => $d->tanggal, 'no_ref' => $no_ref, 'keterangan' => 'Penjualan '.$nama_item_jr . $barcode . ' - Kepada: ' . $d->nama, 'ref_type' => 'invoice_details', 'ref_id' => $id, 'created_by' => $this->session->userdata('user_id') ]); $jid = $this->db->insert_id(); // ========================= // 🔥 PIUTANG (DEBIT) // ========================= $this->db->insert('journal_details', [ 'journal_id' => $jid, 'account_id' => $this->akun_piutang, 'debit' => $nilai_jurnal, 'kredit' => 0 ]); // ========================= // 🔥 PENDAPATAN (KREDIT) Pendapatan Diterima Dimuka // ========================= $this->db->insert('journal_details', [ 'journal_id' => $jid, 'account_id' => $this->pendapatan_dimuka, 'debit' => 0, 'kredit' => $nilai_jurnal ]); // ========================= // 🔥 HPP & PERSEDIAAN // ========================= if($total_hpp > 0){ // HPP (debit) $this->db->insert('journal_details', [ 'journal_id' => $jid, 'account_id' => $this->persediaan_dijual, 'debit' => $total_hpp, 'kredit' => 0 ]); // Persediaan (kredit) $this->db->insert('journal_details', [ 'journal_id' => $jid, 'account_id' => $this->akun_persediaan, 'debit' => 0, 'kredit' => $total_hpp ]); } } // ========================= // 🔥 DELETE JURNAL // ========================= private function delete_jurnal_item($id) { $d = $this->db->get_where('invoice_details', ['id'=>$id])->row(); $j = $this->db->get_where('journals', [ 'ref_type'=>'invoice_details', 'ref_id'=>$id ])->result(); foreach($j as $row){ $this->db->delete('journal_details', ['journal_id'=>$row->id]); $this->db->delete('journals', ['id'=>$row->id]); } if($d && $d->items_id){ $this->update_item_stok($d->items_id); // ✅ BENAR } } // ========================= // 🔥 DELETE STOK // ========================= private function delete_stok_item($id) { $d = $this->db->get_where('invoice_details', ['id'=>$id])->row(); $this->db->delete('stock_logs', [ 'ref_type'=>'invoice_details', 'ref_id'=>$id ]); if($d && $d->items_id){ $this->update_item_stok($d->items_id); // ✅ BENAR } } // ========================= // 🔥 DELETE INVOICES INSTALLMENT // ========================= private function delete_invoice_installments($invoice_detail_id,$source_cicilan_id) { $cek = $this->db ->where_in('status', ['paid', 'partial']) ->where('invoice_detail_id', $invoice_detail_id) ->count_all_results('invoice_installments'); // kalau TIDAK ADA paid/partial → hapus if ($cek === 0) { $this->db->delete('invoice_installments', [ 'invoice_detail_id' => $invoice_detail_id ]); } if ($source_cicilan_id) { $this->db->update('invoice_installments', [ 'is_billed' => 0 ], ['id' => $source_cicilan_id]); } } // ========================= // 🔥 GET STOK // ========================= private function get_stok($item_id) { return $this->db->query(" SELECT COALESCE(SUM(qty * IF(tipe='masuk',1,-1)),0) as stok FROM stock_logs WHERE item_id = ? ", [$item_id])->row()->stok; } // ========================= // ADD ITEM - FIXED HELPER // ========================= private function commit() { if ($this->db->trans_status() === FALSE){ $this->db->trans_rollback(); echo json_encode(['status'=>false, 'message'=>'Transaksi gagal']); } else { $this->db->trans_commit(); echo json_encode(['status'=>true, 'message'=>'Berhasil']); } } private function rollback($message) { $this->db->trans_rollback(); echo json_encode(['status'=>false, 'message'=>$message]); } // ========================= // UPDATE TOTAL // ========================= private function update_total($invoice_id) { $this->db->select_sum('subtotal', 'total'); $this->db->where('invoice_id', $invoice_id); $total = $this->db->get('invoice_details')->row()->total ?? 0; $this->db->where('id', $invoice_id) ->update('invoices', ['total' => $total]); } // ========================= // UPDATE TOTAL // ========================= private function generate_no_ref() { $tanggal = date('Y-m-d'); $prefix = 'INV-'.date('Ymd').'-'; // ambil nomor terakhir hari ini $last = $this->db->query(" SELECT no_ref FROM journals WHERE DATE(tanggal) = ? AND no_ref LIKE ? ORDER BY no_ref DESC LIMIT 1 ", [$tanggal, $prefix.'%'])->row(); if($last){ $last_number = (int) substr($last->no_ref, -4); $next = $last_number + 1; } else { $next = 1; } return $prefix . str_pad($next, 4, '0', STR_PAD_LEFT); } // ========================= // GET ACCOUNTS // ========================= public function get_accounts() { $data = $this->db ->group_start() ->where('tipe', 'revenue') ->or_where('id', 70) ->group_end() ->order_by('kode_akun', 'ASC') ->get('accounts') ->result(); echo json_encode($data); } public function get_accounts_assets() { $data = $this->db ->where_in('kode_akun', [101,102,207,201,202,203,204,205,209]) ->order_by('kode_akun', 'ASC') ->get('accounts') ->result(); echo json_encode($data); } public function check_items_by_account($account_id) { $exists = $this->db ->where('account_id', $account_id) ->limit(1) ->get('items') ->num_rows() > 0; echo json_encode([ 'has_item' => $exists ]); } public function get_items_by_wh_account($warehouse_id, $account_id) { $this->db->select(' items.id, items.nama_barang, items.harga_jual, items.kode_detail, COALESCE(SUM( stock_logs.qty * IF(stock_logs.tipe="masuk",1,-1) ),0) as stok '); $this->db->where('status', 'active'); $this->db->from('items'); $this->db->join('stock_logs', 'stock_logs.item_id = items.id AND stock_logs.warehouse_id = '.$this->db->escape($warehouse_id), 'left'); $this->db->where('items.account_id', $account_id); $this->db->group_by('items.id'); // ✅ INI KUNCINYA $this->db->having('stok >', 0); $data = $this->db->get()->result(); echo json_encode($data); } public function gudang_list() { echo json_encode( $this->db->get('warehouses')->result() ); } public function get_payment($invoice_id) { $this->db->select(' id, tanggal, jumlah, metode, created_at '); $this->db->from('payments'); $this->db->where('invoice_id', $invoice_id); $this->db->order_by('id','DESC'); $data = $this->db->get()->result(); $result = []; $this->db->from('invoices'); $this->db->where('invoices.id', $invoice_id); $invoice = $this->db->get()->row(); foreach ($data as $row) { $result[] = [ 'id' => $row->id, 'tanggal' => date('d-m-Y', strtotime($row->tanggal)), 'jumlah' => 'Rp '.number_format($row->jumlah,0,',','.'), 'metode' => $row->metode, 'created_at' => date('d-m-Y H:i', strtotime($row->created_at)) ]; } echo json_encode([ 'data' => $result, 'invoice' => $invoice ]); } // Update Stok Cache private function update_item_stok($item_id) { $stok = $this->db->query(" SELECT COALESCE(SUM(qty * IF(tipe='masuk',1,-1)),0) as stok FROM stock_logs WHERE item_id = ? ", [$item_id])->row()->stok; $this->db->where('id', $item_id) ->update('items', [ 'stok' => $stok ]); } private function generate_installments($invoice_id, $detail_id, $customer_id, $total, $tenor, $start_date, $account_id, $total_hpp, $item_id = null) { $nominal = round($total / $tenor); $sisa = $total - ($nominal * $tenor); for ($i = 1; $i <= $tenor; $i++) { $tgl = date('Y-m-d', strtotime("+".($i-1)." month", strtotime($start_date))); $nilai = $nominal; if ($i == $tenor) { $nilai += $sisa; } $is_billed = ($i == 1) ? 1 : 0; $this->db->insert('invoice_installments', [ 'invoice_id' => $invoice_id, 'invoice_detail_id' => $detail_id, // 🔥 relasi ke item 'customer_id' => $customer_id, 'angsuran_ke' => $i, 'total_angsuran' => $tenor, 'jatuh_tempo' => $tgl, 'nominal' => $nilai, 'account_id' => $account_id, 'item_id' => $item_id ?: null, 'cicilan_hpp_barang' => $total_hpp, 'status' => 'unpaid', 'is_billed' => $is_billed ]); } } public function save_cicilan() { $cicilan_ids = $this->input->post('cicilan_id'); $invoice_id = $this->input->post('invoice_id'); if (empty($cicilan_ids)) { echo json_encode([ 'status' => false, 'message' => 'Tidak ada data dipilih' ]); return; } $this->db->trans_start(); foreach ($cicilan_ids as $id) { // ========================= // AMBIL CICILAN // ========================= $cicilan = $this->db->get_where('invoice_installments', [ 'id' => $id, 'is_billed' => 0 // penting! ])->row(); if (!$cicilan) continue; // ========================= // CEK DUPLIKAT // ========================= $cek = $this->db->get_where('invoice_details', [ 'source_cicilan_id' => $id ])->row(); if ($cek) continue; $sisa = $cicilan->nominal - $cicilan->paid_amount; // ========================= // AMBIL REFERENSI ITEM // ========================= $inv_items = $this->db ->select('account_id, items_id, nama_item, keterangan, subtotal_asli, is_cicilan') ->where('id', $cicilan->invoice_detail_id) ->get('invoice_details') ->row(); if(!$inv_items) continue; // ========================= // FORMAT NAMA // ========================= if($cicilan->paid_amount > 0){ $priodecicilan = '( Sisa Cicilan ke ' . $cicilan->angsuran_ke . '/' . $cicilan->total_angsuran . ' )'; } else { $priodecicilan = '( Cicilan ke ' . $cicilan->angsuran_ke . '/' . $cicilan->total_angsuran . ' )'; } $nama_item = preg_replace( '/\s*\(\s*Cicilan.*$/i', '', $inv_items->nama_item ) . ' ' . $priodecicilan; // ========================= // INSERT // ========================= $this->db->insert('invoice_details', [ 'invoice_id' => $invoice_id, 'tanggal' => $cicilan->jatuh_tempo, 'account_id' => $inv_items->account_id, 'items_id' => $inv_items->items_id ?: null, 'nama_item' => $nama_item, 'qty' => 1, 'harga' => $sisa, 'subtotal' => $sisa, 'warehouse_id' => null, 'keterangan' => $inv_items->keterangan, 'subtotal_asli' => $inv_items->subtotal_asli, 'is_cicilan' => $inv_items->is_cicilan, 'source_cicilan_id' => $cicilan->id, 'total_hpp_barang' => $cicilan->cicilan_hpp_barang ]); // ========================= // UPDATE STATUS // ========================= $this->db->update('invoice_installments', [ 'is_billed' => 1 ], ['id' => $id]); } $this->update_total($invoice_id); $this->db->trans_complete(); echo json_encode([ 'status' => $this->db->trans_status() ]); } public function bayar() { $this->output->set_content_type('application/json'); $invoice_id = $this->input->post('invoice_id'); $tanggal_bayar = $this->input->post('tanggal_bayar'); $detail = $this->input->post('detail'); if (!$invoice_id || empty($detail)) { echo json_encode(['status' => false, 'message' => 'Data tidak valid']); return; } $invoice = $this->db->get_where('invoices', ['id' => $invoice_id])->row(); if (!$invoice) { echo json_encode(['status' => false, 'message' => 'Invoice tidak ditemukan']); return; } $customer = $this->db->get_where('customers', ['id' => $invoice->customer_id])->row(); if (!$customer) { echo json_encode(['status' => false, 'message' => 'Customer tidak ditemukan']); return; } // ========================= // HITUNG TOTAL BAYAR // ========================= $total_bayar_input = 0; foreach ($detail as $d) { $total_bayar_input += (float)$d['amount']; } if ($total_bayar_input <= 0) { echo json_encode(['status' => false, 'message' => 'Total bayar harus > 0']); return; } // ========================= // DATA INVOICE // ========================= $paid_before = (float)$invoice->total_bayar; $total_invoice = (float)$invoice->total; $new_total_bayar = $paid_before + $total_bayar_input; $sisa_setelah = $total_invoice - $new_total_bayar; if ($new_total_bayar > $total_invoice) { echo json_encode([ 'status' => false, 'message' => 'Pembayaran melebihi total invoice' ]); return; } // ========================= // KONFIGURASI AKUN // ========================= $akun_tabungan = 70; $akun_barang = 62; $akun_piutang = 19; $akun_pendapatan_ditahan = 78; $akun_hpp = isset($this->akun_hpp) ? $this->akun_hpp : 81; $persediaan_dijual = isset($this->persediaan_dijual) ? $this->persediaan_dijual : 50; // ========================= // HITUNG CICILAN // ========================= $cicilan = $this->db->query(" SELECT ii.* FROM invoice_installments ii JOIN ( SELECT invoice_detail_id, MIN(angsuran_ke) as angsuran_ke FROM invoice_installments WHERE customer_id = ? AND status != 'paid' GROUP BY invoice_detail_id ) x ON x.invoice_detail_id = ii.invoice_detail_id AND x.angsuran_ke = ii.angsuran_ke ", [$customer->id])->result(); $total_cicilan = 0; foreach ($cicilan as $c) { $total_cicilan += ($c->nominal - $c->paid_amount); } // ========================= // TOTAL PER KATEGORI // ========================= $total_tabungan = (float)$this->db ->select('SUM(subtotal) as total') ->where('invoice_id', $invoice_id) ->where('account_id', $akun_tabungan) ->get('invoice_details')->row()->total ?? 0; $total_barang = (float)$this->db ->select('SUM(subtotal) as total') ->where('invoice_id', $invoice_id) ->where('account_id', $akun_barang) ->get('invoice_details')->row()->total ?? 0; $total_other = (float)$this->db ->select('SUM(subtotal) as total') ->where('invoice_id', $invoice_id) ->where_not_in('account_id', [$akun_tabungan, $akun_barang]) ->get('invoice_details')->row()->total ?? 0; // ========================= // SISA PER KATEGORI // ========================= $sisa_other = max(0, $total_other - $paid_before); $sisa_tabungan = max(0, $total_tabungan - max(0, $paid_before - $total_other)); $sisa_barang = max(0, $total_barang - max(0, $paid_before - $total_other - $total_tabungan)); // ========================= // ALOKASI PEMBAYARAN // ========================= $sisa_bayar = $total_bayar_input; $bayar_cicilan = min($sisa_bayar, $total_cicilan); $sisa_bayar -= $bayar_cicilan; $bayar_other = min($sisa_bayar, $sisa_other); $sisa_bayar -= $bayar_other; $bayar_tabungan = min($sisa_bayar, $sisa_tabungan); $sisa_bayar -= $bayar_tabungan; $bayar_barang = min($sisa_bayar, $sisa_barang); $sisa_bayar -= $bayar_barang; // ========================= // AMBIL INVOICE DETAIL // ========================= $inv_detail_items = $this->db ->select(' invoice_details.id, invoice_details.account_id, invoice_details.subtotal, invoice_details.recognized_amount, invoice_details.total_hpp_barang, invoice_details.recognized_hpp, invoice_details.is_cicilan, accounts.priority ') ->from('invoice_details') ->join('accounts', 'accounts.id = invoice_details.account_id', 'left') ->where('invoice_details.invoice_id', $invoice_id) ->where('invoice_details.account_id !=', $akun_tabungan) ->order_by('accounts.priority', 'ASC') ->order_by('invoice_details.id', 'ASC') ->get() ->result(); // ========================= // DEBIT PENDAPATAN DITAHAN // ========================= // Tidak termasuk tabungan! $debit_pendapatan_ditahan = $bayar_cicilan + $bayar_other + $bayar_barang; // ========================= // MULAI TRANSAKSI // ========================= $this->db->trans_start(); // UPDATE cicilan if ($bayar_cicilan > 0) { $sisa = $bayar_cicilan; foreach ($cicilan as $c) { if ($sisa <= 0) break; $kurang = $c->nominal - $c->paid_amount; $bayar = min($sisa, $kurang); $sisa -= $bayar; $new_paid = $c->paid_amount + $bayar; $status = ($new_paid >= $c->nominal) ? 'paid' : 'partial'; $this->db->where('id', $c->id)->update('invoice_installments', [ 'paid_amount' => $new_paid, 'status' => $status ]); } } $no_ref = 'PAY-' . date('YmdHis'); $this->db->insert('journals', [ 'tanggal' => $tanggal_bayar, 'no_ref' => $no_ref, 'keterangan'=> 'Pembayaran Invoice ' . $invoice->no_invoice . ' (' . $customer->nama . ')', 'ref_type' => 'invoice_payment', 'ref_id' => $invoice_id, 'created_by'=> $this->session->userdata('user_id') ]); $journal_id = $this->db->insert_id(); // ========================= // DEBIT SIDE // ========================= $total_debit = 0; $jurnal_debit = []; // 1. Kas/Bank (dari detail) foreach ($detail as $d) { if (empty($d['account_id']) || $d['amount'] <= 0) continue; $this->db->insert('journal_details', [ 'journal_id' => $journal_id, 'account_id' => $d['account_id'], 'debit' => $d['amount'], 'kredit' => 0 ]); $total_debit += (float)$d['amount']; $jurnal_debit[] = [ 'account_id' => $d['account_id'], 'debit' => (float)$d['amount'], 'jenis' => 'kas' ]; } // ========================= // KREDIT SIDE // ========================= $total_kredit = 0; $jurnal_kredit = []; // 1. Piutang (KREDIT) $kredit_piutang = $total_bayar_input - $bayar_tabungan; if ($kredit_piutang > 0) { $this->db->insert('journal_details', [ 'journal_id' => $journal_id, 'account_id' => $akun_piutang, 'debit' => 0, 'kredit' => $kredit_piutang ]); $total_kredit += $kredit_piutang; $jurnal_kredit[] = [ 'account_id' => $akun_piutang, 'kredit' => $kredit_piutang, 'jenis' => 'piutang' ]; } // 2. Tabungan (KREDIT) if ($bayar_tabungan > 0) { $this->db->insert('journal_details', [ 'journal_id' => $journal_id, 'account_id' => $akun_tabungan, 'debit' => 0, 'kredit' => $bayar_tabungan ]); $total_kredit += $bayar_tabungan; $jurnal_kredit[] = [ 'account_id' => $akun_tabungan, 'kredit' => $bayar_tabungan, 'jenis' => 'tabungan' ]; } // ========================= // PENGAKUAN PENDAPATAN & HPP // ========================= $sisa_pengakuan = $bayar_cicilan + $bayar_other + $bayar_barang; $account_recognition = []; $total_hpp = 0; foreach ($inv_detail_items as $i) { if ($sisa_pengakuan <= 0) break; $belum_diakui = (float)$i->subtotal - (float)$i->recognized_amount; if ($belum_diakui <= 0) continue; $nominal = min($sisa_pengakuan, $belum_diakui); if (!isset($account_recognition[$i->account_id])) { $account_recognition[$i->account_id] = 0; } $account_recognition[$i->account_id] += $nominal; // Update recognized amount $this->db->query(" UPDATE invoice_details SET recognized_amount = recognized_amount + ? WHERE id = ? ", [$nominal, $i->id]); if ($i->account_id == $akun_barang ) { $persentase = $nominal / (float)$i->subtotal; $hpp_item = (float)$i->total_hpp_barang * $persentase; $hpp_item = round($hpp_item, 2); $sisa_hpp = max(0, (float)$i->total_hpp_barang - (float)$i->recognized_hpp); $hpp_item = min($hpp_item, $sisa_hpp); if ($hpp_item > 0) { // Cara 1: Pakai update() method $this->db->where('id', $i->id); $this->db->update('invoice_details', [ 'recognized_hpp' => $i->recognized_hpp + $hpp_item ]); $total_hpp += $hpp_item; } } $sisa_pengakuan -= $nominal; } // ========================= // ALOKASI SISA KE AKUN LAIN // ========================= $total_diaku = array_sum($account_recognition); $sisa_yg_harus_diaku = $bayar_cicilan + $bayar_other + $bayar_barang; $selisih_alokasi = $sisa_yg_harus_diaku - $total_diaku; if ($selisih_alokasi > 0) { $akun_other_list = $this->db ->select('account_id, SUM(subtotal) as total') ->from('invoice_details') ->where('invoice_id', $invoice_id) ->where_not_in('account_id', [$akun_tabungan, $akun_barang]) ->group_by('account_id') ->get() ->result(); foreach ($akun_other_list as $ao) { if ($selisih_alokasi <= 0) break; $belum_diaku_akun = (float)$ao->total; $alokasi = min($selisih_alokasi, $belum_diaku_akun); if (!isset($account_recognition[$ao->account_id])) { $account_recognition[$ao->account_id] = 0; } $account_recognition[$ao->account_id] += $alokasi; $selisih_alokasi -= $alokasi; } } // ========================= // Insert DEBIT pendapatan // ========================= if ($debit_pendapatan_ditahan > 0) { $this->db->insert('journal_details', [ 'journal_id' => $journal_id, 'account_id' => $akun_pendapatan_ditahan, 'debit' => $debit_pendapatan_ditahan, 'kredit' => 0 ]); $total_debit += $debit_pendapatan_ditahan; $jurnal_debit[] = [ 'account_id' => $akun_pendapatan_ditahan, 'debit' => $debit_pendapatan_ditahan, 'jenis' => 'pendapatan_ditahan' ]; } // ========================= // Insert KREDIT pendapatan // ========================= foreach ($account_recognition as $account_id => $nominal) { if ($nominal <= 0) continue; $this->db->insert('journal_details', [ 'journal_id' => $journal_id, 'account_id' => $account_id, 'debit' => 0, 'kredit' => $nominal ]); $total_kredit += $nominal; $jurnal_kredit[] = [ 'account_id' => $account_id, 'kredit' => $nominal, 'jenis' => 'pendapatan' ]; } // ========================= // JURNAL HPP // ========================= if ($total_hpp > 0) { $this->db->insert('journal_details', [ 'journal_id' => $journal_id, 'account_id' => $akun_hpp, 'debit' => $total_hpp, 'kredit' => 0 ]); $total_debit += $total_hpp; $jurnal_debit[] = [ 'account_id' => $akun_hpp, 'debit' => $total_hpp, 'jenis' => 'hpp_barang' ]; $this->db->insert('journal_details', [ 'journal_id' => $journal_id, 'account_id' => $persediaan_dijual, 'debit' => 0, 'kredit' => $total_hpp ]); $total_kredit += $total_hpp; $jurnal_kredit[] = [ 'account_id' => $persediaan_dijual, 'kredit' => $total_hpp, 'jenis' => 'persediaan_dijual' ]; } // ========================= // VALIDASI JURNAL // ========================= if (round($total_debit, 2) !== round($total_kredit, 2)) { $this->db->trans_rollback(); echo json_encode([ 'status' => false, 'message' => 'Jurnal tidak balance!' ]); return; } // ========================= // INSERT PAYMENTS // ========================= $this->db->insert('payments', [ 'invoice_id' => $invoice_id, 'tanggal' => $tanggal_bayar, 'jumlah' => $total_bayar_input, 'metode' => 'manual', 'journal_id'=> $journal_id ]); // ========================= // UPDATE STATUS INVOICE // ========================= if ($new_total_bayar >= $total_invoice) { $status = 'paid'; $sisa_setelah = 0; } elseif ($new_total_bayar > 0) { $status = 'partial'; } else { $status = 'unpaid'; } $this->db->where('id', $invoice_id)->update('invoices', [ 'total_bayar' => $new_total_bayar, 'sisa_piutang' => $sisa_setelah, 'status' => $status, 'updated_at' => date('Y-m-d H:i:s'), 'updated_by' => $this->session->userdata('user_id') ]); // ========================= // SELESAIKAN TRANSAKSI // ========================= $this->db->trans_complete(); // ========================= // RESPONSE // ========================= if (!$this->db->trans_status()) { echo json_encode([ 'status' => false, 'message' => 'Gagal menyelesaikan pembayaran' ]); return; } // Logging log_activity( 'invoice', 'create', 'Pembayaran invoice ' . $invoice->no_invoice . ' - ' . $customer->nama, 'success' ); echo json_encode([ 'status' => true, 'message' => 'Pembayaran berhasil' ]); } }