Files
accounting_dev_v2/application/libraries/FixedAssetService.php
T
2026-09-11 16:03:00 +07:00

304 lines
50 KiB
PHP

<?php
defined('BASEPATH') OR exit('No direct script access allowed');
require_once APPPATH.'exceptions/BusinessException.php';
class FixedAssetService
{
private $CI;
public function __construct()
{
$this->CI =& get_instance();
$this->CI->load->library(array('PostingService','FiscalPeriodService','NumberingService','AccountMappingService','AuditService'));
}
private function company()
{
return (int) $this->CI->session->userdata('company_id');
}
private function transaction(callable $callback)
{
$db=$this->CI->db;$db->trans_begin();
try {
$result=$callback();
if(!$db->trans_status())throw new RuntimeException('Transaksi aset gagal disimpan.');
$db->trans_commit();return$result;
} catch(Throwable $e) {
$db->trans_rollback();throw$e;
}
}
private function asset($id, $forUpdate = true)
{
$sql='SELECT * FROM assets WHERE id=? AND company_id=?'.($forUpdate?' FOR UPDATE':'');
$asset=$this->CI->db->query($sql,array((int)$id,$this->company()))->row();
if(!$asset)throw new BusinessException('Aset tidak ditemukan.',array(),404);
return$asset;
}
private function account($id, $label)
{
$row=$this->CI->db->get_where('accounts',array('id'=>(int)$id,'company_id'=>$this->company()))->row();
if(!$row)throw new BusinessException($label.' tidak valid untuk perusahaan aktif.');
if(!(int)$row->is_active)throw new BusinessException($label.' '.$row->kode_akun.' - '.$row->nama_akun.' sudah nonaktif. Pilih akun aktif.');
if(!(int)$row->allow_posting)throw new BusinessException($label.' '.$row->kode_akun.' - '.$row->nama_akun.' merupakan akun induk/non-posting. Pilih akun detail yang dapat menerima jurnal.');
return$row;
}
private function location($id)
{
$row=$this->CI->db->get_where('lokasi_asset',array('id'=>(int)$id,'company_id'=>$this->company(),'is_active'=>1))->row();
if(!$row)throw new BusinessException('Lokasi aset tidak valid atau sudah nonaktif.');
return$row;
}
private function history($assetId,$from,$to,$notes,$userId)
{
$this->CI->db->insert('asset_workflow_history',array('company_id'=>$this->company(),'asset_id'=>(int)$assetId,'from_status'=>$from?:null,'to_status'=>$to,'notes'=>trim((string)$notes)?:null,'user_id'=>(int)$userId,'created_at'=>date('Y-m-d H:i:s')));
}
private function event($asset,$type,$date,$amount,$before,$after,$notes,$journalId,$key,$userId,$from=null,$to=null,$reversalOf=null,$sourceType=null,$sourceId=null)
{
if($this->CI->db->get_where('asset_events',array('idempotency_key'=>$key))->row())throw new BusinessException('Transaksi aset yang sama sudah pernah diproses.');
$this->CI->db->insert('asset_events',array(
'company_id'=>$this->company(),'asset_id'=>$asset->id,'event_no'=>$this->CI->numberingservice->next('asset_event',$date),'event_type'=>$type,'event_date'=>$date,'amount'=>round((float)$amount,2),'book_value_before'=>round((float)$before,2),'book_value_after'=>round((float)$after,2),'from_location_id'=>$from?:null,'to_location_id'=>$to?:null,'notes'=>trim((string)$notes)?:null,'source_type'=>$sourceType?:$asset->source_type,'source_id'=>$sourceId?:$asset->source_id,'journal_id'=>$journalId?:null,'idempotency_key'=>$key,'reversal_of_event_id'=>$reversalOf?:null,'created_by'=>(int)$userId,'created_at'=>date('Y-m-d H:i:s')
));
return(int)$this->CI->db->insert_id();
}
private function calculateMonthly($cost,$residual,$life,$method='straight_line')
{
$life=max(1,(int)$life);$base=max(0,(float)$cost-(float)$residual);
if($method==='declining_balance')return round($base*(2/$life),2);
return round($base/$life,2);
}
private function validateDraft(array $data)
{
foreach(array('nama_asset','tanggal_perolehan','category_id','lokasi_asset_id','asset_account_id','accumulated_depreciation_account_id','depreciation_expense_account_id','masa_manfaat','idempotency_key')as$key)if(empty($data[$key]))throw new BusinessException('Data '.$key.' wajib diisi.');
$date=DateTime::createFromFormat('Y-m-d',$data['tanggal_perolehan']);if(!$date||$date->format('Y-m-d')!==$data['tanggal_perolehan'])throw new BusinessException('Tanggal perolehan tidak valid.');
if(!in_array($data['source_type'],array('manual','warehouse','purchase','opening','donation','other'),true))throw new BusinessException('Sumber perolehan tidak valid.');
if(!in_array($data['source_journal_mode'],array('post_journal','linked_journal','no_journal'),true))throw new BusinessException('Perlakuan jurnal tidak valid.');
if((float)$data['nilai_residu']<0||(int)$data['masa_manfaat']<1)throw new BusinessException('Nilai residu atau masa manfaat tidak valid.');
$this->location($data['lokasi_asset_id']);
$category=$this->CI->db->get_where('asset_categories',array('id'=>(int)$data['category_id'],'company_id'=>$this->company(),'is_active'=>1))->row();if(!$category)throw new BusinessException('Kategori aset tidak valid.');
$this->account($data['asset_account_id'],'Akun aset');$this->account($data['accumulated_depreciation_account_id'],'Akun akumulasi penyusutan');$this->account($data['depreciation_expense_account_id'],'Akun beban penyusutan');
if($data['source_type']==='warehouse'){
if(empty($data['item_id'])||empty($data['barcode_id'])||(float)$data['qty']<=0)throw new BusinessException('Barang, barcode, dan qty gudang wajib diisi.');
}else{
if((float)$data['nilai_perolehan']<=0)throw new BusinessException('Nilai perolehan harus lebih dari nol.');
if((float)$data['nilai_residu']>(float)$data['nilai_perolehan'])throw new BusinessException('Nilai residu tidak boleh melebihi nilai perolehan.');
if($data['source_journal_mode']==='post_journal')$this->account($data['account_kredit_id'],'Akun lawan perolehan');
if($data['source_journal_mode']==='linked_journal'){
$journal=$this->CI->db->get_where('journals',array('id'=>(int)$data['source_journal_id'],'company_id'=>$this->company(),'status'=>'posted'))->row();if(!$journal)throw new BusinessException('Jurnal sumber tidak ditemukan atau belum posted.');
if($this->CI->db->where('source_journal_id',(int)$journal->id)->where('workflow_status','capitalized')->count_all_results('assets')>0)throw new BusinessException('Jurnal sumber sudah digunakan oleh aset lain.');
}
}
}
private function warehouseValuation(array $data)
{
$barcode=$this->CI->db->query("SELECT b.*,i.company_id,i.status item_status,i.harga_beli FROM item_barcodes b JOIN items i ON i.id=b.item_id WHERE b.id=? AND b.item_id=? FOR UPDATE",array((int)$data['barcode_id'],(int)$data['item_id']))->row();
$qty=(float)$data['qty'];
if(!$barcode||(int)$barcode->company_id!==$this->company()||$barcode->item_status!=='active'||$barcode->status!=='available'||!$barcode->warehouse_id)throw new BusinessException('Barang/barcode gudang tidak tersedia.');
if((int)$barcode->warehouse_id!==(int)$data['warehouse_id'])throw new BusinessException('Gudang barang tidak sesuai dengan barcode yang dipilih.');
$available=(float)$barcode->qty_sisa-(float)$barcode->reserved_qty-(float)$barcode->return_reserved_qty;
if($qty<=0||$qty>$available+.0001)throw new BusinessException('Qty aset melebihi stok barcode yang tersedia.');
$balance=$this->CI->db->query("SELECT COALESCE(SUM(IF(direction='in',qty,-qty)),0) qty,COALESCE(SUM(IF(direction='in',value,-value)),0) value FROM inventory_ledger WHERE item_id=? AND warehouse_id=?",array((int)$barcode->item_id,(int)$barcode->warehouse_id))->row();
$unitCost=(float)$balance->qty>0?(float)$balance->value/(float)$balance->qty:(float)$barcode->harga_beli;
return array('unit_cost'=>round($unitCost,4),'cost'=>round($unitCost*$qty,2));
}
public function createDraft(array $data,$userId)
{
return$this->transaction(function()use($data,$userId){
$this->validateDraft($data);
$key=preg_replace('/[^A-Za-z0-9_.-]/','',trim($data['idempotency_key']));if($key==='')throw new BusinessException('Kunci transaksi wajib tersedia.');
$existing=$this->CI->db->get_where('assets',array('idempotency_key'=>$key))->row();if($existing)return(int)$existing->id;
$number=$this->CI->numberingservice->next('fixed_asset',$data['tanggal_perolehan']);
$valuation=$data['source_type']==='warehouse'?$this->warehouseValuation($data):null;$cost=$valuation?$valuation['cost']:round((float)$data['nilai_perolehan'],2);$unitCost=$valuation?$valuation['unit_cost']:$cost;$residual=round((float)$data['nilai_residu'],2);if($residual>$cost)throw new BusinessException('Nilai residu tidak boleh melebihi nilai perolehan.');$life=(int)$data['masa_manfaat'];$method=$data['depreciation_method']==='declining_balance'?'declining_balance':'straight_line';
$row=array(
'company_id'=>$this->company(),'document_no'=>$number,'kode_asset'=>$number,'nama_asset'=>trim($data['nama_asset']),'category_id'=>(int)$data['category_id'],'responsible_employee_id'=>!empty($data['responsible_employee_id'])?(int)$data['responsible_employee_id']:null,'source_type'=>$data['source_type'],'source_id'=>!empty($data['source_id'])?(int)$data['source_id']:null,'source_reference_no'=>trim((string)$data['source_reference_no'])?:null,'supplier_id'=>!empty($data['supplier_id'])?(int)$data['supplier_id']:null,'source_journal_mode'=>$data['source_journal_mode'],'source_journal_id'=>!empty($data['source_journal_id'])?(int)$data['source_journal_id']:null,'item_id'=>!empty($data['item_id'])?(int)$data['item_id']:null,'barcode_id'=>!empty($data['barcode_id'])?(int)$data['barcode_id']:null,'warehouse_id'=>!empty($data['warehouse_id'])?(int)$data['warehouse_id']:null,'bin_id'=>!empty($data['bin_id'])?(int)$data['bin_id']:null,'qty'=>$data['source_type']==='warehouse'?(float)$data['qty']:1,'harga_per_unit'=>$unitCost,'nilai_perolehan'=>$cost,'tanggal_perolehan'=>$data['tanggal_perolehan'],'capitalization_date'=>!empty($data['capitalization_date'])?$data['capitalization_date']:$data['tanggal_perolehan'],'masa_manfaat'=>$life,'nilai_residu'=>$residual,'penyusutan_per_bulan'=>$this->calculateMonthly($cost,$residual,$life,$method),'akumulasi_penyusutan'=>0,'nilai_buku'=>$cost,'sumber'=>$data['source_type']==='warehouse'?'gudang':'pembelian','account_debit_id'=>(int)$data['asset_account_id'],'account_kredit_id'=>!empty($data['account_kredit_id'])?(int)$data['account_kredit_id']:null,'asset_account_id'=>(int)$data['asset_account_id'],'accumulated_depreciation_account_id'=>(int)$data['accumulated_depreciation_account_id'],'depreciation_expense_account_id'=>(int)$data['depreciation_expense_account_id'],'lokasi_asset_id'=>(int)$data['lokasi_asset_id'],'department_id'=>!empty($data['department_id'])?(int)$data['department_id']:null,'project_id'=>!empty($data['project_id'])?(int)$data['project_id']:null,'cost_center_id'=>!empty($data['cost_center_id'])?(int)$data['cost_center_id']:null,'depreciation_method'=>$method,'metode'=>'garis_lurus','workflow_status'=>'draft','lifecycle_status'=>'draft','qr_code'=>'ASSET:'.$number,'idempotency_key'=>$key,'keterangan'=>trim((string)$data['keterangan']),'created_by'=>(int)$userId,'created_at'=>date('Y-m-d H:i:s'),'version'=>1
);
$this->CI->db->insert('assets',$row);$id=(int)$this->CI->db->insert_id();$this->history($id,null,'draft','Dokumen perolehan dibuat.',$userId);$this->CI->auditservice->record('fixed_asset','asset',$id,'create_draft',null,$row,$userId);return$id;
});
}
public function updateDraft($id,array$data,$userId)
{
return $this->transaction(function()use($id,$data,$userId){
$asset=$this->asset($id);
if($asset->workflow_status!=='draft')throw new BusinessException('Hanya draft yang dapat diubah.');
$this->validateDraft($data);
$before=clone$asset;
$valuation=$data['source_type']==='warehouse'?$this->warehouseValuation($data):null;
$cost=$valuation?$valuation['cost']:round((float)$data['nilai_perolehan'],2);
$unitCost=$valuation?$valuation['unit_cost']:$cost;
$residual=round((float)$data['nilai_residu'],2);
if($residual>$cost)throw new BusinessException('Nilai residu tidak boleh melebihi nilai perolehan.');
$method=$data['depreciation_method']==='declining_balance'?'declining_balance':'straight_line';
$update=array(
'nama_asset'=>trim($data['nama_asset']),
'category_id'=>(int)$data['category_id'],
'responsible_employee_id'=>!empty($data['responsible_employee_id'])?(int)$data['responsible_employee_id']:null,
'source_type'=>$data['source_type'],
'source_reference_no'=>trim((string)$data['source_reference_no'])?:null,
'source_journal_mode'=>$data['source_journal_mode'],
'source_journal_id'=>!empty($data['source_journal_id'])?(int)$data['source_journal_id']:null,
'item_id'=>!empty($data['item_id'])?(int)$data['item_id']:null,
'barcode_id'=>!empty($data['barcode_id'])?(int)$data['barcode_id']:null,
'warehouse_id'=>!empty($data['warehouse_id'])?(int)$data['warehouse_id']:null,
'bin_id'=>!empty($data['bin_id'])?(int)$data['bin_id']:null,
'qty'=>$data['source_type']==='warehouse'?(float)$data['qty']:1,
'nilai_perolehan'=>$cost,
'harga_per_unit'=>$unitCost,
'nilai_buku'=>$cost,
'nilai_residu'=>$residual,
'tanggal_perolehan'=>$data['tanggal_perolehan'],
'capitalization_date'=>!empty($data['capitalization_date'])?$data['capitalization_date']:$data['tanggal_perolehan'],
'masa_manfaat'=>(int)$data['masa_manfaat'],
'penyusutan_per_bulan'=>$this->calculateMonthly($cost,$residual,$data['masa_manfaat'],$method),
'depreciation_method'=>$method,
'asset_account_id'=>(int)$data['asset_account_id'],
'account_debit_id'=>(int)$data['asset_account_id'],
'account_kredit_id'=>!empty($data['account_kredit_id'])?(int)$data['account_kredit_id']:null,
'accumulated_depreciation_account_id'=>(int)$data['accumulated_depreciation_account_id'],
'depreciation_expense_account_id'=>(int)$data['depreciation_expense_account_id'],
'lokasi_asset_id'=>(int)$data['lokasi_asset_id'],
'keterangan'=>trim((string)$data['keterangan']),
'updated_by'=>(int)$userId,
'updated_at'=>date('Y-m-d H:i:s'),
'version'=>(int)$asset->version+1
);
$this->CI->db->where(array('id'=>$asset->id,'company_id'=>$this->company(),'version'=>$asset->version))->update('assets',$update);
if($this->CI->db->affected_rows()!==1)throw new BusinessException('Draft telah berubah pada sesi lain. Muat ulang halaman.');
$this->CI->auditservice->record('fixed_asset','asset',$asset->id,'update_draft',$before,$update,$userId);
return$asset->id;
});
}
public function submit($id,$userId)
{
return$this->transaction(function()use($id,$userId){$a=$this->asset($id);if($a->workflow_status!=='draft')throw new BusinessException('Aset tidak berstatus draft.');$attachments=$this->CI->db->where(array('entity_type'=>'asset','entity_id'=>$a->id,'company_id'=>$this->company()))->count_all_results('transaction_attachments');if($attachments<1)throw new BusinessException('Minimal satu dokumen pendukung wajib diunggah sebelum pengajuan.');$this->CI->db->where('id',$a->id)->update('assets',array('workflow_status'=>'submitted','submitted_by'=>(int)$userId,'submitted_at'=>date('Y-m-d H:i:s'),'rejection_reason'=>null,'version'=>(int)$a->version+1));$this->history($a->id,'draft','submitted','Dokumen perolehan diajukan.',$userId);return$a->id;});
}
public function approve($id,$userId,$isMaster=false)
{
return$this->transaction(function()use($id,$userId,$isMaster){$a=$this->asset($id);if($a->workflow_status!=='submitted')throw new BusinessException('Aset tidak menunggu persetujuan.');if(!$isMaster&&(int)$a->created_by===(int)$userId)throw new BusinessException('Pembuat dokumen tidak boleh menyetujui dokumennya sendiri.');$this->CI->db->where('id',$a->id)->update('assets',array('workflow_status'=>'approved','approved_by'=>(int)$userId,'approved_at'=>date('Y-m-d H:i:s'),'version'=>(int)$a->version+1));$this->history($a->id,'submitted','approved','Dokumen perolehan disetujui.',$userId);return$a->id;});
}
public function reject($id,$reason,$userId,$isMaster=false)
{
return$this->transaction(function()use($id,$reason,$userId,$isMaster){$a=$this->asset($id);if($a->workflow_status!=='submitted')throw new BusinessException('Aset tidak menunggu persetujuan.');if(!$isMaster&&(int)$a->created_by===(int)$userId)throw new BusinessException('Pembuat dokumen tidak boleh menolak dokumennya sendiri.');if(trim($reason)==='')throw new BusinessException('Alasan penolakan wajib diisi.');$this->CI->db->where('id',$a->id)->update('assets',array('workflow_status'=>'rejected','rejected_by'=>(int)$userId,'rejected_at'=>date('Y-m-d H:i:s'),'rejection_reason'=>trim($reason),'version'=>(int)$a->version+1));$this->history($a->id,'submitted','rejected',$reason,$userId);return$a->id;});
}
public function reopenRejected($id,$userId)
{
return$this->transaction(function()use($id,$userId){$a=$this->asset($id);if($a->workflow_status!=='rejected')throw new BusinessException('Dokumen tidak berstatus ditolak.');if((int)$a->created_by!==(int)$userId&&$this->CI->session->userdata('role')!=='Admin')throw new BusinessException('Hanya pembuat atau Master Admin yang dapat membuka kembali draft.');$this->CI->db->where('id',$a->id)->update('assets',array('workflow_status'=>'draft','version'=>(int)$a->version+1));$this->history($a->id,'rejected','draft','Dibuka kembali untuk diperbaiki.',$userId);return$a->id;});
}
public function capitalize($id,$date,$userId)
{
return$this->transaction(function()use($id,$date,$userId){$a=$this->asset($id);if($a->workflow_status!=='approved')throw new BusinessException('Aset harus disetujui sebelum kapitalisasi.');$date=$date?:$a->capitalization_date;if(!$date)throw new BusinessException('Tanggal kapitalisasi wajib diisi.');$this->CI->fiscalperiodservice->assertOpen($date);$journalId=null;$cost=(float)$a->nilai_perolehan;$unitCost=(float)$a->harga_per_unit;$barcode=null;
if($a->source_type==='warehouse'){
$this->CI->fiscalperiodservice->assertOpen($date);
$barcode=$this->CI->db->query("SELECT b.*,i.company_id,i.status item_status,i.kode_detail,i.nama_barang,k.tracking_type FROM item_barcodes b JOIN items i ON i.id=b.item_id LEFT JOIN kode_barang k ON k.id=i.kode_id WHERE b.id=? AND b.item_id=? FOR UPDATE",array((int)$a->barcode_id,(int)$a->item_id))->row();
if(!$barcode||(int)$barcode->company_id!==$this->company()||$barcode->item_status!=='active'||$barcode->status!=='available'||!$barcode->warehouse_id||(float)$barcode->reserved_qty>0||(float)$barcode->return_reserved_qty>0)throw new BusinessException('Barang/barcode gudang tidak lagi tersedia.');
$qty=(float)$a->qty;if(strtoupper((string)$barcode->tracking_type)==='UNIT'&&abs($qty-1)>.0001)throw new BusinessException('Barang UNIT wajib dikapitalisasi satu unit per aset.');if($qty<=0||$qty>(float)$barcode->qty_sisa)throw new BusinessException('Qty kapitalisasi melebihi saldo barcode.');
$balance=$this->CI->db->query("SELECT COALESCE(SUM(IF(direction='in',qty,-qty)),0) qty,COALESCE(SUM(IF(direction='in',value,-value)),0) value FROM inventory_ledger WHERE item_id=? AND warehouse_id=? FOR UPDATE",array($a->item_id,$barcode->warehouse_id))->row();if((float)$balance->qty<$qty)throw new BusinessException('Saldo inventory ledger tidak mencukupi.');
$unitCost=(float)$balance->qty>0?round((float)$balance->value/(float)$balance->qty,4):0;if($unitCost<=0)throw new BusinessException('Nilai biaya inventory ledger tidak tersedia.');$cost=round($unitCost*$qty,2);if((float)$a->nilai_residu>$cost)throw new BusinessException('Nilai residu melebihi nilai perolehan dari ledger.');
$inventory=$this->account($this->CI->accountmappingservice->get('inventory'),'Akun persediaan');$assetAccount=$this->account($a->asset_account_id,'Akun aset');
$description='[AUTO] Kapitalisasi persediaan menjadi aset | Dokumen '.$a->document_no.' | Aset '.$a->kode_asset.' - '.$a->nama_asset.' | Barang '.$barcode->kode_detail.' - '.$barcode->nama_barang.' | Barcode '.$barcode->barcode.' | Qty '.number_format($qty,4,',','.').' | Biaya ledger Rp '.number_format($cost,2,',','.').' | Tanggal '.$date.' | Alasan '.($a->keterangan?:'-');
$journalId=$this->CI->postingservice->post(array('tanggal'=>$date,'no_ref'=>'CAP-'.$a->document_no.'-V'.$a->version,'keterangan'=>$description,'ref_type'=>'asset_capitalization','ref_id'=>$a->id,'created_by'=>(int)$userId),array(array('account_id'=>$assetAccount->id,'debit'=>$cost,'kredit'=>0),array('account_id'=>$inventory->id,'debit'=>0,'kredit'=>$cost)),false,false);
$ledgerKey='ASSET-CAP-'.$a->id.'-V'.$a->version;$this->CI->db->insert('inventory_ledger',array('item_id'=>$a->item_id,'warehouse_id'=>$barcode->warehouse_id,'bin_id'=>$barcode->bin_id?:null,'barcode_id'=>$barcode->id,'batch_id'=>$barcode->batch_id?:null,'movement_date'=>$date,'direction'=>'out','qty'=>$qty,'unit_cost'=>$unitCost,'value'=>$cost,'document_type'=>'asset_capitalization','document_id'=>$a->id,'document_line_id'=>$a->id,'idempotency_key'=>$ledgerKey,'created_at'=>date('Y-m-d H:i:s')));
$this->CI->db->insert('stock_logs',array('company_id'=>$this->company(),'item_id'=>$a->item_id,'warehouse_id'=>$barcode->warehouse_id,'barcode_id'=>$barcode->id,'qty'=>$qty,'tipe'=>'keluar','unit_cost'=>$unitCost,'keterangan'=>$description,'ref_type'=>'asset_capitalization','ref_id'=>$a->id,'idempotency_key'=>$ledgerKey));
$this->CI->db->insert('item_movements',array('item_id'=>$a->item_id,'barcode_id'=>$barcode->id,'qty'=>$qty,'from_type'=>'warehouse','from_id'=>$barcode->warehouse_id,'to_type'=>'asset','to_id'=>$a->id,'movement_type'=>'installation','notes'=>$description,'created_at'=>date('Y-m-d H:i:s')));
$remaining=round((float)$barcode->qty_sisa-$qty,4);$this->CI->db->where(array('id'=>$barcode->id,'version'=>$barcode->version))->update('item_barcodes',array('qty_sisa'=>$remaining,'status'=>$remaining<=0?'installed':'available','version'=>(int)$barcode->version+1));if($this->CI->db->affected_rows()!==1)throw new BusinessException('Barcode berubah saat kapitalisasi.');$this->synchronizeItem($a->item_id);
}else{
if($a->source_journal_mode==='post_journal'){
$this->CI->fiscalperiodservice->assertOpen($date);$assetAccount=$this->account($a->asset_account_id,'Akun aset');$counter=$this->account($a->account_kredit_id,'Akun lawan perolehan');$description='[AUTO] Kapitalisasi aset manual | Dokumen '.$a->document_no.' | Aset '.$a->kode_asset.' - '.$a->nama_asset.' | Sumber '.strtoupper($a->source_type).' | Referensi '.($a->source_reference_no?:'-').' | Nilai Rp '.number_format($cost,2,',','.').' | Tanggal '.$date.' | Alasan '.($a->keterangan?:'-');$journalId=$this->CI->postingservice->post(array('tanggal'=>$date,'no_ref'=>'CAP-'.$a->document_no.'-V'.$a->version,'keterangan'=>$description,'ref_type'=>'asset_capitalization','ref_id'=>$a->id,'created_by'=>(int)$userId),array(array('account_id'=>$assetAccount->id,'debit'=>$cost,'kredit'=>0),array('account_id'=>$counter->id,'debit'=>0,'kredit'=>$cost)),false,false);
}elseif($a->source_journal_mode==='linked_journal'){$source=$this->CI->db->get_where('journals',array('id'=>$a->source_journal_id,'company_id'=>$this->company(),'status'=>'posted'))->row();if(!$source)throw new BusinessException('Jurnal sumber tidak lagi valid.');$journalId=(int)$source->id;}
}
$monthly=$this->calculateMonthly($cost,$a->nilai_residu,$a->masa_manfaat,$a->depreciation_method);$update=array('workflow_status'=>'capitalized','lifecycle_status'=>'active','capitalization_date'=>$date,'warehouse_id'=>$barcode?$barcode->warehouse_id:$a->warehouse_id,'bin_id'=>$barcode?$barcode->bin_id:$a->bin_id,'harga_per_unit'=>$a->source_type==='warehouse'?$unitCost:$a->harga_per_unit,'nilai_perolehan'=>$cost,'nilai_buku'=>$cost,'penyusutan_per_bulan'=>$monthly,'acquisition_journal_id'=>$journalId,'capitalized_by'=>(int)$userId,'capitalized_at'=>date('Y-m-d H:i:s'),'version'=>(int)$a->version+1);$this->CI->db->where('id',$a->id)->update('assets',$update);$fresh=(object)array_merge((array)$a,$update);$eventJournal=$a->source_journal_mode==='linked_journal'?null:$journalId;$this->event($fresh,'capitalization',$date,$cost,0,$cost,'Kapitalisasi dokumen '.$a->document_no.': '.($a->keterangan?:'-'),$eventJournal,'ASSET-CAP-EVENT-'.$a->id.'-V'.$a->version,$userId,null,$a->lokasi_asset_id,null,$a->source_type,$a->source_id);$this->history($a->id,'approved','capitalized','Aset dikapitalisasi dan aktif.',$userId);return$a->id;
});
}
private function synchronizeItem($itemId)
{
$qty=(float)$this->CI->db->query("SELECT COALESCE(SUM(IF(direction='in',qty,-qty)),0) qty FROM inventory_ledger WHERE item_id=?",array((int)$itemId))->row()->qty;$this->CI->db->where('id',(int)$itemId)->update('items',array('stok'=>$qty));
}
public function depreciate($id,$through,$userId,$catchup=true)
{
return$this->transaction(function()use($id,$through,$userId,$catchup){$asset=$this->asset($id);if($asset->workflow_status!=='capitalized'||$asset->lifecycle_status!=='active')throw new BusinessException('Hanya aset aktif yang dapat disusutkan.');$end=date('Y-m',strtotime($through));$last=$this->CI->db->where(array('asset_id'=>$asset->id,'company_id'=>$this->company(),'status'=>'posted'))->order_by('period','DESC')->limit(1)->get('asset_depreciation_schedule')->row();$start=$last?date('Y-m',strtotime($last->period.'-01 +1 month')):date('Y-m',strtotime($asset->capitalization_date.' +1 month'));$period=$catchup?$start:$end;$posted=array();while($period<=$end){if($this->CI->db->get_where('asset_depreciation_schedule',array('asset_id'=>$asset->id,'period'=>$period))->row()){$period=date('Y-m',strtotime($period.'-01 +1 month'));continue;}$date=date('Y-m-t',strtotime($period.'-01'));$this->CI->fiscalperiodservice->assertOpen($date);$asset=$this->asset($id);$before=(float)$asset->nilai_buku;$maximum=max(0,$before-(float)$asset->nilai_residu);if($maximum<=.001)break;$calculated=$asset->depreciation_method==='declining_balance'?round($before*(2/max(1,(int)$asset->masa_manfaat)),2):(float)$asset->penyusutan_per_bulan;$amount=min($calculated,$maximum);$acc=$this->account($asset->accumulated_depreciation_account_id,'Akun akumulasi penyusutan');$expense=$this->account($asset->depreciation_expense_account_id,'Akun beban penyusutan');$this->CI->db->insert('asset_depreciation_schedule',array('company_id'=>$this->company(),'asset_id'=>$asset->id,'period'=>$period,'due_date'=>$date,'opening_book_value'=>$before,'depreciation_amount'=>$amount,'closing_book_value'=>$before-$amount,'status'=>'scheduled'));$scheduleId=(int)$this->CI->db->insert_id();$description='[AUTO] Penyusutan aset | '.$asset->kode_asset.' - '.$asset->nama_asset.' | Periode '.$period.' | Metode '.$asset->depreciation_method.' | Nilai Rp '.number_format($amount,2,',','.').' | Nilai buku sebelum Rp '.number_format($before,2,',','.').' | Nilai buku sesudah Rp '.number_format($before-$amount,2,',','.');$journal=$this->CI->postingservice->post(array('tanggal'=>$date,'no_ref'=>'DEP-'.str_replace('-','',$period).'-'.$asset->id,'keterangan'=>$description,'ref_type'=>'asset_depreciation','ref_id'=>$scheduleId,'created_by'=>(int)$userId),array(array('account_id'=>$expense->id,'debit'=>$amount,'kredit'=>0),array('account_id'=>$acc->id,'debit'=>0,'kredit'=>$amount)),false,true);$after=$before-$amount;$event=$this->event($asset,'depreciation',$date,$amount,$before,$after,'Penyusutan periode '.$period.' metode '.$asset->depreciation_method,$journal,'ASSET-DEP-'.$asset->id.'-'.$period,$userId);$this->CI->db->where('id',$scheduleId)->update('asset_depreciation_schedule',array('status'=>'posted','journal_id'=>$journal,'asset_event_id'=>$event,'posted_at'=>date('Y-m-d H:i:s')));$this->CI->db->where('id',$asset->id)->update('assets',array('akumulasi_penyusutan'=>(float)$asset->akumulasi_penyusutan+$amount,'nilai_buku'=>$after,'lifecycle_status'=>$after<=(float)$asset->nilai_residu+.001?'fully_depreciated':'active','version'=>(int)$asset->version+1));$posted[]=$period;$period=date('Y-m',strtotime($period.'-01 +1 month'));}return$posted;});
}
public function mutate($id,$type,$date,$amount,$accountId,$notes,$userId)
{
return$this->transaction(function()use($id,$type,$date,$amount,$accountId,$notes,$userId){$allowed=array('addition','impairment','revaluation','disposal','lost','damaged');if(!in_array($type,$allowed,true))throw new BusinessException('Jenis mutasi aset tidak valid.');if(trim($notes)==='')throw new BusinessException('Alasan/rincian mutasi wajib diisi.');$this->CI->fiscalperiodservice->assertOpen($date);$asset=$this->asset($id);if($asset->workflow_status!=='capitalized'||in_array($asset->lifecycle_status,array('disposed','lost','damaged'),true))throw new BusinessException('Aset tidak dapat menerima mutasi ini.');$amount=round((float)$amount,2);$before=(float)$asset->nilai_buku;$cost=(float)$asset->nilai_perolehan;$accum=(float)$asset->akumulasi_penyusutan;$assetAccount=$this->account($asset->asset_account_id,'Akun aset');$lines=array();$after=$before;$status=$asset->lifecycle_status;$update=array();
if($type==='addition'){$counter=$this->account($accountId,'Akun lawan penambahan nilai');if($amount<=0)throw new BusinessException('Nilai penambahan harus lebih dari nol.');$after=$before+$amount;$lines=array(array('account_id'=>$assetAccount->id,'debit'=>$amount,'kredit'=>0),array('account_id'=>$counter->id,'debit'=>0,'kredit'=>$amount));$update['nilai_perolehan']=$cost+$amount;}
elseif($type==='impairment'){$expense=$this->account($accountId,'Akun rugi impairment');$amount=min($amount,max(0,$before-(float)$asset->nilai_residu));if($amount<=0)throw new BusinessException('Nilai impairment tidak tersedia.');$after=$before-$amount;$lines=array(array('account_id'=>$expense->id,'debit'=>$amount,'kredit'=>0),array('account_id'=>$assetAccount->id,'debit'=>0,'kredit'=>$amount));$update['impairment_total']=(float)$asset->impairment_total+$amount;}
elseif($type==='revaluation'){$counter=$this->account($accountId,'Akun surplus revaluasi');if($amount<=0)throw new BusinessException('Nilai revaluasi harus lebih dari nol.');$after=$before+$amount;$lines=array(array('account_id'=>$assetAccount->id,'debit'=>$amount,'kredit'=>0),array('account_id'=>$counter->id,'debit'=>0,'kredit'=>$amount));$update['revaluation_total']=(float)$asset->revaluation_total+$amount;$update['nilai_perolehan']=$cost+$amount;}
else{$proceeds=$type==='disposal'?$amount:0;if($proceeds<0)throw new BusinessException('Nilai hasil pelepasan tidak valid.');$accAccount=$this->account($asset->accumulated_depreciation_account_id,'Akun akumulasi penyusutan');if($proceeds>0){$cash=$this->account($accountId,'Akun penerimaan pelepasan');$lines[]=array('account_id'=>$cash->id,'debit'=>$proceeds,'kredit'=>0);}if($accum>0)$lines[]=array('account_id'=>$accAccount->id,'debit'=>$accum,'kredit'=>0);$gainLoss=$proceeds-$before;if($gainLoss<0)$lines[]=array('account_id'=>$this->account($this->CI->accountmappingservice->get('asset_disposal_loss'),'Akun rugi pelepasan')->id,'debit'=>abs($gainLoss),'kredit'=>0);if($gainLoss>0)$lines[]=array('account_id'=>$this->account($this->CI->accountmappingservice->get('asset_disposal_gain'),'Akun laba pelepasan')->id,'debit'=>0,'kredit'=>$gainLoss);$lines[]=array('account_id'=>$assetAccount->id,'debit'=>0,'kredit'=>$cost);$after=0;$status=$type==='disposal'?'disposed':$type;$update['disposal_date']=$date;$update['disposal_proceeds']=$proceeds;$update['status']=$type==='disposal'?'dijual':'dihapus';}
$key='ASSET-'.strtoupper($type).'-'.$asset->id.'-'.sha1($date.'|'.$amount.'|'.$notes.'|'.$asset->version);$description='[AUTO] '.strtoupper($type).' aset | '.$asset->kode_asset.' - '.$asset->nama_asset.' | Dokumen '.$asset->document_no.' | Tanggal '.$date.' | Nilai Rp '.number_format($amount,2,',','.').' | Nilai buku sebelum Rp '.number_format($before,2,',','.').' | Nilai buku sesudah Rp '.number_format($after,2,',','.').' | Alasan '.$notes;$journal=$this->CI->postingservice->post(array('tanggal'=>$date,'no_ref'=>'AST-'.strtoupper(substr($type,0,3)).'-'.$asset->id.'-'.date('YmdHis'),'keterangan'=>$description,'ref_type'=>'asset_'.$type,'ref_id'=>$asset->id,'created_by'=>(int)$userId),$lines,false,false);$this->event($asset,$type,$date,$amount,$before,$after,$notes,$journal,$key,$userId);$update=array_merge($update,array('nilai_buku'=>$after,'lifecycle_status'=>$status,'penyusutan_per_bulan'=>$this->calculateMonthly(isset($update['nilai_perolehan'])?$update['nilai_perolehan']:$cost,$asset->nilai_residu,$asset->masa_manfaat,$asset->depreciation_method),'version'=>(int)$asset->version+1));$this->CI->db->where('id',$asset->id)->update('assets',$update);return$journal;});
}
public function transfer($id,$date,$toLocation,$notes,$userId)
{
return$this->transaction(function()use($id,$date,$toLocation,$notes,$userId){$asset=$this->asset($id);if($asset->workflow_status!=='capitalized'||!in_array($asset->lifecycle_status,array('active','fully_depreciated'),true))throw new BusinessException('Aset tidak aktif.');$to=$this->location($toLocation);if((int)$asset->lokasi_asset_id===(int)$to->id)throw new BusinessException('Lokasi tujuan sama dengan lokasi saat ini.');if(trim($notes)==='')throw new BusinessException('Alasan transfer wajib diisi.');$key='ASSET-TRANSFER-'.$asset->id.'-'.$date.'-'.$to->id.'-V'.$asset->version;$this->event($asset,'transfer',$date,0,$asset->nilai_buku,$asset->nilai_buku,$notes,null,$key,$userId,$asset->lokasi_asset_id,$to->id);$this->CI->db->where('id',$asset->id)->update('assets',array('lokasi_asset_id'=>$to->id,'version'=>(int)$asset->version+1,'updated_by'=>$userId,'updated_at'=>date('Y-m-d H:i:s')));return$asset->id;});
}
public function changeResponsibility($id,$date,$employeeId,$notes,$userId)
{
return$this->transaction(function()use($id,$date,$employeeId,$notes,$userId){$asset=$this->asset($id);$employee=$this->CI->db->get_where('k_employees',array('id'=>(int)$employeeId,'company_id'=>$this->company(),'is_active'=>1))->row();if(!$employee)throw new BusinessException('Penanggung jawab tidak valid.');if(trim($notes)==='')throw new BusinessException('Alasan pergantian penanggung jawab wajib diisi.');$key='ASSET-RESP-'.$asset->id.'-'.$date.'-'.$employee->id.'-V'.$asset->version;$event=$this->event($asset,'responsibility_change',$date,0,$asset->nilai_buku,$asset->nilai_buku,$notes,null,$key,$userId);$this->CI->db->insert('asset_responsibility_history',array('company_id'=>$this->company(),'asset_id'=>$asset->id,'effective_date'=>$date,'from_employee_id'=>$asset->responsible_employee_id?:null,'to_employee_id'=>$employee->id,'notes'=>$notes,'asset_event_id'=>$event,'created_by'=>$userId,'created_at'=>date('Y-m-d H:i:s')));$this->CI->db->where('id',$asset->id)->update('assets',array('responsible_employee_id'=>$employee->id,'version'=>(int)$asset->version+1));return$asset->id;});
}
public function maintenance($id,array$data,$userId)
{
return$this->transaction(function()use($id,$data,$userId){$asset=$this->asset($id);if($asset->workflow_status!=='capitalized')throw new BusinessException('Aset belum dikapitalisasi.');if(empty($data['date'])||trim((string)$data['description'])==='')throw new BusinessException('Tanggal dan deskripsi maintenance wajib diisi.');$key=preg_replace('/[^A-Za-z0-9_.-]/','',trim((string)$data['idempotency_key']));if($key===''||$this->CI->db->get_where('asset_maintenance',array('idempotency_key'=>$key))->row())throw new BusinessException('Kunci maintenance kosong atau sudah dipakai.');$number=$this->CI->numberingservice->next('asset_maintenance',$data['date']);$this->CI->db->insert('asset_maintenance',array('company_id'=>$this->company(),'asset_id'=>$asset->id,'maintenance_no'=>$number,'maintenance_date'=>$data['date'],'vendor'=>trim((string)$data['vendor'])?:null,'description'=>trim($data['description']),'cost'=>round((float)$data['cost'],2),'next_due_date'=>!empty($data['next_due_date'])?$data['next_due_date']:null,'status'=>'posted','idempotency_key'=>$key,'created_by'=>$userId,'created_at'=>date('Y-m-d H:i:s')));$maintenanceId=(int)$this->CI->db->insert_id();$event=$this->event($asset,'maintenance',$data['date'],$data['cost'],$asset->nilai_buku,$asset->nilai_buku,'Maintenance '.$number.': '.$data['description'],null,'ASSET-MAINT-EVENT-'.$maintenanceId,$userId,null,null,null,'asset_maintenance',$maintenanceId);$this->CI->db->where('id',$maintenanceId)->update('asset_maintenance',array('asset_event_id'=>$event));return$maintenanceId;});
}
public function createOpname($locationId,$date,$notes,$key,$userId)
{
return$this->transaction(function()use($locationId,$date,$notes,$key,$userId){$location=$this->location($locationId);$key=preg_replace('/[^A-Za-z0-9_.-]/','',trim($key));if($key===''||$this->CI->db->get_where('asset_opnames',array('idempotency_key'=>$key))->row())throw new BusinessException('Kunci opname kosong atau sudah dipakai.');$no=$this->CI->numberingservice->next('asset_opname',$date);$this->CI->db->insert('asset_opnames',array('company_id'=>$this->company(),'opname_no'=>$no,'opname_date'=>$date,'location_id'=>$location->id,'status'=>'draft','notes'=>trim((string)$notes)?:null,'snapshot_at'=>date('Y-m-d H:i:s'),'idempotency_key'=>$key,'created_by'=>$userId));$id=(int)$this->CI->db->insert_id();$assets=$this->CI->db->where(array('company_id'=>$this->company(),'workflow_status'=>'capitalized','lokasi_asset_id'=>$location->id))->where_in('lifecycle_status',array('active','fully_depreciated'))->get('assets')->result();foreach($assets as$a)$this->CI->db->insert('asset_opname_lines',array('company_id'=>$this->company(),'asset_opname_id'=>$id,'asset_id'=>$a->id,'expected_status'=>$a->lifecycle_status,'actual_status'=>$a->lifecycle_status,'expected_location_id'=>$location->id,'actual_location_id'=>$location->id,'is_found'=>1));return$id;});
}
public function saveOpnameLines($id,array$lines,$userId)
{
return$this->transaction(function()use($id,$lines,$userId){$opname=$this->CI->db->query('SELECT * FROM asset_opnames WHERE id=? AND company_id=? FOR UPDATE',array((int)$id,$this->company()))->row();if(!$opname||$opname->status!=='draft')throw new BusinessException('Hanya draft opname yang dapat diubah.');foreach($lines as$line){$existing=$this->CI->db->select('ol.*')->from('asset_opname_lines ol')->join('assets a','a.id=ol.asset_id')->where(array('ol.id'=>(int)$line['id'],'ol.asset_opname_id'=>$opname->id,'ol.company_id'=>$this->company(),'a.company_id'=>$this->company()))->get()->row();if(!$existing)throw new BusinessException('Baris opname tidak valid.');$status=in_array($line['actual_status'],array('active','fully_depreciated','lost','damaged'),true)?$line['actual_status']:$existing->expected_status;$this->CI->db->where('id',$existing->id)->update('asset_opname_lines',array('actual_status'=>$status,'actual_location_id'=>!empty($line['actual_location_id'])?(int)$line['actual_location_id']:null,'is_found'=>!empty($line['is_found'])?1:0,'notes'=>trim((string)$line['notes'])?:null,'checked_at'=>date('Y-m-d H:i:s'),'checked_by'=>$userId));}return$opname->id;});
}
public function opnameState($id,$action,$reason,$userId,$isMaster=false)
{
return$this->transaction(function()use($id,$action,$reason,$userId,$isMaster){$o=$this->CI->db->query('SELECT * FROM asset_opnames WHERE id=? AND company_id=? FOR UPDATE',array((int)$id,$this->company()))->row();if(!$o)throw new BusinessException('Asset opname tidak ditemukan.');if($action==='submit'){if($o->status!=='draft')throw new BusinessException('Opname tidak berstatus draft.');if(!$this->CI->db->where(array('entity_type'=>'asset_opname','entity_id'=>$o->id,'company_id'=>$this->company()))->count_all_results('transaction_attachments'))throw new BusinessException('Bukti opname wajib diunggah sebelum diajukan.');$update=array('status'=>'submitted','submitted_by'=>$userId,'submitted_at'=>date('Y-m-d H:i:s'));}elseif($action==='approve'){if($o->status!=='submitted')throw new BusinessException('Opname tidak menunggu persetujuan.');if(!$isMaster&&(int)$o->created_by===(int)$userId)throw new BusinessException('Pembuat opname tidak boleh menyetujui sendiri.');$update=array('status'=>'approved','approved_by'=>$userId,'approved_at'=>date('Y-m-d H:i:s'));}elseif($action==='reject'){if($o->status!=='submitted'||trim($reason)==='')throw new BusinessException('Status atau alasan penolakan tidak valid.');$update=array('status'=>'rejected','rejected_by'=>$userId,'rejected_at'=>date('Y-m-d H:i:s'),'rejection_reason'=>trim($reason));}elseif($action==='post'){if($o->status!=='approved')throw new BusinessException('Opname harus disetujui sebelum posting.');$lines=$this->CI->db->where(array('asset_opname_id'=>$o->id,'company_id'=>$this->company()))->get('asset_opname_lines')->result();foreach($lines as$line){$asset=$this->asset($line->asset_id);$notes='Opname '.$o->opname_no.' | '.($line->is_found?'Ditemukan':'Tidak ditemukan').' | Status fisik '.$line->actual_status.' | '.($line->notes?:'-');$to=$line->actual_location_id?:$asset->lokasi_asset_id;$this->event($asset,'opname_adjustment',$o->opname_date,0,$asset->nilai_buku,$asset->nilai_buku,$notes,null,'ASSET-OPNAME-'.$o->id.'-'.$line->id,$userId,$asset->lokasi_asset_id,$to,null,'asset_opname',$o->id);if($line->is_found&&$to)$this->CI->db->where('id',$asset->id)->update('assets',array('lokasi_asset_id'=>$to,'version'=>(int)$asset->version+1));} $update=array('status'=>'posted','posted_by'=>$userId,'posted_at'=>date('Y-m-d H:i:s'));}else throw new BusinessException('Aksi opname tidak valid.');$this->CI->db->where('id',$o->id)->update('asset_opnames',$update);return$o->id;});
}
public function reverseEvent($eventId,$date,$reason,$userId)
{
return$this->transaction(function()use($eventId,$date,$reason,$userId){if(trim($reason)==='')throw new BusinessException('Alasan reversal wajib diisi.');$this->CI->fiscalperiodservice->assertOpen($date);$event=$this->CI->db->query('SELECT e.* FROM asset_events e WHERE e.id=? AND e.company_id=? FOR UPDATE',array((int)$eventId,$this->company()))->row();if(!$event||$event->event_type==='reversal')throw new BusinessException('Event aset tidak valid untuk reversal.');if($this->CI->db->get_where('asset_events',array('reversal_of_event_id'=>$event->id))->row())throw new BusinessException('Event aset sudah pernah direversal.');$latest=$this->CI->db->where(array('asset_id'=>$event->asset_id,'company_id'=>$this->company()))->where('event_type !=','reversal')->order_by('event_date','DESC')->order_by('id','DESC')->limit(1)->get('asset_events')->row();if(!$latest||(int)$latest->id!==(int)$event->id)throw new BusinessException('Hanya event terakhir aset yang dapat direversal.');$asset=$this->asset($event->asset_id);$journal=null;if($event->journal_id){$original=$this->CI->db->get_where('journals',array('id'=>$event->journal_id,'company_id'=>$this->company()))->row();$details=$this->CI->db->where('journal_id',$event->journal_id)->get('journal_details')->result();if(!$original||!$details)throw new BusinessException('Jurnal event tidak ditemukan.');$lines=array();foreach($details as$d)$lines[]=array('account_id'=>$d->account_id,'debit'=>$d->kredit,'kredit'=>$d->debit);$journal=$this->CI->postingservice->post(array('tanggal'=>$date,'no_ref'=>'REV-AST-'.$event->id.'-'.date('YmdHis'),'keterangan'=>'[AUTO] Reversal event '.$event->event_no.' aset '.$asset->kode_asset.' - '.$asset->nama_asset.' | Alasan '.$reason,'ref_type'=>'asset_event_reversal','ref_id'=>$event->id,'reversal_of'=>$original->id,'reversal_reason'=>$reason,'created_by'=>$userId),$lines,false,true);$this->CI->db->where('id',$original->id)->update('journals',array('is_reversed'=>1,'status'=>'reversed'));}
$update=array('nilai_buku'=>$event->book_value_before,'version'=>(int)$asset->version+1);
if($event->event_type==='depreciation'){$update['akumulasi_penyusutan']=max(0,(float)$asset->akumulasi_penyusutan-(float)$event->amount);$update['lifecycle_status']='active';$this->CI->db->where('asset_event_id',$event->id)->update('asset_depreciation_schedule',array('status'=>'reversed','reversal_journal_id'=>$journal,'reversed_at'=>date('Y-m-d H:i:s')));}
elseif($event->event_type==='transfer')$update['lokasi_asset_id']=$event->from_location_id;
elseif($event->event_type==='responsibility_change'){$h=$this->CI->db->get_where('asset_responsibility_history',array('asset_event_id'=>$event->id))->row();if($h)$update['responsible_employee_id']=$h->from_employee_id;}
elseif($event->event_type==='addition')$update['nilai_perolehan']=max(0,(float)$asset->nilai_perolehan-(float)$event->amount);
elseif($event->event_type==='revaluation'){$update['nilai_perolehan']=max(0,(float)$asset->nilai_perolehan-(float)$event->amount);$update['revaluation_total']=max(0,(float)$asset->revaluation_total-(float)$event->amount);}
elseif($event->event_type==='impairment')$update['impairment_total']=max(0,(float)$asset->impairment_total-(float)$event->amount);
elseif(in_array($event->event_type,array('disposal','lost','damaged'),true)){$update['lifecycle_status']='active';$update['status']='aktif';$update['disposal_date']=null;$update['disposal_proceeds']=0;}
elseif($event->event_type==='capitalization'){$update['workflow_status']='approved';$update['lifecycle_status']='draft';$update['acquisition_journal_id']=null;if($asset->source_type==='warehouse'){$barcode=$this->CI->db->query('SELECT * FROM item_barcodes WHERE id=? FOR UPDATE',array((int)$asset->barcode_id))->row();if(!$barcode)throw new BusinessException('Barcode sumber aset tidak ditemukan.');$key='ASSET-CAP-REV-'.$asset->id.'-V'.$asset->version;$this->CI->db->insert('inventory_ledger',array('item_id'=>$asset->item_id,'warehouse_id'=>$asset->warehouse_id,'bin_id'=>$asset->bin_id?:null,'barcode_id'=>$barcode->id,'batch_id'=>$barcode->batch_id?:null,'movement_date'=>$date,'direction'=>'in','qty'=>$asset->qty,'unit_cost'=>$asset->harga_per_unit,'value'=>$asset->nilai_perolehan,'document_type'=>'asset_capitalization_reversal','document_id'=>$asset->id,'document_line_id'=>$event->id,'idempotency_key'=>$key,'created_at'=>date('Y-m-d H:i:s')));$this->CI->db->insert('stock_logs',array('company_id'=>$this->company(),'item_id'=>$asset->item_id,'warehouse_id'=>$asset->warehouse_id,'barcode_id'=>$barcode->id,'qty'=>$asset->qty,'tipe'=>'masuk','unit_cost'=>$asset->harga_per_unit,'keterangan'=>'[AUTO] Reversal kapitalisasi aset '.$asset->kode_asset.' | '.$reason,'ref_type'=>'asset_capitalization_reversal','ref_id'=>$asset->id,'idempotency_key'=>$key));$this->CI->db->insert('item_movements',array('item_id'=>$asset->item_id,'barcode_id'=>$barcode->id,'qty'=>$asset->qty,'from_type'=>'asset','from_id'=>$asset->id,'to_type'=>'warehouse','to_id'=>$asset->warehouse_id,'movement_type'=>'uninstallation','notes'=>'Reversal kapitalisasi: '.$reason,'created_at'=>date('Y-m-d H:i:s')));$this->CI->db->where('id',$barcode->id)->update('item_barcodes',array('qty_sisa'=>(float)$barcode->qty_sisa+(float)$asset->qty,'status'=>'available','version'=>(int)$barcode->version+1));$this->synchronizeItem($asset->item_id);}}
$this->CI->db->where('id',$asset->id)->update('assets',$update);$this->event($asset,'reversal',$date,$event->amount,$event->book_value_after,$event->book_value_before,'Reversal '.$event->event_no.': '.$reason,$journal,'ASSET-REV-'.$event->id,$userId,$event->to_location_id,$event->from_location_id,$event->id,'asset_event',$event->id);return$journal;
});
}
public function saveCategory(array$data,$userId)
{
$id=(int)($data['id']??0);$code=strtoupper(preg_replace('/[^A-Z0-9_-]/i','',trim($data['code'])));$name=trim($data['name']);if($code===''||$name==='')throw new BusinessException('Kode dan nama kategori wajib diisi.');$row=array('company_id'=>$this->company(),'code'=>$code,'name'=>$name,'default_useful_life_months'=>max(1,(int)$data['life']),'default_residual_percent'=>max(0,(float)$data['residual_percent']),'depreciation_method'=>$data['method']==='declining_balance'?'declining_balance':'straight_line','asset_account_id'=>(int)$data['asset_account_id']?:null,'accumulated_depreciation_account_id'=>(int)$data['accumulated_account_id']?:null,'depreciation_expense_account_id'=>(int)$data['expense_account_id']?:null,'is_active'=>!empty($data['is_active'])?1:0,'updated_at'=>date('Y-m-d H:i:s'),'updated_by'=>$userId);if($id){$this->CI->db->where(array('id'=>$id,'company_id'=>$this->company()))->update('asset_categories',$row);if(!$this->CI->db->affected_rows()&&!$this->CI->db->get_where('asset_categories',array('id'=>$id,'company_id'=>$this->company()))->row())throw new BusinessException('Kategori tidak ditemukan.');return$id;}$this->CI->db->insert('asset_categories',$row);return(int)$this->CI->db->insert_id();
}
public function saveLocation(array$data,$userId)
{
$id=(int)($data['id']??0);$code=strtoupper(preg_replace('/[^A-Z0-9_-]/i','',trim($data['code'])));$name=trim($data['name']);if($code===''||$name==='')throw new BusinessException('Kode dan nama lokasi wajib diisi.');$row=array('company_id'=>$this->company(),'code'=>$code,'nama'=>$name,'address'=>trim((string)$data['address'])?:null,'responsible_employee_id'=>!empty($data['responsible_employee_id'])?(int)$data['responsible_employee_id']:null,'is_active'=>!empty($data['is_active'])?1:0,'updated_at'=>date('Y-m-d H:i:s'),'updated_by'=>$userId);if($id){$this->CI->db->where(array('id'=>$id,'company_id'=>$this->company()))->update('lokasi_asset',$row);if(!$this->CI->db->affected_rows()&&!$this->CI->db->get_where('lokasi_asset',array('id'=>$id,'company_id'=>$this->company()))->row())throw new BusinessException('Lokasi tidak ditemukan.');return$id;}$this->CI->db->insert('lokasi_asset',$row);return(int)$this->CI->db->insert_id();
}
}