ultimatepos/Modules/Connector/Http/Controllers/Api/V2/ManufacturingExtendedController.php

972 lines
43 KiB
PHP

<?php
namespace Modules\Connector\Http\Controllers\Api\V2;
use App\Business;
use App\BusinessLocation;
use App\Media;
use App\Services\Api\BusinessContextService;
use App\Transaction;
use App\Utils\BusinessUtil;
use App\Utils\ModuleUtil;
use App\Utils\ProductUtil;
use App\Utils\TransactionUtil;
use App\Variation;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Modules\Manufacturing\Entities\MfgIngredientGroup;
use Modules\Manufacturing\Entities\MfgRecipe;
use Modules\Manufacturing\Entities\MfgRecipeIngredient;
use Modules\Manufacturing\Utils\ManufacturingUtil;
class ManufacturingExtendedController extends BaseController
{
public function __construct(
protected BusinessContextService $contextService,
protected ModuleUtil $moduleUtil,
protected ManufacturingUtil $mfgUtil,
protected ProductUtil $productUtil,
protected TransactionUtil $transactionUtil,
protected BusinessUtil $businessUtil
) {
}
protected function ensureManufacturing(): int
{
if (! $this->moduleUtil->isModuleInstalled('Manufacturing')) {
abort(403, 'Unauthorized action.');
}
return $this->contextService->getBusinessId(request());
}
protected function recipeQuery(int $business_id)
{
return MfgRecipe::join('variations as v', 'mfg_recipes.variation_id', '=', 'v.id')
->join('product_variations as pv', 'v.product_variation_id', '=', 'pv.id')
->join('products as p', 'v.product_id', '=', 'p.id')
->leftJoin('categories as c', 'p.category_id', '=', 'c.id')
->leftJoin('categories as sc', 'p.sub_category_id', '=', 'sc.id')
->join('units as u', 'p.unit_id', '=', 'u.id')
->where('p.business_id', $business_id)
->select(
'mfg_recipes.*',
DB::raw('IF(p.type="variable", CONCAT(p.name, " - ", pv.name, " - ", v.name, " (", v.sub_sku, ")"), CONCAT(p.name, " (", v.sub_sku, ")")) as recipe_name'),
'u.short_name as unit_name',
'c.name as category',
'sc.name as sub_category'
);
}
public function recipes(Request $request)
{
$business_id = $this->ensureManufacturing();
$query = $this->recipeQuery($business_id)->with(['ingredients', 'sub_unit']);
if ($request->filled('search')) {
$keyword = $request->input('search');
$query->where(function ($q) use ($keyword) {
$q->where('p.name', 'like', "%{$keyword}%")
->orWhere('v.sub_sku', 'like', "%{$keyword}%");
});
}
$items = $query->orderByDesc('mfg_recipes.id')->paginate($request->input('per_page', 25));
$items->getCollection()->transform(function ($row) {
$row->recipe_total = $this->mfgUtil->getRecipeTotal($row);
$row->unit_cost = $row->total_quantity > 0 ? $row->recipe_total / $row->total_quantity : 0;
return $row;
});
return $this->paginated($items);
}
public function showRecipe(Request $request, $id)
{
$business_id = $this->ensureManufacturing();
$recipe = MfgRecipe::with(['variation', 'variation.product', 'variation.product_variation', 'sub_unit', 'variation.product.unit'])
->findOrFail($id);
$variation = Variation::join('products as p', 'p.id', '=', 'variations.product_id')
->where('p.business_id', $business_id)
->where('variations.id', $recipe->variation_id)
->exists();
if (! $variation) {
abort(404);
}
$ingredients = $this->mfgUtil->getIngredientDetails($recipe, $business_id);
return $this->success([
'recipe' => $recipe,
'ingredients' => $ingredients,
'recipe_total' => $this->mfgUtil->getRecipeTotal($recipe),
'production_cost' => $this->mfgUtil->getProductionCost($recipe),
]);
}
public function recipeByVariation(Request $request, $variation_id)
{
$business_id = $this->ensureManufacturing();
$variation = Variation::join('products as p', 'p.id', '=', 'variations.product_id')
->join('product_variations as pv', 'pv.id', '=', 'variations.product_variation_id')
->join('units as u', 'u.id', '=', 'p.unit_id')
->where('p.business_id', $business_id)
->select('p.name as product_name', 'p.type as product_type', 'variations.*', 'pv.name as product_variation_name', 'p.unit_id', 'u.short_name as unit_name')
->findOrFail($variation_id);
$with = [
'ingredients' => fn ($q) => $q->orderBy('sort_order', 'asc'),
'ingredients.variation', 'ingredients.variation.product', 'ingredients.variation.product.unit',
'ingredients.variation.product_variation', 'ingredients.sub_unit', 'ingredients.ingredient_group',
];
$recipe = MfgRecipe::where('variation_id', $variation_id)->with($with)->first();
$copy_recipe = null;
if (empty($recipe) && $request->filled('copy_recipe_id')) {
$copy_recipe = MfgRecipe::with($with)->find($request->input('copy_recipe_id'));
}
$ingredients = [];
$total_production_cost = 0;
if (! empty($recipe) || ! empty($copy_recipe)) {
$ingredients_obj = ! empty($copy_recipe) ? $copy_recipe->ingredients : $recipe->ingredients;
if (! empty($recipe)) {
$total_production_cost = $this->mfgUtil->getProductionCost($recipe);
}
foreach ($ingredients_obj as $ingredient) {
if (empty($ingredient->variation)) {
continue;
}
$ingredient_sub_units = $this->transactionUtil->getSubUnits($business_id, $ingredient->variation->product->unit->id);
$multiplier = ! empty($ingredient->sub_unit_id) ? ($ingredient->sub_unit->base_unit_multiplier ?? 1) : 1;
if (empty($multiplier)) {
$multiplier = 1;
}
$ingredients[] = [
'ingredient_id' => $ingredient->variation->id,
'ingredient_line_id' => empty($copy_recipe) ? $ingredient->id : null,
'full_name' => $ingredient->variation->full_name,
'dpp_inc_tax' => $ingredient->variation->dpp_inc_tax,
'quantity' => $ingredient->quantity,
'waste_percent' => $ingredient->waste_percent ?? 0,
'sort_order' => $ingredient->sort_order,
'sub_unit_id' => $ingredient->sub_unit_id,
'unit' => $ingredient->variation->product->unit->short_name,
'sub_units' => $ingredient_sub_units,
'mfg_ingredient_group_id' => $ingredient->mfg_ingredient_group_id,
'ingredient_group_name' => $ingredient->ingredient_group->name ?? '',
'ig_description' => $ingredient->ingredient_group->description ?? '',
];
}
}
$sub_units = $this->moduleUtil->getSubUnits($business_id, $variation->unit_id);
return $this->success([
'variation' => $variation,
'recipe' => $recipe,
'ingredients' => $ingredients,
'sub_units' => $sub_units,
'total_production_cost' => $total_production_cost,
]);
}
public function storeRecipe(Request $request)
{
$business_id = $this->ensureManufacturing();
$request->validate([
'variation_id' => 'required|integer',
'ingredients' => 'required|array|min:1',
]);
try {
$input = $request->only([
'variation_id', 'ingredients', 'instructions', 'ingredients_cost',
'waste_percent', 'total_quantity', 'extra_cost', 'production_cost_type', 'sub_unit_id',
]);
$input['total'] = $request->input('total', $request->input('final_price'));
$variation = Variation::findOrFail($input['variation_id']);
DB::beginTransaction();
$recipe = MfgRecipe::updateOrCreate(
['variation_id' => $input['variation_id']],
[
'product_id' => $variation->product_id,
'final_price' => $this->moduleUtil->num_uf($input['total'] ?? 0),
'ingredients_cost' => $input['ingredients_cost'] ?? 0,
'waste_percent' => $this->moduleUtil->num_uf($input['waste_percent'] ?? 0),
'total_quantity' => $this->moduleUtil->num_uf($input['total_quantity'] ?? 1),
'extra_cost' => $this->moduleUtil->num_uf($input['extra_cost'] ?? 0),
'production_cost_type' => $input['production_cost_type'] ?? 'fixed',
'instructions' => $input['instructions'] ?? null,
'sub_unit_id' => $request->input('sub_unit_id') ?: null,
]
);
$edited_ingredients = [];
$ingredient_groups = $request->input('ingredient_groups', []);
$ingredient_group_descriptions = $request->input('ingredient_group_description', []);
$created_ig_groups = [];
$ingredients = [];
foreach ($input['ingredients'] as $value) {
$ingredient_variation = Variation::with(['product'])->findOrFail($value['ingredient_id']);
if (! empty($value['ingredient_line_id'])) {
$ingredient = MfgRecipeIngredient::find($value['ingredient_line_id']);
$edited_ingredients[] = $ingredient->id;
} else {
$ingredient = new MfgRecipeIngredient(['variation_id' => $value['ingredient_id']]);
}
$ingredient->quantity = $this->moduleUtil->num_uf($value['quantity']);
$ingredient->waste_percent = $this->moduleUtil->num_uf($value['waste_percent'] ?? 0);
$ingredient->sort_order = $this->moduleUtil->num_uf($value['sort_order'] ?? 0);
$ingredient->sub_unit_id = ! empty($value['sub_unit_id']) && $value['sub_unit_id'] != $ingredient_variation->product->unit_id
? $value['sub_unit_id'] : null;
if (isset($value['ig_index'])) {
$ig_name = $ingredient_groups[$value['ig_index']] ?? '';
$ig_description = $ingredient_group_descriptions[$value['ig_index']] ?? '';
if (! empty($created_ig_groups[$value['ig_index']])) {
$ingredient_group = $created_ig_groups[$value['ig_index']];
} elseif (empty($value['mfg_ingredient_group_id'])) {
$ingredient_group = MfgIngredientGroup::create([
'name' => $ig_name,
'business_id' => $business_id,
'description' => $ig_description,
]);
} else {
$ingredient_group = MfgIngredientGroup::where('business_id', $business_id)
->findOrFail($value['mfg_ingredient_group_id']);
if ($ingredient_group->name != $ig_name || $ingredient_group->description != $ig_description) {
$ingredient_group->update(['name' => $ig_name, 'description' => $ig_description]);
}
}
$created_ig_groups[$value['ig_index']] = $ingredient_group;
$ingredient->mfg_ingredient_group_id = $ingredient_group->id;
}
$ingredients[] = $ingredient;
}
if (! empty($edited_ingredients)) {
MfgRecipeIngredient::where('mfg_recipe_id', $recipe->id)
->whereNotIn('id', $edited_ingredients)
->delete();
} else {
MfgRecipeIngredient::where('mfg_recipe_id', $recipe->id)->delete();
}
$recipe->ingredients()->saveMany($ingredients);
DB::commit();
return $this->success(['id' => $recipe->id], __('lang_v1.added_success'));
} catch (\Exception $e) {
DB::rollBack();
\Log::emergency('File:'.$e->getFile().'Line:'.$e->getLine().'Message:'.$e->getMessage());
return $this->error(__('messages.something_went_wrong'), 500);
}
}
public function destroyRecipe(Request $request, $id)
{
$this->ensureManufacturing();
try {
MfgRecipe::where('id', $id)->delete();
return $this->success(null, __('lang_v1.deleted_success'));
} catch (\Exception $e) {
return $this->error(__('messages.something_went_wrong'), 500);
}
}
public function recipeDropdown(Request $request)
{
$business_id = $this->ensureManufacturing();
$dropdown = MfgRecipe::forDropdown($business_id, false);
$items = [];
foreach ($dropdown as $id => $name) {
$items[] = ['id' => $id, 'name' => $name];
}
return $this->success($items);
}
public function recipeDetails(Request $request)
{
$business_id = $this->ensureManufacturing();
$request->validate([
'variation_id' => 'required|integer',
'location_id' => 'nullable|integer',
]);
$variation_id = $request->input('variation_id');
$location_id = $request->input('location_id');
$recipe = MfgRecipe::where('variation_id', $variation_id)
->with(['variation', 'variation.product', 'variation.product.unit', 'sub_unit', 'ingredients' => fn ($q) => $q->orderBy('sort_order')])
->first();
if (empty($recipe)) {
return $this->error(__('manufacturing::lang.no_recipe_found'), 404);
}
$ingredients = $this->mfgUtil->getIngredientDetails($recipe, $business_id, $location_id);
$sub_units = $this->moduleUtil->getSubUnits($business_id, $recipe->variation->product->unit->id);
return $this->success([
'recipe' => $recipe,
'ingredients' => $ingredients,
'sub_units' => $sub_units,
'production_cost' => $this->mfgUtil->getProductionCost($recipe),
'recipe_total' => $this->mfgUtil->getRecipeTotal($recipe),
]);
}
public function isRecipeExist(Request $request, $variation_id)
{
$this->ensureManufacturing();
return $this->success(['exists' => MfgRecipe::where('variation_id', $variation_id)->exists()]);
}
public function productions(Request $request)
{
$business_id = $this->ensureManufacturing();
$query = Transaction::join('business_locations AS bl', 'transactions.location_id', '=', 'bl.id')
->join('purchase_lines as pl', 'pl.transaction_id', '=', 'transactions.id')
->leftJoin('units as su', 'pl.sub_unit_id', '=', 'su.id')
->join('variations as v', 'v.id', '=', 'pl.variation_id')
->join('product_variations as pv', 'pv.id', '=', 'v.product_variation_id')
->join('products as p', 'p.id', '=', 'v.product_id')
->join('units as u', 'p.unit_id', '=', 'u.id')
->where('transactions.business_id', $business_id)
->where('transactions.type', 'production_purchase')
->select(
'transactions.id',
'transaction_date',
'ref_no',
'bl.name as location_name',
DB::raw('IF(p.type="variable", CONCAT(p.name, " - ", pv.name, " - ", v.name, " (", v.sub_sku, ")"), CONCAT(p.name, " (", v.sub_sku, ")")) as product_name'),
'pl.quantity',
'final_total',
'su.short_name as sub_unit_name',
'su.base_unit_multiplier',
'u.short_name as unit_name',
'mfg_is_final',
'status'
)
->groupBy('transactions.id');
if ($request->filled('start_date') && $request->filled('end_date')) {
$query->whereDate('transactions.transaction_date', '>=', $request->input('start_date'))
->whereDate('transactions.transaction_date', '<=', $request->input('end_date'));
}
if ($request->filled('location_id')) {
$query->where('transactions.location_id', $request->input('location_id'));
}
if ($request->boolean('is_final')) {
$query->where('transactions.mfg_is_final', 1);
}
return $this->paginated($query->orderByDesc('transactions.transaction_date')->paginate($request->input('per_page', 25)));
}
public function showProduction(Request $request, $id)
{
$business_id = $this->ensureManufacturing();
$production_purchase = Transaction::where('business_id', $business_id)
->where('type', 'production_purchase')
->with(['purchase_lines', 'purchase_lines.variations', 'purchase_lines.variations.product', 'purchase_lines.sub_unit', 'location'])
->findOrFail($id);
$production_sell = Transaction::where('business_id', $business_id)
->where('type', 'production_sell')
->where('mfg_parent_production_purchase_id', $production_purchase->id)
->with(['sell_lines', 'sell_lines.variations', 'sell_lines.variations.product', 'sell_lines.sub_unit'])
->first();
$purchase_line = $production_purchase->purchase_lines->first();
$ingredients = [];
if ($production_sell) {
foreach ($production_sell->sell_lines as $sell_line) {
$variation = $sell_line->variations;
$sell_line_qty = empty($sell_line->sub_unit) ? $sell_line->quantity : $sell_line->quantity / $sell_line->sub_unit->base_unit_multiplier;
$ingredients[] = [
'sell_line_id' => $sell_line->id,
'full_name' => $variation->full_name ?? '',
'quantity' => $sell_line_qty,
'unit' => empty($sell_line->sub_unit) ? $variation->product->unit->short_name : $sell_line->sub_unit->short_name,
'waste_percent' => $sell_line->mfg_waste_percent ?? 0,
];
}
}
return $this->success([
'production' => $production_purchase,
'purchase_line' => $purchase_line,
'ingredients' => $ingredients,
]);
}
public function editProduction(Request $request, $id)
{
$business_id = $this->ensureManufacturing();
$production_purchase = Transaction::where('business_id', $business_id)
->where('type', 'production_purchase')
->with(['purchase_lines', 'purchase_lines.variations', 'purchase_lines.variations.product', 'purchase_lines.sub_unit', 'location'])
->findOrFail($id);
if ($production_purchase->mfg_is_final == 1) {
return $this->error(__('messages.something_went_wrong'), 403);
}
$production_sell = Transaction::where('business_id', $business_id)
->where('type', 'production_sell')
->where('mfg_parent_production_purchase_id', $production_purchase->id)
->with(['sell_lines', 'sell_lines.variations', 'sell_lines.variations.product', 'sell_lines.sub_unit'])
->first();
$purchase_line = $production_purchase->purchase_lines->first();
$base_unit_multiplier = ! empty($purchase_line->sub_unit) ? $purchase_line->sub_unit->base_unit_multiplier : 1;
$quantity = $purchase_line->quantity / $base_unit_multiplier;
if (! empty($production_purchase->mfg_wasted_units)) {
$quantity += $production_purchase->mfg_wasted_units;
}
$ingredients = [];
if ($production_sell) {
foreach ($production_sell->sell_lines as $sell_line) {
$variation = $sell_line->variations;
$multiplier = 1;
if (! empty($sell_line->sub_unit)) {
$multiplier = $sell_line->sub_unit->base_unit_multiplier ?: 1;
}
$ingredients[] = [
'id' => $sell_line->id,
'variation_id' => $variation->id,
'full_name' => $variation->full_name,
'quantity' => $sell_line->quantity / $multiplier,
'waste_percent' => $sell_line->mfg_waste_percent ?? 0,
'sub_unit_id' => $sell_line->sub_unit_id,
'unit' => empty($sell_line->sub_unit) ? $variation->product->unit->short_name : $sell_line->sub_unit->short_name,
];
}
}
return $this->success([
'production' => $production_purchase,
'purchase_line' => $purchase_line,
'ingredients' => $ingredients,
'form' => [
'ref_no' => $production_purchase->ref_no,
'transaction_date' => $production_purchase->transaction_date,
'location_id' => $production_purchase->location_id,
'variation_id' => $purchase_line->variation_id,
'quantity' => $quantity,
'mfg_wasted_units' => $production_purchase->mfg_wasted_units ?? 0,
'production_cost' => $production_purchase->mfg_production_cost ?? 0,
'mfg_production_cost_type' => $production_purchase->mfg_production_cost_type ?? 'fixed',
'sub_unit_id' => $purchase_line->sub_unit_id,
'final_total' => $production_purchase->final_total,
],
]);
}
public function updateProduction(Request $request, $id)
{
$business_id = $this->ensureManufacturing();
$request->validate([
'transaction_date' => 'required',
'location_id' => 'required',
'final_total' => 'required',
]);
try {
$transaction = Transaction::where('business_id', $business_id)
->where('type', 'production_purchase')
->findOrFail($id);
if ($transaction->mfg_is_final == 1) {
return $this->error(__('messages.something_went_wrong'), 403);
}
$manufacturing_settings = $this->mfgUtil->getSettings($business_id);
$transaction_data = $request->only(['ref_no', 'transaction_date', 'location_id', 'final_total']);
$is_final = $request->boolean('finalize') ? 1 : 0;
$transaction_data['status'] = $is_final ? 'received' : 'pending';
$transaction_data['payment_status'] = 'due';
$transaction_data['transaction_date'] = $this->productUtil->uf_date($transaction_data['transaction_date'], true);
$transaction_data['final_total'] = $this->productUtil->num_uf($transaction_data['final_total']);
$variation_id = $request->input('variation_id');
$variation = Variation::with(['product'])->findOrFail($variation_id);
$quantity = $request->input('quantity');
$waste_units = $this->productUtil->num_uf($request->input('mfg_wasted_units', 0));
$uf_qty = $this->productUtil->num_uf($quantity);
if (! empty($waste_units)) {
$uf_qty -= $waste_units;
$quantity = $this->productUtil->num_f($uf_qty);
}
$unit_purchase_line_total_f = $this->productUtil->num_f($this->productUtil->num_uf($transaction_data['final_total']) / max($uf_qty, 1));
$transaction_data['mfg_wasted_units'] = $waste_units;
$transaction_data['mfg_production_cost'] = $this->productUtil->num_uf($request->input('production_cost', 0));
$transaction_data['mfg_production_cost_type'] = $request->input('mfg_production_cost_type', 'fixed');
$transaction_data['mfg_is_final'] = $is_final;
$purchase_line_data = [
'variation_id' => $variation_id,
'quantity' => $quantity,
'product_id' => $variation->product_id,
'product_unit_id' => $variation->product->unit_id,
'pp_without_discount' => $unit_purchase_line_total_f,
'discount_percent' => 0,
'purchase_price' => $unit_purchase_line_total_f,
'purchase_price_inc_tax' => $unit_purchase_line_total_f,
'item_tax' => 0,
'purchase_line_tax_id' => null,
'mfg_date' => $this->transactionUtil->format_date($transaction_data['transaction_date']),
];
if ($request->filled('sub_unit_id')) {
$purchase_line_data['sub_unit_id'] = $request->input('sub_unit_id');
}
DB::beginTransaction();
$transaction->update($transaction_data);
$currency_details = $this->transactionUtil->purchaseCurrencyDetails($business_id);
$update_product_price = ! empty($manufacturing_settings['enable_updating_product_price']) && $is_final;
$this->productUtil->createOrUpdatePurchaseLines($transaction, [$purchase_line_data], $currency_details, $update_product_price);
$this->productUtil->adjustStockOverSelling($transaction);
$production_sell = Transaction::where('business_id', $business_id)
->where('type', 'production_sell')
->with('sell_lines', 'sell_lines.variations', 'sell_lines.product')
->where('mfg_parent_production_purchase_id', $transaction->id)
->firstOrFail();
$production_sell->update([
'transaction_date' => $transaction->transaction_date,
'status' => $is_final ? 'final' : 'draft',
'payment_status' => 'due',
'final_total' => $transaction->final_total,
]);
$ingredient_quantities = $request->input('ingredients', []);
$sell_lines = [];
foreach ($production_sell->sell_lines as $sell_line) {
$v = $sell_line->variations;
$line_key = $sell_line->id;
$line_sub_unit_id = $ingredient_quantities[$line_key]['sub_unit_id'] ?? null;
$line_multiplier = 1;
if (! empty($line_sub_unit_id)) {
$sub_units = $this->productUtil->getSubUnits($business_id, $sell_line->product->unit_id);
$line_multiplier = $sub_units[$line_sub_unit_id]['multiplier'] ?? 1;
}
$sell_lines[] = [
'product_id' => $v->product_id,
'variation_id' => $v->id,
'quantity' => $this->productUtil->num_uf($ingredient_quantities[$line_key]['quantity'] ?? 0),
'item_tax' => 0,
'tax_id' => null,
'unit_price' => $v->dpp_inc_tax * $line_multiplier,
'unit_price_inc_tax' => $v->dpp_inc_tax * $line_multiplier,
'enable_stock' => $sell_line->product->enable_stock,
'product_unit_id' => $v->product->unit_id,
'sub_unit_id' => $line_sub_unit_id,
'base_unit_multiplier' => $line_multiplier,
'mfg_waste_percent' => $this->productUtil->num_uf($ingredient_quantities[$line_key]['mfg_waste_percent'] ?? 0),
'mfg_ingredient_group_id' => $ingredient_quantities[$line_key]['mfg_ingredient_group_id'] ?? null,
];
}
if (! empty($sell_lines)) {
$this->transactionUtil->createOrUpdateSellLines(
$production_sell,
$sell_lines,
$transaction->location_id,
false,
'draft',
['mfg_waste_percent' => 'mfg_waste_percent', 'mfg_ingredient_group_id' => 'mfg_ingredient_group_id']
);
}
if ($is_final) {
foreach ($sell_lines as $sell_line) {
if ($sell_line['enable_stock']) {
$line_qty = $sell_line['quantity'] * $sell_line['base_unit_multiplier'];
$this->productUtil->decreaseProductQuantity(
$sell_line['product_id'],
$sell_line['variation_id'],
$production_sell->location_id,
$line_qty
);
}
}
$business_details = $this->businessUtil->getDetails($business_id);
$pos_settings = empty($business_details->pos_settings)
? $this->businessUtil->defaultPosSettings()
: json_decode($business_details->pos_settings, true);
$business = [
'id' => $business_id,
'accounting_method' => request()->input('accounting_method', 'fifo'),
'location_id' => $production_sell->location_id,
'pos_settings' => $pos_settings,
];
$this->transactionUtil->mapPurchaseSell($business, $production_sell->sell_lines, 'production_purchase');
}
DB::commit();
return $this->success(['id' => $transaction->id], __('lang_v1.updated_success'));
} catch (\Exception $e) {
DB::rollBack();
\Log::emergency('File:'.$e->getFile().'Line:'.$e->getLine().'Message:'.$e->getMessage());
return $this->error($e->getMessage() ?: __('messages.something_went_wrong'), 500);
}
}
public function manufacturingReport(Request $request)
{
$business_id = $this->ensureManufacturing();
$start_date = $request->input('start_date');
$end_date = $request->input('end_date');
$location_id = $request->input('location_id');
$production_totals = $this->mfgUtil->getProductionTotals($business_id, $location_id, $start_date, $end_date);
$total_sold = $this->mfgUtil->getTotalSold($business_id, $location_id, $start_date, $end_date);
return $this->success([
'total_production' => $production_totals['total_production'] ?? 0,
'total_production_cost' => $production_totals['total_production_cost'] ?? 0,
'total_sold' => $total_sold,
]);
}
public function updateRecipePrices(Request $request)
{
$business_id = $this->ensureManufacturing();
$request->validate([
'recipe_ids' => 'required|array|min:1',
'unit_prices' => 'required|array',
]);
try {
$recipe_ids = $request->input('recipe_ids');
$unit_prices = $request->input('unit_prices');
$recipes = MfgRecipe::with(['variation', 'sub_unit', 'variation.product', 'variation.product.product_tax'])
->whereIn('id', $recipe_ids)
->get();
DB::beginTransaction();
foreach ($recipes as $recipe) {
$variation = $recipe->variation;
$unit_price = $this->moduleUtil->num_uf($unit_prices[$recipe->id] ?? 0);
if (! empty($recipe->sub_unit->base_unit_multiplier)) {
$unit_price = $unit_price / $recipe->sub_unit->base_unit_multiplier;
}
$unit_price_exc_tax = $unit_price;
if (! empty($variation->product->product_tax)) {
$tax_percent = $variation->product->product_tax->amount;
$unit_price_exc_tax = $this->transactionUtil->calc_percentage_base($unit_price, $tax_percent);
}
$variation->default_purchase_price = $unit_price_exc_tax;
$variation->dpp_inc_tax = $unit_price;
$profit_margin = $this->transactionUtil->get_percent($unit_price, $variation->sell_price_inc_tax);
$sell_price_excluding_tax = $this->transactionUtil->calc_percentage($unit_price_exc_tax, $profit_margin, $unit_price_exc_tax);
$variation->default_sell_price = $sell_price_excluding_tax;
$variation->profit_percent = $profit_margin;
$variation->save();
}
DB::commit();
return $this->success(null, __('lang_v1.updated_succesfully'));
} catch (\Exception $e) {
DB::rollBack();
return $this->error(__('messages.something_went_wrong'), 500);
}
}
public function storeProduction(Request $request)
{
$business_id = $this->ensureManufacturing();
$request->validate([
'transaction_date' => 'required',
'location_id' => 'required|integer',
'variation_id' => 'required|integer',
'quantity' => 'required',
'final_total' => 'required',
'ingredients' => 'required|array',
]);
try {
$manufacturing_settings = $this->mfgUtil->getSettings($business_id);
$user_id = Auth::id();
$transaction_data = $request->only(['ref_no', 'transaction_date', 'location_id', 'final_total']);
$is_final = $request->boolean('finalize') ? 1 : 0;
$transaction_data['business_id'] = $business_id;
$transaction_data['created_by'] = $user_id;
$transaction_data['type'] = 'production_purchase';
$transaction_data['status'] = $is_final ? 'received' : 'pending';
$transaction_data['payment_status'] = 'due';
$transaction_data['transaction_date'] = $this->productUtil->uf_date($transaction_data['transaction_date'], true);
$transaction_data['final_total'] = $this->productUtil->num_uf($transaction_data['final_total']);
$ref_count = $this->productUtil->setAndGetReferenceCount($transaction_data['type']);
if (empty($transaction_data['ref_no'])) {
$prefix = $manufacturing_settings['ref_no_prefix'] ?? null;
$transaction_data['ref_no'] = $this->productUtil->generateReferenceNumber($transaction_data['type'], $ref_count, null, $prefix);
}
$variation_id = $request->input('variation_id');
$variation = Variation::with(['product'])->findOrFail($variation_id);
$quantity = $request->input('quantity');
$waste_units = $this->productUtil->num_uf($request->input('mfg_wasted_units', 0));
$uf_qty = $this->productUtil->num_uf($quantity);
if (! empty($waste_units)) {
$uf_qty -= $waste_units;
$quantity = $this->productUtil->num_f($uf_qty);
}
$unit_purchase_line_total = $this->productUtil->num_uf($transaction_data['final_total']) / max($uf_qty, 1);
$unit_purchase_line_total_f = $this->productUtil->num_f($unit_purchase_line_total);
$transaction_data['mfg_wasted_units'] = $waste_units;
$transaction_data['mfg_production_cost'] = $this->productUtil->num_uf($request->input('production_cost', 0));
$transaction_data['mfg_production_cost_type'] = $request->input('mfg_production_cost_type', 'fixed');
$transaction_data['mfg_is_final'] = $is_final;
$purchase_line_data = [
'variation_id' => $variation_id,
'quantity' => $quantity,
'product_id' => $variation->product_id,
'product_unit_id' => $variation->product->unit_id,
'pp_without_discount' => $unit_purchase_line_total_f,
'discount_percent' => 0,
'purchase_price' => $unit_purchase_line_total_f,
'purchase_price_inc_tax' => $unit_purchase_line_total_f,
'item_tax' => 0,
'purchase_line_tax_id' => null,
'mfg_date' => $this->transactionUtil->format_date($transaction_data['transaction_date']),
];
if ($request->filled('sub_unit_id')) {
$purchase_line_data['sub_unit_id'] = $request->input('sub_unit_id');
}
if ($request->filled('lot_number')) {
$purchase_line_data['lot_number'] = $request->input('lot_number');
}
if ($request->filled('exp_date')) {
$purchase_line_data['exp_date'] = $request->input('exp_date');
}
DB::beginTransaction();
$transaction = Transaction::create($transaction_data);
$currency_details = $this->transactionUtil->purchaseCurrencyDetails($business_id);
$update_product_price = ! empty($manufacturing_settings['enable_updating_product_price']) && $is_final;
$this->productUtil->createOrUpdatePurchaseLines($transaction, [$purchase_line_data], $currency_details, $update_product_price);
$this->productUtil->adjustStockOverSelling($transaction);
$transaction_sell_data = [
'business_id' => $business_id,
'location_id' => $transaction->location_id,
'transaction_date' => $transaction->transaction_date,
'created_by' => $transaction->created_by,
'status' => $is_final ? 'final' : 'draft',
'type' => 'production_sell',
'mfg_parent_production_purchase_id' => $transaction->id,
'payment_status' => 'due',
'final_total' => $transaction->final_total,
];
$sell_lines = [];
$ingredient_quantities = $request->input('ingredients', []);
$recipe = MfgRecipe::where('variation_id', $variation_id)->first();
$all_variation_details = $this->mfgUtil->getIngredientDetails($recipe, $business_id);
foreach ($all_variation_details as $variation_details) {
$v = $variation_details['variation'];
$line_key = $variation_details['id'];
$line_sub_unit_id = $ingredient_quantities[$line_key]['sub_unit_id'] ?? null;
$line_multiplier = ! empty($line_sub_unit_id) ? ($variation_details['sub_units'][$line_sub_unit_id]['multiplier'] ?? 1) : 1;
$mfg_waste_percent = $this->productUtil->num_uf($ingredient_quantities[$line_key]['mfg_waste_percent'] ?? 0);
$sell_lines[] = [
'product_id' => $v->product_id,
'variation_id' => $v->id,
'quantity' => $this->productUtil->num_uf($ingredient_quantities[$line_key]['quantity'] ?? 0),
'item_tax' => 0,
'tax_id' => null,
'unit_price' => $v->dpp_inc_tax * $line_multiplier,
'unit_price_inc_tax' => $v->dpp_inc_tax * $line_multiplier,
'enable_stock' => $variation_details['enable_stock'],
'product_unit_id' => $v->product->unit_id,
'sub_unit_id' => $line_sub_unit_id,
'base_unit_multiplier' => $line_multiplier,
'mfg_waste_percent' => $mfg_waste_percent,
'mfg_ingredient_group_id' => $ingredient_quantities[$line_key]['mfg_ingredient_group_id'] ?? null,
];
}
$production_sell = Transaction::create($transaction_sell_data);
if (! empty($sell_lines)) {
$this->transactionUtil->createOrUpdateSellLines(
$production_sell,
$sell_lines,
$transaction_sell_data['location_id'],
null,
null,
['mfg_waste_percent' => 'mfg_waste_percent', 'mfg_ingredient_group_id' => 'mfg_ingredient_group_id']
);
}
if ($production_sell->status == 'final') {
foreach ($sell_lines as $sell_line) {
if ($sell_line['enable_stock']) {
$line_qty = $sell_line['quantity'] * $sell_line['base_unit_multiplier'];
$this->productUtil->decreaseProductQuantity(
$sell_line['product_id'],
$sell_line['variation_id'],
$production_sell->location_id,
$line_qty
);
}
}
$business_details = $this->businessUtil->getDetails($business_id);
$pos_settings = empty($business_details->pos_settings)
? $this->businessUtil->defaultPosSettings()
: json_decode($business_details->pos_settings, true);
$business = [
'id' => $business_id,
'accounting_method' => request()->input('accounting_method', 'fifo'),
'location_id' => $production_sell->location_id,
'pos_settings' => $pos_settings,
];
$this->transactionUtil->mapPurchaseSell($business, $production_sell->sell_lines, 'production_purchase');
}
DB::commit();
return $this->success(['id' => $transaction->id], __('lang_v1.added_success'));
} catch (\Exception $e) {
DB::rollBack();
\Log::emergency('File:'.$e->getFile().'Line:'.$e->getLine().'Message:'.$e->getMessage());
return $this->error($e->getMessage() ?: __('messages.something_went_wrong'), 500);
}
}
public function destroyProduction(Request $request, $id)
{
$business_id = $this->ensureManufacturing();
try {
$production = Transaction::where('business_id', $business_id)
->where('type', 'production_purchase')
->where('mfg_is_final', 0)
->findOrFail($id);
DB::beginTransaction();
Transaction::where('business_id', $business_id)
->where('type', 'production_sell')
->where('mfg_parent_production_purchase_id', $production->id)
->delete();
$production->delete();
DB::commit();
return $this->success(null, __('lang_v1.deleted_success'));
} catch (\Exception $e) {
DB::rollBack();
return $this->error(__('messages.something_went_wrong'), 500);
}
}
public function settings(Request $request)
{
$business_id = $this->ensureManufacturing();
return $this->success($this->mfgUtil->getSettings($business_id));
}
public function updateSettings(Request $request)
{
$business_id = $this->ensureManufacturing();
try {
$settings = $request->only(['ref_no_prefix']);
$settings['disable_editing_ingredient_qty'] = $request->boolean('disable_editing_ingredient_qty');
$settings['enable_updating_product_price'] = $request->boolean('enable_updating_product_price');
Business::where('id', $business_id)->update(['manufacturing_settings' => json_encode($settings)]);
return $this->success($settings, __('lang_v1.updated_success'));
} catch (\Exception $e) {
return $this->error(__('messages.something_went_wrong'), 500);
}
}
public function resources(Request $request)
{
$business_id = $this->ensureManufacturing();
return $this->success([
'locations' => BusinessLocation::forDropdown($business_id, true),
'recipe_dropdown' => MfgRecipe::forDropdown($business_id),
]);
}
}