972 lines
43 KiB
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),
|
|
]);
|
|
}
|
|
}
|