選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

Budget.php 7.5KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. <?php
  2. namespace App\Models\Accounting;
  3. use App\Concerns\Blamable;
  4. use App\Concerns\CompanyOwned;
  5. use App\Enums\Accounting\BudgetStatus;
  6. use Filament\Actions\Action;
  7. use Filament\Actions\MountableAction;
  8. use Filament\Actions\ReplicateAction;
  9. use Illuminate\Database\Eloquent\Builder;
  10. use Illuminate\Database\Eloquent\Casts\Attribute;
  11. use Illuminate\Database\Eloquent\Factories\HasFactory;
  12. use Illuminate\Database\Eloquent\Model;
  13. use Illuminate\Database\Eloquent\Relations\HasMany;
  14. use Illuminate\Support\Carbon;
  15. class Budget extends Model
  16. {
  17. use Blamable;
  18. use CompanyOwned;
  19. use HasFactory;
  20. protected $fillable = [
  21. 'company_id',
  22. 'name',
  23. 'start_date',
  24. 'end_date',
  25. 'status', // draft, active, closed
  26. 'interval_type', // day, week, month, quarter, year
  27. 'notes',
  28. 'approved_at',
  29. 'closed_at',
  30. 'created_by',
  31. 'updated_by',
  32. ];
  33. protected $casts = [
  34. 'start_date' => 'date',
  35. 'end_date' => 'date',
  36. 'status' => BudgetStatus::class,
  37. 'approved_at' => 'datetime',
  38. 'closed_at' => 'datetime',
  39. ];
  40. public function budgetItems(): HasMany
  41. {
  42. return $this->hasMany(BudgetItem::class);
  43. }
  44. public function isDraft(): bool
  45. {
  46. return $this->status === BudgetStatus::Draft;
  47. }
  48. public function isActive(): bool
  49. {
  50. return $this->status === BudgetStatus::Active;
  51. }
  52. public function isClosed(): bool
  53. {
  54. return $this->status === BudgetStatus::Closed;
  55. }
  56. public function wasApproved(): bool
  57. {
  58. return $this->approved_at !== null;
  59. }
  60. public function wasClosed(): bool
  61. {
  62. return $this->closed_at !== null;
  63. }
  64. public function canBeApproved(): bool
  65. {
  66. return $this->isDraft() && ! $this->wasApproved();
  67. }
  68. public function canBeClosed(): bool
  69. {
  70. return $this->isActive() && ! $this->wasClosed();
  71. }
  72. public function hasItems(): bool
  73. {
  74. return $this->budgetItems()->exists();
  75. }
  76. public function scopeDraft(Builder $query): Builder
  77. {
  78. return $query->where('status', BudgetStatus::Draft);
  79. }
  80. public function scopeActive(Builder $query): Builder
  81. {
  82. return $query->where('status', BudgetStatus::Active);
  83. }
  84. public function scopeClosed(Builder $query): Builder
  85. {
  86. return $query->where('status', BudgetStatus::Closed);
  87. }
  88. public function scopeCurrentlyActive(Builder $query): Builder
  89. {
  90. return $query->active()
  91. ->where('start_date', '<=', now())
  92. ->where('end_date', '>=', now());
  93. }
  94. protected function isCurrentlyInPeriod(): Attribute
  95. {
  96. return Attribute::get(function () {
  97. return now()->between($this->start_date, $this->end_date);
  98. });
  99. }
  100. /**
  101. * Approve a draft budget
  102. */
  103. public function approveDraft(?Carbon $approvedAt = null): void
  104. {
  105. if (! $this->canBeApproved()) {
  106. throw new \RuntimeException('Budget cannot be approved.');
  107. }
  108. $approvedAt ??= now();
  109. $this->update([
  110. 'status' => BudgetStatus::Active,
  111. 'approved_at' => $approvedAt,
  112. ]);
  113. }
  114. /**
  115. * Close an active budget
  116. */
  117. public function close(?Carbon $closedAt = null): void
  118. {
  119. if (! $this->canBeClosed()) {
  120. throw new \RuntimeException('Budget cannot be closed.');
  121. }
  122. $closedAt ??= now();
  123. $this->update([
  124. 'status' => BudgetStatus::Closed,
  125. 'closed_at' => $closedAt,
  126. ]);
  127. }
  128. /**
  129. * Reopen a closed budget
  130. */
  131. public function reopen(): void
  132. {
  133. if (! $this->isClosed()) {
  134. throw new \RuntimeException('Only closed budgets can be reopened.');
  135. }
  136. $this->update([
  137. 'status' => BudgetStatus::Active,
  138. 'closed_at' => null,
  139. ]);
  140. }
  141. /**
  142. * Get Action for approving a draft budget
  143. */
  144. public static function getApproveDraftAction(string $action = Action::class): MountableAction
  145. {
  146. return $action::make('approveDraft')
  147. ->label('Approve')
  148. ->icon('heroicon-m-check-circle')
  149. ->visible(function (self $record) {
  150. return $record->canBeApproved();
  151. })
  152. ->databaseTransaction()
  153. ->successNotificationTitle('Budget approved')
  154. ->action(function (self $record, MountableAction $action) {
  155. $record->approveDraft();
  156. $action->success();
  157. });
  158. }
  159. /**
  160. * Get Action for closing an active budget
  161. */
  162. public static function getCloseAction(string $action = Action::class): MountableAction
  163. {
  164. return $action::make('close')
  165. ->label('Close')
  166. ->icon('heroicon-m-lock-closed')
  167. ->color('warning')
  168. ->visible(function (self $record) {
  169. return $record->canBeClosed();
  170. })
  171. ->requiresConfirmation()
  172. ->databaseTransaction()
  173. ->successNotificationTitle('Budget closed')
  174. ->action(function (self $record, MountableAction $action) {
  175. $record->close();
  176. $action->success();
  177. });
  178. }
  179. /**
  180. * Get Action for reopening a closed budget
  181. */
  182. public static function getReopenAction(string $action = Action::class): MountableAction
  183. {
  184. return $action::make('reopen')
  185. ->label('Reopen')
  186. ->icon('heroicon-m-lock-open')
  187. ->visible(function (self $record) {
  188. return $record->isClosed();
  189. })
  190. ->requiresConfirmation()
  191. ->databaseTransaction()
  192. ->successNotificationTitle('Budget reopened')
  193. ->action(function (self $record, MountableAction $action) {
  194. $record->reopen();
  195. $action->success();
  196. });
  197. }
  198. /**
  199. * Get Action for duplicating a budget
  200. */
  201. public static function getReplicateAction(string $action = ReplicateAction::class): MountableAction
  202. {
  203. return $action::make()
  204. ->excludeAttributes([
  205. 'status',
  206. 'approved_at',
  207. 'closed_at',
  208. 'created_by',
  209. 'updated_by',
  210. 'created_at',
  211. 'updated_at',
  212. ])
  213. ->modal(false)
  214. ->beforeReplicaSaved(function (self $original, self $replica) {
  215. $replica->status = BudgetStatus::Draft;
  216. $replica->name = $replica->name . ' (Copy)';
  217. // Optionally adjust dates for the new budget period
  218. // $replica->start_date = now()->startOfMonth();
  219. // $replica->end_date = now()->endOfYear();
  220. })
  221. ->databaseTransaction()
  222. ->after(function (self $original, self $replica) {
  223. // Clone budget items
  224. $original->budgetItems->each(function (BudgetItem $item) use ($replica) {
  225. $newItem = $item->replicate([
  226. 'budget_id',
  227. 'created_by',
  228. 'updated_by',
  229. 'created_at',
  230. 'updated_at',
  231. ]);
  232. $newItem->budget_id = $replica->id;
  233. $newItem->save();
  234. });
  235. })
  236. ->successRedirectUrl(static function (self $replica) {
  237. return BudgetResource::getUrl('edit', ['record' => $replica]);
  238. });
  239. }
  240. }