1728 lines
62 KiB
PHP
1728 lines
62 KiB
PHP
<?php
|
|
defined('BASEPATH') OR exit('No direct script access allowed');
|
|
|
|
class Items extends MY_Controller {
|
|
private function companyId(){ return (int)$this->session->userdata('company_id'); }
|
|
private function companyItem($id){ return $this->db->get_where('items',array('id'=>(int)$id,'company_id'=>$this->companyId()))->row(); }
|
|
|
|
public function __construct()
|
|
{
|
|
parent::__construct();
|
|
|
|
if (!$this->session->userdata('logged_in')) {
|
|
redirect('auth');
|
|
}
|
|
}
|
|
|
|
public function index()
|
|
{
|
|
$data = ["active_menu" => "items"];
|
|
$this->load->view('partials/header', $data);
|
|
$this->load->view('items/layout');
|
|
$this->load->view('partials/footer');
|
|
}
|
|
|
|
public function draft_items()
|
|
{
|
|
$data = ["active_menu" => "draft_items"];
|
|
$this->load->view('partials/header', $data);
|
|
$this->load->view('items/draft_items');
|
|
$this->load->view('partials/footer');
|
|
}
|
|
|
|
public function item_barcodes($item_id)
|
|
{
|
|
if(!$this->companyItem($item_id)) show_404();
|
|
$data = [
|
|
"active_menu" => "draft_items",
|
|
"item_id" => $item_id
|
|
];
|
|
|
|
$this->load->view('partials/header', $data);
|
|
$this->load->view('items/barcode_sn', $data);
|
|
$this->load->view('partials/footer');
|
|
}
|
|
|
|
public function item_barcodes_history()
|
|
{
|
|
$data = [
|
|
"active_menu" => "item_historys"
|
|
];
|
|
|
|
$this->load->view('partials/header', $data);
|
|
$this->load->view('items/item_historys', $data);
|
|
$this->load->view('partials/footer');
|
|
}
|
|
|
|
public function scan_history()
|
|
{
|
|
$barcode = trim($this->input->post('barcode'));
|
|
|
|
if(empty($barcode)){
|
|
echo json_encode([
|
|
'status'=>false,
|
|
'message'=>'QR ID tidak boleh kosong'
|
|
]);
|
|
return;
|
|
}
|
|
|
|
$item = $this->db
|
|
->select("
|
|
item_barcodes.id,
|
|
item_barcodes.barcode,
|
|
item_barcodes.serial_number,
|
|
item_barcodes.status,
|
|
item_barcodes.qty_awal,
|
|
item_barcodes.qty_sisa,
|
|
items.nama_barang,
|
|
items.kode_detail
|
|
")
|
|
->from('item_barcodes')
|
|
->join('items','items.id=item_barcodes.item_id')
|
|
->where('items.company_id',$this->companyId())
|
|
->group_start()->where('item_barcodes.barcode',$barcode)
|
|
->or_where('item_barcodes.serial_number',$barcode)->group_end()
|
|
->get()
|
|
->row();
|
|
|
|
if(!$item){
|
|
|
|
echo json_encode([
|
|
'status'=>false,
|
|
'message'=>'Barcode tidak ditemukan.'
|
|
]);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
$history = $this->db
|
|
->select("
|
|
item_movements.*,
|
|
w1.nama as from_warehouse,
|
|
w2.nama as to_warehouse
|
|
")
|
|
->from('item_movements')
|
|
|
|
->join(
|
|
'warehouses w1',
|
|
'w1.id=item_movements.from_id AND item_movements.from_type="warehouse"',
|
|
'left'
|
|
)
|
|
|
|
->join(
|
|
'warehouses w2',
|
|
'w2.id=item_movements.to_id AND item_movements.to_type="warehouse"',
|
|
'left'
|
|
)
|
|
|
|
->where('barcode_id',$item->id)
|
|
|
|
->order_by('created_at','DESC')
|
|
|
|
->get()
|
|
|
|
->result();
|
|
|
|
echo json_encode([
|
|
|
|
'status'=>true,
|
|
|
|
'item'=>$item,
|
|
|
|
'history'=>$history
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
public function load_data_barcode($item_id)
|
|
{
|
|
$item = $this->companyItem($item_id);
|
|
|
|
if (!$item) {
|
|
echo json_encode([
|
|
'status' => false,
|
|
'message' => 'Data barang tidak ditemukan.'
|
|
]);
|
|
return;
|
|
}
|
|
|
|
$next = $this->db
|
|
->where('item_id', $item_id)
|
|
->where('status', 'pending')
|
|
->where('serial_number IS NULL', NULL, false)
|
|
->order_by('id', 'ASC')
|
|
->get('item_barcodes')
|
|
->row();
|
|
|
|
$start = max(0, (int)$this->input->get('start'));
|
|
$length = min(100, max(10, (int)($this->input->get('length') ?: 50)));
|
|
$search = trim((string)$this->input->get('search', true));
|
|
$total = (int)$this->db->where('item_id', $item_id)->count_all_results('item_barcodes');
|
|
$done = (int)$this->db->where('item_id', $item_id)->where('serial_number IS NOT NULL', NULL, false)->where('serial_number !=', '')->count_all_results('item_barcodes');
|
|
|
|
$this->db->where('item_id', $item_id);
|
|
if ($search !== '') {
|
|
$this->db->group_start()->like('barcode', $search)->or_like('serial_number', $search)->group_end();
|
|
}
|
|
$filtered = (int)$this->db->count_all_results('item_barcodes');
|
|
|
|
$this->db->where('item_id', $item_id);
|
|
if ($search !== '') {
|
|
$this->db->group_start()->like('barcode', $search)->or_like('serial_number', $search)->group_end();
|
|
}
|
|
$list = $this->db
|
|
->order_by('id', 'ASC')
|
|
->limit($length, $start)
|
|
->get('item_barcodes')
|
|
->result();
|
|
|
|
echo json_encode([
|
|
'status' => true,
|
|
'item_status' => $item->status,
|
|
'item_sale_price' => (float)$item->harga_jual,
|
|
'item_purchase_price' => (float)$item->harga_beli,
|
|
'next' => $next,
|
|
'list' => $list,
|
|
'total' => $total,
|
|
'done' => $done,
|
|
'filtered' => $filtered,
|
|
'start' => $start,
|
|
'length' => $length,
|
|
'percent' => $total > 0 ? round(($done / $total) * 100) : 0
|
|
]);
|
|
}
|
|
|
|
public function print_barcode($item_id)
|
|
{
|
|
if(!$this->companyItem($item_id)) show_404();
|
|
require_once APPPATH.'third_party/fpdf/fpdf.php';
|
|
require_once APPPATH.'libraries/phpqrcode/qrlib.php';
|
|
|
|
$printStatus=$this->input->get('status',true)?:'available';
|
|
if(!in_array($printStatus,['available','sold_out','all'],true))$printStatus='available';
|
|
$this->db->where('item_id',$item_id);
|
|
if($printStatus==='all')$this->db->where_in('status',['available','sold_out']);else$this->db->where('status',$printStatus);
|
|
$barcodes=$this->db->order_by('id','ASC')->get('item_barcodes')->result();
|
|
|
|
if(empty($barcodes)){
|
|
show_error('Barcode tidak ditemukan.');
|
|
}
|
|
|
|
$column=(int)$this->input->get('column');
|
|
if(!in_array($column,[1,2,3])){
|
|
$column=3;
|
|
}
|
|
|
|
$duplicate=(int)$this->input->get('duplicat');
|
|
if($duplicate<1){
|
|
$duplicate=1;
|
|
}
|
|
|
|
$labelWidth=33.3;
|
|
$labelHeight=23.2;
|
|
|
|
$spaceX=0;
|
|
$spaceY=0;
|
|
|
|
$rowPerPage=6;
|
|
|
|
$paperWidth=$labelWidth*$column;
|
|
$paperHeight=$labelHeight*$rowPerPage;
|
|
|
|
$marginLeft=0;
|
|
$marginTop=0;
|
|
|
|
$labelPerPage=$column*$rowPerPage;
|
|
|
|
$pdf=new FPDF('P','mm',array($paperWidth,$paperHeight));
|
|
$pdf->SetMargins(0,0,0);
|
|
$pdf->SetAutoPageBreak(false);
|
|
$pdf->AddPage();
|
|
|
|
$tempDir=FCPATH.'uploads/temp_qr/';
|
|
if(!is_dir($tempDir)){
|
|
mkdir($tempDir,0777,true);
|
|
}
|
|
|
|
$printData=[];
|
|
foreach($barcodes as $row){
|
|
for($i=0;$i<$duplicate;$i++){
|
|
$printData[]=$row;
|
|
}
|
|
}
|
|
|
|
foreach($printData as $i=>$row){
|
|
|
|
if($i>0 && $i%$labelPerPage==0){
|
|
$pdf->AddPage();
|
|
}
|
|
|
|
$pageIndex=$i%$labelPerPage;
|
|
|
|
$col=$pageIndex%$column;
|
|
$line=floor($pageIndex/$column);
|
|
|
|
// $x=$marginLeft+(($labelWidth+$spaceX)*$col);
|
|
// $y=$marginTop+(($labelHeight+$spaceY)*$line);
|
|
|
|
$x=$col*$labelWidth;
|
|
$y=$line*$labelHeight;
|
|
|
|
$safeBarcode=preg_replace('/[^A-Za-z0-9_-]/','_',basename((string)$row->barcode));
|
|
$qrFile=$tempDir.$safeBarcode.'.png';
|
|
|
|
if(!file_exists($qrFile)){
|
|
QRcode::png(
|
|
$row->barcode,
|
|
$qrFile,
|
|
QR_ECLEVEL_L,
|
|
4,
|
|
1
|
|
);
|
|
}
|
|
|
|
$qrSize=15;
|
|
|
|
$pdf->Image(
|
|
$qrFile,
|
|
$x+16,
|
|
$y+4,
|
|
$qrSize,
|
|
$qrSize
|
|
);
|
|
|
|
$textWidth=14;
|
|
|
|
$pdf->SetFont('Arial','B',7);
|
|
|
|
$pdf->SetXY($x+2,$y+4);
|
|
|
|
$pdf->MultiCell(
|
|
$textWidth,
|
|
3,
|
|
$row->barcode,
|
|
0,
|
|
'L'
|
|
);
|
|
|
|
$serial=empty($row->serial_number)?'-':$row->serial_number;
|
|
|
|
$pdf->SetFont('Arial','B',4.5);
|
|
|
|
$pdf->SetXY($x+2,$y+11);
|
|
|
|
$pdf->MultiCell(
|
|
$textWidth+2,
|
|
3,
|
|
'SN: ' . $serial,
|
|
0,
|
|
'L'
|
|
);
|
|
}
|
|
|
|
$pdf->Output(
|
|
'I',
|
|
'Barcode-'.$item_id.'.pdf'
|
|
);
|
|
}
|
|
|
|
public function save_serial_number()
|
|
{
|
|
if (strtoupper($this->input->method()) !== 'POST') show_404();
|
|
$barcode_id = $this->input->post('barcode_id');
|
|
$serial = trim($this->input->post('serial_number'));
|
|
|
|
if($serial==""){
|
|
echo json_encode(array(
|
|
'status'=>false,
|
|
'message'=>'Serial Number wajib diisi'
|
|
));
|
|
return;
|
|
}
|
|
|
|
$cek = $this->db
|
|
->where('serial_number',$serial)
|
|
->count_all_results('item_barcodes');
|
|
if($cek){
|
|
echo json_encode(array(
|
|
'status'=>false,
|
|
'message'=>'Serial Number sudah digunakan'
|
|
));
|
|
return;
|
|
}
|
|
|
|
$barcode = $this->db->select('item_barcodes.*')->from('item_barcodes')->join('items','items.id=item_barcodes.item_id')->where(array('item_barcodes.id'=>$barcode_id,'items.company_id'=>$this->companyId(),'item_barcodes.status'=>'pending'))->get()->row();
|
|
if(!$barcode){echo json_encode(array('status'=>false,'message'=>'Barcode pending tidak ditemukan pada perusahaan aktif.'));return;}
|
|
$this->db->where('id',$barcode_id);
|
|
$this->db->update('item_barcodes',array(
|
|
'serial_number'=>$serial,
|
|
'status'=>'pending'
|
|
));
|
|
// langsung kirim data terbaru
|
|
$_GET['item_id']=$barcode->item_id;
|
|
$this->load_data_barcode($barcode->item_id);
|
|
}
|
|
|
|
public function delete_serial_number()
|
|
{
|
|
if (strtoupper($this->input->method()) !== 'POST') show_404();
|
|
$barcode_id = (int)$this->input->post('barcode_id');
|
|
|
|
if ($barcode_id <= 0) {
|
|
echo json_encode([
|
|
'status' => false,
|
|
'message' => 'Barcode tidak valid.'
|
|
]);
|
|
return;
|
|
}
|
|
|
|
// Ambil data barcode
|
|
$barcode = $this->db->select('item_barcodes.*')->from('item_barcodes')->join('items','items.id=item_barcodes.item_id')->where(array('item_barcodes.id'=>$barcode_id,'items.company_id'=>$this->companyId(),'item_barcodes.status'=>'pending'))->get()->row();
|
|
|
|
if (!$barcode) {
|
|
echo json_encode([
|
|
'status' => false,
|
|
'message' => 'Barcode tidak ditemukan.'
|
|
]);
|
|
return;
|
|
}
|
|
|
|
if ($barcode->status === 'available' || !empty($barcode->activated_at)) {
|
|
echo json_encode(['status'=>false,'message'=>'Serial unit yang sudah diposting tidak dapat dihapus.']);
|
|
return;
|
|
}
|
|
|
|
// Hapus Serial Number
|
|
$this->db
|
|
->where('id', $barcode_id)
|
|
->update('item_barcodes', [
|
|
'serial_number' => NULL,
|
|
'status' => 'pending'
|
|
]);
|
|
|
|
$this->db->where('id',$barcode->item_id)->update('items',['status'=>'draft']);
|
|
|
|
// Kirim ulang data terbaru
|
|
$this->load_data_barcode($barcode->item_id);
|
|
}
|
|
|
|
public function post_barcode()
|
|
{
|
|
return $this->barcodeError('Posting hanya dapat dilakukan dari tabel Barang Pending.');
|
|
$barcodeId = (int)$this->input->post('barcode_id');
|
|
$salePrice = (float)$this->input->post('sale_price');
|
|
$barcode = $this->db->get_where('item_barcodes', ['id'=>$barcodeId])->row();
|
|
|
|
if (!$barcode) return $this->barcodeError('Barcode tidak ditemukan.');
|
|
if ($barcode->status === 'available') return $this->barcodeError('Unit barcode sudah diposting.');
|
|
if (trim((string)$barcode->serial_number) === '') return $this->barcodeError('Serial number wajib diisi sebelum unit diposting.');
|
|
if ($salePrice <= 0) return $this->barcodeError('Harga jual satuan wajib lebih dari nol.');
|
|
|
|
$this->db->trans_start();
|
|
$this->db->where('id',$barcodeId)->update('item_barcodes',[
|
|
'sale_price'=>$salePrice,
|
|
'status'=>'available',
|
|
'activated_at'=>date('Y-m-d H:i:s'),
|
|
'activated_by'=>(int)$this->session->userdata('user_id')
|
|
]);
|
|
$this->db->where('id',$barcode->item_id)->update('items',[
|
|
'harga_jual'=>$salePrice,
|
|
'status'=>'active'
|
|
]);
|
|
$this->db->trans_complete();
|
|
|
|
if ($this->db->trans_status() === false) return $this->barcodeError('Unit barcode gagal diposting.');
|
|
log_activity('items','posting','Posting unit barcode ID '.$barcodeId.' dengan harga jual '.$salePrice,'success');
|
|
$this->load_data_barcode($barcode->item_id);
|
|
}
|
|
|
|
private function barcodeError($message)
|
|
{
|
|
echo json_encode(['status'=>false,'message'=>$message]);
|
|
}
|
|
|
|
public function get_barcode_item()
|
|
{
|
|
$barcode = trim($this->input->post('barcode'));
|
|
|
|
if ($barcode == '') {
|
|
echo json_encode([
|
|
'status' => false,
|
|
'message' => 'Barcode kosong.'
|
|
]);
|
|
return;
|
|
}
|
|
|
|
$row = $this->db
|
|
->select('
|
|
ib.id AS barcode_id,
|
|
ib.barcode,
|
|
ib.serial_number,
|
|
i.id AS item_id,
|
|
i.kode_detail,
|
|
i.nama_barang,
|
|
i.status,
|
|
i.harga_beli,
|
|
i.harga_jual
|
|
')
|
|
->from('item_barcodes ib')
|
|
->join('items i', 'i.id = ib.item_id')
|
|
->where('ib.qty_sisa !=', 0)
|
|
->where('i.status', 'active')
|
|
->group_start()
|
|
->where('ib.barcode', $barcode)
|
|
->or_like('i.nama_barang', $barcode, 'both')
|
|
->or_like('ib.serial_number', $barcode, 'both')
|
|
->group_end()
|
|
// ->limit(15)
|
|
->get()
|
|
->result();
|
|
|
|
if(!$row){
|
|
echo json_encode([
|
|
'status'=>false,
|
|
'message'=>'Barcode tidak ditemukan.',
|
|
'barcode' => $barcode
|
|
]);
|
|
return;
|
|
}
|
|
|
|
echo json_encode([
|
|
'status'=>true,
|
|
'data'=>$row
|
|
]);
|
|
}
|
|
|
|
public function get_list_barcode_id($item_id)
|
|
{
|
|
// $item_id = trim($this->input->post('item_id'));
|
|
|
|
if ($item_id == '') {
|
|
echo json_encode([
|
|
'status' => false,
|
|
'message' => 'Item belum dipilih'
|
|
]);
|
|
return;
|
|
}
|
|
|
|
$row = $this->db
|
|
->select('
|
|
ib.id AS barcode_id,
|
|
ib.barcode,
|
|
ib.serial_number,
|
|
ib.qty_awal,
|
|
ib.qty_sisa,
|
|
i.id AS item_id,
|
|
i.kode_detail,
|
|
i.nama_barang,
|
|
i.status,
|
|
i.harga_beli,
|
|
i.harga_jual
|
|
')
|
|
->from('item_barcodes ib')
|
|
->join('items i','i.id=ib.item_id')
|
|
->where('ib.item_id',$item_id)
|
|
->where('ib.qty_sisa !=', 0)
|
|
->where_in('ib.status', ['installed', 'reserved', 'available'])
|
|
->get()
|
|
->result();
|
|
|
|
if(!$row){
|
|
echo json_encode([
|
|
'status'=>false,
|
|
'message'=>'Item barang tidak ditemukan.'
|
|
]);
|
|
return;
|
|
}
|
|
|
|
echo json_encode($row);
|
|
}
|
|
|
|
|
|
// =========================
|
|
// GET DATA (FIX)
|
|
// =========================
|
|
public function get_data()
|
|
{
|
|
$status = $this->input->get('s') ?? 'all';
|
|
$server = (int)$this->input->get('server') === 1;
|
|
$draw = (int)$this->input->get('draw');
|
|
$start = max(0,(int)$this->input->get('start'));
|
|
$length = min(100,max(10,(int)$this->input->get('length')));
|
|
|
|
$allowed = ['draft','active','all'];
|
|
if (!in_array($status, $allowed)) {
|
|
$status = 'all';
|
|
}
|
|
|
|
$this->db->select('
|
|
items.id,
|
|
items.nama_barang,
|
|
items.kode_detail,
|
|
items.harga_beli,
|
|
items.harga_jual,
|
|
items.tanggal_pembelian,
|
|
items.created_at,
|
|
items.stok,
|
|
items.status,
|
|
items.kode_id,
|
|
kode.need_serial_number,
|
|
COALESCE((SELECT SUM(CASE WHEN ibs.status IN("available","reserved") THEN GREATEST(ibs.qty_sisa-COALESCE(ibs.reserved_qty,0)-COALESCE(ibs.return_reserved_qty,0),0) ELSE 0 END) FROM item_barcodes ibs WHERE ibs.item_id=items.id),items.stok) operational_available,
|
|
(SELECT COUNT(*) FROM item_barcodes iba WHERE iba.item_id=items.id AND iba.status IN("available","reserved") AND GREATEST(iba.qty_sisa-COALESCE(iba.reserved_qty,0)-COALESCE(iba.return_reserved_qty,0),0)>0) operational_barcode_count,
|
|
COALESCE((SELECT SUM(GREATEST(ibp.qty_sisa-COALESCE(ibp.reserved_qty,0)-COALESCE(ibp.return_reserved_qty,0),0)) FROM item_barcodes ibp WHERE ibp.item_id=items.id AND ibp.status="pending"),items.stok) pending_available,
|
|
(SELECT COUNT(*) FROM item_barcodes ibp WHERE ibp.item_id=items.id AND ibp.status="pending" AND GREATEST(ibp.qty_sisa-COALESCE(ibp.reserved_qty,0)-COALESCE(ibp.return_reserved_qty,0),0)>0) pending_barcode_count,
|
|
(SELECT COUNT(*) FROM item_barcodes ibc WHERE ibc.item_id=items.id) total_barcodes,
|
|
(SELECT COUNT(*) FROM item_barcodes ibc WHERE ibc.item_id=items.id AND NULLIF(ibc.serial_number,"") IS NOT NULL) completed_serials,
|
|
(SELECT COUNT(*) FROM item_barcodes ibc WHERE ibc.item_id=items.id AND ibc.status="available") posted_barcodes,
|
|
(
|
|
SELECT GROUP_CONCAT(DISTINCT w.nama SEPARATOR ", ")
|
|
FROM stock_logs sl
|
|
LEFT JOIN warehouses w ON sl.warehouse_id = w.id
|
|
WHERE sl.item_id = items.id
|
|
) as gudang
|
|
');
|
|
|
|
$this->db->join('kode_barang kode', 'items.kode_id = kode.id', 'left');
|
|
if ($this->db->field_exists('company_id','items')) {
|
|
$this->db->where('items.company_id', (int)$this->session->userdata('company_id'));
|
|
}
|
|
|
|
if ($status === 'draft') {
|
|
$this->db->where('EXISTS(SELECT 1 FROM goods_receipt_lines grlp WHERE grlp.item_id=items.id)',null,false)->group_start()->where('items.status','draft')->or_where('EXISTS(SELECT 1 FROM item_barcodes ibp WHERE ibp.item_id=items.id AND ibp.status="pending")',null,false)->group_end();
|
|
} elseif ($status !== 'all') {
|
|
$this->db->where('items.status', $status);
|
|
}
|
|
|
|
if($server){
|
|
$search=$this->input->get('search');
|
|
$search=is_array($search)?trim((string)($search['value']??'')):'';
|
|
if($search!=='')$this->db->group_start()->like('items.kode_detail',$search)->or_like('items.nama_barang',$search)->group_end();
|
|
}
|
|
|
|
$this->db->from('items');
|
|
$filtered=$server?$this->db->count_all_results('',false):0;
|
|
|
|
// urutan custom
|
|
$this->db->order_by('(items.stok = 0)', 'ASC', false);
|
|
$this->db->order_by('items.tanggal_pembelian', 'ASC');
|
|
|
|
if($server)$this->db->limit($length,$start);
|
|
|
|
$data = $this->db->get()->result();
|
|
|
|
$result = [];
|
|
$no = $server?$start+1:1;
|
|
|
|
foreach($data as $row){
|
|
|
|
$btnDetail = '<button class="btn btn-info btn-sm btn-detail m-1" data-id="'.htmlspecialchars($row->id, ENT_QUOTES, 'UTF-8').'">Detail</button>';
|
|
$btnEdit = '<button class="btn btn-secondary btn-sm btn-editItem m-1" data-id="'.htmlspecialchars($row->id, ENT_QUOTES, 'UTF-8').'">Edit</button>';
|
|
$btnDelete = '<button class="btn btn-danger btn-sm btn-delete m-1" data-id="'.htmlspecialchars($row->id, ENT_QUOTES, 'UTF-8').'">Delete</button>';
|
|
$btnInsertSn = '<a href="'.base_url('items/item_barcodes/'.$row->id).'" class="btn btn-secondary btn-sm m-1">Barcode</a>';
|
|
$btnPrintBarcode = '<button class="btn btn-secondary btn-sm btn-print m-1" data-id="'.htmlspecialchars($row->id, ENT_QUOTES, 'UTF-8').'"><i class="bi bi-printer"></i> Print Barcode</button>';
|
|
$btnPost = '<button class="btn btn-success btn-sm btn-updatePosting m-1" data-id="'.htmlspecialchars($row->id, ENT_QUOTES, 'UTF-8').'" data-buy="'.htmlspecialchars($row->harga_beli,ENT_QUOTES,'UTF-8').'" data-sale="'.htmlspecialchars($row->harga_jual,ENT_QUOTES,'UTF-8').'">Posting</button>';
|
|
|
|
if ($row->need_serial_number == 'true') {
|
|
$btnSN = $btnInsertSn . ((int)$row->posted_barcodes > 0 ? $btnPrintBarcode : '');
|
|
$btnPostview = (int)$row->completed_serials > (int)$row->posted_barcodes ? $btnPost : '';
|
|
|
|
} else {
|
|
|
|
$btnSN = $btnPrintBarcode;
|
|
$btnPostview = $btnPost;
|
|
}
|
|
|
|
$isAdmin=$this->session->userdata('role')==='Admin';
|
|
$canUpdate=$isAdmin||check_permission('items','can_update');
|
|
$canPost=$isAdmin||check_permission('items','can_post');
|
|
$canExport=$isAdmin||check_permission('items','can_export');
|
|
if($status==='active'){
|
|
$action = $btnDetail . ($canExport?$btnPrintBarcode:'');
|
|
|
|
} elseif($status==='draft'){
|
|
$action = $btnDetail . ($canPost?$btnPostview:'') . ($canUpdate?$btnSN:'');
|
|
|
|
} else {
|
|
|
|
$action = $btnDetail . ($canExport?$btnPrintBarcode:'');
|
|
|
|
}
|
|
|
|
$barcodeCount = $status === 'active' ? (int)$row->operational_barcode_count : (int)$row->pending_barcode_count;
|
|
$stockAvailable = $status === 'active' ? (float)$row->operational_available : (float)$row->pending_available;
|
|
$barcodeStock = '<span title="Jumlah barcode / jumlah stok"><strong>'.number_format($barcodeCount, 0, ',', '.').'</strong> / '.number_format($stockAvailable, 0, ',', '.').'</span>';
|
|
|
|
$result[] = [
|
|
$no++,
|
|
$row->tanggal_pembelian,
|
|
$row->kode_detail,
|
|
$row->nama_barang,
|
|
$row->gudang ?? '-',
|
|
$barcodeStock,
|
|
number_format($row->harga_beli ?? 0, 2, ',', '.'),
|
|
$status==='draft'?'<input type="text" class="form-control form-control-sm format-rupiah pending-sale-price" data-id="'.(int)$row->id.'" data-buy="'.htmlspecialchars($row->harga_beli,ENT_QUOTES,'UTF-8').'" value="'.($row->harga_jual>0?number_format($row->harga_jual,2,',','.'):'').'" placeholder="Minimal '.number_format($row->harga_beli,2,',','.').'" inputmode="decimal">':number_format($row->harga_jual??0,2,',','.'),
|
|
$row->created_at,
|
|
$action
|
|
];
|
|
}
|
|
|
|
$payload=array("data"=>$result);
|
|
if($server){$payload['draw']=$draw;$payload['recordsTotal']=$filtered;$payload['recordsFiltered']=$filtered;}
|
|
echo json_encode($payload);
|
|
}
|
|
|
|
// =========================
|
|
// DETAIL
|
|
// =========================
|
|
public function detail_simple($id)
|
|
{
|
|
echo json_encode(
|
|
$this->db->get_where('items',['id'=>$id])->row()
|
|
);
|
|
}
|
|
|
|
// =========================
|
|
// DETAIL
|
|
// =========================
|
|
public function detail($id)
|
|
{
|
|
$item = $this->companyItem($id);
|
|
if (!$item) {
|
|
return json_response(false, 'Barang tidak ditemukan.', array(), 404);
|
|
}
|
|
|
|
if ($item->status === 'active') {
|
|
$availability = $this->db->query("SELECT COUNT(*) barcode_count,COALESCE(SUM(CASE WHEN status IN('available','reserved') THEN GREATEST(qty_sisa-COALESCE(reserved_qty,0)-COALESCE(return_reserved_qty,0),0) ELSE 0 END),0) available_stock,COALESCE(SUM(COALESCE(reserved_qty,0)+COALESCE(return_reserved_qty,0)),0) reserved_stock FROM item_barcodes WHERE item_id=?", array((int)$item->id))->row();
|
|
$item->physical_stock = (float)$item->stok;
|
|
$item->available_stock = $availability && (int)$availability->barcode_count > 0 ? (float)$availability->available_stock : (float)$item->stok;
|
|
$item->reserved_stock = $availability ? (float)$availability->reserved_stock : 0;
|
|
}
|
|
|
|
$logs = $this->db
|
|
->select('stock_logs.*, warehouses.nama as gudang')
|
|
->join('warehouses','warehouses.id = stock_logs.warehouse_id','left')
|
|
->where('item_id',$id)
|
|
->order_by('stock_logs.id','DESC')
|
|
->get('stock_logs')
|
|
->result();
|
|
|
|
return json_response(true, 'Detail barang berhasil dimuat.', array(
|
|
'item' => $item,
|
|
'logs' => $logs
|
|
));
|
|
}
|
|
|
|
public function stock_log_pdf($id)
|
|
{
|
|
$item=$this->companyItem($id);
|
|
if(!$item)show_404();
|
|
$logs=$this->db->select('sl.*,w.nama gudang')->from('stock_logs sl')->join('warehouses w','w.id=sl.warehouse_id','left')->where('sl.item_id',(int)$id)->order_by('sl.id','ASC')->get()->result();
|
|
require_once APPPATH.'third_party/fpdf/fpdf.php';
|
|
$pdf=new FPDF('L','mm','A4');$pdf->SetMargins(12,12,12);$pdf->AddPage();$pdf->SetFont('Arial','B',15);$pdf->Cell(0,8,'STOCK LOG BARANG',0,1,'C');$pdf->SetFont('Arial','',9);$pdf->Cell(0,5,$item->kode_detail.' - '.$item->nama_barang,0,1,'C');$pdf->Ln(5);
|
|
$widths=[12,34,42,20,20,105,35];foreach(['No','Tanggal','Gudang','Qty','Tipe','Keterangan','Referensi']as$i=>$label){$pdf->SetFont('Arial','B',8);$pdf->Cell($widths[$i],7,$label,1,0,'C');}$pdf->Ln();$balance=0;$pdf->SetFont('Arial','',8);foreach($logs as$i=>$log){$signed=$log->tipe==='masuk'?(float)$log->qty:-(float)$log->qty;$balance+=$signed;$pdf->Cell($widths[0],7,$i+1,1,0,'C');$pdf->Cell($widths[1],7,substr($log->created_at,0,19),1);$pdf->Cell($widths[2],7,substr($log->gudang?:'-',0,24),1);$pdf->Cell($widths[3],7,number_format(abs($log->qty),0,',','.'),1,0,'R');$pdf->Cell($widths[4],7,strtoupper($log->tipe),1,0,'C');$pdf->Cell($widths[5],7,substr($log->keterangan?:'-',0,70),1);$pdf->Cell($widths[6],7,substr(($log->ref_type?:'-').' #'.($log->ref_id?:'-'),0,22),1,1);}$pdf->SetFont('Arial','B',9);$pdf->Cell(108,8,'SALDO STOK',1,0,'R');$pdf->Cell(20,8,number_format($balance,0,',','.'),1,0,'R');$pdf->Cell(140,8,'',1,1);$pdf->Output('I','Stock-Log-'.$item->kode_detail.'.pdf');
|
|
}
|
|
|
|
public function get_accounts()
|
|
{
|
|
$data = $this->db
|
|
->where_in('kode_akun', ['1010','1020','2010','1220','1250','1050'])
|
|
->order_by('kode_akun','ASC')
|
|
->get('accounts')
|
|
->result();
|
|
|
|
echo json_encode($data);
|
|
}
|
|
|
|
public function get_accounts_biaya()
|
|
{
|
|
$data = $this->db
|
|
->where(['tipe' => 'expense', 'posisi'=> 'debit', 'kategori'=> 'laba_rugi'])
|
|
->order_by('kode_akun','ASC')
|
|
->get('accounts')
|
|
->result();
|
|
|
|
echo json_encode($data);
|
|
}
|
|
|
|
// =========================
|
|
// SAVE (FIX TOTAL)
|
|
// =========================
|
|
public function save()
|
|
{
|
|
return $this->legacyMutationDisabled('Penambahan stok langsung dinonaktifkan. Gunakan Saldo Awal atau Barang Masuk pada Operasional Stok.');
|
|
$nama_barang = $this->input->post('nama_barang');
|
|
$qty = (int)$this->input->post('qty');
|
|
$harga_beli = (float)$this->input->post('harga_beli');
|
|
$harga_jual = (float)$this->input->post('harga_jual');
|
|
$warehouse_id = $this->input->post('warehouse_id');
|
|
$account_kas = $this->input->post('account_kas');
|
|
$kode_id = $this->input->post('kode_id');
|
|
$tanggal_beli = $this->input->post('tanggal_beli');
|
|
$tracking_qty = $this->input->post('tracking_qty');
|
|
$status = $this->input->post('status');
|
|
|
|
// ================= VALIDASI =================
|
|
if(!$nama_barang || $qty <= 0 || $harga_beli <= 0 || !$warehouse_id || ($status !== 'draft' && !$account_kas)){
|
|
echo json_encode([
|
|
'status'=>false,
|
|
'message'=>'Data tidak lengkap / tidak valid',
|
|
'account_id' => $account_kas
|
|
]);
|
|
return;
|
|
}
|
|
|
|
// ================= VALIDASI HARGA =================
|
|
if($harga_beli > $harga_jual){
|
|
echo json_encode([
|
|
'status'=>false,
|
|
'message'=>'Harga beli tidak boleh lebih besar dari harga jual'
|
|
]);
|
|
return;
|
|
}
|
|
|
|
$kode = $this->db->get_where('kode_barang',['id'=>$kode_id])->row();
|
|
|
|
$tanggal_kode = date('dmy', strtotime($tanggal_beli));
|
|
$kode_detail = $kode->kode_barang . '-' . $tanggal_kode;
|
|
|
|
if ($kode->tracking_type != 'UNIT') {
|
|
|
|
if (empty($tracking_qty) || $tracking_qty <= 0) {
|
|
echo json_encode(array(
|
|
'status' => false,
|
|
'message' => 'Jumlah Per (Roll, Box, Dus) harus diisi'
|
|
));
|
|
return;
|
|
}
|
|
|
|
if ($qty % $tracking_qty != 0) {
|
|
echo json_encode(array(
|
|
'status' => false,
|
|
'message' => 'Jumlah Barang harus habis dibagi Jumlah Per (Roll, Box, Dus)'
|
|
));
|
|
return;
|
|
}
|
|
|
|
}
|
|
|
|
if ($kode->tracking_type == 'UNIT') {
|
|
$unit_qty = $qty;
|
|
$tracking_qty = 1;
|
|
} else {
|
|
$unit_qty = $qty / $tracking_qty;
|
|
}
|
|
|
|
$total = $qty * $harga_beli;
|
|
|
|
// Barang pending hanya master/rencana pembelian. Belum membentuk stok,
|
|
// barcode, pergerakan barang, maupun jurnal sampai diterima melalui PO.
|
|
if ($status === 'draft') {
|
|
$this->db->insert('items',array('stok'=>0,'planned_qty'=>$qty,'planned_warehouse_id'=>$warehouse_id,'planned_tracking_qty'=>$tracking_qty,'kode_detail'=>$kode_detail,'kode_id'=>$kode_id,'nama_barang'=>$nama_barang,'harga_beli'=>$harga_beli,'harga_jual'=>$harga_jual,'tanggal_pembelian'=>$tanggal_beli,'status'=>'draft'));
|
|
$itemId=(int)$this->db->insert_id();
|
|
log_activity('items','create','Membuat master barang pending: '.$nama_barang,'success');
|
|
echo json_encode(array('status'=>true,'message'=>'Barang pending dibuat. Stok baru aktif setelah penerimaan PO.','item_id'=>$itemId));
|
|
return;
|
|
}
|
|
|
|
$this->db->trans_start();
|
|
|
|
// ================= INSERT ITEM =================
|
|
$this->db->insert('items',[
|
|
'stok' =>$qty,
|
|
'kode_detail' =>$kode_detail,
|
|
'kode_id' =>$kode_id,
|
|
'nama_barang' =>$nama_barang,
|
|
'harga_beli' =>$harga_beli,
|
|
'harga_jual' =>$harga_jual,
|
|
'tanggal_pembelian' => $tanggal_beli,
|
|
'status' => $status
|
|
]);
|
|
|
|
$item_id = $this->db->insert_id();
|
|
|
|
// ================= STOCK MASUK =================
|
|
$this->db->insert('stock_logs',[
|
|
'item_id'=>$item_id,
|
|
'warehouse_id'=>$warehouse_id,
|
|
'qty'=>$qty,
|
|
'tipe'=>'masuk',
|
|
'keterangan'=>'Pembelian awal',
|
|
'created_at'=>date('Y-m-d H:i:s')
|
|
]);
|
|
|
|
// ================= Item Barcode Masuk =================
|
|
for ($i = 1; $i <= $unit_qty; $i++) {
|
|
|
|
// $barcode = sprintf('%s-%04d', $kode_detail, $i);
|
|
|
|
$barcode = strtoupper($kode_detail) . '-' . strtoupper(bin2hex(random_bytes(2)));
|
|
|
|
$this->db->insert('item_barcodes',[
|
|
'item_id' => $item_id,
|
|
'barcode' => $barcode,
|
|
'serial_number' => null,
|
|
'warehouse_id' => $warehouse_id,
|
|
'qty_awal' => $tracking_qty,
|
|
'qty_sisa' => $tracking_qty,
|
|
'created_at' => date('Y-m-d H:i:s')
|
|
]);
|
|
|
|
$barcode_id = $this->db->insert_id();
|
|
|
|
$this->db->insert('item_movements',[
|
|
'item_id' => $item_id,
|
|
'barcode_id' => $barcode_id,
|
|
'qty' => $tracking_qty,
|
|
'from_type' => 'supplier', //enum('supplier','warehouse','technician','customer')
|
|
'to_type' => 'warehouse', //enum('supplier','warehouse','technician','customer')
|
|
'movement_type' => 'purchase', //enum('purchase','transfer','installation','uninstallation','supplier_return','customer_return','adjustment')
|
|
'notes' => 'Pembelian barang dari supplier',
|
|
'created_at' => date('Y-m-d H:i:s')
|
|
]);
|
|
}
|
|
|
|
// ================= JURNAL =================
|
|
$no_ref = 'BLI-'.date('Ymd').'-'.$item_id;
|
|
|
|
$this->db->insert('journals',[
|
|
'tanggal'=>$tanggal_beli,
|
|
'no_ref'=>$no_ref,
|
|
'ref_type'=> 'new_items',
|
|
'ref_id'=>$item_id,
|
|
'keterangan'=>'Pembelian '.$nama_barang,
|
|
'created_by' => $this->session->userdata('user_id')
|
|
]);
|
|
|
|
$jid = $this->db->insert_id();
|
|
|
|
// 🔥 Debit Persediaan
|
|
$this->db->insert('journal_details',[
|
|
'journal_id'=>$jid,
|
|
'account_id'=>21, // persediaan
|
|
'debit'=>$total,
|
|
'kredit'=>0
|
|
]);
|
|
|
|
// 🔥 Kredit Kas (DINAMIS)
|
|
foreach ($account_kas as $row) {
|
|
|
|
$this->db->insert('journal_details', [
|
|
'journal_id' => $jid,
|
|
'account_id' => $row['account_id'],
|
|
'debit' => 0,
|
|
'kredit' => str_replace('.', '', $row['nominal'])
|
|
]);
|
|
|
|
}
|
|
|
|
$this->db->trans_complete();
|
|
|
|
// ================= CEK TRANSAKSI =================
|
|
if ($this->db->trans_status() === FALSE){
|
|
echo json_encode([
|
|
'status'=>false,
|
|
'message'=>'Gagal simpan data'
|
|
]);
|
|
} else {
|
|
|
|
log_activity(
|
|
'items',
|
|
'create',
|
|
'Menambahkan item: ' . $nama_barang,
|
|
'success'
|
|
);
|
|
|
|
echo json_encode([
|
|
'status'=>true,
|
|
'message'=>'Berhasil tambah item'
|
|
]);
|
|
}
|
|
}
|
|
|
|
// =========================
|
|
// UPDATE (FIX AKUN)
|
|
// =========================
|
|
public function update()
|
|
{
|
|
return $this->legacyMutationDisabled('Barang aktif tidak dapat diubah melalui form lama. Gunakan master Jenis/Kode Barang atau dokumen persediaan yang sesuai.');
|
|
$id = $this->input->post('id');
|
|
$nama_barang = $this->input->post('nama_barang');
|
|
$harga_baru = (float)$this->input->post('harga_beli');
|
|
$harga_jual = (float)$this->input->post('harga_jual');
|
|
|
|
$item = $this->db->get_where('items',['id'=>$id])->row();
|
|
|
|
if(!$item){
|
|
echo json_encode(['status'=>false,'message'=>'Item tidak ditemukan']);
|
|
return;
|
|
}
|
|
|
|
$this->db->trans_start();
|
|
|
|
$this->db->where('id',$id)->update('items',[
|
|
'nama_barang'=>$nama_barang,
|
|
'harga_beli'=>$harga_baru,
|
|
'harga_jual'=>$harga_jual
|
|
]);
|
|
|
|
if($item->harga_beli != $harga_baru){
|
|
|
|
$stok = $this->db->select('SUM(qty * IF(tipe="masuk",1,-1)) as stok')
|
|
->where('item_id',$id)
|
|
->get('stock_logs')
|
|
->row()->stok ?? 0;
|
|
|
|
$selisih = ($harga_baru - $item->harga_beli) * $stok;
|
|
|
|
if($selisih != 0){
|
|
|
|
$this->db->insert('journals',[
|
|
'tanggal'=>date('Y-m-d'),
|
|
'no_ref'=>'ADJ-'.date('YmdHis').'-'.$id,
|
|
'keterangan'=>'Penyesuaian harga '.$item->nama_barang,
|
|
'created_by' => $this->session->userdata('user_id')
|
|
]);
|
|
|
|
$jid = $this->db->insert_id();
|
|
|
|
if($selisih > 0){
|
|
// 🔥 naik → pendapatan
|
|
$this->db->insert_batch('journal_details',[
|
|
[
|
|
'journal_id'=>$jid,
|
|
'account_id'=>21,
|
|
'debit'=>$selisih,
|
|
'kredit'=>0
|
|
],
|
|
[
|
|
'journal_id'=>$jid,
|
|
'account_id'=>65, // ✅ BENAR
|
|
'debit'=>0,
|
|
'kredit'=>$selisih
|
|
]
|
|
]);
|
|
}else{
|
|
// 🔥 turun → beban
|
|
$this->db->insert_batch('journal_details',[
|
|
[
|
|
'journal_id'=>$jid,
|
|
'account_id'=>64, // ✅ BENAR
|
|
'debit'=>abs($selisih),
|
|
'kredit'=>0
|
|
],
|
|
[
|
|
'journal_id'=>$jid,
|
|
'account_id'=>21,
|
|
'debit'=>0,
|
|
'kredit'=>abs($selisih)
|
|
]
|
|
]);
|
|
}
|
|
}
|
|
}
|
|
|
|
$this->db->trans_complete();
|
|
|
|
log_activity(
|
|
'items',
|
|
'update',
|
|
'Update item: ' . $nama_barang,
|
|
'success'
|
|
);
|
|
|
|
$this->update_item_stok($id);
|
|
|
|
echo json_encode(['status'=>true,'message'=>'Berhasil update + adjustment']);
|
|
}
|
|
|
|
public function delete($id)
|
|
{
|
|
echo json_encode(array('status'=>false,'message'=>'Barang tidak dapat dihapus. Gunakan status nonaktif pada master bila sudah tidak digunakan.'));
|
|
return;
|
|
// cek item ada
|
|
$item = $this->db->get_where('items',['id'=>$id])->row();
|
|
|
|
if(!$item){
|
|
echo json_encode(['status'=>false,'message'=>'Item tidak ditemukan']);
|
|
return;
|
|
}
|
|
|
|
// Master/item yang telah menjadi bagian dokumen pembelian tidak boleh
|
|
// dihapus. Menghapusnya akan meninggalkan item_id yatim pada baris PO
|
|
// dan membuat penerimaan gagal saat menulis stock_logs.
|
|
if ($this->db->table_exists('purchase_order_lines')) {
|
|
$cekPembelian = $this->db->where('item_id', (int) $id)
|
|
->count_all_results('purchase_order_lines');
|
|
|
|
if ($cekPembelian > 0) {
|
|
echo json_encode([
|
|
'status'=>false,
|
|
'message'=>'Barang tidak dapat dihapus karena sudah digunakan pada Purchase Order. Nonaktifkan barang bila tidak akan digunakan lagi.'
|
|
]);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// cek ada transaksi stock
|
|
$cekStock = $this->db->where('item_id',$id)->count_all_results('stock_logs');
|
|
|
|
if($cekStock > 0){
|
|
echo json_encode([
|
|
'status'=>false,
|
|
'message'=>'Tidak bisa dihapus, sudah ada transaksi stok'
|
|
]);
|
|
return;
|
|
}
|
|
|
|
$this->db->trans_start();
|
|
|
|
// hapus item
|
|
$this->db->delete('stock_logs',['item_id'=>$id]);
|
|
$this->db->delete('item_movements',['item_id'=>$id]);
|
|
$this->db->delete('item_barcodes',['item_id'=>$id]);
|
|
$this->db->delete('items',['id'=>$id]);
|
|
|
|
$j = $this->db->get_where('journals', [
|
|
'ref_type'=>'new_items',
|
|
'ref_id'=>$id
|
|
])->result();
|
|
|
|
foreach($j as $row){
|
|
$this->db->delete('journal_details', ['journal_id'=>$row->id]);
|
|
$this->db->delete('journals', ['id'=>$row->id]);
|
|
}
|
|
|
|
$this->db->trans_complete();
|
|
|
|
if ($this->db->trans_status() === FALSE){
|
|
echo json_encode(['status'=>false,'message'=>'Gagal hapus']);
|
|
} else {
|
|
|
|
log_activity(
|
|
'items',
|
|
'delete',
|
|
'Hapus item ID: ' . $id,
|
|
'success'
|
|
);
|
|
|
|
echo json_encode(['status'=>true,'message'=>'Berhasil dihapus']);
|
|
}
|
|
}
|
|
|
|
public function posting()
|
|
{
|
|
if (strtoupper($this->input->method()) !== 'POST') show_404();
|
|
$id = $this->input->post('id');
|
|
|
|
if(!$id){
|
|
echo json_encode([
|
|
'status' => false,
|
|
'message' => 'ID tidak valid'
|
|
]);
|
|
return;
|
|
}
|
|
|
|
// cek item ada
|
|
$item = $this->db->get_where('items', ['id' => $id, 'company_id'=>(int)$this->session->userdata('company_id')])->row();
|
|
|
|
if(!$item){
|
|
echo json_encode([
|
|
'status' => false,
|
|
'message' => 'Item tidak ditemukan'
|
|
]);
|
|
return;
|
|
}
|
|
|
|
$catalog = $this->db->get_where('kode_barang',['id'=>$item->kode_id])->row();
|
|
$serialRequired=$catalog&&strtolower((string)$catalog->need_serial_number)==='true';
|
|
$submittedSalePrice=(float)$this->input->post('harga_jual');
|
|
if($submittedSalePrice>0)$item->harga_jual=$submittedSalePrice;
|
|
if ((float)$item->harga_jual <= 0) {
|
|
echo json_encode(['status'=>false,'message'=>'Harga jual wajib diisi sebelum barang diposting.']);
|
|
return;
|
|
}
|
|
if ((float)$item->harga_jual < (float)$item->harga_beli) {
|
|
echo json_encode(['status'=>false,'message'=>'Harga jual tidak boleh lebih rendah dari harga beli Rp '.number_format($item->harga_beli,2,',','.')]);
|
|
return;
|
|
}
|
|
|
|
// cegah double posting
|
|
if(!$serialRequired&&$item->status == 'active'){
|
|
echo json_encode([
|
|
'status' => false,
|
|
'message' => 'Barang sudah diterima sebelumnya'
|
|
]);
|
|
return;
|
|
}
|
|
|
|
$this->db->trans_start();
|
|
|
|
if($serialRequired){
|
|
$ready=$this->db->where('item_id',$id)->where('status','pending')->where('serial_number IS NOT NULL',null,false)->where('serial_number !=','')->count_all_results('item_barcodes');
|
|
if($ready<=0){$this->db->trans_complete();echo json_encode(['status'=>false,'message'=>'Belum ada serial number yang siap diposting.']);return;}
|
|
$this->db->where('item_id',$id)->where('status','pending')->where('serial_number IS NOT NULL',null,false)->where('serial_number !=','')->update('item_barcodes',[
|
|
'sale_price'=>$item->harga_jual,'status'=>'available','activated_at'=>date('Y-m-d H:i:s'),'activated_by'=>(int)$this->session->userdata('user_id')
|
|
]);
|
|
$posted=$this->db->affected_rows();
|
|
}else{
|
|
$this->db->where('item_id',$id)->where('status','pending')->update('item_barcodes',[
|
|
'sale_price'=>$item->harga_jual,'status'=>'available','activated_at'=>date('Y-m-d H:i:s'),'activated_by'=>(int)$this->session->userdata('user_id')
|
|
]);
|
|
$posted=$this->db->affected_rows();
|
|
}
|
|
$notReady=$this->db->where('item_id',$id)->where('status','pending')->count_all_results('item_barcodes');
|
|
$this->db->where('id', $id);
|
|
$this->db->update('items', [
|
|
'status' => 'active',
|
|
'harga_jual' => $item->harga_jual
|
|
]);
|
|
|
|
$this->db->trans_complete();
|
|
|
|
if ($this->db->trans_status() === FALSE){
|
|
|
|
echo json_encode([
|
|
'status' => false,
|
|
'message' => 'Gagal memproses data'
|
|
]);
|
|
|
|
} else {
|
|
|
|
// log aktivitas
|
|
log_activity(
|
|
'items',
|
|
'posting',
|
|
'Barang diterima | ID: '.$id.' | Nama: '.$item->nama_barang,
|
|
'success'
|
|
);
|
|
|
|
echo json_encode([
|
|
'status' => true,
|
|
'message' => $serialRequired?($posted.' unit berhasil diposting dan '.$notReady.' unit masih belum siap.'):('Barang berhasil diposting. '.$posted.' barcode siap digunakan.'),
|
|
'posted_count'=>$posted,
|
|
'pending_count'=>$notReady
|
|
]);
|
|
}
|
|
}
|
|
|
|
// =========================
|
|
// KELUAR BARANG (FIX TANPA stok column)
|
|
// =========================
|
|
public function keluarkan()
|
|
{
|
|
return $this->legacyMutationDisabled('Pengeluaran langsung dinonaktifkan. Buat dokumen Barang Keluar pada Operasional Stok agar ledger, barcode, dan jurnal tetap konsisten.');
|
|
$account_biaya = (int)$this->input->post('account_biaya');
|
|
$tanggal_keluar = $this->input->post('tanggal_keluar');
|
|
$keterangan_keluar = $this->input->post('keterangan_keluar');
|
|
|
|
// mode baru: single barcode terpilih
|
|
$selected_barcode = (int)$this->input->post('selected_barcode');
|
|
if ($selected_barcode > 0) {
|
|
|
|
if (!$account_biaya || !$tanggal_keluar) {
|
|
echo json_encode(['status'=>false,'message'=>'Data tidak lengkap']);
|
|
return;
|
|
}
|
|
|
|
$barcode = $this->db
|
|
->select('
|
|
ib.id as barcode_id,
|
|
ib.item_id,
|
|
ib.qty_sisa,
|
|
ib.barcode,
|
|
ib.warehouse_id,
|
|
ib.status as barcode_status,
|
|
i.nama_barang,
|
|
i.harga_beli
|
|
')
|
|
->from('item_barcodes ib')
|
|
->join('items i', 'i.id = ib.item_id')
|
|
->where('ib.id', $selected_barcode)
|
|
->get()
|
|
->row();
|
|
|
|
if (!$barcode) {
|
|
echo json_encode(['status'=>false,'message'=>'Barcode tidak ditemukan']);
|
|
return;
|
|
}
|
|
|
|
if ((float)$barcode->qty_sisa <= 0) {
|
|
echo json_encode(['status'=>false,'message'=>'Qty barcode sudah habis']);
|
|
return;
|
|
}
|
|
|
|
if (empty($barcode->warehouse_id)) {
|
|
echo json_encode(['status'=>false,'message'=>'Warehouse barcode tidak valid']);
|
|
return;
|
|
}
|
|
|
|
$qty_keluar = 1;
|
|
$nilai = $qty_keluar * (float)$barcode->harga_beli;
|
|
|
|
$this->db->trans_start();
|
|
|
|
// stock log keluar per item
|
|
$this->db->insert('stock_logs',[
|
|
'item_id' => $barcode->item_id,
|
|
'warehouse_id' => $barcode->warehouse_id,
|
|
'qty' => $qty_keluar,
|
|
'tipe' => 'keluar',
|
|
'keterangan' => $keterangan_keluar
|
|
]);
|
|
$stock_log_id = $this->db->insert_id();
|
|
|
|
// jurnal
|
|
$this->db->insert('journals',[
|
|
'tanggal' => $tanggal_keluar,
|
|
'no_ref' => 'GOUT-'.date('YmdHis').'-'.$barcode->item_id,
|
|
'keterangan' => 'Pengeluaran Barang '.$barcode->nama_barang.' ( '.$keterangan_keluar.' )',
|
|
'ref_type' => 'stock_logs',
|
|
'ref_id' => $stock_log_id,
|
|
'created_by' => $this->session->userdata('user_id')
|
|
]);
|
|
$jid = $this->db->insert_id();
|
|
|
|
$this->db->insert('journal_details',[
|
|
'journal_id'=>$jid,
|
|
'account_id'=>$account_biaya,
|
|
'debit'=>$nilai,
|
|
'kredit'=>0
|
|
]);
|
|
|
|
$this->db->insert('journal_details',[
|
|
'journal_id'=>$jid,
|
|
'account_id'=>21,
|
|
'debit'=>0,
|
|
'kredit'=>$nilai
|
|
]);
|
|
|
|
// START - Item Movement & item barcodes update status
|
|
$this->db->where('barcode_id', $barcode->barcode_id);
|
|
$this->db->order_by('created_at', 'DESC');
|
|
$this->db->limit(1);
|
|
$item_move = $this->db->get('item_movements')->row();
|
|
|
|
if($barcode->qty_sisa < $qty_keluar) {
|
|
echo json_encode(['status'=>false, 'message'=>'Stok tidak cukup. Sisa: '.$barcode->qty_sisa]);
|
|
$this->db->trans_rollback();
|
|
return;
|
|
}
|
|
|
|
$this->db->insert('item_movements',[
|
|
'item_id' => $barcode->item_id,
|
|
'barcode_id' => $barcode->barcode_id,
|
|
'qty' => $qty_keluar,
|
|
'from_type' => $item_move ? $item_move->to_type : 'warehouse', //enum('supplier','warehouse','technician','customer','invoice','asset')
|
|
'to_type' => 'technician', //enum('supplier','warehouse','technician','customer','invoice','asset')
|
|
'movement_type' => 'installation', //enum('purchase','transfer','installation','uninstallation','supplier_return','customer_return','adjustment','sold_out','asset')
|
|
'notes' => $keterangan_keluar,
|
|
'created_at' => date('Y-m-d H:i:s')
|
|
]);
|
|
|
|
// update qty_sisa barcode
|
|
$new_qty_sisa = (float)$barcode->qty_sisa - $qty_keluar;
|
|
$this->db->where('id', $barcode->barcode_id)->update('item_barcodes', [
|
|
'qty_sisa' => $new_qty_sisa,
|
|
'status' => $new_qty_sisa <= 0 ? 'installed' : $barcode->barcode_status
|
|
]);
|
|
|
|
$this->db->where('barcode', $barcode->barcode)->where('status', 'active')->update('item_technician', ['status' => 'inactive', 'deleted_at' => date('Y-m-d H:i:s')]);
|
|
|
|
// END - Item Movement & item barcodes update status
|
|
|
|
$this->db->trans_complete();
|
|
|
|
if (!$this->db->trans_status()) {
|
|
echo json_encode(['status'=>false,'message'=>'Gagal proses']);
|
|
return;
|
|
}
|
|
|
|
$this->update_item_stok($barcode->item_id);
|
|
|
|
log_activity(
|
|
'items',
|
|
'stock_out',
|
|
'Keluar barcode: '.$barcode->barcode_id.' | '.$barcode->nama_barang.' qty: '.$qty_keluar.' ( '.$keterangan_keluar.' )',
|
|
'success'
|
|
);
|
|
|
|
echo json_encode(['status'=>true,'message'=>'Barang berhasil dikeluarkan']);
|
|
return;
|
|
}
|
|
|
|
// fallback flow lama
|
|
$warehouse_id_keluar = $this->input->post('warehouse_id_keluar');
|
|
$barang_id = $this->input->post('barang_id');
|
|
$qty_keluar = (int)$this->input->post('qty_keluar');
|
|
|
|
if(!$barang_id || !$warehouse_id_keluar || $qty_keluar <= 0){
|
|
echo json_encode(['status'=>false,'message'=>'Data tidak valid']);
|
|
return;
|
|
}
|
|
|
|
$item = $this->db->get_where('items',['id'=>$barang_id])->row();
|
|
if(!$item){
|
|
echo json_encode(['status'=>false,'message'=>'Item tidak ditemukan']);
|
|
return;
|
|
}
|
|
|
|
$stok = $this->db->select('
|
|
COALESCE(SUM(qty * IF(tipe="masuk",1,-1)),0) as stok
|
|
')
|
|
->where('item_id',$barang_id)
|
|
->where('warehouse_id',$warehouse_id_keluar)
|
|
->get('stock_logs')
|
|
->row()->stok;
|
|
|
|
if($stok < $qty_keluar){
|
|
echo json_encode(['status'=>false,'message'=>'Stok tidak cukup']);
|
|
return;
|
|
}
|
|
|
|
$nilai = $qty_keluar * (float)$item->harga_beli;
|
|
|
|
$this->db->trans_start();
|
|
|
|
$this->db->insert('stock_logs',[
|
|
'item_id'=>$barang_id,
|
|
'warehouse_id'=>$warehouse_id_keluar,
|
|
'qty'=>$qty_keluar,
|
|
'tipe'=>'keluar',
|
|
'keterangan'=>$keterangan_keluar
|
|
]);
|
|
|
|
$stock_log_id = $this->db->insert_id();
|
|
|
|
$this->db->insert('journals',[
|
|
'tanggal'=>$tanggal_keluar,
|
|
'no_ref'=>'GOUT-'.date('YmdHis').'-'.$barang_id,
|
|
'keterangan'=>'Pengeluaran Barang '.$item->nama_barang . ' ( ' . $keterangan_keluar . ' )',
|
|
'ref_type' =>'stock_logs',
|
|
'ref_id' => $stock_log_id,
|
|
'created_by' => $this->session->userdata('user_id')
|
|
]);
|
|
|
|
$jid = $this->db->insert_id();
|
|
|
|
$this->db->insert('journal_details',[
|
|
'journal_id'=>$jid,
|
|
'account_id'=>$account_biaya,
|
|
'debit'=>$nilai,
|
|
'kredit'=>0
|
|
]);
|
|
|
|
$this->db->insert('journal_details',[
|
|
'journal_id'=>$jid,
|
|
'account_id'=>21,
|
|
'debit'=>0,
|
|
'kredit'=>$nilai
|
|
]);
|
|
|
|
$this->db->trans_complete();
|
|
|
|
if (!$this->db->trans_status()) {
|
|
echo json_encode(['status'=>false,'message'=>'Gagal proses']);
|
|
return;
|
|
}
|
|
|
|
log_activity(
|
|
'items',
|
|
'stock_out',
|
|
'Keluar barang: ' . $item->nama_barang . ' qty: ' . $qty_keluar . ' ( ' . $keterangan_keluar . ' )',
|
|
'success'
|
|
);
|
|
|
|
$this->update_item_stok($barang_id);
|
|
|
|
echo json_encode(['status'=>true,'message'=>'Barang berhasil dikeluarkan']);
|
|
}
|
|
|
|
public function list()
|
|
{
|
|
echo json_encode(
|
|
$this->db->get('warehouses')->result()
|
|
);
|
|
}
|
|
|
|
|
|
public function adjust()
|
|
{
|
|
return $this->legacyMutationDisabled('Adjustment langsung dinonaktifkan. Gunakan dokumen Penyesuaian dengan workflow approval pada Operasional Stok.');
|
|
$item_id = $this->input->post('item_id');
|
|
$qty = (int)$this->input->post('qty');
|
|
$warehouse_id = $this->input->post('warehouse_id');
|
|
$ket = $this->input->post('keterangan');
|
|
|
|
if($qty == 0){
|
|
echo json_encode(['status'=>false,'message'=>'Qty tidak boleh 0']);
|
|
return;
|
|
}
|
|
|
|
$item = $this->db->get_where('items',['id'=>$item_id])->row();
|
|
|
|
$nilai = abs($qty) * $item->harga_beli;
|
|
|
|
$this->db->trans_start();
|
|
|
|
// 🔥 stock log
|
|
$this->db->insert('stock_logs',[
|
|
'item_id'=>$item_id,
|
|
'warehouse_id'=>$warehouse_id,
|
|
'qty'=>abs($qty),
|
|
'tipe'=> $qty > 0 ? 'masuk' : 'keluar',
|
|
'keterangan'=>'Adjustment: '.$ket
|
|
]);
|
|
|
|
// 🔥 jurnal
|
|
$this->db->insert('journals',[
|
|
'tanggal'=>date('Y-m-d'),
|
|
'no_ref'=>'ADJ-'.date('Ymd').'-'.$item_id,
|
|
'keterangan'=>'Penyesuaian '.$item->nama_barang,
|
|
'created_by' => $this->session->userdata('user_id')
|
|
]);
|
|
|
|
$jid = $this->db->insert_id();
|
|
|
|
if($qty > 0){
|
|
// stok naik
|
|
$this->db->insert_batch('journal_details',[
|
|
[
|
|
'journal_id'=>$jid,
|
|
'account_id'=>21, // persediaan
|
|
'debit'=>$nilai,
|
|
'kredit'=>0
|
|
],
|
|
[
|
|
'journal_id'=>$jid,
|
|
'account_id'=>65, // modal / selisih
|
|
'debit'=>0,
|
|
'kredit'=>$nilai
|
|
]
|
|
]);
|
|
}else{
|
|
// stok turun
|
|
$this->db->insert_batch('journal_details',[
|
|
[
|
|
'journal_id'=>$jid,
|
|
'account_id'=>64,
|
|
'debit'=>$nilai,
|
|
'kredit'=>0
|
|
],
|
|
[
|
|
'journal_id'=>$jid,
|
|
'account_id'=>21,
|
|
'debit'=>0,
|
|
'kredit'=>$nilai
|
|
]
|
|
]);
|
|
}
|
|
|
|
$this->db->trans_complete();
|
|
|
|
log_activity(
|
|
'items',
|
|
'adjust',
|
|
'Adjustment item ID: ' . $item_id . ' qty: ' . $qty,
|
|
'success'
|
|
);
|
|
|
|
$this->update_item_stok($item_id);
|
|
|
|
echo json_encode(['status'=>true,'message'=>'Adjustment berhasil']);
|
|
}
|
|
|
|
private function update_item_stok($item_id)
|
|
{
|
|
$stok = $this->db->query("
|
|
SELECT COALESCE(SUM(qty * IF(tipe='masuk',1,-1)),0) as stok
|
|
FROM stock_logs
|
|
WHERE item_id = ?
|
|
", [$item_id])->row()->stok;
|
|
|
|
$this->db->where('id', $item_id)
|
|
->update('items', [
|
|
'stok' => $stok
|
|
]);
|
|
}
|
|
|
|
public function get_items_by_wh_id($warehouse_id)
|
|
{
|
|
$this->db->select('
|
|
items.id,
|
|
items.nama_barang,
|
|
items.harga_jual,
|
|
items.kode_detail,
|
|
COALESCE(SUM(
|
|
inventory_ledger.qty * IF(inventory_ledger.direction="in",1,-1)
|
|
),0) as stok
|
|
');
|
|
$this->db->where('items.status', 'active');
|
|
$this->db->where('items.company_id', $this->companyId());
|
|
$this->db->from('items');
|
|
|
|
$this->db->join('inventory_ledger',
|
|
'inventory_ledger.item_id = items.id
|
|
AND inventory_ledger.warehouse_id = '.$this->db->escape($warehouse_id),
|
|
'left');
|
|
|
|
$this->db->group_by('items.id');
|
|
|
|
// ✅ INI KUNCINYA
|
|
$this->db->having('stok >', 0);
|
|
|
|
|
|
$data = $this->db->get()->result();
|
|
|
|
echo json_encode($data);
|
|
}
|
|
|
|
public function update_barcode_item()
|
|
{
|
|
return $this->legacyMutationDisabled('Pembuatan barcode massal tanpa dokumen sumber dinonaktifkan. Barcode dibuat dari penerimaan atau dokumen persediaan.');
|
|
$items = $this->db
|
|
->select("
|
|
i.id,
|
|
i.nama_barang,
|
|
i.stok,
|
|
i.kode_detail,
|
|
kb.tracking_type,
|
|
kb.kode_barang
|
|
")
|
|
->from('items i')
|
|
->join('kode_barang kb', 'kb.id=i.kode_id')
|
|
->where('i.status', 'active')
|
|
->get()
|
|
->result();
|
|
|
|
$this->db->trans_begin();
|
|
|
|
foreach ($items as $item) {
|
|
|
|
// ===========================================
|
|
// Qty per barcode
|
|
// ===========================================
|
|
|
|
if ($item->tracking_type == 'UNIT') {
|
|
$qty_per_barcode = 1;
|
|
} else {
|
|
$qty_per_barcode = ($item->kode_barang == 'FO1') ? 1000 : 2000;
|
|
}
|
|
|
|
// ===========================================
|
|
// Jumlah barcode yang seharusnya
|
|
// ===========================================
|
|
|
|
$target = (int) ceil($item->stok / $qty_per_barcode);
|
|
|
|
if ($target <= 0) {
|
|
continue;
|
|
}
|
|
|
|
// ===========================================
|
|
// Jumlah barcode yang sudah ada
|
|
// ===========================================
|
|
|
|
$barcode_exist = $this->db
|
|
->where('item_id', $item->id)
|
|
->count_all_results('item_barcodes');
|
|
|
|
if ($barcode_exist >= $target) {
|
|
continue;
|
|
}
|
|
|
|
// ===========================================
|
|
// Total qty barcode yang sudah ada
|
|
// ===========================================
|
|
|
|
$qty_existing = $this->db
|
|
->select_sum('qty_awal')
|
|
->where('item_id', $item->id)
|
|
->get('item_barcodes')
|
|
->row();
|
|
|
|
$qty_existing = (float) ($qty_existing->qty_awal ?? 0);
|
|
|
|
// Sisa stok yang belum memiliki barcode
|
|
$qty_remaining = $item->stok - $qty_existing;
|
|
|
|
if ($qty_remaining <= 0) {
|
|
continue;
|
|
}
|
|
|
|
$need = $target - $barcode_exist;
|
|
|
|
for ($i = 1; $i <= $need; $i++) {
|
|
|
|
// Barcode terakhir akan berisi sisa stok
|
|
$qty = min($qty_per_barcode, $qty_remaining);
|
|
|
|
$barcode = strtoupper($item->kode_detail) . '-' . strtoupper(bin2hex(random_bytes(2)));
|
|
|
|
// =============================
|
|
// Insert Barcode
|
|
// =============================
|
|
|
|
$this->db->insert('item_barcodes', [
|
|
'item_id' => $item->id,
|
|
'barcode' => $barcode,
|
|
'warehouse_id' => null,
|
|
'qty_awal' => $qty,
|
|
'qty_sisa' => $qty,
|
|
'status' => 'available'
|
|
]);
|
|
|
|
$barcode_id = $this->db->insert_id();
|
|
|
|
// =============================
|
|
// Insert Movement
|
|
// =============================
|
|
|
|
$this->db->insert('item_movements', [
|
|
'item_id' => $item->id,
|
|
'barcode_id' => $barcode_id,
|
|
'qty' => $qty,
|
|
'from_type' => 'supplier',
|
|
'from_id' => null,
|
|
'to_type' => 'warehouse',
|
|
'to_id' => null,
|
|
'movement_type' => 'purchase',
|
|
'reference_type' => 'system_generate',
|
|
'reference_id' => null,
|
|
'notes' => 'Generate barcode otomatis'
|
|
]);
|
|
|
|
$qty_remaining -= $qty;
|
|
|
|
if ($qty_remaining <= 0) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($this->db->trans_status() === FALSE) {
|
|
|
|
$this->db->trans_rollback();
|
|
|
|
echo json_encode([
|
|
'status' => false,
|
|
'message' => 'Gagal generate barcode.'
|
|
]);
|
|
return;
|
|
}
|
|
|
|
$this->db->trans_commit();
|
|
|
|
echo json_encode([
|
|
'status' => true,
|
|
'message' => 'Barcode berhasil diperbarui.'
|
|
]);
|
|
}
|
|
|
|
private function legacyMutationDisabled($message)
|
|
{
|
|
return $this->output->set_status_header(422)->set_content_type('application/json')->set_output(json_encode(array('status'=>false,'message'=>$message)));
|
|
}
|
|
}
|