Update besar apk finance

This commit is contained in:
Wian Drs
2026-09-11 16:03:00 +07:00
parent d42483b350
commit 4ef15c22ae
897 changed files with 98056 additions and 15979 deletions
+21
View File
@@ -0,0 +1,21 @@
<?php
defined('BASEPATH') OR exit('No direct script access allowed');
class AccountModel extends CI_Model
{
public function existingIds(array $ids)
{
if (empty($ids)) {
return array();
}
$rows = $this->db
->select('id')
->where_in('id', array_values(array_unique($ids)))
->where('is_active', 1)
->where('allow_posting', 1)
->get('accounts')
->result_array();
return array_map('intval', array_column($rows, 'id'));
}
}
+31
View File
@@ -0,0 +1,31 @@
<?php
defined('BASEPATH') OR exit('No direct script access allowed');
class FiscalPeriodModel extends CI_Model
{
public function find($id)
{
return $this->db->get_where('accounting_periods', array('id' => (int) $id))->row();
}
public function findByDate($date)
{
return $this->db
->where('start_date <=', $date)
->where('end_date >=', $date)
->limit(1)
->get('accounting_periods')
->row();
}
public function allWithFiscalYear()
{
return $this->db
->select('accounting_periods.*, fiscal_years.name fiscal_year_name')
->from('accounting_periods')
->join('fiscal_years', 'fiscal_years.id = accounting_periods.fiscal_year_id')
->order_by('accounting_periods.start_date', 'DESC')
->get()
->result();
}
}
+143
View File
@@ -0,0 +1,143 @@
<?php
defined('BASEPATH') OR exit('No direct script access allowed');
class FixedAssetModel extends CI_Model
{
public function dashboard($companyId)
{
$row = $this->db->query("SELECT
COUNT(*) total_assets,
SUM(workflow_status='capitalized' AND lifecycle_status IN('active','fully_depreciated')) active_assets,
COALESCE(SUM(IF(workflow_status='capitalized' AND lifecycle_status NOT IN('disposed','lost','damaged'),nilai_perolehan,0)),0) acquisition_cost,
COALESCE(SUM(IF(workflow_status='capitalized' AND lifecycle_status NOT IN('disposed','lost','damaged'),akumulasi_penyusutan,0)),0) accumulated_depreciation,
COALESCE(SUM(IF(workflow_status='capitalized' AND lifecycle_status NOT IN('disposed','lost','damaged'),nilai_buku,0)),0) book_value,
SUM(lifecycle_status='fully_depreciated') fully_depreciated,
SUM(workflow_status IN('submitted','approved')) pending_workflow
FROM assets WHERE company_id=?", array((int) $companyId))->row();
$row->maintenance_due = (int) $this->db->where('company_id', (int) $companyId)->where('status', 'posted')->where('next_due_date IS NOT NULL', null, false)->where('next_due_date <=', date('Y-m-d', strtotime('+30 days')))->count_all_results('asset_maintenance');
$row->open_opnames = (int) $this->db->where('company_id', (int) $companyId)->where_in('status', array('draft','submitted','approved'))->count_all_results('asset_opnames');
$row->depreciation_due = (int) $this->db->where('company_id', (int) $companyId)->where('workflow_status', 'capitalized')->where('lifecycle_status', 'active')->where('nilai_buku > nilai_residu', null, false)->count_all_results('assets');
return $row;
}
private function registerQuery($companyId, array $filters, $search = '')
{
$this->db->select('a.id,a.document_no,a.kode_asset,a.nama_asset,a.tanggal_perolehan,a.capitalization_date,a.source_type,a.workflow_status,a.lifecycle_status,a.nilai_perolehan,a.akumulasi_penyusutan,a.nilai_buku,a.nilai_residu,a.masa_manfaat,a.qty,a.qr_code,c.name category_name,l.nama location_name,e.full_name responsible_name,i.kode_detail,b.barcode,b.serial_number');
$this->db->from('assets a');
$this->db->join('asset_categories c', 'c.id=a.category_id', 'left');
$this->db->join('lokasi_asset l', 'l.id=a.lokasi_asset_id', 'left');
$this->db->join('k_employees e', 'e.id=a.responsible_employee_id', 'left');
$this->db->join('items i', 'i.id=a.item_id', 'left');
$this->db->join('item_barcodes b', 'b.id=CAST(a.barcode_id AS UNSIGNED)', 'left', false);
$this->db->where('a.company_id', (int) $companyId);
foreach (array('category_id','lokasi_asset_id','source_type','workflow_status','lifecycle_status','responsible_employee_id') as $field) {
if (!empty($filters[$field])) $this->db->where('a.'.$field, $filters[$field]);
}
if (!empty($filters['date_from'])) $this->db->where('a.tanggal_perolehan >=', $filters['date_from']);
if (!empty($filters['date_to'])) $this->db->where('a.tanggal_perolehan <=', $filters['date_to']);
if ($search !== '') {
$this->db->group_start()->like('a.document_no', $search)->or_like('a.kode_asset', $search)->or_like('a.nama_asset', $search)->or_like('c.name', $search)->or_like('l.nama', $search)->or_like('e.full_name', $search)->or_like('b.barcode', $search)->or_like('b.serial_number', $search)->group_end();
}
}
public function registerPage($companyId, array $filters, $start, $length, $search)
{
$this->registerQuery($companyId, $filters, $search);
$filtered = (int) $this->db->count_all_results('', false);
$rows = $this->db->order_by('a.id', 'DESC')->limit((int) $length, (int) $start)->get()->result();
$total = (int) $this->db->where('company_id', (int) $companyId)->count_all_results('assets');
return array('rows'=>$rows,'total'=>$total,'filtered'=>$filtered);
}
public function detail($id, $companyId)
{
$asset = $this->db->select('a.*,c.name category_name,c.code category_code,l.nama location_name,l.code location_code,e.full_name responsible_name,i.kode_detail,i.nama_barang,w.nama warehouse_name,bin.code bin_code,b.barcode,b.serial_number,uc.nama creator_name,us.nama submitter_name,ua.nama approver_name,up.nama capitalizer_name,j.no_ref source_journal_no,aj.no_ref acquisition_journal_no')
->from('assets a')->join('asset_categories c','c.id=a.category_id','left')->join('lokasi_asset l','l.id=a.lokasi_asset_id','left')->join('k_employees e','e.id=a.responsible_employee_id','left')->join('items i','i.id=a.item_id','left')->join('warehouses w','w.id=a.warehouse_id','left')->join('warehouse_bins bin','bin.id=a.bin_id','left')->join('item_barcodes b','b.id=CAST(a.barcode_id AS UNSIGNED)','left',false)->join('users uc','uc.id=a.created_by','left')->join('users us','us.id=a.submitted_by','left')->join('users ua','ua.id=a.approved_by','left')->join('users up','up.id=a.capitalized_by','left')->join('journals j','j.id=a.source_journal_id','left')->join('journals aj','aj.id=a.acquisition_journal_id','left')->where(array('a.id'=>(int)$id,'a.company_id'=>(int)$companyId))->get()->row();
if (!$asset) return null;
$asset->events = $this->db->select('ev.*,u.nama user_name,fl.nama from_location,tl.nama to_location,j.no_ref journal_no')->from('asset_events ev')->join('users u','u.id=ev.created_by','left')->join('lokasi_asset fl','fl.id=ev.from_location_id','left')->join('lokasi_asset tl','tl.id=ev.to_location_id','left')->join('journals j','j.id=ev.journal_id','left')->where(array('ev.asset_id'=>$asset->id,'ev.company_id'=>(int)$companyId))->order_by('ev.event_date','DESC')->order_by('ev.id','DESC')->get()->result();
$asset->schedule = $this->db->where(array('asset_id'=>$asset->id,'company_id'=>(int)$companyId))->order_by('period','DESC')->get('asset_depreciation_schedule')->result();
$asset->maintenance = $this->db->where(array('asset_id'=>$asset->id,'company_id'=>(int)$companyId))->order_by('maintenance_date','DESC')->get('asset_maintenance')->result();
$asset->workflow = $this->db->select('h.*,u.nama user_name')->from('asset_workflow_history h')->join('users u','u.id=h.user_id','left')->where(array('h.asset_id'=>$asset->id,'h.company_id'=>(int)$companyId))->order_by('h.id')->get()->result();
$asset->responsibilities = $this->db->select('h.*,f.full_name from_name,t.full_name to_name,u.nama creator_name')->from('asset_responsibility_history h')->join('k_employees f','f.id=h.from_employee_id','left')->join('k_employees t','t.id=h.to_employee_id','left')->join('users u','u.id=h.created_by','left')->where(array('h.asset_id'=>$asset->id,'h.company_id'=>(int)$companyId))->order_by('h.effective_date','DESC')->get()->result();
$asset->attachments = $this->db->where(array('entity_type'=>'asset','entity_id'=>$asset->id,'company_id'=>(int)$companyId))->order_by('id')->get('transaction_attachments')->result();
return $asset;
}
public function categories($companyId, $activeOnly = false)
{
$this->db->where('company_id', (int) $companyId);
if ($activeOnly) $this->db->where('is_active', 1);
return $this->db->order_by('name')->get('asset_categories')->result();
}
public function locations($companyId, $activeOnly = false)
{
$this->db->where('company_id', (int) $companyId);
if ($activeOnly) $this->db->where('is_active', 1);
return $this->db->order_by('nama')->get('lokasi_asset')->result();
}
public function availableInventory($companyId, $search, $page = 1, $limit = 20)
{
$offset = (max(1, (int) $page)-1) * (int) $limit;
$params = array((int) $companyId);
$where = '';
if (trim($search) !== '') {
$where = ' AND (i.kode_detail LIKE ? OR i.nama_barang LIKE ? OR b.barcode LIKE ? OR b.serial_number LIKE ?)';
$needle = '%'.trim($search).'%';
array_push($params, $needle, $needle, $needle, $needle);
}
$params[] = (int) $limit + 1;
$params[] = $offset;
return $this->db->query("SELECT b.id barcode_id,b.item_id,b.barcode,b.serial_number,b.warehouse_id,b.bin_id,b.batch_id,b.qty_sisa,b.reserved_qty,b.return_reserved_qty,i.kode_detail,i.nama_barang,i.harga_beli,k.tracking_type,k.need_serial_number,w.nama warehouse_name,wb.code bin_code,bt.batch_no,
GREATEST(b.qty_sisa-b.reserved_qty-b.return_reserved_qty,0) available_qty,
COALESCE((SELECT SUM(IF(il.direction='in',il.value,-il.value))/NULLIF(SUM(IF(il.direction='in',il.qty,-il.qty)),0) FROM inventory_ledger il WHERE il.item_id=b.item_id AND il.warehouse_id=b.warehouse_id),i.harga_beli,0) ledger_unit_cost
FROM item_barcodes b JOIN items i ON i.id=b.item_id LEFT JOIN kode_barang k ON k.id=i.kode_id LEFT JOIN warehouses w ON w.id=b.warehouse_id LEFT JOIN warehouse_bins wb ON wb.id=b.bin_id LEFT JOIN inventory_batches bt ON bt.id=b.batch_id
WHERE i.company_id=? AND i.status='active' AND b.status='available' AND b.warehouse_id IS NOT NULL AND b.qty_sisa-b.reserved_qty-b.return_reserved_qty>0 {$where}
ORDER BY i.nama_barang,b.barcode LIMIT ? OFFSET ?", $params)->result();
}
public function employees($companyId, $search, $page = 1, $limit = 20)
{
$this->db->select('id,employee_code,full_name')->where(array('company_id'=>(int)$companyId,'is_active'=>1));
if (trim($search) !== '') $this->db->group_start()->like('employee_code',$search)->or_like('full_name',$search)->group_end();
return $this->db->order_by('full_name')->limit((int)$limit+1,(max(1,(int)$page)-1)*(int)$limit)->get('k_employees')->result();
}
public function accounts($companyId)
{
return $this->db->select('id,kode_akun,nama_akun,tipe,sub_tipe')
->where(array('company_id'=>(int)$companyId,'is_active'=>1,'allow_posting'=>1))
->order_by('kode_akun')->get('accounts')->result();
}
public function opnames($companyId)
{
return $this->db->select('o.*,l.nama location_name,u.nama creator_name,a.nama approver_name')->from('asset_opnames o')->join('lokasi_asset l','l.id=o.location_id','left')->join('users u','u.id=o.created_by','left')->join('users a','a.id=o.approved_by','left')->where('o.company_id',(int)$companyId)->order_by('o.id','DESC')->limit(200)->get()->result();
}
public function opnameDetail($id, $companyId)
{
$row=$this->db->select('o.*,l.nama location_name,u.nama creator_name,s.nama submitter_name,a.nama approver_name,p.nama poster_name')->from('asset_opnames o')->join('lokasi_asset l','l.id=o.location_id','left')->join('users u','u.id=o.created_by','left')->join('users s','s.id=o.submitted_by','left')->join('users a','a.id=o.approved_by','left')->join('users p','p.id=o.posted_by','left')->where(array('o.id'=>(int)$id,'o.company_id'=>(int)$companyId))->get()->row();
if(!$row)return null;
$row->lines=$this->db->select('ol.*,a.kode_asset,a.nama_asset,el.nama expected_location,al.nama actual_location')->from('asset_opname_lines ol')->join('assets a','a.id=ol.asset_id')->join('lokasi_asset el','el.id=ol.expected_location_id','left')->join('lokasi_asset al','al.id=ol.actual_location_id','left')->where(array('ol.asset_opname_id'=>$row->id,'ol.company_id'=>(int)$companyId))->order_by('a.kode_asset')->get()->result();
$row->attachments=$this->db->where(array('entity_type'=>'asset_opname','entity_id'=>$row->id,'company_id'=>(int)$companyId))->order_by('id')->get('transaction_attachments')->result();
return$row;
}
public function reconciliation($companyId, $asOf)
{
$costRows=$this->db->query("SELECT a.asset_account_id account_id,ac.kode_akun,ac.nama_akun,SUM(a.nilai_perolehan) subledger,COALESCE((SELECT SUM(jd.debit-jd.kredit) FROM journal_details jd JOIN journals j ON j.id=jd.journal_id WHERE j.company_id=? AND j.status='posted' AND j.tanggal<=? AND jd.account_id=a.asset_account_id),0) gl_value FROM assets a LEFT JOIN accounts ac ON ac.id=a.asset_account_id WHERE a.company_id=? AND a.workflow_status='capitalized' AND a.lifecycle_status NOT IN('disposed','lost','damaged') AND a.capitalization_date<=? GROUP BY a.asset_account_id,ac.kode_akun,ac.nama_akun",array((int)$companyId,$asOf,(int)$companyId,$asOf))->result();
$accRows=$this->db->query("SELECT a.accumulated_depreciation_account_id account_id,ac.kode_akun,ac.nama_akun,SUM(a.akumulasi_penyusutan) subledger,COALESCE((SELECT SUM(jd.kredit-jd.debit) FROM journal_details jd JOIN journals j ON j.id=jd.journal_id WHERE j.company_id=? AND j.status='posted' AND j.tanggal<=? AND jd.account_id=a.accumulated_depreciation_account_id),0) gl_value FROM assets a LEFT JOIN accounts ac ON ac.id=a.accumulated_depreciation_account_id WHERE a.company_id=? AND a.workflow_status='capitalized' AND a.lifecycle_status NOT IN('disposed','lost','damaged') AND a.capitalization_date<=? GROUP BY a.accumulated_depreciation_account_id,ac.kode_akun,ac.nama_akun",array((int)$companyId,$asOf,(int)$companyId,$asOf))->result();
$costSub=$costGl=$accSub=$accGl=0.0;foreach($costRows as$r){$costSub+=(float)$r->subledger;$costGl+=(float)$r->gl_value;$r->difference=(float)$r->subledger-(float)$r->gl_value;}foreach($accRows as$r){$accSub+=(float)$r->subledger;$accGl+=(float)$r->gl_value;$r->difference=(float)$r->subledger-(float)$r->gl_value;}
return array('as_of'=>$asOf,'cost_subledger'=>$costSub,'cost_gl'=>$costGl,'cost_difference'=>$costSub-$costGl,'accumulated_subledger'=>$accSub,'accumulated_gl'=>$accGl,'accumulated_difference'=>$accSub-$accGl,'book_value'=>$costSub-$accSub,'status'=>abs($costSub-$costGl)<.01&&abs($accSub-$accGl)<.01?'matched':'variance','cost_details'=>$costRows,'accumulated_details'=>$accRows);
}
public function report($companyId, $type, array $filters)
{
if ($type === 'depreciation') return $this->db->select('s.period,a.kode_asset,a.nama_asset,s.opening_book_value,s.depreciation_amount,s.closing_book_value,s.status,j.no_ref journal_no')->from('asset_depreciation_schedule s')->join('assets a','a.id=s.asset_id')->join('journals j','j.id=s.journal_id','left')->where('s.company_id',(int)$companyId)->where('s.due_date >=',$filters['date_from'])->where('s.due_date <=',$filters['date_to'])->order_by('s.period','DESC')->order_by('a.kode_asset')->get()->result();
if ($type === 'maintenance') return $this->db->select('m.maintenance_no,m.maintenance_date,m.next_due_date,a.kode_asset,a.nama_asset,m.vendor,m.description,m.cost,m.status')->from('asset_maintenance m')->join('assets a','a.id=m.asset_id')->where('m.company_id',(int)$companyId)->where('m.maintenance_date >=',$filters['date_from'])->where('m.maintenance_date <=',$filters['date_to'])->order_by('m.maintenance_date','DESC')->get()->result();
if ($type === 'events') return $this->db->select('e.event_no,e.event_date,e.event_type,a.kode_asset,a.nama_asset,e.amount,e.book_value_before,e.book_value_after,e.notes,j.no_ref journal_no')->from('asset_events e')->join('assets a','a.id=e.asset_id')->join('journals j','j.id=e.journal_id','left')->where('e.company_id',(int)$companyId)->where('e.event_date >=',$filters['date_from'])->where('e.event_date <=',$filters['date_to'])->order_by('e.event_date','DESC')->order_by('e.id','DESC')->get()->result();
$page=$this->registerPage($companyId,$filters,0,10000,'');return$page['rows'];
}
}
+79
View File
@@ -0,0 +1,79 @@
<?php
defined('BASEPATH') OR exit('No direct script access allowed');
class InventoryModel extends CI_Model
{
public function calculatedStock($itemId)
{
$row=$this->db->query("SELECT COALESCE(SUM(IF(direction='in',qty,-qty)),0) stok FROM inventory_ledger WHERE item_id=?",array((int)$itemId))->row();
return $row?(float)$row->stok:0.0;
}
public function synchronizeStock($itemId)
{
$stock=$this->calculatedStock($itemId);
if(!$this->db->where('id',(int)$itemId)->update('items',array('stok'=>$stock)))throw new RuntimeException('Gagal menyinkronkan cache stok barang.');
return $stock;
}
public function synchronizeCompany($companyId)
{
$items=$this->db->select('id')->where('company_id',(int)$companyId)->get('items')->result();
foreach($items as$item)$this->synchronizeStock($item->id);
return count($items);
}
public function dashboard($companyId)
{
$companyId=(int)$companyId;
$row=$this->db->query("SELECT COUNT(CASE WHEN i.status='active' THEN 1 END) active_items,COUNT(CASE WHEN i.status='draft' OR EXISTS(SELECT 1 FROM item_barcodes ib WHERE ib.item_id=i.id AND ib.status='pending') THEN 1 END) pending_items,COALESCE(SUM(CASE WHEN x.qty>0 THEN x.value ELSE 0 END),0) inventory_value,COUNT(CASE WHEN x.qty<=i.reorder_point THEN 1 END) reorder_items FROM items i LEFT JOIN(SELECT item_id,SUM(IF(direction='in',qty,-qty)) qty,SUM(IF(direction='in',value,-value)) value FROM inventory_ledger GROUP BY item_id)x ON x.item_id=i.id WHERE i.company_id=?",array($companyId))->row();
$row->open_documents=(int)$this->db->where('company_id',$companyId)->where_in('status',array('draft','submitted','approved'))->count_all_results('stock_documents');
$row->open_reservations=(int)$this->db->where('company_id',$companyId)->where_in('status',array('active','partially_released'))->count_all_results('stock_reservations');
$row->expiring_batches=(int)$this->db->where('company_id',$companyId)->where('is_active',1)->where('expiry_date IS NOT NULL',null,false)->where('expiry_date <=',date('Y-m-d',strtotime('+30 days')))->count_all_results('inventory_batches');
return$row;
}
public function recentDocuments($companyId,$limit=8)
{
return$this->db->select('d.*,wf.nama from_warehouse,wt.nama to_warehouse,u.nama creator_name')->from('stock_documents d')->join('warehouses wf','wf.id=d.from_warehouse_id','left')->join('warehouses wt','wt.id=d.to_warehouse_id','left')->join('users u','u.id=d.created_by','left')->where('d.company_id',(int)$companyId)->order_by('d.id','DESC')->limit((int)$limit)->get()->result();
}
public function actionItems($companyId,$limit=8)
{
return$this->db->query("SELECT i.id,i.kode_detail,i.nama_barang,i.harga_beli,i.harga_jual,COUNT(DISTINCT CASE WHEN ib.status='pending' THEN ib.id END) pending_barcodes,COUNT(DISTINCT CASE WHEN ib.status='pending' AND (kb.need_serial_number<>'true' OR NULLIF(ib.serial_number,'') IS NOT NULL) THEN ib.id END) ready_barcodes FROM items i LEFT JOIN kode_barang kb ON kb.id=i.kode_id LEFT JOIN item_barcodes ib ON ib.item_id=i.id WHERE i.company_id=? AND EXISTS(SELECT 1 FROM goods_receipt_lines grl WHERE grl.item_id=i.id) AND(i.status='draft' OR ib.status='pending') GROUP BY i.id,i.kode_detail,i.nama_barang,i.harga_beli,i.harga_jual ORDER BY i.id DESC LIMIT ".(int)$limit,array((int)$companyId))->result();
}
public function documentDetail($id,$companyId)
{
$d=$this->db->select('d.*,c.name company_name,c.company_code,wf.nama from_warehouse,wt.nama to_warehouse,uc.nama creator_name,us.nama submitter_name,ua.nama approver_name,up.nama poster_name,ur.nama rejecter_name')->from('stock_documents d')->join('companies c','c.id=d.company_id','left')->join('warehouses wf','wf.id=d.from_warehouse_id','left')->join('warehouses wt','wt.id=d.to_warehouse_id','left')->join('users uc','uc.id=d.created_by','left')->join('users us','us.id=d.submitted_by','left')->join('users ua','ua.id=d.approved_by','left')->join('users up','up.id=d.posted_by','left')->join('users ur','ur.id=d.rejected_by','left')->where(array('d.id'=>(int)$id,'d.company_id'=>(int)$companyId))->get()->row();
if(!$d)return null;
$d->lines=$this->db->select('l.*,i.kode_detail,i.nama_barang,k.unit,k.tracking_type,k.need_serial_number,b.barcode,b.serial_number,bt.batch_no,fb.code from_bin,tb.code to_bin')->from('stock_document_lines l')->join('items i','i.id=l.item_id')->join('kode_barang k','k.id=i.kode_id','left')->join('item_barcodes b','b.id=l.barcode_id','left')->join('inventory_batches bt','bt.id=l.batch_id','left')->join('warehouse_bins fb','fb.id=l.from_bin_id','left')->join('warehouse_bins tb','tb.id=l.to_bin_id','left')->where('l.stock_document_id',$d->id)->order_by('l.line_no')->order_by('l.id')->get()->result();
$d->history=$this->db->select('h.*,u.nama user_name')->from('stock_document_status_history h')->join('users u','u.id=h.user_id','left')->where('h.stock_document_id',$d->id)->order_by('h.id')->get()->result();
$d->attachments=$this->db->where(array('entity_type'=>'stock_document','entity_id'=>$d->id))->order_by('id')->get('transaction_attachments')->result();
return$d;
}
public function valuation($companyId,array$filters=array())
{
$params=array((int)$companyId);$where='';
if(!empty($filters['warehouse_id'])){$where.=' AND l.warehouse_id=?';$params[]=(int)$filters['warehouse_id'];}
if(!empty($filters['item_id'])){$where.=' AND l.item_id=?';$params[]=(int)$filters['item_id'];}
return$this->db->query("SELECT i.id,i.kode_detail,i.nama_barang,l.warehouse_id,w.nama warehouse_name,i.costing_method,i.minimum_stock,i.reorder_point,i.last_movement_at,SUM(IF(l.direction='in',l.qty,-l.qty)) qty,SUM(IF(l.direction='in',l.value,-l.value)) inventory_value FROM inventory_ledger l JOIN items i ON i.id=l.item_id JOIN warehouses w ON w.id=l.warehouse_id WHERE i.company_id=? {$where} GROUP BY i.id,i.kode_detail,i.nama_barang,l.warehouse_id,w.nama,i.costing_method,i.minimum_stock,i.reorder_point,i.last_movement_at ORDER BY i.nama_barang,w.nama",$params)->result();
}
public function reconciliation($companyId,$asOf)
{
$companyId=(int)$companyId;
$ledger=(float)$this->db->query("SELECT COALESCE(SUM(IF(l.direction='in',l.value,-l.value)),0) total FROM inventory_ledger l JOIN items i ON i.id=l.item_id WHERE i.company_id=? AND l.movement_date<=?",array($companyId,$asOf))->row()->total;
$stockLog=(float)$this->db->query("SELECT COALESCE(SUM(IF(s.tipe='masuk',s.qty*COALESCE(s.unit_cost,0),-s.qty*COALESCE(s.unit_cost,0))),0) total FROM stock_logs s JOIN items i ON i.id=s.item_id WHERE i.company_id=? AND DATE(s.created_at)<=?",array($companyId,$asOf))->row()->total;
$mapping=$this->db->where(array('mapping_key'=>'inventory','is_active'=>1))->get('system_account_mappings')->row();$gl=0;
if($mapping)$gl=(float)$this->db->query("SELECT COALESCE(SUM(d.debit-d.kredit),0) total FROM journals j JOIN journal_details d ON d.journal_id=j.id WHERE d.account_id=? AND j.company_id=? AND j.tanggal<=? AND j.status IN('posted','reversed')",array($mapping->account_id,$companyId,$asOf))->row()->total;
$cache=(int)$this->db->query("SELECT COUNT(*) total FROM items i LEFT JOIN(SELECT item_id,SUM(IF(direction='in',qty,-qty)) qty FROM inventory_ledger WHERE movement_date<=? GROUP BY item_id)l ON l.item_id=i.id WHERE i.company_id=? AND ABS(i.stok-COALESCE(l.qty,0))>.0001",array($asOf,$companyId))->row()->total;
$barcode=(int)$this->db->query("SELECT COUNT(*) total FROM(SELECT l.item_id,l.warehouse_id,l.qty-COALESCE(b.qty,0) variance FROM(SELECT il.item_id,il.warehouse_id,SUM(IF(il.direction='in',il.qty,-il.qty)) qty FROM inventory_ledger il JOIN items i ON i.id=il.item_id WHERE i.company_id=? AND il.movement_date<=? GROUP BY il.item_id,il.warehouse_id)l LEFT JOIN(SELECT item_id,warehouse_id,SUM(qty_sisa) qty FROM item_barcodes GROUP BY item_id,warehouse_id)b ON b.item_id=l.item_id AND b.warehouse_id=l.warehouse_id HAVING ABS(variance)>.0001)x",array($companyId,$asOf))->row()->total;
$details=array();
$details['ledger_vs_stock_log']=$this->db->query("SELECT i.id item_id,i.kode_detail,i.nama_barang,w.nama warehouse_name,COALESCE(l.qty,0) ledger_qty,COALESCE(s.qty,0) stock_log_qty,COALESCE(l.inventory_value,0) ledger_value,COALESCE(s.stock_log_value,0) stock_log_value,COALESCE(l.qty,0)-COALESCE(s.qty,0) qty_difference,COALESCE(l.inventory_value,0)-COALESCE(s.stock_log_value,0) value_difference FROM(SELECT item_id,warehouse_id FROM inventory_ledger WHERE movement_date<=? UNION SELECT item_id,COALESCE(warehouse_id,0) FROM stock_logs WHERE DATE(created_at)<=?)p JOIN items i ON i.id=p.item_id LEFT JOIN warehouses w ON w.id=p.warehouse_id LEFT JOIN(SELECT item_id,warehouse_id,SUM(IF(direction='in',qty,-qty)) qty,SUM(IF(direction='in',value,-value)) inventory_value FROM inventory_ledger WHERE movement_date<=? GROUP BY item_id,warehouse_id)l ON l.item_id=p.item_id AND l.warehouse_id=p.warehouse_id LEFT JOIN(SELECT item_id,COALESCE(warehouse_id,0) warehouse_id,SUM(IF(tipe='masuk',qty,-qty)) qty,SUM(IF(tipe='masuk',qty*COALESCE(unit_cost,0),-qty*COALESCE(unit_cost,0))) stock_log_value FROM stock_logs WHERE DATE(created_at)<=? GROUP BY item_id,COALESCE(warehouse_id,0))s ON s.item_id=p.item_id AND s.warehouse_id=p.warehouse_id WHERE i.company_id=? HAVING ABS(qty_difference)>.0001 OR ABS(value_difference)>.01 ORDER BY ABS(value_difference) DESC,ABS(qty_difference) DESC LIMIT 50",array($asOf,$asOf,$asOf,$asOf,$companyId))->result();
$details['barcode']=$this->db->query("SELECT i.id item_id,i.kode_detail,i.nama_barang,w.nama warehouse_name,l.qty ledger_qty,COALESCE(b.qty,0) barcode_qty,l.qty-COALESCE(b.qty,0) difference FROM(SELECT il.item_id,il.warehouse_id,SUM(IF(il.direction='in',il.qty,-il.qty)) qty FROM inventory_ledger il WHERE il.movement_date<=? GROUP BY il.item_id,il.warehouse_id)l JOIN items i ON i.id=l.item_id LEFT JOIN warehouses w ON w.id=l.warehouse_id LEFT JOIN(SELECT item_id,warehouse_id,SUM(qty_sisa) qty FROM item_barcodes GROUP BY item_id,warehouse_id)b ON b.item_id=l.item_id AND b.warehouse_id=l.warehouse_id WHERE i.company_id=? HAVING ABS(difference)>.0001 ORDER BY ABS(difference) DESC LIMIT 50",array($asOf,$companyId))->result();
$details['cache']=$this->db->query("SELECT i.id item_id,i.kode_detail,i.nama_barang,i.stok cache_qty,COALESCE(l.qty,0) ledger_qty,i.stok-COALESCE(l.qty,0) difference FROM items i LEFT JOIN(SELECT item_id,SUM(IF(direction='in',qty,-qty)) qty FROM inventory_ledger WHERE movement_date<=? GROUP BY item_id)l ON l.item_id=i.id WHERE i.company_id=? HAVING ABS(difference)>.0001 ORDER BY ABS(difference) DESC LIMIT 50",array($asOf,$companyId))->result();
return array('ledger_value'=>$ledger,'stock_log_value'=>$stockLog,'gl_value'=>$gl,'difference'=>$ledger-$gl,'cache_variance_count'=>$cache,'barcode_variance_count'=>$barcode,'status'=>abs($ledger-$gl)<.01&&abs($ledger-$stockLog)<.01&&$cache===0&&$barcode===0?'matched':'variance','details'=>$details);
}
}
+24
View File
@@ -0,0 +1,24 @@
<?php
defined('BASEPATH') OR exit('No direct script access allowed');
class InvoiceModel extends CI_Model
{
public function find($id)
{
return $this->db->get_where('invoices', array('id' => (int) $id))->row();
}
public function createDraft(array $data)
{
$data['status'] = 'draft';
if (!$this->db->insert('invoices', $data)) {
throw new RuntimeException('Gagal membuat draft invoice.');
}
return (int) $this->db->insert_id();
}
public function updateById($id, array $data)
{
return $this->db->where('id', (int) $id)->update('invoices', $data);
}
}
+69
View File
@@ -0,0 +1,69 @@
<?php
defined('BASEPATH') OR exit('No direct script access allowed');
class JournalModel extends CI_Model
{
public function createHeader(array $data)
{
if (!$this->db->insert('journals', $data)) {
throw new RuntimeException('Gagal membuat header jurnal.');
}
return (int) $this->db->insert_id();
}
public function addLines(array $lines)
{
if (empty($lines) || !$this->db->insert_batch('journal_details', $lines)) {
throw new RuntimeException('Gagal membuat detail jurnal.');
}
}
public function referenceExists($reference)
{
return $this->db->where('no_ref', $reference)->count_all_results('journals') > 0;
}
public function sourceExists($type, $id)
{
return $this->db
->where('ref_type', $type)
->where('ref_id', $id)
->count_all_results('journals') > 0;
}
public function find($id)
{
return $this->db->get_where('journals', array('id' => (int) $id))->row();
}
public function findForUpdate($id)
{
return $this->db->query('SELECT * FROM journals WHERE id = ? FOR UPDATE', array((int) $id))->row();
}
public function lines($journalId)
{
return $this->db->where('journal_id', (int) $journalId)->order_by('id', 'ASC')->get('journal_details')->result_array();
}
public function updateStatus($journalId, $status, array $extra = array())
{
return $this->db->where('id', (int) $journalId)->update('journals', array_merge(array(
'status' => $status,
'updated_at' => date('Y-m-d H:i:s')
), $extra));
}
public function addStatusHistory($journalId, $from, $to, $userId, $notes = null)
{
return $this->db->insert('journal_status_histories', array(
'journal_id' => (int) $journalId,
'from_status' => $from,
'to_status' => $to,
'notes' => $notes,
'changed_by' => (int) $userId,
'changed_at' => date('Y-m-d H:i:s')
));
}
}
+183 -31
View File
@@ -8,6 +8,12 @@ class PeralatanTeknisi_model extends CI_Model {
parent::__construct();
}
private function companyId()
{
$companyId = (int) $this->session->userdata('company_id');
return $companyId > 0 ? $companyId : 1;
}
public function get_teknisi_list()
{
return $this->db
@@ -15,8 +21,9 @@ class PeralatanTeknisi_model extends CI_Model {
->from('k_employees e')
->join('k_positions p', 'p.id = e.position_id', 'left')
->join('k_departments d', 'd.id = e.department_id', 'left')
->where('e.company_id', $this->companyId())
->where('e.is_active', 1)
->where('(e.resign_date IS NULL OR e.resign_date = \'0000-00-00\')')
->where_in('LOWER(p.position_name)', ['teknisi', 'noc'])
->order_by('e.full_name', 'ASC')
->get()
->result();
@@ -42,6 +49,8 @@ class PeralatanTeknisi_model extends CI_Model {
->join('items i', 'i.id = it.item_id', 'left')
->join('k_employees ke', 'ke.id = it.user_id', 'left');
$this->db->where('i.company_id', $this->companyId());
if ($user_id) {
$this->db->where('it.user_id', $user_id);
}
@@ -65,6 +74,7 @@ class PeralatanTeknisi_model extends CI_Model {
->from('item_barcodes ib')
->join('items i', 'i.id = ib.item_id', 'left')
->where('ib.barcode', $barcode)
->where('i.company_id', $this->companyId())
->where('ib.qty_sisa >', 0)
->get()
->row();
@@ -75,6 +85,7 @@ class PeralatanTeknisi_model extends CI_Model {
->from('item_barcodes ib')
->join('items i', 'i.id = ib.item_id', 'left')
->where('ib.serial_number', $barcode)
->where('i.company_id', $this->companyId())
->where('ib.qty_sisa >', 0)
->get()
->row();
@@ -89,8 +100,10 @@ class PeralatanTeknisi_model extends CI_Model {
->select('it.user_id, ke.full_name, ke.employee_code')
->from('item_technician it')
->join('k_employees ke', 'ke.id = it.user_id', 'left')
->join('items i', 'i.id = it.item_id', 'inner')
->where('it.barcode', $barcode)
->where('it.status', 'active')
->where('i.company_id', $this->companyId())
->get()
->row();
}
@@ -106,31 +119,36 @@ class PeralatanTeknisi_model extends CI_Model {
$barcode = trim($barcode);
if ($barcode == '') continue;
$item = $this->db
->select('it.*, ib.id as barcode_id, tec.full_name as teknisi_name')
->from('item_technician it')
->join('item_barcodes ib', 'ib.barcode = it.barcode', 'left')
->join('k_employees tec', 'tec.id = it.user_id', 'left')
->where('it.barcode', $barcode)
->where('it.status', 'active')
->get()
->row();
$item = $this->db->query(
"SELECT it.*, ib.id AS barcode_id, ib.warehouse_id, ib.qty_sisa, tec.full_name AS teknisi_name
FROM item_technician it
JOIN item_barcodes ib ON ib.barcode = it.barcode
JOIN items i ON i.id = it.item_id AND i.company_id = ?
LEFT JOIN k_employees tec ON tec.id = it.user_id
WHERE it.barcode = ? AND it.status = 'active'
FOR UPDATE",
[$this->companyId(), $barcode]
)->row();
if (!$item) {
$errors[] = "Barcode $barcode tidak ditemukan atau sudah dikembalikan";
continue;
}
$assigned=(float)($item->assigned_qty?:$item->qty_sisa);$carried=(float)$item->carried_qty;$installed=(float)$item->installed_qty;
if($carried>0&&$installed>0){$errors[]="Barcode $barcode memiliki status sebagian dibawa dan sebagian dipasang; gunakan Detail Teknisi untuk menentukan jumlah dan asal pengembalian";continue;}
$source=$installed>0?'installed':'carried';$qty=$assigned;
$this->db->insert('item_movements', [
'item_id' => $item->item_id,
'barcode_id' => $item->barcode_id,
'qty' => 1,
'from_type' => 'technician',
'from_id' => $item->user_id,
'qty' => $qty,
'from_type' => $source === 'installed' ? 'customer' : 'technician',
'from_id' => $source === 'installed' ? $item->customer_id : $item->user_id,
'to_type' => 'warehouse',
'to_id' => null,
'to_id' => $item->warehouse_id,
'movement_type' => 'transfer',
'notes' => 'Pengembalian dari: ' . $item->teknisi_name . ' ke gudang',
'notes' => (($item->usage_status??'carried') === 'installed' ? 'Pengembalian dari customer melalui teknisi: ' : 'Pengembalian dari teknisi: ') . $item->teknisi_name . ' ke gudang',
'created_at' => $now
]);
@@ -139,9 +157,14 @@ class PeralatanTeknisi_model extends CI_Model {
->where('status', 'active')
->update('item_technician', [
'status' => 'inactive',
'assigned_qty' => 0,
'carried_qty' => 0,
'installed_qty' => 0,
'deleted_at' => $now
]);
$this->db->insert('technician_item_status_logs',array('company_id'=>$this->companyId(),'user_id'=>$item->user_id,'item_id'=>$item->item_id,'barcode_id'=>$item->barcode_id,'barcode'=>$item->barcode,'from_status'=>$source,'to_status'=>'returned','qty'=>$qty,'customer_id'=>$source==='installed'?$item->customer_id:null,'event_date'=>date('Y-m-d'),'notes'=>'Pengembalian batch ke gudang','created_by'=>(int)$this->session->userdata('user_id'),'created_at'=>$now));
if ($item->barcode_id) {
$this->db
->where('id', $item->barcode_id)
@@ -171,8 +194,10 @@ class PeralatanTeknisi_model extends CI_Model {
return $this->db
->select("DATE(it.created_at) as tanggal")
->from('item_technician it')
->join('items i', 'i.id = it.item_id', 'inner')
->where('it.user_id', $user_id)
->where('it.status', 'active')
->where('i.company_id', $this->companyId())
->group_by("DATE(it.created_at)")
->order_by("DATE(it.created_at)", "DESC")
->get()
@@ -189,22 +214,34 @@ class PeralatanTeknisi_model extends CI_Model {
->select('id, full_name, employee_code')
->from('k_employees')
->where('id', $user_id)
->where('company_id', $this->companyId())
->get()
->row();
if (!$technician) {
return ['status' => false, 'message' => 'Teknisi tidak ditemukan pada perusahaan aktif.'];
}
$this->db->trans_start();
foreach ($barcodes as $barcode) {
$barcode = trim($barcode);
if ($barcode == '') continue;
$barcode_row = $this->db
->select('ib.id as barcode_id, ib.item_id, ib.barcode')
->from('item_barcodes ib')
->where('ib.barcode', $barcode)
->where_not_in('ib.status', ['damaged', 'lost', 'returned_supplier', 'sold_out'])
->get()
->row();
$barcode_row = $this->db->query(
"SELECT ib.id AS barcode_id, ib.item_id, ib.barcode, ib.warehouse_id,ib.qty_sisa,
COALESCE(k.tracking_type,'QTY') tracking_type
FROM item_barcodes ib
JOIN items i ON i.id = ib.item_id AND i.company_id = ?
LEFT JOIN kode_barang k ON k.id=i.kode_id
WHERE ib.barcode = ?
AND ib.status = 'available'
AND ib.qty_sisa > 0
AND COALESCE(ib.reserved_qty, 0) = 0
AND COALESCE(ib.return_reserved_qty, 0) = 0
FOR UPDATE",
[$this->companyId(), $barcode]
)->row();
if (!$barcode_row) {
$errors[] = "Barcode $barcode tidak valid atau tidak tersedia";
@@ -228,9 +265,14 @@ class PeralatanTeknisi_model extends CI_Model {
$this->db->insert('item_technician', [
'user_id' => $user_id,
'company_id' => $this->companyId(),
'barcode' => $barcode_row->barcode,
'item_id' => $barcode_row->item_id,
'status' => 'active',
'usage_status' => 'carried',
'assigned_qty' => (float)$barcode_row->qty_sisa,
'carried_qty' => (float)$barcode_row->qty_sisa,
'installed_qty' => 0,
'created_at' => $now
]);
@@ -242,7 +284,7 @@ class PeralatanTeknisi_model extends CI_Model {
->row();
$from_type = 'warehouse';
$from_id = null;
$from_id = $barcode_row->warehouse_id;
if ($last_movement) {
$from_type = $last_movement->to_type;
$from_id = $last_movement->to_id;
@@ -251,7 +293,7 @@ class PeralatanTeknisi_model extends CI_Model {
$this->db->insert('item_movements', [
'item_id' => $barcode_row->item_id,
'barcode_id' => $barcode_row->barcode_id,
'qty' => 1,
'qty' => (float)$barcode_row->qty_sisa,
'from_type' => $from_type,
'from_id' => $from_id,
'to_type' => 'technician',
@@ -307,6 +349,7 @@ class PeralatanTeknisi_model extends CI_Model {
->from('k_employees e')
->join('k_positions p', 'p.id = e.position_id', 'left')
->join('k_departments d', 'd.id = e.department_id', 'left')
->where('e.company_id', $this->companyId())
->where('(e.resign_date IS NULL OR e.resign_date = \'0000-00-00\')')
->where_in('LOWER(p.position_name)', ['teknisi', 'noc']);
@@ -336,6 +379,7 @@ class PeralatanTeknisi_model extends CI_Model {
$this->db
->from('k_employees e')
->join('k_positions p', 'p.id = e.position_id', 'left')
->where('e.company_id', $this->companyId())
->where('(e.resign_date IS NULL OR e.resign_date = \'0000-00-00\')')
->where_in('LOWER(p.position_name)', ['teknisi', 'noc']);
@@ -348,6 +392,7 @@ class PeralatanTeknisi_model extends CI_Model {
->from('k_employees e')
->join('k_positions p', 'p.id = e.position_id', 'left')
->join('k_departments d', 'd.id = e.department_id', 'left')
->where('e.company_id', $this->companyId())
->where('(e.resign_date IS NULL OR e.resign_date = \'0000-00-00\')')
->where_in('LOWER(p.position_name)', ['teknisi', 'noc']);
@@ -370,6 +415,7 @@ class PeralatanTeknisi_model extends CI_Model {
->join('k_positions p', 'p.id = e.position_id', 'left')
->join('k_departments d', 'd.id = e.department_id', 'left')
->where('e.id', $user_id)
->where('e.company_id', $this->companyId())
->get()
->row();
}
@@ -387,17 +433,32 @@ class PeralatanTeknisi_model extends CI_Model {
$this->db
->select("
it.barcode,
it.user_id,
i.nama_barang,
ib.serial_number,
ib.qty_sisa,
COALESCE(k.tracking_type, 'QTY') tracking_type,
COALESCE(NULLIF(it.assigned_qty,0),ib.qty_sisa) assigned_qty,
COALESCE(it.carried_qty,CASE WHEN COALESCE(it.usage_status,'carried')='carried' THEN ib.qty_sisa ELSE 0 END) carried_qty,
COALESCE(it.installed_qty,CASE WHEN it.usage_status='installed' THEN ib.qty_sisa ELSE 0 END) installed_qty,
it.status,
COALESCE(it.usage_status, 'carried') usage_status,
it.usage_date,
it.customer_id,
c.nama customer_name,
ib.id barcode_id,
COALESCE((SELECT MAX(l.attachment_id) FROM technician_item_status_logs l WHERE l.company_id=it.company_id AND l.barcode_id=ib.id),
(SELECT MAX(ta.id) FROM transaction_attachments ta WHERE ta.entity_type='technician_installation' AND ta.entity_id=ib.id)) proof_id,
DATE_FORMAT(it.created_at, '%d/%m/%Y') as tanggal
")
->from('item_technician it')
->join('items i', 'i.id = it.item_id', 'left')
->join('kode_barang k', 'k.id = i.kode_id', 'left')
->join('item_barcodes ib', 'ib.barcode = it.barcode', 'left')
->join('customers c', 'c.id = it.customer_id', 'left')
->where('it.user_id', $user_id)
->where('it.status', 'active');
->where('it.status', 'active')
->where('i.company_id', $this->companyId());
if (!empty($search)) {
$this->db->group_start()
@@ -424,8 +485,10 @@ class PeralatanTeknisi_model extends CI_Model {
{
$this->db
->from('item_technician it')
->join('items i', 'i.id = it.item_id', 'inner')
->where('it.user_id', $user_id)
->where('it.status', 'active');
->where('it.status', 'active')
->where('i.company_id', $this->companyId());
return $this->db->count_all_results();
}
@@ -437,7 +500,8 @@ class PeralatanTeknisi_model extends CI_Model {
->join('items i', 'i.id = it.item_id', 'left')
->join('item_barcodes ib', 'ib.barcode = it.barcode', 'left')
->where('it.user_id', $user_id)
->where('it.status', 'active');
->where('it.status', 'active')
->where('i.company_id', $this->companyId());
if (!empty($search)) {
$this->db->group_start()
@@ -469,9 +533,10 @@ class PeralatanTeknisi_model extends CI_Model {
->join('item_barcodes ib', 'ib.id = im.barcode_id', 'left')
->join('k_employees from_ke', 'from_ke.id = im.from_id', 'left')
->join('k_employees to_ke', 'to_ke.id = im.to_id', 'left')
->where('im.movement_type', 'installation')
->where_in('im.movement_type', ['installation', 'transfer'])
->where('im.from_type', 'technician')
->where('im.to_type', 'technician')
->where('i.company_id', $this->companyId())
->order_by('im.created_at', 'DESC')
->limit(200)
->get()
@@ -496,6 +561,7 @@ class PeralatanTeknisi_model extends CI_Model {
->join('items i', 'i.id = it.item_id', 'left')
->join('k_employees ke', 'ke.id = it.user_id', 'left')
->where('it.barcode', $barcode)
->where('i.company_id', $this->companyId())
->get()
->row();
}
@@ -515,7 +581,8 @@ class PeralatanTeknisi_model extends CI_Model {
->join('items i', 'i.id = it.item_id', 'left')
->join('item_barcodes ib', 'ib.barcode = it.barcode', 'left')
->where('it.user_id', $user_id)
->where('it.status', 'active');
->where('it.status', 'active')
->where('i.company_id', $this->companyId());
if ($tanggal AND $tanggal != 'all') {
$this->db->where('DATE(it.created_at)', $tanggal);
@@ -534,23 +601,108 @@ class PeralatanTeknisi_model extends CI_Model {
public function get_log_barang()
{
return $this->db
return $this->get_log_barang_filtered(array());
}
public function get_log_barang_filtered(array $filters)
{
$this->db
->select("
it.barcode,
it.item_id,
it.user_id,
it.status,
COALESCE(it.usage_status, 'carried') usage_status,
COALESCE(it.assigned_qty,0) assigned_qty,
COALESCE(it.carried_qty,0) carried_qty,
COALESCE(it.installed_qty,0) installed_qty,
it.usage_date,
it.customer_id,
it.usage_notes,
it.created_at,
it.deleted_at,
ke.full_name as teknisi_name,
ke.employee_code,
c.nama customer_name,
i.kode_detail as item_code,
i.nama_barang
i.nama_barang,
ib.serial_number,
ib.qty_sisa,
(SELECT COUNT(*) FROM transaction_attachments ta WHERE ta.entity_type='technician_installation' AND ta.entity_id=ib.id) attachment_count
")
->from('item_technician it')
->join('k_employees ke', 'ke.id = it.user_id', 'left')
->join('items i', 'i.id = it.item_id', 'left')
->join('item_barcodes ib', 'ib.barcode = it.barcode', 'left')
->join('customers c', 'c.id = it.customer_id', 'left')
->where('i.company_id', $this->companyId());
if(!empty($filters['user_id']))$this->db->where('it.user_id',(int)$filters['user_id']);
if(!empty($filters['customer_id']))$this->db->where('it.customer_id',(int)$filters['customer_id']);
$status=trim((string)($filters['status']??''));
if($status==='returned'){
$this->db->where('it.status','inactive');
}elseif($status==='installed'){
$this->db->where('it.status','active')->where('it.installed_qty >',0);
}elseif($status==='carried'){
$this->db->where('it.status','active')->where('it.carried_qty >',0);
}
$from=trim((string)($filters['date_from']??''));$to=trim((string)($filters['date_to']??''));
if($from!==''||$to!==''){
$from=$from!==''?$from:'1900-01-01';$to=$to!==''?$to:'2999-12-31';
$this->db->group_start()->where("DATE(it.created_at) BETWEEN ".$this->db->escape($from).' AND '.$this->db->escape($to),null,false)->or_where("it.usage_date BETWEEN ".$this->db->escape($from).' AND '.$this->db->escape($to),null,false)->or_where("DATE(it.deleted_at) BETWEEN ".$this->db->escape($from).' AND '.$this->db->escape($to),null,false)->group_end();
}
return $this->db
->order_by('it.created_at', 'DESC')
->get()
->result();
}
public function change_item_status($barcode,$userId,array $data,$actorId)
{
$barcode=trim((string)$barcode);$company=$this->companyId();$now=date('Y-m-d H:i:s');
$this->db->trans_begin();
try{
$key=trim((string)($data['idempotency_key']??''));
if($key!==''){$duplicate=$this->db->where(array('company_id'=>$company,'idempotency_key'=>$key))->get('technician_item_status_logs')->row();if($duplicate){$this->db->trans_commit();return array('duplicate'=>true,'log_id'=>(int)$duplicate->id,'qty'=>(float)$duplicate->qty,'status'=>$duplicate->to_status);}}
$row=$this->db->query("SELECT it.*,ib.id barcode_id,ib.qty_sisa,ib.warehouse_id,i.nama_barang,e.full_name technician_name,COALESCE(k.tracking_type,'QTY') tracking_type FROM item_technician it JOIN item_barcodes ib ON ib.barcode=it.barcode JOIN items i ON i.id=it.item_id JOIN k_employees e ON e.id=it.user_id LEFT JOIN kode_barang k ON k.id=i.kode_id WHERE it.company_id=? AND it.user_id=? AND it.barcode=? AND it.status='active' FOR UPDATE",array($company,(int)$userId,$barcode))->row();
if(!$row)throw new RuntimeException('Barang aktif pada teknisi tidak ditemukan.');
$target=(string)$data['status'];$tracking=strtoupper((string)$row->tracking_type)==='UNIT'?'UNIT':'QTY';
$assigned=(float)$row->assigned_qty;$carried=(float)$row->carried_qty;$installed=(float)$row->installed_qty;
if($assigned<=0){$assigned=(float)$row->qty_sisa;if(($row->usage_status??'carried')==='installed')$installed=$assigned;else$carried=$assigned;}
$qty=$tracking==='UNIT'?1:(float)($data['qty']??0);
if($qty<=0)throw new RuntimeException('Jumlah yang diubah wajib lebih dari 0.');
$source='';$customerId=null;$customerName='-';
if($target==='installed'){$source='carried';$available=$carried;$customerId=(int)($data['customer_id']??0);if($customerId<1)throw new RuntimeException('Customer wajib dipilih untuk status Dipasang.');$customer=$this->db->select('id,nama')->where(array('id'=>$customerId,'company_id'=>$company))->get('customers')->row();if(!$customer)throw new RuntimeException('Customer tidak ditemukan pada perusahaan aktif.');$customerName=$customer->nama;if($installed>0&&!empty($row->customer_id)&&(int)$row->customer_id!==$customerId)throw new RuntimeException('Sebagian barcode ini sudah dipasang pada customer lain. Kembalikan dahulu ke teknisi sebelum memilih customer berbeda.');}
elseif($target==='carried'){$source='installed';$available=$installed;$customerId=(int)$row->customer_id;if($customerId<1)throw new RuntimeException('Customer asal pemasangan tidak ditemukan.');$customer=$this->db->select('nama')->where(array('id'=>$customerId,'company_id'=>$company))->get('customers')->row();$customerName=$customer?$customer->nama:'-';}
elseif($target==='warehouse'){$source=(string)($data['source_status']??'');if(!in_array($source,array('carried','installed'),true)){if($carried>0&&$installed<=0)$source='carried';elseif($installed>0&&$carried<=0)$source='installed';else throw new RuntimeException('Pilih asal barang yang akan dikembalikan ke gudang.');}$available=$source==='installed'?$installed:$carried;if($source==='installed'){$customerId=(int)$row->customer_id;$customer=$customerId?$this->db->select('nama')->where(array('id'=>$customerId,'company_id'=>$company))->get('customers')->row():null;$customerName=$customer?$customer->nama:'-';}}
else throw new RuntimeException('Status tujuan tidak valid.');
if($qty>$available+0.00001)throw new RuntimeException('Jumlah melebihi saldo status asal ('.number_format($available,4,',','.').').');
if($source==='carried')$carried-=$qty;else$installed-=$qty;
if($target==='installed')$installed+=$qty;elseif($target==='carried')$carried+=$qty;else$assigned-=$qty;
$carried=max(0,$carried);$installed=max(0,$installed);$assigned=max(0,$assigned);
$usage=$installed>0&&$carried>0?'mixed':($installed>0?'installed':'carried');$inactive=$assigned<=0.00001;
$update=array('assigned_qty'=>$assigned,'carried_qty'=>$carried,'installed_qty'=>$installed,'usage_status'=>$usage,'usage_date'=>$installed>0?($target==='installed'?$data['date']:$row->usage_date):null,'customer_id'=>$installed>0?$customerId:null,'usage_notes'=>$data['notes'],'updated_by'=>(int)$actorId,'updated_at'=>$now);
if($inactive){$update['status']='inactive';$update['deleted_at']=$now;}
$this->db->where(array('company_id'=>$company,'user_id'=>(int)$userId,'barcode'=>$barcode,'status'=>'active'))->update('item_technician',$update);
$labels=array('carried'=>'Dibawa Teknisi','installed'=>'Dipasang di Customer','warehouse'=>'Dikembalikan ke Gudang');
$notes='[AUTO] Perubahan status peralatan | Barang '.$row->nama_barang.' | Barcode '.$row->barcode.' | Teknisi '.$row->technician_name.' | Dari '.$labels[$source].' ke '.$labels[$target].' | Qty '.number_format($qty,4,',','.').' | Tanggal '.$data['date'].' | Customer '.$customerName.' | Keterangan '.$data['notes'];
$movement=array('item_id'=>$row->item_id,'barcode_id'=>$row->barcode_id,'qty'=>$qty,'from_type'=>$source==='installed'?'customer':'technician','from_id'=>$source==='installed'?$customerId:$row->user_id,'to_type'=>$target==='installed'?'customer':($target==='carried'?'technician':'warehouse'),'to_id'=>$target==='installed'?$customerId:($target==='carried'?$row->user_id:$row->warehouse_id),'movement_type'=>$target==='installed'?'installation':($target==='carried'?'uninstallation':'transfer'),'notes'=>mb_substr($notes,0,250),'created_at'=>$now);
$this->db->insert('item_movements',$movement);
$this->db->insert('technician_item_status_logs',array('company_id'=>$company,'user_id'=>(int)$userId,'item_id'=>$row->item_id,'barcode_id'=>$row->barcode_id,'barcode'=>$row->barcode,'from_status'=>$source,'to_status'=>$target==='warehouse'?'returned':$target,'qty'=>$qty,'customer_id'=>$customerId?:null,'event_date'=>$data['date'],'notes'=>$data['notes'],'idempotency_key'=>$key?:null,'created_by'=>(int)$actorId,'created_at'=>$now));
$logId=(int)$this->db->insert_id();
$this->db->where('id',$row->barcode_id)->update('item_barcodes',array('status'=>$inactive?'available':'installed'));
if($this->db->trans_status()===false)throw new RuntimeException('Perubahan status barang gagal disimpan.');
$this->db->trans_commit();
return array('log_id'=>$logId,'barcode_id'=>(int)$row->barcode_id,'status'=>$target,'qty'=>$qty,'customer_name'=>$customerName,'technician_name'=>$row->technician_name,'remaining_qty'=>$assigned);
}catch(Throwable$e){$this->db->trans_rollback();throw$e;}
}
public function get_status_history($userId)
{
return $this->db->select('l.*,i.kode_detail,i.nama_barang,c.nama customer_name,u.nama actor_name')->from('technician_item_status_logs l')->join('items i','i.id=l.item_id','left')->join('customers c','c.id=l.customer_id','left')->join('users u','u.id=l.created_by','left')->where(array('l.company_id'=>$this->companyId(),'l.user_id'=>(int)$userId))->order_by('l.event_date','DESC')->order_by('l.id','DESC')->get()->result();
}
}