0 LEFT JOIN clients ON client_id = invoice_client_id LEFT JOIN expenses ON expense_id = stock_expense_id LEFT JOIN users ON user_id = stock_user_id AND stock_user_id > 0 WHERE $range_sql ORDER BY stock_created_at DESC, stock_id DESC"); while ($row = mysqli_fetch_assoc($sql)) { $row['type_label'] = productStockTypes()[$row['stock_type']]['label'] ?? $row['stock_type']; $row['invoice_open'] = $row['invoice_number'] !== null && hasClientAccess(intval($row['invoice_client_id'])); $moves[] = $row; } $product_columns = [ 'product_name' => ['label' => 'Product', 'html' => function ($row) { return '' . escapeHtml($row['product_name']) . '' . ($row['product_code'] || $row['vendor_name'] ? '
' . escapeHtml(implode(' ยท ', array_filter([$row['product_code'], $row['vendor_name']]))) . '
' : ''); }], 'product_code' => ['label' => 'Code', 'screen' => false], 'vendor_name' => ['label' => 'Vendor', 'screen' => false], 'product_location' => ['label' => 'Location', 'class' => 'small'], 'on_hand' => ['label' => 'On shelf', 'type' => 'decimal', 'decimals' => 2, 'html' => function ($row) { $tone = $row['status'] === 'Out' ? 'text-danger-emphasis fw-semibold' : ($row['status'] === 'Reorder' ? 'text-warning-emphasis fw-semibold' : ''); return '' . escapeHtml(billingQuantity($row['on_hand'])) . ''; }], 'product_reorder_level' => ['label' => 'Reorder at', 'type' => 'decimal', 'decimals' => 2, 'html' => function ($row) { return floatval($row['product_reorder_level']) > 0 ? escapeHtml(billingQuantity($row['product_reorder_level'])) : '-'; }], 'value' => ['label' => 'Value', 'type' => 'money', 'title' => 'On the shelf, at its cost today'], 'received' => ['label' => 'In', 'type' => 'decimal', 'decimals' => 2, 'title' => 'Received in the period', 'html' => function ($row) { return floatval($row['received']) ? escapeHtml(billingQuantity($row['received'])) : '-'; }], 'sold' => ['label' => 'Sold', 'type' => 'decimal', 'decimals' => 2, 'title' => 'Invoiced in the period, less what came back', 'html' => function ($row) { return floatval($row['sold']) ? escapeHtml(billingQuantity($row['sold'])) : '-'; }], 'written_off' => ['label' => 'Written off', 'type' => 'decimal', 'decimals' => 2, 'html' => function ($row) { return floatval($row['written_off']) ? escapeHtml(billingQuantity($row['written_off'])) : '-'; }], 'sold_value' => ['label' => 'Cost of sales', 'type' => 'money', 'title' => 'What what was sold cost you'], 'status' => ['label' => 'Status', 'html' => function ($row) { $tones = ['Out' => 'danger', 'Reorder' => 'warning', 'In stock' => 'success', 'Not tracked' => 'secondary']; $tone = $tones[$row['status']] ?? 'secondary'; return '' . escapeHtml($row['status']) . ($row['needs_reorder'] && floatval($row['product_reorder_qty']) > 0 ? ' ' . escapeHtml(billingQuantity($row['product_reorder_qty'])) : '') . ''; }], ]; $move_columns = [ 'stock_created_at' => ['label' => 'When', 'type' => 'date', 'class' => 'small text-nowrap', 'html' => function ($row) { return '' . escapeHtml(date('M j, Y', strtotime($row['stock_created_at']))) . ''; }], 'product_name' => ['label' => 'Product', 'html' => function ($row) { return '' . escapeHtml($row['product_name']) . ''; }], 'type_label' => ['label' => 'What', 'html' => function ($row) { $type = productStockTypes()[$row['stock_type']] ?? productStockTypes()['adjusted']; return '' . escapeHtml($type['label']) . ''; }], 'stock_qty' => ['label' => 'Qty', 'type' => 'decimal', 'decimals' => 2, 'html' => function ($row) { return '' . escapeHtml(productStockQty($row['stock_qty'])) . ''; }], 'unit_cost' => ['label' => 'Unit cost', 'type' => 'money'], 'value' => ['label' => 'Value', 'type' => 'money', 'title' => 'The quantity at the unit cost'], 'reference' => ['label' => 'For', 'class' => 'small', 'csv' => function ($row) { return trim(implode(' - ', array_filter([$row['invoice_open'] ? $row['invoice_prefix'] . $row['invoice_number'] . ' ' . $row['client_name'] : '', $row['expense_description'] !== null ? $row['expense_date'] . ' ' . $row['expense_description'] : '', (string) $row['stock_note']]))); }, 'html' => function ($row) { $html = []; if ($row['invoice_open']) { $html[] = '' . escapeHtml($row['invoice_prefix'] . $row['invoice_number']) . ' ' . escapeHtml($row['client_name']); } if ($row['expense_description'] !== null) { $html[] = '' . escapeHtml(mb_strimwidth($row['expense_date'] . ' - ' . $row['expense_description'], 0, 70, '...')); } if (trim((string) $row['stock_note']) !== '' && !($row['invoice_open'] && trim($row['stock_note']) === $row['invoice_prefix'] . $row['invoice_number'])) { $html[] = '' . escapeHtml($row['stock_note']) . ''; } return implode('
', $html) ?: '-'; }], 'user_name' => ['label' => 'By', 'class' => 'small text-nowrap'], ]; if (reportExporting('moves')) { reportCsv('Stock Moves ' . $period['start'] . ' to ' . $period['end'], $move_columns, $moves); } if (reportExporting()) { reportCsv('Inventory ' . $period['start'] . ' to ' . $period['end'], $product_columns, $products, ['product_name' => 'Total', 'value' => round($value_now, 2), 'received' => $in_units, 'sold' => $out_units, 'sold_value' => round($out_value, 2)]); } $buckets = reportBuckets($period); $bucket_sql = function ($types, $sign) use ($buckets, $range_sql, $value_sql) { return "SELECT " . reportBucketSql('stock_created_at', $buckets['unit']) . " AS b, $sign SUM($value_sql) AS v FROM product_stock JOIN products ON product_id = stock_product_id WHERE stock_type IN ($types) AND $range_sql GROUP BY b"; }; $series = [ ['label' => 'Received', 'tone' => 'success', 'data' => reportSeries($bucket_sql("'received'", ''), $buckets['keys'])], ['label' => 'Cost of sales', 'tone' => 'accent', 'data' => reportSeries($bucket_sql("'sold', 'returned'", '-'), $buckets['keys'])], ['label' => 'Written off', 'tone' => 'danger', 'data' => reportSeries($bucket_sql("'written_off'", '-'), $buckets['keys'])], ]; require_once "includes/inc_report_layout.php"; ?>
$period, 'export' => ['products' => 'Products', 'moves' => 'Stock moves'], ]); ?> 'On the shelf', 'value' => reportMoney($value_now), 'title' => 'At each product\'s cost today', 'sub' => escapeHtml(billingQuantity($units_now)) . ' units'], ['label' => 'Received', 'value' => reportMoney($in_value), 'tone' => $in_value > 0 ? 'success' : '', 'sub' => escapeHtml(billingQuantity($in_units)) . ' units in the period'], ['label' => 'Cost of sales', 'value' => reportMoney($out_value), 'title' => 'What the stock invoiced in the period cost you, less what came back', 'sub' => escapeHtml(billingQuantity($out_units)) . ' units sold' . ($off_value > 0 ? ' · ' . escapeHtml(reportMoney($off_value)) . ' written off' : '')], ['label' => 'To reorder', 'value' => reportCount($reorder), 'tone' => $reorder ? 'warning' : '', 'url' => '/agent/products.php?type=reorder', 'sub' => $reorder ? 'at or under their reorder level' : 'nothing low'], ]) ?>
'bar', 'labels' => $buckets['labels'], 'series' => $series, 'format' => 'money'], '', 'Stock received, sold and written off over the period, at cost') ?>
reportEmpty('No product tracks its stock. Turn on Track stock on a product to see it here.', 'fa-warehouse', 'secondary'), 'export_table' => 'products', 'totals' => ['product_name' => 'Total', 'value' => round($value_now, 2), 'received' => $in_units, 'sold' => $out_units, 'sold_value' => round($out_value, 2)], 'totals_html' => ['received' => escapeHtml(billingQuantity($in_units)), 'sold' => escapeHtml(billingQuantity($out_units))], ]) ?> reportEmpty('Nothing came in or went out in this period.', 'fa-warehouse', 'secondary'), 'limit' => 300, 'export_table' => 'moves', 'sortable' => true, ]) ?>