|
@@ -266,89 +266,133 @@ class Bill extends Document
|
266
|
266
|
public function createInitialTransaction(?Carbon $postedAt = null): void
|
267
|
267
|
{
|
268
|
268
|
$postedAt ??= $this->date;
|
269
|
|
-
|
270
|
|
- $total = $this->formatAmountToDefaultCurrency($this->getRawOriginal('total'));
|
271
|
|
-
|
272
|
|
- $transaction = $this->transactions()->create([
|
273
|
|
- 'company_id' => $this->company_id,
|
274
|
|
- 'type' => TransactionType::Journal,
|
275
|
|
- 'posted_at' => $postedAt,
|
276
|
|
- 'amount' => $total,
|
277
|
|
- 'description' => 'Bill Creation for Bill #' . $this->bill_number,
|
278
|
|
- ]);
|
279
|
|
-
|
280
|
269
|
$baseDescription = "{$this->vendor->name}: Bill #{$this->bill_number}";
|
281
|
270
|
|
282
|
|
- $transaction->journalEntries()->create([
|
283
|
|
- 'company_id' => $this->company_id,
|
|
271
|
+ $journalEntryData = [];
|
|
272
|
+
|
|
273
|
+ $totalInBillCurrency = $this->getRawOriginal('total');
|
|
274
|
+ $journalEntryData[] = [
|
284
|
275
|
'type' => JournalEntryType::Credit,
|
285
|
276
|
'account_id' => Account::getAccountsPayableAccount($this->company_id)->id,
|
286
|
|
- 'amount' => $total,
|
|
277
|
+ 'amount_in_bill_currency' => $totalInBillCurrency,
|
287
|
278
|
'description' => $baseDescription,
|
288
|
|
- ]);
|
|
279
|
+ ];
|
289
|
280
|
|
290
|
|
- $totalLineItemSubtotalCents = $this->convertAmountToDefaultCurrency((int) $this->lineItems()->sum('subtotal'));
|
291
|
|
- $billDiscountTotalCents = $this->convertAmountToDefaultCurrency((int) $this->getRawOriginal('discount_total'));
|
292
|
|
- $remainingDiscountCents = $billDiscountTotalCents;
|
|
281
|
+ $totalLineItemSubtotalInBillCurrency = (int) $this->lineItems()->sum('subtotal');
|
|
282
|
+ $billDiscountTotalInBillCurrency = (int) $this->getRawOriginal('discount_total');
|
|
283
|
+ $remainingDiscountInBillCurrency = $billDiscountTotalInBillCurrency;
|
293
|
284
|
|
294
|
285
|
foreach ($this->lineItems as $index => $lineItem) {
|
295
|
286
|
$lineItemDescription = "{$baseDescription} › {$lineItem->offering->name}";
|
|
287
|
+ $lineItemSubtotalInBillCurrency = $lineItem->getRawOriginal('subtotal');
|
296
|
288
|
|
297
|
|
- $lineItemSubtotal = $this->formatAmountToDefaultCurrency($lineItem->getRawOriginal('subtotal'));
|
298
|
|
-
|
299
|
|
- $transaction->journalEntries()->create([
|
300
|
|
- 'company_id' => $this->company_id,
|
|
289
|
+ $journalEntryData[] = [
|
301
|
290
|
'type' => JournalEntryType::Debit,
|
302
|
291
|
'account_id' => $lineItem->offering->expense_account_id,
|
303
|
|
- 'amount' => $lineItemSubtotal,
|
|
292
|
+ 'amount_in_bill_currency' => $lineItemSubtotalInBillCurrency,
|
304
|
293
|
'description' => $lineItemDescription,
|
305
|
|
- ]);
|
|
294
|
+ ];
|
306
|
295
|
|
307
|
296
|
foreach ($lineItem->adjustments as $adjustment) {
|
308
|
|
- $adjustmentAmount = $this->formatAmountToDefaultCurrency($lineItem->calculateAdjustmentTotalAmount($adjustment));
|
|
297
|
+ $adjustmentAmountInBillCurrency = $lineItem->calculateAdjustmentTotalAmount($adjustment);
|
309
|
298
|
|
310
|
299
|
if ($adjustment->isNonRecoverablePurchaseTax()) {
|
311
|
|
- $transaction->journalEntries()->create([
|
312
|
|
- 'company_id' => $this->company_id,
|
|
300
|
+ $journalEntryData[] = [
|
313
|
301
|
'type' => JournalEntryType::Debit,
|
314
|
302
|
'account_id' => $lineItem->offering->expense_account_id,
|
315
|
|
- 'amount' => $adjustmentAmount,
|
|
303
|
+ 'amount_in_bill_currency' => $adjustmentAmountInBillCurrency,
|
316
|
304
|
'description' => "{$lineItemDescription} ({$adjustment->name})",
|
317
|
|
- ]);
|
|
305
|
+ ];
|
318
|
306
|
} elseif ($adjustment->account_id) {
|
319
|
|
- $transaction->journalEntries()->create([
|
320
|
|
- 'company_id' => $this->company_id,
|
|
307
|
+ $journalEntryData[] = [
|
321
|
308
|
'type' => $adjustment->category->isDiscount() ? JournalEntryType::Credit : JournalEntryType::Debit,
|
322
|
309
|
'account_id' => $adjustment->account_id,
|
323
|
|
- 'amount' => $adjustmentAmount,
|
|
310
|
+ 'amount_in_bill_currency' => $adjustmentAmountInBillCurrency,
|
324
|
311
|
'description' => $lineItemDescription,
|
325
|
|
- ]);
|
|
312
|
+ ];
|
326
|
313
|
}
|
327
|
314
|
}
|
328
|
315
|
|
329
|
|
- if ($this->discount_method->isPerDocument() && $totalLineItemSubtotalCents > 0) {
|
330
|
|
- $lineItemSubtotalCents = $this->convertAmountToDefaultCurrency((int) $lineItem->getRawOriginal('subtotal'));
|
|
316
|
+ // Handle per-document discount allocation
|
|
317
|
+ if ($this->discount_method->isPerDocument() && $totalLineItemSubtotalInBillCurrency > 0) {
|
|
318
|
+ $lineItemSubtotalInBillCurrency = (int) $lineItem->getRawOriginal('subtotal');
|
331
|
319
|
|
332
|
320
|
if ($index === $this->lineItems->count() - 1) {
|
333
|
|
- $lineItemDiscount = $remainingDiscountCents;
|
|
321
|
+ $lineItemDiscountInBillCurrency = $remainingDiscountInBillCurrency;
|
334
|
322
|
} else {
|
335
|
|
- $lineItemDiscount = (int) round(
|
336
|
|
- ($lineItemSubtotalCents / $totalLineItemSubtotalCents) * $billDiscountTotalCents
|
|
323
|
+ $lineItemDiscountInBillCurrency = (int) round(
|
|
324
|
+ ($lineItemSubtotalInBillCurrency / $totalLineItemSubtotalInBillCurrency) * $billDiscountTotalInBillCurrency
|
337
|
325
|
);
|
338
|
|
- $remainingDiscountCents -= $lineItemDiscount;
|
|
326
|
+ $remainingDiscountInBillCurrency -= $lineItemDiscountInBillCurrency;
|
339
|
327
|
}
|
340
|
328
|
|
341
|
|
- if ($lineItemDiscount > 0) {
|
342
|
|
- $transaction->journalEntries()->create([
|
343
|
|
- 'company_id' => $this->company_id,
|
|
329
|
+ if ($lineItemDiscountInBillCurrency > 0) {
|
|
330
|
+ $journalEntryData[] = [
|
344
|
331
|
'type' => JournalEntryType::Credit,
|
345
|
332
|
'account_id' => Account::getPurchaseDiscountAccount($this->company_id)->id,
|
346
|
|
- 'amount' => CurrencyConverter::convertCentsToFormatSimple($lineItemDiscount),
|
|
333
|
+ 'amount_in_bill_currency' => $lineItemDiscountInBillCurrency,
|
347
|
334
|
'description' => "{$lineItemDescription} (Proportional Discount)",
|
348
|
|
- ]);
|
|
335
|
+ ];
|
349
|
336
|
}
|
350
|
337
|
}
|
351
|
338
|
}
|
|
339
|
+
|
|
340
|
+ // Convert amounts to default currency
|
|
341
|
+ $totalDebitsInDefaultCurrency = 0;
|
|
342
|
+ $totalCreditsInDefaultCurrency = 0;
|
|
343
|
+
|
|
344
|
+ foreach ($journalEntryData as &$entry) {
|
|
345
|
+ $entry['amount_in_default_currency'] = $this->formatAmountToDefaultCurrency($entry['amount_in_bill_currency']);
|
|
346
|
+
|
|
347
|
+ if ($entry['type'] === JournalEntryType::Debit) {
|
|
348
|
+ $totalDebitsInDefaultCurrency += $entry['amount_in_default_currency'];
|
|
349
|
+ } else {
|
|
350
|
+ $totalCreditsInDefaultCurrency += $entry['amount_in_default_currency'];
|
|
351
|
+ }
|
|
352
|
+ }
|
|
353
|
+
|
|
354
|
+ unset($entry);
|
|
355
|
+
|
|
356
|
+ // Handle currency conversion imbalance
|
|
357
|
+ $imbalance = $totalDebitsInDefaultCurrency - $totalCreditsInDefaultCurrency;
|
|
358
|
+ if ($imbalance !== 0) {
|
|
359
|
+ $targetType = $imbalance > 0 ? JournalEntryType::Credit : JournalEntryType::Debit;
|
|
360
|
+ $adjustmentAmount = abs($imbalance);
|
|
361
|
+
|
|
362
|
+ // Find last entry of target type and adjust it
|
|
363
|
+ $lastKey = array_key_last(array_filter($journalEntryData, fn ($entry) => $entry['type'] === $targetType, ARRAY_FILTER_USE_BOTH));
|
|
364
|
+ $journalEntryData[$lastKey]['amount_in_default_currency'] += $adjustmentAmount;
|
|
365
|
+
|
|
366
|
+ if ($targetType === JournalEntryType::Debit) {
|
|
367
|
+ $totalDebitsInDefaultCurrency += $adjustmentAmount;
|
|
368
|
+ } else {
|
|
369
|
+ $totalCreditsInDefaultCurrency += $adjustmentAmount;
|
|
370
|
+ }
|
|
371
|
+ }
|
|
372
|
+
|
|
373
|
+ if ($totalDebitsInDefaultCurrency !== $totalCreditsInDefaultCurrency) {
|
|
374
|
+ throw new \Exception('Journal entries do not balance for Bill #' . $this->bill_number . '. Debits: ' . $totalDebitsInDefaultCurrency . ', Credits: ' . $totalCreditsInDefaultCurrency);
|
|
375
|
+ }
|
|
376
|
+
|
|
377
|
+ // Create the transaction using the sum of debits
|
|
378
|
+ $transaction = $this->transactions()->create([
|
|
379
|
+ 'company_id' => $this->company_id,
|
|
380
|
+ 'type' => TransactionType::Journal,
|
|
381
|
+ 'posted_at' => $postedAt,
|
|
382
|
+ 'amount' => $totalDebitsInDefaultCurrency,
|
|
383
|
+ 'description' => 'Bill Creation for Bill #' . $this->bill_number,
|
|
384
|
+ ]);
|
|
385
|
+
|
|
386
|
+ // Create all journal entries
|
|
387
|
+ foreach ($journalEntryData as $entry) {
|
|
388
|
+ $transaction->journalEntries()->create([
|
|
389
|
+ 'company_id' => $this->company_id,
|
|
390
|
+ 'type' => $entry['type'],
|
|
391
|
+ 'account_id' => $entry['account_id'],
|
|
392
|
+ 'amount' => $entry['amount_in_default_currency'],
|
|
393
|
+ 'description' => $entry['description'],
|
|
394
|
+ ]);
|
|
395
|
+ }
|
352
|
396
|
}
|
353
|
397
|
|
354
|
398
|
public function updateInitialTransaction(): void
|
|
@@ -374,11 +418,9 @@ class Bill extends Document
|
374
|
418
|
return $amountCents;
|
375
|
419
|
}
|
376
|
420
|
|
377
|
|
- public function formatAmountToDefaultCurrency(int $amountCents): string
|
|
421
|
+ public function formatAmountToDefaultCurrency(int $amountCents): int
|
378
|
422
|
{
|
379
|
|
- $convertedCents = $this->convertAmountToDefaultCurrency($amountCents);
|
380
|
|
-
|
381
|
|
- return CurrencyConverter::convertCentsToFormatSimple($convertedCents);
|
|
423
|
+ return $this->convertAmountToDefaultCurrency($amountCents);
|
382
|
424
|
}
|
383
|
425
|
|
384
|
426
|
public static function getReplicateAction(string $action = ReplicateAction::class): MountableAction
|