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.4KB

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