Files
keinasystem/backend/apps/materials/models.py
Akira 72b4d670fe 完璧に動作しています。
テスト	結果
確定取消 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 — 編集画面ヘッダーに「確定取消」ボタン + 在庫情報再取得
2026-03-15 13:28:02 +09:00

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}'
)