Coverage for src/inheritance_calculator_core/models/inheritance.py: 95%

1"""相続計算結果モデル"""

2 from typing import List, Optional, Any

3 from fractions import Fraction

4 from enum import Enum

5 from uuid import UUID

6

7 from pydantic import BaseModel, Field, field_validator, ConfigDict

8

9 from .person import Person

10

11

12 class HeritageRank(str, Enum):

13 """相続順位"""

14 SPOUSE = "spouse" # 配偶者（常に相続人）

15 FIRST = "first" # 第1順位（子・直系卑属）

16 SECOND = "second" # 第2順位（直系尊属）

17 THIRD = "third" # 第3順位（兄弟姉妹）

18

19

20 class SubstitutionType(str, Enum):

21 """代襲相続タイプ"""

22 NONE = "none" # 代襲なし

23 CHILD = "child" # 子の代襲（孫、曾孫...）

24 SIBLING = "sibling" # 兄弟姉妹の代襲（甥・姪のみ）

25

26

27 class Heir(BaseModel):

28 """

29 相続人

30

31 相続人の資格と相続割合を表現。

32 """

33

34 model_config = ConfigDict(

35 validate_assignment=True,

36 extra='forbid',

37 )

38

39 person: Person = Field(..., description="相続人")

40 rank: HeritageRank = Field(..., description="相続順位")

41 share: Fraction = Field(..., description="相続割合（法定相続分）")

42 share_percentage: float = Field(..., description="相続割合（百分率）")

43 is_substitution: bool = Field(default=False, description="代襲相続人か")

44 substitution_type: SubstitutionType = Field(

45 default=SubstitutionType.NONE, description="代襲相続タイプ"

46 )

47 substituted_person: Optional[Person] = Field(

48 default=None, description="被代襲者（代襲相続の場合）"

49 )

50 generation: int = Field(default=1, description="代襲世代数（1=直接相続人）")

51 is_retransfer: bool = Field(default=False, description="再転相続人か")

52 retransfer_from: Optional[Person] = Field(

53 default=None, description="再転相続元の相続人（遺産分割前に死亡した相続人）"

54 )

55 original_share: Optional[Fraction] = Field(

56 default=None, description="再転相続前の元の相続分"

57 )

58

59 @field_validator('share_percentage', mode='before')

60 @classmethod

61 def calculate_percentage(cls, v: Any, info: Any) -> float:

62 """相続割合の百分率を計算"""

63 # shareから計算（vが0.0の場合も含む）

64 values = info.data

65 share = values.get('share')

66 if share is not None:

67 return float(share) * 100

68

69 # すでに計算されている場合はそのまま返す

70 if isinstance(v, float):

71 return v

72

73 return 0.0

74

75 @field_validator('substitution_type')

76 @classmethod

77 def validate_substitution_type(cls, v: SubstitutionType, info: Any) -> SubstitutionType:

78 """代襲相続タイプの検証"""

79 values = info.data

80 is_substitution = values.get('is_substitution', False)

81

82 # 代襲相続でない場合、タイプはNONEでなければならない

83 if not is_substitution and v != SubstitutionType.NONE:

84 raise ValueError("Non-substitution heir must have NONE type")

85

86 # 代襲相続の場合、タイプはNONE以外でなければならない

87 if is_substitution and v == SubstitutionType.NONE:

88 raise ValueError("Substitution heir must have valid substitution type")

89

90 return v

91

92 def __str__(self) -> str:

93 """文字列表現"""

94 substitution_mark = ""

95 if self.is_substitution:

96 substitution_mark = f"（代襲: {self.substituted_person.name if self.substituted_person else '不明'}）"

97

98 share_str = f"{self.share} ({self.share_percentage:.2f}%)"

99 return f"{self.person.name} - {self.rank.value} - {share_str}{substitution_mark}"

100

101

102 class InheritanceResult(BaseModel):

103 """

104 相続計算結果

105

106 相続人の確定と相続割合の計算結果を表現。

107 """

108

109 model_config = ConfigDict(

110 validate_assignment=True,

111 extra='forbid',

112 )

113

114 decedent: Person = Field(..., description="被相続人")

115 heirs: List[Heir] = Field(default_factory=list, description="相続人リスト")

116 has_spouse: bool = Field(default=False, description="配偶者がいるか")

117 has_children: bool = Field(default=False, description="子がいるか")

118 has_parents: bool = Field(default=False, description="直系尊属がいるか")

119 has_siblings: bool = Field(default=False, description="兄弟姉妹がいるか")

120 calculation_basis: List[str] = Field(

121 default_factory=list, description="計算根拠（適用した民法条文等）"

122 )

123

124 @field_validator('heirs')

125 @classmethod

126 def validate_total_share(cls, v: List[Heir]) -> List[Heir]:

127 """相続割合の合計が1（100%）であることを検証"""

128 if not v:

129 return v

130

131 total = sum(heir.share for heir in v)

132 if total != Fraction(1, 1):

133 raise ValueError(

134 f"Total share must be 1 (100%), but got {total} ({float(total) * 100}%)"

135 )

136

137 return v

138

139 def add_heir(

140 self,

141 person: Person,

142 rank: HeritageRank,

143 share: Fraction,

144 is_substitution: bool = False,

145 substitution_type: SubstitutionType = SubstitutionType.NONE,

146 substituted_person: Optional[Person] = None,

147 generation: int = 1

148 ) -> None:

149 """

150 相続人を追加

151

152 Args:

153 person: 相続人

154 rank: 相続順位

155 share: 相続割合

156 is_substitution: 代襲相続人か

157 substitution_type: 代襲相続タイプ

158 substituted_person: 被代襲者

159 generation: 代襲世代数

160 """

161 heir = Heir(

162 person=person,

163 rank=rank,

164 share=share,

165 share_percentage=float(share) * 100,

166 is_substitution=is_substitution,

167 substitution_type=substitution_type,

168 substituted_person=substituted_person,

169 generation=generation

170 )

171 self.heirs.append(heir)

172

173 def add_calculation_basis(self, basis: str) -> None:

174 """

175 計算根拠を追加

176

177 Args:

178 basis: 計算根拠（民法条文等）

179 """

180 self.calculation_basis.append(basis)

181

182 def get_heirs_by_rank(self, rank: HeritageRank) -> List[Heir]:

183 """

184 指定された順位の相続人を取得

185

186 Args:

187 rank: 相続順位

188

189 Returns:

190 相続人のリスト

191 """

192 return [heir for heir in self.heirs if heir.rank == rank]

193

194 def get_substitution_heirs(self) -> List[Heir]:

195 """

196 代襲相続人のリストを取得

197

198 Returns:

199 代襲相続人のリスト

200 """

201 return [heir for heir in self.heirs if heir.is_substitution]

202

203 @property

204 def total_heirs(self) -> int:

205 """相続人の総数"""

206 return len(self.heirs)

207

208 def __str__(self) -> str:

209 """文字列表現"""

210 result = [f"被相続人: {self.decedent.name}"]

211 result.append(f"相続人総数: {self.total_heirs}名")

212 result.append("---")

213

214 for heir in self.heirs:

215 result.append(str(heir))

216

217 if self.calculation_basis:

218 result.append("---")

219 result.append("計算根拠:")

220 for basis in self.calculation_basis:

221 result.append(f" - {basis}")

222

223 return "\n".join(result)