profit = $profit; $this->records = $records; $this->snapshotter = $snapshotter; $this->pos = $pos; $this->settings = $settings; $this->resolver = $resolver; $this->inventory = $inventory; } /** * Build a report by type. * * @param string $type Report type. * @param array $args Range args. * @return array */ public function build( string $type, array $args ): array { switch ( $type ) { case 'profit_by_category': return $this->profit_by_category( $args ); case 'sales_vs_cost': return $this->sales_vs_cost( $args ); case 'supplier_spending': return $this->supplier_spending( $args ); case 'purchase_history': return $this->purchase_history( $args ); case 'inventory_value': return $this->inventory_value(); case 'profit_by_product': default: return $this->profit_by_product( $args ); } } /** * Profit by product/variation. * * @param array $args Range args. * @return array */ private function profit_by_product( array $args ): array { $rows = array_map( static fn( array $r ): array => array( 'product' => $r['label'], 'sku' => $r['sku'], 'units' => $r['units'], 'revenue' => $r['revenue'], 'cost' => $r['cost'], 'profit' => $r['profit'], 'margin' => $r['margin'], ), $this->profit->by_product( $args ) ); return array( 'title' => __( 'Profit by product', 'profit-engine' ), 'columns' => array( array( 'key' => 'product', 'label' => __( 'Product', 'profit-engine' ), 'type' => 'text', ), array( 'key' => 'sku', 'label' => __( 'SKU', 'profit-engine' ), 'type' => 'text', ), array( 'key' => 'units', 'label' => __( 'Units', 'profit-engine' ), 'type' => 'number', ), array( 'key' => 'revenue', 'label' => __( 'Revenue', 'profit-engine' ), 'type' => 'money', ), array( 'key' => 'cost', 'label' => __( 'Cost', 'profit-engine' ), 'type' => 'money', ), array( 'key' => 'profit', 'label' => __( 'Profit', 'profit-engine' ), 'type' => 'money', ), array( 'key' => 'margin', 'label' => __( 'Margin', 'profit-engine' ), 'type' => 'percent', ), ), 'rows' => $rows, ); } /** * Profit grouped by product category (first category wins). * * @param array $args Range args. * @return array */ private function profit_by_category( array $args ): array { $by_product = $this->profit->by_product( $args ); $groups = array(); foreach ( $by_product as $r ) { $cat = $this->primary_category( (int) $r['product_id'] ); if ( ! isset( $groups[ $cat ] ) ) { $groups[ $cat ] = array( 'category' => $cat, 'revenue' => 0.0, 'cost' => 0.0, 'profit' => 0.0, 'units' => 0.0, ); } $groups[ $cat ]['revenue'] += (float) $r['revenue']; $groups[ $cat ]['cost'] += (float) $r['cost']; $groups[ $cat ]['profit'] += (float) $r['profit']; $groups[ $cat ]['units'] += (float) $r['units']; } $rows = array_values( $groups ); foreach ( $rows as &$row ) { $row['margin'] = $row['revenue'] > 0 ? $row['profit'] / $row['revenue'] : 0.0; } unset( $row ); usort( $rows, static fn( $a, $b ): int => $b['profit'] <=> $a['profit'] ); return array( 'title' => __( 'Profit by category', 'profit-engine' ), 'columns' => array( array( 'key' => 'category', 'label' => __( 'Category', 'profit-engine' ), 'type' => 'text', ), array( 'key' => 'units', 'label' => __( 'Units', 'profit-engine' ), 'type' => 'number', ), array( 'key' => 'revenue', 'label' => __( 'Revenue', 'profit-engine' ), 'type' => 'money', ), array( 'key' => 'cost', 'label' => __( 'Cost', 'profit-engine' ), 'type' => 'money', ), array( 'key' => 'profit', 'label' => __( 'Profit', 'profit-engine' ), 'type' => 'money', ), array( 'key' => 'margin', 'label' => __( 'Margin', 'profit-engine' ), 'type' => 'percent', ), ), 'rows' => $rows, ); } /** * Sales vs cost time series. * * @param array $args Range args. * @return array */ private function sales_vs_cost( array $args ): array { [ $from, $to ] = DateRange::resolve( $args ); $rows = $this->records->series( $from, $to, $this->snapshotter->counted_statuses(), DateRange::bucket_for( $from, $to ) ); return array( 'title' => __( 'Sales vs cost', 'profit-engine' ), 'columns' => array( array( 'key' => 'period', 'label' => __( 'Period', 'profit-engine' ), 'type' => 'text', ), array( 'key' => 'revenue', 'label' => __( 'Revenue', 'profit-engine' ), 'type' => 'money', ), array( 'key' => 'cost', 'label' => __( 'Cost', 'profit-engine' ), 'type' => 'money', ), array( 'key' => 'profit', 'label' => __( 'Profit', 'profit-engine' ), 'type' => 'money', ), ), 'rows' => $rows, 'chart' => array( 'x' => 'period', 'series' => array( 'revenue', 'cost', 'profit' ), ), ); } /** * Purchase spending by supplier. * * @param array $args Range args. * @return array */ private function supplier_spending( array $args ): array { [ $from, $to ] = DateRange::resolve( $args ); $rows = array_map( static fn( array $r ): array => array( 'supplier' => (string) ( $r['supplier_name'] ?? __( 'Unknown', 'profit-engine' ) ), 'orders' => (int) $r['orders'], 'total' => (float) $r['total'], ), $this->pos->supplier_spending( $from, $to ) ); return array( 'title' => __( 'Supplier spending', 'profit-engine' ), 'columns' => array( array( 'key' => 'supplier', 'label' => __( 'Supplier', 'profit-engine' ), 'type' => 'text', ), array( 'key' => 'orders', 'label' => __( 'Orders', 'profit-engine' ), 'type' => 'number', ), array( 'key' => 'total', 'label' => __( 'Spend', 'profit-engine' ), 'type' => 'money', ), ), 'rows' => $rows, ); } /** * Purchase history. * * @param array $args Range args. * @return array */ private function purchase_history( array $args ): array { [ $from, $to ] = DateRange::resolve( $args ); $rows = array_map( static fn( array $po ): array => array( 'po_number' => (string) $po['po_number'], 'supplier' => (string) ( $po['supplier_name'] ?? '' ), 'status' => (string) $po['status'], 'date' => (string) ( $po['ordered_at'] ?: $po['created_at'] ), 'total' => Money::to_float( $po['grand_total'] ), ), $this->pos->history( $from, $to ) ); return array( 'title' => __( 'Purchase history', 'profit-engine' ), 'columns' => array( array( 'key' => 'po_number', 'label' => __( 'PO', 'profit-engine' ), 'type' => 'text', ), array( 'key' => 'supplier', 'label' => __( 'Supplier', 'profit-engine' ), 'type' => 'text', ), array( 'key' => 'status', 'label' => __( 'Status', 'profit-engine' ), 'type' => 'text', ), array( 'key' => 'date', 'label' => __( 'Date', 'profit-engine' ), 'type' => 'date', ), array( 'key' => 'total', 'label' => __( 'Total', 'profit-engine' ), 'type' => 'money', ), ), 'rows' => $rows, ); } /** * Inventory value at cost. * * @return array */ private function inventory_value(): array { $rows = $this->inventory->rows(); return array( 'title' => __( 'Inventory value', 'profit-engine' ), 'columns' => array( array( 'key' => 'product', 'label' => __( 'Product', 'profit-engine' ), 'type' => 'text', ), array( 'key' => 'sku', 'label' => __( 'SKU', 'profit-engine' ), 'type' => 'text', ), array( 'key' => 'stock', 'label' => __( 'Stock', 'profit-engine' ), 'type' => 'number', ), array( 'key' => 'unit_cost', 'label' => __( 'Unit cost', 'profit-engine' ), 'type' => 'money', ), array( 'key' => 'value', 'label' => __( 'Value', 'profit-engine' ), 'type' => 'money', ), ), 'rows' => $rows, ); } /** * The primary (first) category name for a unit's parent product. * * @param int $unit_id Purchasable-unit ID. */ private function primary_category( int $unit_id ): string { $product = $this->resolver->product( $unit_id ); if ( ! $product ) { return __( 'Uncategorized', 'profit-engine' ); } $parent_id = $product->get_parent_id() ? (int) $product->get_parent_id() : $unit_id; $terms = get_the_terms( $parent_id, 'product_cat' ); if ( is_array( $terms ) && ! empty( $terms ) ) { return $terms[0]->name; } return __( 'Uncategorized', 'profit-engine' ); } }