refactor: update material usage and remaining material precision to four decimal places across cutting management components for improved accuracy
This commit is contained in:
parent
9c205cf3d0
commit
175e0dc5af
@ -24,8 +24,8 @@ public function rules(): array
|
||||
'integer',
|
||||
Rule::exists('raw_material_prices', 'id')->whereNull('deleted_at'),
|
||||
],
|
||||
'material_usage' => ['required', 'numeric', 'decimal:0,2', 'gt:0'],
|
||||
'remaining_material' => ['required', 'numeric', 'decimal:0,2', 'gte:0'],
|
||||
'material_usage' => ['required', 'numeric', 'decimal:0,4', 'gt:0'],
|
||||
'remaining_material' => ['required', 'numeric', 'decimal:0,4', 'gte:0'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@ -39,8 +39,8 @@ public function rules(): array
|
||||
'distinct',
|
||||
Rule::exists('raw_material_prices', 'id')->whereNull('deleted_at'),
|
||||
];
|
||||
$rules['materials.*.material_usage'] = ['required', 'numeric', 'decimal:0,2', 'gt:0'];
|
||||
$rules['materials.*.remaining_material'] = ['required', 'numeric', 'decimal:0,2', 'gte:0'];
|
||||
$rules['materials.*.material_usage'] = ['required', 'numeric', 'decimal:0,4', 'gt:0'];
|
||||
$rules['materials.*.remaining_material'] = ['required', 'numeric', 'decimal:0,4', 'gte:0'];
|
||||
|
||||
$rules['results'] = ['required', 'array', 'min:1'];
|
||||
$rules['results.*.product_variant_id'] = [
|
||||
|
||||
@ -31,8 +31,8 @@ class CuttingMaterial extends Model
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'material_usage' => 'decimal:2',
|
||||
'remaining_material' => 'decimal:2',
|
||||
'material_usage' => 'decimal:4',
|
||||
'remaining_material' => 'decimal:4',
|
||||
];
|
||||
}
|
||||
|
||||
@ -172,7 +172,7 @@ private function formatCmQuantity(float|string|null $value): ?string
|
||||
return null;
|
||||
}
|
||||
|
||||
$formatted = rtrim(rtrim(number_format($cm, 2, ',', '.'), '0'), ',');
|
||||
$formatted = rtrim(rtrim(number_format($cm, 4, ',', '.'), '0'), ',');
|
||||
|
||||
return "{$formatted} cm";
|
||||
}
|
||||
@ -195,18 +195,18 @@ private function formatCmQuantityInput(float|string|null $value): ?string
|
||||
return null;
|
||||
}
|
||||
|
||||
return rtrim(rtrim(number_format($cm, 2, '.', ''), '0'), '.');
|
||||
return rtrim(rtrim(number_format($cm, 4, '.', ''), '0'), '.');
|
||||
}
|
||||
|
||||
private function formatQuantity(float|string|null $value): string
|
||||
{
|
||||
$formatted = rtrim(rtrim(number_format((float) $value, 2, ',', '.'), '0'), ',');
|
||||
$formatted = rtrim(rtrim(number_format((float) $value, 4, ',', '.'), '0'), ',');
|
||||
|
||||
return "{$formatted} {$this->unitAbbreviation}";
|
||||
}
|
||||
|
||||
private function formatQuantityInput(float|string|null $value): string
|
||||
{
|
||||
return rtrim(rtrim(number_format((float) $value, 2, '.', ''), '0'), '.');
|
||||
return rtrim(rtrim(number_format((float) $value, 4, '.', ''), '0'), '.');
|
||||
}
|
||||
}
|
||||
|
||||
@ -284,12 +284,12 @@ public function syncDraftMaterial(array $validated, User $user): array
|
||||
->findOrFail($validated['raw_material_price_id']);
|
||||
|
||||
$unit = $price->rawMaterial?->unit;
|
||||
$usageInput = round((float) $validated['material_usage'], 2);
|
||||
$remainingInput = round((float) $validated['remaining_material'], 2);
|
||||
$usageInput = round((float) $validated['material_usage'], 4);
|
||||
$remainingInput = round((float) $validated['remaining_material'], 4);
|
||||
|
||||
if ($unit !== null && $unit->usesLengthUnit()) {
|
||||
$materialUsage = round($unit->fromCm($usageInput), 2);
|
||||
$remainingMaterial = round($unit->fromCm($remainingInput), 2);
|
||||
$materialUsage = round($unit->fromCm($usageInput), 4);
|
||||
$remainingMaterial = round($unit->fromCm($remainingInput), 4);
|
||||
} else {
|
||||
$materialUsage = $usageInput;
|
||||
$remainingMaterial = $remainingInput;
|
||||
@ -655,12 +655,12 @@ private function buildMaterials(array $materials): array
|
||||
}
|
||||
|
||||
$unit = $price->rawMaterial?->unit;
|
||||
$usageInput = round((float) $itemData['material_usage'], 2);
|
||||
$remainingInput = round((float) $itemData['remaining_material'], 2);
|
||||
$usageInput = round((float) $itemData['material_usage'], 4);
|
||||
$remainingInput = round((float) $itemData['remaining_material'], 4);
|
||||
|
||||
if ($unit !== null && $unit->usesLengthUnit()) {
|
||||
$materialUsage = round($unit->fromCm($usageInput), 2);
|
||||
$remainingMaterial = round($unit->fromCm($remainingInput), 2);
|
||||
$materialUsage = round($unit->fromCm($usageInput), 4);
|
||||
$remainingMaterial = round($unit->fromCm($remainingInput), 4);
|
||||
} else {
|
||||
$materialUsage = $usageInput;
|
||||
$remainingMaterial = $remainingInput;
|
||||
@ -898,10 +898,10 @@ private function presentDraftMaterial(CuttingMaterial $item): array
|
||||
|
||||
// Convert stored native unit back to cm for display
|
||||
if ($usesLengthUnit && $unit !== null) {
|
||||
$materialUsageCm = round($unit->toCm((float) $item->material_usage), 2);
|
||||
$remainingMaterialCm = round($unit->toCm((float) $item->remaining_material), 2);
|
||||
$materialUsageDisplay = (string) $materialUsageCm;
|
||||
$remainingMaterialDisplay = (string) $remainingMaterialCm;
|
||||
$materialUsageCm = round($unit->toCm((float) $item->material_usage), 4);
|
||||
$remainingMaterialCm = round($unit->toCm((float) $item->remaining_material), 4);
|
||||
$materialUsageDisplay = rtrim(rtrim(number_format($materialUsageCm, 4, '.', ''), '0'), '.');
|
||||
$remainingMaterialDisplay = rtrim(rtrim(number_format($remainingMaterialCm, 4, '.', ''), '0'), '.');
|
||||
} else {
|
||||
$materialUsageDisplay = $this->formatQuantityInput((float) $item->material_usage);
|
||||
$remainingMaterialDisplay = $this->formatQuantityInput((float) $item->remaining_material);
|
||||
@ -942,7 +942,7 @@ private function presentDraftResult(CuttingResult $item): array
|
||||
|
||||
private function formatQuantityInput(float $value): string
|
||||
{
|
||||
return rtrim(rtrim(number_format($value, 2, '.', ''), '0'), '.');
|
||||
return rtrim(rtrim(number_format($value, 4, '.', ''), '0'), '.');
|
||||
}
|
||||
|
||||
private function appendCostPreview(Cutting $cutting): void
|
||||
|
||||
@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('cutting_materials', function (Blueprint $table): void {
|
||||
$table->decimal('material_usage', 18, 4)->change();
|
||||
$table->decimal('remaining_material', 18, 4)->change();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('cutting_materials', function (Blueprint $table): void {
|
||||
$table->decimal('material_usage', 18, 2)->change();
|
||||
$table->decimal('remaining_material', 18, 2)->change();
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -36,7 +36,7 @@ return '';
|
||||
let decimalPart = parts[1];
|
||||
|
||||
if (decimalPart !== undefined) {
|
||||
decimalPart = decimalPart.slice(0, 2);
|
||||
decimalPart = decimalPart.slice(0, 4);
|
||||
|
||||
return `${integerPart},${decimalPart}`;
|
||||
}
|
||||
@ -68,7 +68,7 @@ function parseDecimal(value: string): string {
|
||||
const cleanParts = clean.split('.');
|
||||
|
||||
if (cleanParts[1] !== undefined) {
|
||||
clean = `${cleanParts[0]}.${cleanParts[1].slice(0, 2)}`;
|
||||
clean = `${cleanParts[0]}.${cleanParts[1].slice(0, 4)}`;
|
||||
}
|
||||
|
||||
return clean;
|
||||
|
||||
@ -3,11 +3,8 @@ import { useForm } from '@inertiajs/vue3';
|
||||
import { Minus, Plus, Save, Scissors, Search, Trash2 } from '@lucide/vue';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { toast } from 'vue-sonner';
|
||||
import PosCatalogCard from '../../shared/PosCatalogCard.vue';
|
||||
import PosCatalogVariantThumb from '../../shared/PosCatalogVariantThumb.vue';
|
||||
import { DecimalInput } from '@/components/form/decimal-input';
|
||||
import { NumberInput } from '@/components/form/number-input';
|
||||
import { RupiahInput } from '@/components/form/rupiah-input';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
@ -45,6 +42,8 @@ import type {
|
||||
CuttingRawMaterialCatalogItem,
|
||||
CuttingResultCartItem,
|
||||
} from '@/types/cutting';
|
||||
import PosCatalogCard from '../../shared/PosCatalogCard.vue';
|
||||
import PosCatalogVariantThumb from '../../shared/PosCatalogVariantThumb.vue';
|
||||
|
||||
function usesLengthUnit(unit: string): boolean {
|
||||
return unit === 'yard' || unit === 'meter';
|
||||
@ -167,7 +166,7 @@ const estimatedCostPerUnit = computed(() => {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Math.round(totalProductionCost.value / totalResultPieces.value);
|
||||
return Math.round(totalMaterialCost.value / totalResultPieces.value);
|
||||
});
|
||||
|
||||
function populateForm() {
|
||||
@ -314,7 +313,8 @@ async function decreaseMaterialQty(priceId: number) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextQty = (Number(item.material_usage) || 0) - 1;
|
||||
const decrement = item.uses_length_unit ? 100 : 1;
|
||||
const nextQty = (Number(item.material_usage) || 0) - decrement;
|
||||
|
||||
if (nextQty <= 0) {
|
||||
const index = materialCart.value.findIndex(
|
||||
@ -355,8 +355,9 @@ async function addMaterial(
|
||||
);
|
||||
|
||||
const defaultUsage = usesLengthUnit(rawMaterial.unit) ? '100' : '1';
|
||||
const increment = usesLengthUnit(rawMaterial.unit) ? 100 : 1;
|
||||
const nextUsage = existing
|
||||
? String((Number(existing.material_usage) || 0) + 1)
|
||||
? String((Number(existing.material_usage) || 0) + increment)
|
||||
: defaultUsage;
|
||||
const remaining = existing?.remaining_material ?? '0';
|
||||
|
||||
@ -1002,7 +1003,7 @@ function submit() {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<Field>
|
||||
<FieldLabel class="text-xs">
|
||||
Pemakaian ({{ item.uses_length_unit ? 'cm' : item.unit_abbreviation }})
|
||||
@ -1035,16 +1036,6 @@ function submit() {
|
||||
"
|
||||
/>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel class="text-xs">
|
||||
Harga Modal
|
||||
</FieldLabel>
|
||||
<div
|
||||
class="flex h-8 items-center rounded-md border bg-muted/40 px-2 text-xs font-medium tabular-nums"
|
||||
>
|
||||
{{ formatRupiah(materialLineCost(item)) }}
|
||||
</div>
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -1151,41 +1142,6 @@ function submit() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div class="grid gap-3 sm:grid-cols-2">
|
||||
<Field>
|
||||
<FieldLabel for="sewing_cost">Jasa Jahit</FieldLabel>
|
||||
<RupiahInput
|
||||
id="sewing_cost"
|
||||
v-model="form.sewing_cost"
|
||||
/>
|
||||
<FieldError
|
||||
:errors="formErrors(form, 'sewing_cost')"
|
||||
/>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel for="other_cost">Biaya Lainnya</FieldLabel>
|
||||
<RupiahInput
|
||||
id="other_cost"
|
||||
v-model="form.other_cost"
|
||||
/>
|
||||
<FieldError
|
||||
:errors="formErrors(form, 'other_cost')"
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
class="w-full"
|
||||
:disabled="materialCart.length === 0"
|
||||
@click="costModalOpen = true"
|
||||
>
|
||||
Lihat Harga Modal
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
class="w-full"
|
||||
@ -1238,17 +1194,9 @@ function submit() {
|
||||
{{ formatRupiah(materialLineCost(item)) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-3 border-t pt-2">
|
||||
<span class="text-muted-foreground">Jasa Jahit</span>
|
||||
<span class="font-medium tabular-nums">{{ formatRupiah(sewingCostAmount) }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<span class="text-muted-foreground">Biaya Lainnya</span>
|
||||
<span class="font-medium tabular-nums">{{ formatRupiah(otherCostAmount) }}</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-3 border-t pt-2 font-semibold">
|
||||
<span>Total Harga Modal</span>
|
||||
<span class="tabular-nums">{{ formatRupiah(totalProductionCost) }}</span>
|
||||
<span class="tabular-nums">{{ formatRupiah(totalMaterialCost) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1345,7 +1293,7 @@ function submit() {
|
||||
<div class="border-t pt-3">
|
||||
<div class="flex items-center justify-between text-sm font-semibold">
|
||||
<span>Total Harga Modal</span>
|
||||
<span class="text-primary">{{ formatRupiah(totalProductionCost) }}</span>
|
||||
<span class="text-primary">{{ formatRupiah(totalMaterialCost) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
|
||||
@ -15,6 +15,37 @@ import {
|
||||
EmptyHeader,
|
||||
EmptyTitle,
|
||||
} from '@/components/ui/empty';
|
||||
|
||||
const CM_PER_YARD = 91.44;
|
||||
const CM_PER_METER = 100;
|
||||
|
||||
function formatTotalMaterialUsage(totalUsage: number | null | undefined, materials: any[]): string {
|
||||
if (!totalUsage || !materials.length) {
|
||||
return '0';
|
||||
}
|
||||
|
||||
const unit = materials[0]?.raw_material_price?.raw_material?.unit;
|
||||
|
||||
if (unit === 'yard') {
|
||||
return (totalUsage * CM_PER_YARD).toFixed(4);
|
||||
}
|
||||
|
||||
if (unit === 'meter') {
|
||||
return (totalUsage * CM_PER_METER).toFixed(4);
|
||||
}
|
||||
|
||||
return totalUsage.toFixed(4);
|
||||
}
|
||||
|
||||
function getTotalMaterialUsageUnit(materials: any[]): string {
|
||||
const unit = materials[0]?.raw_material_price?.raw_material?.unit;
|
||||
|
||||
if (unit === 'yard' || unit === 'meter') {
|
||||
return 'cm';
|
||||
}
|
||||
|
||||
return materials[0]?.raw_material_price?.raw_material?.unit_abbreviation ?? '';
|
||||
}
|
||||
import type { CuttingListItem } from '@/types/cutting';
|
||||
|
||||
const props = defineProps<{
|
||||
@ -100,11 +131,7 @@ const totalCompleted = computed(() => props.cuttings.length);
|
||||
</div>
|
||||
<div class="flex items-center justify-between text-[11px]">
|
||||
<span class="text-muted-foreground">Total Pemakaian Bahan:</span>
|
||||
<span class="font-semibold tabular-nums">{{ cutting.total_material_usage ?? 0 }}</span>
|
||||
</div>
|
||||
<div v-if="cutting.estimated_cost_per_unit_formatted" class="flex items-center justify-between text-[11px]">
|
||||
<span class="text-muted-foreground">Harga Modal:</span>
|
||||
<span class="font-semibold">{{ cutting.estimated_cost_per_unit_formatted }}/pcs</span>
|
||||
<span class="font-semibold tabular-nums">{{ formatTotalMaterialUsage(cutting.total_material_usage, cutting.materials) }} {{ getTotalMaterialUsageUnit(cutting.materials) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -207,11 +207,6 @@ function getTotalMaterialUsageUnit(materials: any[]): string {
|
||||
<span>Total Pemakaian Bahan <strong class="text-primary">{{
|
||||
formatTotalMaterialUsage(cutting.total_material_usage, cutting.materials) }}
|
||||
{{ getTotalMaterialUsageUnit(cutting.materials) }}</strong></span>
|
||||
<span>Total Biaya Produksi <strong class="text-primary">{{
|
||||
cutting.total_production_cost_formatted ?? 0 }}</strong></span>
|
||||
<span>Harga Modal <strong class="text-primary">{{
|
||||
cutting.estimated_cost_per_unit_formatted ??
|
||||
0 }}</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -15,6 +15,37 @@ import {
|
||||
EmptyHeader,
|
||||
EmptyTitle,
|
||||
} from '@/components/ui/empty';
|
||||
|
||||
const CM_PER_YARD = 91.44;
|
||||
const CM_PER_METER = 100;
|
||||
|
||||
function formatTotalMaterialUsage(totalUsage: number | null | undefined, materials: any[]): string {
|
||||
if (!totalUsage || !materials.length) {
|
||||
return '0';
|
||||
}
|
||||
|
||||
const unit = materials[0]?.raw_material_price?.raw_material?.unit;
|
||||
|
||||
if (unit === 'yard') {
|
||||
return (totalUsage * CM_PER_YARD).toFixed(4);
|
||||
}
|
||||
|
||||
if (unit === 'meter') {
|
||||
return (totalUsage * CM_PER_METER).toFixed(4);
|
||||
}
|
||||
|
||||
return totalUsage.toFixed(4);
|
||||
}
|
||||
|
||||
function getTotalMaterialUsageUnit(materials: any[]): string {
|
||||
const unit = materials[0]?.raw_material_price?.raw_material?.unit;
|
||||
|
||||
if (unit === 'yard' || unit === 'meter') {
|
||||
return 'cm';
|
||||
}
|
||||
|
||||
return materials[0]?.raw_material_price?.raw_material?.unit_abbreviation ?? '';
|
||||
}
|
||||
import type { CuttingListItem } from '@/types/cutting';
|
||||
|
||||
const props = defineProps<{
|
||||
@ -100,7 +131,7 @@ const totalInProgress = computed(() => props.cuttings.length);
|
||||
</div>
|
||||
<div class="flex items-center justify-between text-[11px]">
|
||||
<span class="text-muted-foreground">Total Pemakaian Bahan:</span>
|
||||
<span class="font-semibold tabular-nums">{{ cutting.total_material_usage ?? 0 }}</span>
|
||||
<span class="font-semibold tabular-nums">{{ formatTotalMaterialUsage(cutting.total_material_usage, cutting.materials) }} {{ getTotalMaterialUsageUnit(cutting.materials) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -477,36 +477,6 @@ function actionIcon(status: string) {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="cutting.estimated_cost_per_unit_formatted"
|
||||
class="rounded-lg border bg-muted/40 p-3 text-sm space-y-1"
|
||||
>
|
||||
<p class="font-medium">Harga Modal Batch</p>
|
||||
<p class="flex justify-between gap-2 text-muted-foreground">
|
||||
<span>Bahan baku</span>
|
||||
<span>{{ cutting.total_material_cost_formatted ?? '-' }}</span>
|
||||
</p>
|
||||
<p class="flex justify-between gap-2 text-muted-foreground">
|
||||
<span>Jasa jahit</span>
|
||||
<span>{{ cutting.sewing_cost_formatted ?? '-' }}</span>
|
||||
</p>
|
||||
<p class="flex justify-between gap-2 text-muted-foreground">
|
||||
<span>Biaya lainnya</span>
|
||||
<span>{{ cutting.other_cost_formatted ?? '-' }}</span>
|
||||
</p>
|
||||
<p class="flex justify-between gap-2 font-medium">
|
||||
<span>Total</span>
|
||||
<span>{{ cutting.total_production_cost_formatted ?? '-' }}</span>
|
||||
</p>
|
||||
<p class="flex justify-between gap-2 text-muted-foreground">
|
||||
<span>Total hasil</span>
|
||||
<span>{{ cutting.total_result_pieces ?? 0 }} pcs</span>
|
||||
</p>
|
||||
<p class="font-semibold tabular-nums pt-1 border-t">
|
||||
{{ cutting.estimated_cost_per_unit_formatted }} / pcs
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<div v-for="(result, index) in verifyForm.results" :key="result.product_variant_id" class="p-3 border rounded-lg space-y-3">
|
||||
<div class="font-medium text-sm">
|
||||
|
||||
Loading…
Reference in New Issue
Block a user