|  | @@ -3,13 +3,14 @@
 | 
		
	
		
			
			| 3 | 3 |  namespace App\Filament\Company\Resources\Purchases;
 | 
		
	
		
			
			| 4 | 4 |  
 | 
		
	
		
			
			| 5 | 5 |  use App\Enums\Accounting\BillStatus;
 | 
		
	
		
			
			|  | 6 | +use App\Enums\Accounting\DocumentDiscountMethod;
 | 
		
	
		
			
			| 6 | 7 |  use App\Enums\Accounting\PaymentMethod;
 | 
		
	
		
			
			| 7 | 8 |  use App\Filament\Company\Resources\Purchases\BillResource\Pages;
 | 
		
	
		
			
			|  | 9 | +use App\Filament\Forms\Components\BillTotals;
 | 
		
	
		
			
			| 8 | 10 |  use App\Filament\Tables\Actions\ReplicateBulkAction;
 | 
		
	
		
			
			| 9 | 11 |  use App\Filament\Tables\Filters\DateRangeFilter;
 | 
		
	
		
			
			| 10 | 12 |  use App\Models\Accounting\Adjustment;
 | 
		
	
		
			
			| 11 | 13 |  use App\Models\Accounting\Bill;
 | 
		
	
		
			
			| 12 |  | -use App\Models\Accounting\DocumentLineItem;
 | 
		
	
		
			
			| 13 | 14 |  use App\Models\Banking\BankAccount;
 | 
		
	
		
			
			| 14 | 15 |  use App\Models\Common\Offering;
 | 
		
	
		
			
			| 15 | 16 |  use App\Utilities\Currency\CurrencyConverter;
 | 
		
	
	
		
			
			|  | @@ -26,7 +27,6 @@ use Filament\Tables;
 | 
		
	
		
			
			| 26 | 27 |  use Filament\Tables\Table;
 | 
		
	
		
			
			| 27 | 28 |  use Illuminate\Database\Eloquent\Builder;
 | 
		
	
		
			
			| 28 | 29 |  use Illuminate\Database\Eloquent\Collection;
 | 
		
	
		
			
			| 29 |  | -use Illuminate\Database\Eloquent\Model;
 | 
		
	
		
			
			| 30 | 30 |  use Illuminate\Support\Facades\Auth;
 | 
		
	
		
			
			| 31 | 31 |  
 | 
		
	
		
			
			| 32 | 32 |  class BillResource extends Resource
 | 
		
	
	
		
			
			|  | @@ -71,129 +71,44 @@ class BillResource extends Resource
 | 
		
	
		
			
			| 71 | 71 |                                          return now()->addDays($company->defaultBill->payment_terms->getDays());
 | 
		
	
		
			
			| 72 | 72 |                                      })
 | 
		
	
		
			
			| 73 | 73 |                                      ->required(),
 | 
		
	
		
			
			|  | 74 | +                                Forms\Components\Select::make('discount_method')
 | 
		
	
		
			
			|  | 75 | +                                    ->label('Discount Method')
 | 
		
	
		
			
			|  | 76 | +                                    ->options(DocumentDiscountMethod::class)
 | 
		
	
		
			
			|  | 77 | +                                    ->selectablePlaceholder(false)
 | 
		
	
		
			
			|  | 78 | +                                    ->default(DocumentDiscountMethod::PerLineItem)
 | 
		
	
		
			
			|  | 79 | +                                    ->afterStateUpdated(function ($state, Forms\Set $set) {
 | 
		
	
		
			
			|  | 80 | +                                        $discountMethod = DocumentDiscountMethod::parse($state);
 | 
		
	
		
			
			|  | 81 | +
 | 
		
	
		
			
			|  | 82 | +                                        if ($discountMethod->isPerDocument()) {
 | 
		
	
		
			
			|  | 83 | +                                            $set('lineItems.*.purchaseDiscounts', []);
 | 
		
	
		
			
			|  | 84 | +                                        }
 | 
		
	
		
			
			|  | 85 | +                                    })
 | 
		
	
		
			
			|  | 86 | +                                    ->live(),
 | 
		
	
		
			
			| 74 | 87 |                              ])->grow(true),
 | 
		
	
		
			
			| 75 | 88 |                          ])->from('md'),
 | 
		
	
		
			
			| 76 | 89 |                          TableRepeater::make('lineItems')
 | 
		
	
		
			
			| 77 | 90 |                              ->relationship()
 | 
		
	
		
			
			| 78 |  | -                            ->saveRelationshipsUsing(function (TableRepeater $component, Forms\Contracts\HasForms $livewire, ?array $state) {
 | 
		
	
		
			
			| 79 |  | -                                if (! is_array($state)) {
 | 
		
	
		
			
			| 80 |  | -                                    $state = [];
 | 
		
	
		
			
			| 81 |  | -                                }
 | 
		
	
		
			
			| 82 |  | -
 | 
		
	
		
			
			| 83 |  | -                                $relationship = $component->getRelationship();
 | 
		
	
		
			
			| 84 |  | -
 | 
		
	
		
			
			| 85 |  | -                                $existingRecords = $component->getCachedExistingRecords();
 | 
		
	
		
			
			| 86 |  | -
 | 
		
	
		
			
			| 87 |  | -                                $recordsToDelete = [];
 | 
		
	
		
			
			| 88 |  | -
 | 
		
	
		
			
			| 89 |  | -                                foreach ($existingRecords->pluck($relationship->getRelated()->getKeyName()) as $keyToCheckForDeletion) {
 | 
		
	
		
			
			| 90 |  | -                                    if (array_key_exists("record-{$keyToCheckForDeletion}", $state)) {
 | 
		
	
		
			
			| 91 |  | -                                        continue;
 | 
		
	
		
			
			| 92 |  | -                                    }
 | 
		
	
		
			
			| 93 |  | -
 | 
		
	
		
			
			| 94 |  | -                                    $recordsToDelete[] = $keyToCheckForDeletion;
 | 
		
	
		
			
			| 95 |  | -                                    $existingRecords->forget("record-{$keyToCheckForDeletion}");
 | 
		
	
		
			
			|  | 91 | +                            ->saveRelationshipsUsing(null)
 | 
		
	
		
			
			|  | 92 | +                            ->dehydrated(true)
 | 
		
	
		
			
			|  | 93 | +                            ->headers(function (Forms\Get $get) {
 | 
		
	
		
			
			|  | 94 | +                                $hasDiscounts = DocumentDiscountMethod::parse($get('discount_method'))->isPerLineItem();
 | 
		
	
		
			
			|  | 95 | +
 | 
		
	
		
			
			|  | 96 | +                                $headers = [
 | 
		
	
		
			
			|  | 97 | +                                    Header::make('Items')->width($hasDiscounts ? '15%' : '20%'),
 | 
		
	
		
			
			|  | 98 | +                                    Header::make('Description')->width($hasDiscounts ? '25%' : '30%'),  // Increase when no discounts
 | 
		
	
		
			
			|  | 99 | +                                    Header::make('Quantity')->width('10%'),
 | 
		
	
		
			
			|  | 100 | +                                    Header::make('Price')->width('10%'),
 | 
		
	
		
			
			|  | 101 | +                                    Header::make('Taxes')->width($hasDiscounts ? '15%' : '20%'),       // Increase when no discounts
 | 
		
	
		
			
			|  | 102 | +                                ];
 | 
		
	
		
			
			|  | 103 | +
 | 
		
	
		
			
			|  | 104 | +                                if ($hasDiscounts) {
 | 
		
	
		
			
			|  | 105 | +                                    $headers[] = Header::make('Discounts')->width('15%');
 | 
		
	
		
			
			| 96 | 106 |                                  }
 | 
		
	
		
			
			| 97 | 107 |  
 | 
		
	
		
			
			| 98 |  | -                                $relationship
 | 
		
	
		
			
			| 99 |  | -                                    ->whereKey($recordsToDelete)
 | 
		
	
		
			
			| 100 |  | -                                    ->get()
 | 
		
	
		
			
			| 101 |  | -                                    ->each(static fn (Model $record) => $record->delete());
 | 
		
	
		
			
			| 102 |  | -
 | 
		
	
		
			
			| 103 |  | -                                $childComponentContainers = $component->getChildComponentContainers(
 | 
		
	
		
			
			| 104 |  | -                                    withHidden: $component->shouldSaveRelationshipsWhenHidden(),
 | 
		
	
		
			
			| 105 |  | -                                );
 | 
		
	
		
			
			| 106 |  | -
 | 
		
	
		
			
			| 107 |  | -                                $itemOrder = 1;
 | 
		
	
		
			
			| 108 |  | -                                $orderColumn = $component->getOrderColumn();
 | 
		
	
		
			
			| 109 |  | -
 | 
		
	
		
			
			| 110 |  | -                                $translatableContentDriver = $livewire->makeFilamentTranslatableContentDriver();
 | 
		
	
		
			
			| 111 |  | -
 | 
		
	
		
			
			| 112 |  | -                                foreach ($childComponentContainers as $itemKey => $item) {
 | 
		
	
		
			
			| 113 |  | -                                    $itemData = $item->getState(shouldCallHooksBefore: false);
 | 
		
	
		
			
			| 114 |  | -
 | 
		
	
		
			
			| 115 |  | -                                    if ($orderColumn) {
 | 
		
	
		
			
			| 116 |  | -                                        $itemData[$orderColumn] = $itemOrder;
 | 
		
	
		
			
			| 117 |  | -
 | 
		
	
		
			
			| 118 |  | -                                        $itemOrder++;
 | 
		
	
		
			
			| 119 |  | -                                    }
 | 
		
	
		
			
			| 120 |  | -
 | 
		
	
		
			
			| 121 |  | -                                    if ($record = ($existingRecords[$itemKey] ?? null)) {
 | 
		
	
		
			
			| 122 |  | -                                        $itemData = $component->mutateRelationshipDataBeforeSave($itemData, record: $record);
 | 
		
	
		
			
			| 123 |  | -
 | 
		
	
		
			
			| 124 |  | -                                        if ($itemData === null) {
 | 
		
	
		
			
			| 125 |  | -                                            continue;
 | 
		
	
		
			
			| 126 |  | -                                        }
 | 
		
	
		
			
			| 127 |  | -
 | 
		
	
		
			
			| 128 |  | -                                        $translatableContentDriver ?
 | 
		
	
		
			
			| 129 |  | -                                            $translatableContentDriver->updateRecord($record, $itemData) :
 | 
		
	
		
			
			| 130 |  | -                                            $record->fill($itemData)->save();
 | 
		
	
		
			
			| 131 |  | -
 | 
		
	
		
			
			| 132 |  | -                                        continue;
 | 
		
	
		
			
			| 133 |  | -                                    }
 | 
		
	
		
			
			| 134 |  | -
 | 
		
	
		
			
			| 135 |  | -                                    $relatedModel = $component->getRelatedModel();
 | 
		
	
		
			
			| 136 |  | -
 | 
		
	
		
			
			| 137 |  | -                                    $itemData = $component->mutateRelationshipDataBeforeCreate($itemData);
 | 
		
	
		
			
			| 138 |  | -
 | 
		
	
		
			
			| 139 |  | -                                    if ($itemData === null) {
 | 
		
	
		
			
			| 140 |  | -                                        continue;
 | 
		
	
		
			
			| 141 |  | -                                    }
 | 
		
	
		
			
			| 142 |  | -
 | 
		
	
		
			
			| 143 |  | -                                    if ($translatableContentDriver) {
 | 
		
	
		
			
			| 144 |  | -                                        $record = $translatableContentDriver->makeRecord($relatedModel, $itemData);
 | 
		
	
		
			
			| 145 |  | -                                    } else {
 | 
		
	
		
			
			| 146 |  | -                                        $record = new $relatedModel;
 | 
		
	
		
			
			| 147 |  | -                                        $record->fill($itemData);
 | 
		
	
		
			
			| 148 |  | -                                    }
 | 
		
	
		
			
			|  | 108 | +                                $headers[] = Header::make('Amount')->width('10%')->align('right');
 | 
		
	
		
			
			| 149 | 109 |  
 | 
		
	
		
			
			| 150 |  | -                                    $record = $relationship->save($record);
 | 
		
	
		
			
			| 151 |  | -                                    $item->model($record)->saveRelationships();
 | 
		
	
		
			
			| 152 |  | -                                    $existingRecords->push($record);
 | 
		
	
		
			
			| 153 |  | -                                }
 | 
		
	
		
			
			| 154 |  | -
 | 
		
	
		
			
			| 155 |  | -                                $component->getRecord()->setRelation($component->getRelationshipName(), $existingRecords);
 | 
		
	
		
			
			| 156 |  | -
 | 
		
	
		
			
			| 157 |  | -                                /** @var Bill $bill */
 | 
		
	
		
			
			| 158 |  | -                                $bill = $component->getRecord();
 | 
		
	
		
			
			| 159 |  | -
 | 
		
	
		
			
			| 160 |  | -                                // Recalculate totals for line items
 | 
		
	
		
			
			| 161 |  | -                                $bill->lineItems()->each(function (DocumentLineItem $lineItem) {
 | 
		
	
		
			
			| 162 |  | -                                    $lineItem->updateQuietly([
 | 
		
	
		
			
			| 163 |  | -                                        'tax_total' => $lineItem->calculateTaxTotal()->getAmount(),
 | 
		
	
		
			
			| 164 |  | -                                        'discount_total' => $lineItem->calculateDiscountTotal()->getAmount(),
 | 
		
	
		
			
			| 165 |  | -                                    ]);
 | 
		
	
		
			
			| 166 |  | -                                });
 | 
		
	
		
			
			| 167 |  | -
 | 
		
	
		
			
			| 168 |  | -                                $subtotal = $bill->lineItems()->sum('subtotal') / 100;
 | 
		
	
		
			
			| 169 |  | -                                $taxTotal = $bill->lineItems()->sum('tax_total') / 100;
 | 
		
	
		
			
			| 170 |  | -                                $discountTotal = $bill->lineItems()->sum('discount_total') / 100;
 | 
		
	
		
			
			| 171 |  | -                                $grandTotal = $subtotal + $taxTotal - $discountTotal;
 | 
		
	
		
			
			| 172 |  | -
 | 
		
	
		
			
			| 173 |  | -                                $bill->updateQuietly([
 | 
		
	
		
			
			| 174 |  | -                                    'subtotal' => $subtotal,
 | 
		
	
		
			
			| 175 |  | -                                    'tax_total' => $taxTotal,
 | 
		
	
		
			
			| 176 |  | -                                    'discount_total' => $discountTotal,
 | 
		
	
		
			
			| 177 |  | -                                    'total' => $grandTotal,
 | 
		
	
		
			
			| 178 |  | -                                ]);
 | 
		
	
		
			
			| 179 |  | -
 | 
		
	
		
			
			| 180 |  | -                                $bill->refresh();
 | 
		
	
		
			
			| 181 |  | -
 | 
		
	
		
			
			| 182 |  | -                                if (! $bill->initialTransaction) {
 | 
		
	
		
			
			| 183 |  | -                                    $bill->createInitialTransaction();
 | 
		
	
		
			
			| 184 |  | -                                } else {
 | 
		
	
		
			
			| 185 |  | -                                    $bill->updateInitialTransaction();
 | 
		
	
		
			
			| 186 |  | -                                }
 | 
		
	
		
			
			|  | 110 | +                                return $headers;
 | 
		
	
		
			
			| 187 | 111 |                              })
 | 
		
	
		
			
			| 188 |  | -                            ->headers([
 | 
		
	
		
			
			| 189 |  | -                                Header::make('Items')->width('15%'),
 | 
		
	
		
			
			| 190 |  | -                                Header::make('Description')->width('25%'),
 | 
		
	
		
			
			| 191 |  | -                                Header::make('Quantity')->width('10%'),
 | 
		
	
		
			
			| 192 |  | -                                Header::make('Price')->width('10%'),
 | 
		
	
		
			
			| 193 |  | -                                Header::make('Taxes')->width('15%'),
 | 
		
	
		
			
			| 194 |  | -                                Header::make('Discounts')->width('15%'),
 | 
		
	
		
			
			| 195 |  | -                                Header::make('Amount')->width('10%')->align('right'),
 | 
		
	
		
			
			| 196 |  | -                            ])
 | 
		
	
		
			
			| 197 | 112 |                              ->schema([
 | 
		
	
		
			
			| 198 | 113 |                                  Forms\Components\Select::make('offering_id')
 | 
		
	
		
			
			| 199 | 114 |                                      ->relationship('purchasableOffering', 'name')
 | 
		
	
	
		
			
			|  | @@ -209,7 +124,11 @@ class BillResource extends Resource
 | 
		
	
		
			
			| 209 | 124 |                                              $set('description', $offeringRecord->description);
 | 
		
	
		
			
			| 210 | 125 |                                              $set('unit_price', $offeringRecord->price);
 | 
		
	
		
			
			| 211 | 126 |                                              $set('purchaseTaxes', $offeringRecord->purchaseTaxes->pluck('id')->toArray());
 | 
		
	
		
			
			| 212 |  | -                                            $set('purchaseDiscounts', $offeringRecord->purchaseDiscounts->pluck('id')->toArray());
 | 
		
	
		
			
			|  | 127 | +
 | 
		
	
		
			
			|  | 128 | +                                            $discountMethod = DocumentDiscountMethod::parse($get('../../discount_method'));
 | 
		
	
		
			
			|  | 129 | +                                            if ($discountMethod->isPerLineItem()) {
 | 
		
	
		
			
			|  | 130 | +                                                $set('purchaseDiscounts', $offeringRecord->purchaseDiscounts->pluck('id')->toArray());
 | 
		
	
		
			
			|  | 131 | +                                            }
 | 
		
	
		
			
			| 213 | 132 |                                          }
 | 
		
	
		
			
			| 214 | 133 |                                      }),
 | 
		
	
		
			
			| 215 | 134 |                                  Forms\Components\TextInput::make('description'),
 | 
		
	
	
		
			
			|  | @@ -225,15 +144,24 @@ class BillResource extends Resource
 | 
		
	
		
			
			| 225 | 144 |                                      ->default(0),
 | 
		
	
		
			
			| 226 | 145 |                                  Forms\Components\Select::make('purchaseTaxes')
 | 
		
	
		
			
			| 227 | 146 |                                      ->relationship('purchaseTaxes', 'name')
 | 
		
	
		
			
			|  | 147 | +                                    ->saveRelationshipsUsing(null)
 | 
		
	
		
			
			|  | 148 | +                                    ->dehydrated(true)
 | 
		
	
		
			
			| 228 | 149 |                                      ->preload()
 | 
		
	
		
			
			| 229 | 150 |                                      ->multiple()
 | 
		
	
		
			
			| 230 | 151 |                                      ->live()
 | 
		
	
		
			
			| 231 | 152 |                                      ->searchable(),
 | 
		
	
		
			
			| 232 | 153 |                                  Forms\Components\Select::make('purchaseDiscounts')
 | 
		
	
		
			
			| 233 | 154 |                                      ->relationship('purchaseDiscounts', 'name')
 | 
		
	
		
			
			|  | 155 | +                                    ->saveRelationshipsUsing(null)
 | 
		
	
		
			
			|  | 156 | +                                    ->dehydrated(true)
 | 
		
	
		
			
			| 234 | 157 |                                      ->preload()
 | 
		
	
		
			
			| 235 | 158 |                                      ->multiple()
 | 
		
	
		
			
			| 236 | 159 |                                      ->live()
 | 
		
	
		
			
			|  | 160 | +                                    ->hidden(function (Forms\Get $get) {
 | 
		
	
		
			
			|  | 161 | +                                        $discountMethod = DocumentDiscountMethod::parse($get('../../discount_method'));
 | 
		
	
		
			
			|  | 162 | +
 | 
		
	
		
			
			|  | 163 | +                                        return $discountMethod->isPerDocument();
 | 
		
	
		
			
			|  | 164 | +                                    })
 | 
		
	
		
			
			| 237 | 165 |                                      ->searchable(),
 | 
		
	
		
			
			| 238 | 166 |                                  Forms\Components\Placeholder::make('total')
 | 
		
	
		
			
			| 239 | 167 |                                      ->hiddenLabel()
 | 
		
	
	
		
			
			|  | @@ -265,13 +193,7 @@ class BillResource extends Resource
 | 
		
	
		
			
			| 265 | 193 |                                          return CurrencyConverter::formatToMoney($total);
 | 
		
	
		
			
			| 266 | 194 |                                      }),
 | 
		
	
		
			
			| 267 | 195 |                              ]),
 | 
		
	
		
			
			| 268 |  | -                        Forms\Components\Grid::make(6)
 | 
		
	
		
			
			| 269 |  | -                            ->schema([
 | 
		
	
		
			
			| 270 |  | -                                Forms\Components\ViewField::make('totals')
 | 
		
	
		
			
			| 271 |  | -                                    ->columnStart(5)
 | 
		
	
		
			
			| 272 |  | -                                    ->columnSpan(2)
 | 
		
	
		
			
			| 273 |  | -                                    ->view('filament.forms.components.bill-totals'),
 | 
		
	
		
			
			| 274 |  | -                            ]),
 | 
		
	
		
			
			|  | 196 | +                        BillTotals::make(),
 | 
		
	
		
			
			| 275 | 197 |                      ]),
 | 
		
	
		
			
			| 276 | 198 |              ]);
 | 
		
	
		
			
			| 277 | 199 |      }
 | 
		
	
	
		
			
			|  | @@ -281,6 +203,11 @@ class BillResource extends Resource
 | 
		
	
		
			
			| 281 | 203 |          return $table
 | 
		
	
		
			
			| 282 | 204 |              ->defaultSort('due_date')
 | 
		
	
		
			
			| 283 | 205 |              ->columns([
 | 
		
	
		
			
			|  | 206 | +                Tables\Columns\TextColumn::make('id')
 | 
		
	
		
			
			|  | 207 | +                    ->label('ID')
 | 
		
	
		
			
			|  | 208 | +                    ->sortable()
 | 
		
	
		
			
			|  | 209 | +                    ->toggleable(isToggledHiddenByDefault: true)
 | 
		
	
		
			
			|  | 210 | +                    ->searchable(),
 | 
		
	
		
			
			| 284 | 211 |                  Tables\Columns\TextColumn::make('status')
 | 
		
	
		
			
			| 285 | 212 |                      ->badge()
 | 
		
	
		
			
			| 286 | 213 |                      ->searchable(),
 |