テスト 結果 確定取消 API ✅ is_confirmed: false, confirmed_at: null USE トランザクション削除 ✅ current_stock が 27.5→32 に復帰 引当再作成 ✅ reserved_stock = 5.000 に復帰 追加した変更: stock_service.py:81-93 — unconfirm_spreading(): USE削除→確定フラグリセット→引当再作成 fertilizer/views.py — unconfirm アクション(POST /api/fertilizer/plans/{id}/unconfirm/) fertilizer/page.tsx — 一覧に「確定取消」ボタン(確定済み計画のみ表示) FertilizerEditPage.tsx — 編集画面ヘッダーに「確定取消」ボタン + 在庫情報再取得
221 lines
5.8 KiB
Python
221 lines
5.8 KiB
Python
from decimal import Decimal
|
|
|
|
from django.core.validators import MinValueValidator
|
|
from django.db import models
|
|
|
|
|
|
class Material(models.Model):
|
|
"""共通資材マスタ"""
|
|
|
|
class MaterialType(models.TextChoices):
|
|
FERTILIZER = 'fertilizer', '肥料'
|
|
PESTICIDE = 'pesticide', '農薬'
|
|
SEEDLING = 'seedling', '種苗'
|
|
OTHER = 'other', 'その他'
|
|
|
|
class StockUnit(models.TextChoices):
|
|
BAG = 'bag', '袋'
|
|
BOTTLE = 'bottle', '本'
|
|
KG = 'kg', 'kg'
|
|
LITER = 'liter', 'L'
|
|
PIECE = 'piece', '個'
|
|
|
|
name = models.CharField(max_length=100, verbose_name='資材名')
|
|
material_type = models.CharField(
|
|
max_length=20,
|
|
choices=MaterialType.choices,
|
|
verbose_name='資材種別',
|
|
)
|
|
maker = models.CharField(
|
|
max_length=100,
|
|
blank=True,
|
|
default='',
|
|
verbose_name='メーカー',
|
|
)
|
|
stock_unit = models.CharField(
|
|
max_length=20,
|
|
choices=StockUnit.choices,
|
|
default=StockUnit.BAG,
|
|
verbose_name='在庫単位',
|
|
)
|
|
is_active = models.BooleanField(default=True, verbose_name='使用中')
|
|
notes = models.TextField(blank=True, default='', verbose_name='備考')
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
class Meta:
|
|
ordering = ['material_type', 'name']
|
|
constraints = [
|
|
models.UniqueConstraint(
|
|
fields=['material_type', 'name'],
|
|
name='uniq_material_type_name',
|
|
),
|
|
]
|
|
verbose_name = '資材'
|
|
verbose_name_plural = '資材'
|
|
|
|
def __str__(self):
|
|
return f'{self.get_material_type_display()}: {self.name}'
|
|
|
|
|
|
class FertilizerProfile(models.Model):
|
|
"""肥料専用属性"""
|
|
|
|
material = models.OneToOneField(
|
|
Material,
|
|
on_delete=models.CASCADE,
|
|
related_name='fertilizer_profile',
|
|
)
|
|
capacity_kg = models.DecimalField(
|
|
max_digits=8,
|
|
decimal_places=3,
|
|
blank=True,
|
|
null=True,
|
|
verbose_name='1袋重量(kg)',
|
|
)
|
|
nitrogen_pct = models.DecimalField(
|
|
max_digits=5,
|
|
decimal_places=2,
|
|
blank=True,
|
|
null=True,
|
|
verbose_name='窒素(%)',
|
|
)
|
|
phosphorus_pct = models.DecimalField(
|
|
max_digits=5,
|
|
decimal_places=2,
|
|
blank=True,
|
|
null=True,
|
|
verbose_name='リン酸(%)',
|
|
)
|
|
potassium_pct = models.DecimalField(
|
|
max_digits=5,
|
|
decimal_places=2,
|
|
blank=True,
|
|
null=True,
|
|
verbose_name='カリ(%)',
|
|
)
|
|
|
|
class Meta:
|
|
verbose_name = '肥料詳細'
|
|
verbose_name_plural = '肥料詳細'
|
|
|
|
def __str__(self):
|
|
return f'肥料詳細: {self.material.name}'
|
|
|
|
|
|
class PesticideProfile(models.Model):
|
|
"""農薬専用属性"""
|
|
|
|
material = models.OneToOneField(
|
|
Material,
|
|
on_delete=models.CASCADE,
|
|
related_name='pesticide_profile',
|
|
)
|
|
registration_no = models.CharField(
|
|
max_length=100,
|
|
blank=True,
|
|
default='',
|
|
verbose_name='農薬登録番号',
|
|
)
|
|
formulation = models.CharField(
|
|
max_length=100,
|
|
blank=True,
|
|
default='',
|
|
verbose_name='剤型',
|
|
)
|
|
usage_unit = models.CharField(
|
|
max_length=50,
|
|
blank=True,
|
|
default='',
|
|
verbose_name='使用単位',
|
|
)
|
|
dilution_ratio = models.CharField(
|
|
max_length=100,
|
|
blank=True,
|
|
default='',
|
|
verbose_name='希釈倍率',
|
|
)
|
|
active_ingredient = models.CharField(
|
|
max_length=200,
|
|
blank=True,
|
|
default='',
|
|
verbose_name='有効成分',
|
|
)
|
|
category = models.CharField(
|
|
max_length=100,
|
|
blank=True,
|
|
default='',
|
|
verbose_name='分類',
|
|
)
|
|
|
|
class Meta:
|
|
verbose_name = '農薬詳細'
|
|
verbose_name_plural = '農薬詳細'
|
|
|
|
def __str__(self):
|
|
return f'農薬詳細: {self.material.name}'
|
|
|
|
|
|
class StockTransaction(models.Model):
|
|
"""入出庫履歴"""
|
|
|
|
class TransactionType(models.TextChoices):
|
|
PURCHASE = 'purchase', '入庫'
|
|
USE = 'use', '使用'
|
|
RESERVE = 'reserve', '引当'
|
|
ADJUSTMENT_PLUS = 'adjustment_plus', '棚卸増'
|
|
ADJUSTMENT_MINUS = 'adjustment_minus', '棚卸減'
|
|
DISCARD = 'discard', '廃棄'
|
|
|
|
INCREASE_TYPES = {
|
|
TransactionType.PURCHASE,
|
|
TransactionType.ADJUSTMENT_PLUS,
|
|
}
|
|
DECREASE_TYPES = {
|
|
TransactionType.USE,
|
|
TransactionType.RESERVE,
|
|
TransactionType.ADJUSTMENT_MINUS,
|
|
TransactionType.DISCARD,
|
|
}
|
|
|
|
material = models.ForeignKey(
|
|
Material,
|
|
on_delete=models.PROTECT,
|
|
related_name='stock_transactions',
|
|
verbose_name='資材',
|
|
)
|
|
transaction_type = models.CharField(
|
|
max_length=30,
|
|
choices=TransactionType.choices,
|
|
verbose_name='取引種別',
|
|
)
|
|
quantity = models.DecimalField(
|
|
max_digits=10,
|
|
decimal_places=3,
|
|
validators=[MinValueValidator(Decimal('0.001'))],
|
|
verbose_name='数量',
|
|
)
|
|
occurred_on = models.DateField(verbose_name='発生日')
|
|
note = models.TextField(blank=True, default='', verbose_name='備考')
|
|
fertilization_plan = models.ForeignKey(
|
|
'fertilizer.FertilizationPlan',
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name='stock_reservations',
|
|
verbose_name='施肥計画',
|
|
)
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
|
|
class Meta:
|
|
ordering = ['-occurred_on', '-created_at', '-id']
|
|
verbose_name = '入出庫履歴'
|
|
verbose_name_plural = '入出庫履歴'
|
|
|
|
def __str__(self):
|
|
return (
|
|
f'{self.material.name} '
|
|
f'{self.get_transaction_type_display()} '
|
|
f'{self.quantity}'
|
|
)
|