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

Budget.php 8.2KB

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