Você não pode selecionar mais de 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.

Budget.php 9.3KB

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