diff --git a/app/Casts/SeparateTimezoneCaster.php b/app/Casts/SeparateTimezoneCaster.php new file mode 100644 index 0000000000..ef12471230 --- /dev/null +++ b/app/Casts/SeparateTimezoneCaster.php @@ -0,0 +1,44 @@ + $attributes + */ + public function get(Model $model, string $key, mixed $value, array $attributes): ?Carbon + { + if('' === $value || null === $value) { + return null; + } + $timeZone = $attributes[sprintf('%s_tz', $key)] ?? config('app.timezone'); + return Carbon::parse($value, $timeZone)->setTimezone(config('app.timezone')); + } + + /** + * Prepare the given value for storage. + * + * @param array $attributes + */ + public function set(Model $model, string $key, mixed $value, array $attributes): mixed + { + return $value; + } +} diff --git a/app/Console/Commands/Integrity/AddTimezonesToDates.php b/app/Console/Commands/Integrity/AddTimezonesToDates.php index 286d577fcc..8355cc9028 100644 --- a/app/Console/Commands/Integrity/AddTimezonesToDates.php +++ b/app/Console/Commands/Integrity/AddTimezonesToDates.php @@ -58,26 +58,27 @@ class AddTimezonesToDates extends Command */ protected $description = 'Make sure all dates have a timezone.'; + public static array $models = [ + AccountBalance::class => ['date'], // done + AvailableBudget::class => ['start_date', 'end_date'], // done + Bill::class => ['date', 'end_date', 'extension_date'], // done + BudgetLimit::class => ['start_date', 'end_date'], // done + CurrencyExchangeRate::class => ['date'], // done + InvitedUser::class => ['expires'], + PiggyBankEvent::class => ['date'], + PiggyBankRepetition::class => ['startdate', 'targetdate'], + PiggyBank::class => ['startdate', 'targetdate'], // done + Recurrence::class => ['first_date', 'repeat_until', 'latest_date'], + Tag::class => ['date'], + TransactionJournal::class => ['date'], + ]; + /** * Execute the console command. */ public function handle(): void { - $models = [ - AccountBalance::class => ['date'], // done - AvailableBudget::class => ['start_date', 'end_date'], // done - Bill::class => ['date', 'end_date', 'extension_date'], // done - BudgetLimit::class => ['start_date', 'end_date'], // done - CurrencyExchangeRate::class => ['date'], // done - InvitedUser::class => ['expires'], - PiggyBankEvent::class => ['date'], - PiggyBankRepetition::class => ['startdate', 'targetdate'], - PiggyBank::class => ['startdate', 'targetdate'], // done - Recurrence::class => ['first_date', 'repeat_until', 'latest_date'], - Tag::class => ['date'], - TransactionJournal::class => ['date'], - ]; - foreach ($models as $model => $fields) { + foreach (self::$models as $model => $fields) { $this->addTimezoneToModel($model, $fields); } } diff --git a/app/Console/Commands/Integrity/ConvertDatesToUTC.php b/app/Console/Commands/Integrity/ConvertDatesToUTC.php new file mode 100644 index 0000000000..9647976aa1 --- /dev/null +++ b/app/Console/Commands/Integrity/ConvertDatesToUTC.php @@ -0,0 +1,102 @@ + $fields) { + $this->ConvertModeltoUTC($model, $fields); + } + + return Command::SUCCESS; + } + + private function ConvertModeltoUTC(string $model, array $fields): void + { + /** @var string $field */ + foreach ($fields as $field) { + $this->convertFieldtoUTC($model, $field); + } + } + + private function convertFieldtoUTC(string $model, string $field): void { + $this->info(sprintf('Converting %s.%s to UTC', $model, $field)); + $shortModel = str_replace('FireflyIII\Models\\', '', $model); + $timezoneField = sprintf('%s_tz', $field); + $items = new Collection(); + $timeZone = config('app.timezone'); + + try { + $items = $model::where($timezoneField, $timeZone)->get(); + } catch (QueryException $e) { + $this->friendlyError(sprintf('Cannot find timezone information to field "%s" of model "%s". Field does not exist.', $field, $shortModel)); + Log::error($e->getMessage()); + } + if (0 === $items->count()) { + $this->friendlyPositive(sprintf('All timezone information is UTC in field "%s" of model "%s".', $field, $shortModel)); + + return; + } + $this->friendlyInfo(sprintf('Converting field "%s" of model "%s" to UTC.', $field, $shortModel)); + $items->each( + function ($item) use ($field, $timezoneField, $timeZone) { + /** @var Carbon $date */ + $date = Carbon::parse($item->$field, $item->$timezoneField); + $date->setTimezone('UTC'); + $item->$field = $date->format('Y-m-d H:i:s'); + $item->$timezoneField = 'UTC'; + $item->save(); + } + ); + } +} diff --git a/app/Models/AccountBalance.php b/app/Models/AccountBalance.php index baa996167d..5f09ff00fa 100644 --- a/app/Models/AccountBalance.php +++ b/app/Models/AccountBalance.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace FireflyIII\Models; +use FireflyIII\Casts\SeparateTimezoneCaster; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -16,6 +17,13 @@ class AccountBalance extends Model use HasFactory; protected $fillable = ['account_id', 'title', 'transaction_currency_id', 'balance', 'date', 'date_tz']; + protected function casts(): array + { + return [ + 'date' => SeparateTimezoneCaster::class, + ]; + } + public function account(): BelongsTo { return $this->belongsTo(Account::class); diff --git a/app/Models/Bill.php b/app/Models/Bill.php index 655fc729d5..5a9a1516b7 100644 --- a/app/Models/Bill.php +++ b/app/Models/Bill.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace FireflyIII\Models; +use FireflyIII\Casts\SeparateTimezoneCaster; use FireflyIII\Support\Models\ReturnsIntegerIdTrait; use FireflyIII\Support\Models\ReturnsIntegerUserIdTrait; use FireflyIII\User; @@ -49,9 +50,9 @@ class Bill extends Model 'created_at' => 'datetime', 'updated_at' => 'datetime', 'deleted_at' => 'datetime', - 'date' => 'date', - 'end_date' => 'date', - 'extension_date' => 'date', + 'date' => SeparateTimezoneCaster::class, + 'end_date' => SeparateTimezoneCaster::class, + 'extension_date' => SeparateTimezoneCaster::class, 'skip' => 'int', 'automatch' => 'boolean', 'active' => 'boolean', diff --git a/app/Models/BudgetLimit.php b/app/Models/BudgetLimit.php index 1fafd7f2e6..faabad9f54 100644 --- a/app/Models/BudgetLimit.php +++ b/app/Models/BudgetLimit.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace FireflyIII\Models; +use FireflyIII\Casts\SeparateTimezoneCaster; use FireflyIII\Events\Model\BudgetLimit\Created; use FireflyIII\Events\Model\BudgetLimit\Deleted; use FireflyIII\Events\Model\BudgetLimit\Updated; @@ -43,8 +44,8 @@ class BudgetLimit extends Model = [ 'created_at' => 'datetime', 'updated_at' => 'datetime', - 'start_date' => 'date', - 'end_date' => 'date', + 'start_date' => SeparateTimezoneCaster::class, + 'end_date' => SeparateTimezoneCaster::class, 'auto_budget' => 'boolean', ]; protected $dispatchesEvents diff --git a/app/Models/CurrencyExchangeRate.php b/app/Models/CurrencyExchangeRate.php index 3cb848bdf0..dd9063f803 100644 --- a/app/Models/CurrencyExchangeRate.php +++ b/app/Models/CurrencyExchangeRate.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace FireflyIII\Models; +use FireflyIII\Casts\SeparateTimezoneCaster; use FireflyIII\Support\Models\ReturnsIntegerIdTrait; use FireflyIII\Support\Models\ReturnsIntegerUserIdTrait; use FireflyIII\User; @@ -47,7 +48,7 @@ class CurrencyExchangeRate extends Model 'user_id' => 'int', 'from_currency_id' => 'int', 'to_currency_id' => 'int', - 'date' => 'datetime', + 'date' => SeparateTimezoneCaster::class, ]; protected $fillable = ['user_id', 'from_currency_id', 'to_currency_id', 'date', 'date_tz', 'rate']; diff --git a/app/Models/InvitedUser.php b/app/Models/InvitedUser.php index 3360413df0..f0634c27db 100644 --- a/app/Models/InvitedUser.php +++ b/app/Models/InvitedUser.php @@ -24,6 +24,7 @@ declare(strict_types=1); namespace FireflyIII\Models; +use FireflyIII\Casts\SeparateTimezoneCaster; use FireflyIII\Support\Models\ReturnsIntegerIdTrait; use FireflyIII\Support\Models\ReturnsIntegerUserIdTrait; use FireflyIII\User; @@ -41,7 +42,7 @@ class InvitedUser extends Model protected $casts = [ - 'expires' => 'datetime', + 'expires' => SeparateTimezoneCaster::class, 'redeemed' => 'boolean', ]; protected $fillable = ['user_id', 'email', 'invite_code', 'expires', 'expires_tz', 'redeemed']; diff --git a/app/Models/PiggyBankEvent.php b/app/Models/PiggyBankEvent.php index 9dc798ab96..035e47dc49 100644 --- a/app/Models/PiggyBankEvent.php +++ b/app/Models/PiggyBankEvent.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace FireflyIII\Models; +use FireflyIII\Casts\SeparateTimezoneCaster; use FireflyIII\Support\Models\ReturnsIntegerIdTrait; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Model; @@ -39,7 +40,7 @@ class PiggyBankEvent extends Model = [ 'created_at' => 'datetime', 'updated_at' => 'datetime', - 'date' => 'date', + 'date' => SeparateTimezoneCaster::class, ]; protected $fillable = ['piggy_bank_id', 'transaction_journal_id', 'date', 'date_tz', 'amount']; diff --git a/app/Models/PiggyBankRepetition.php b/app/Models/PiggyBankRepetition.php index 5c20d75baa..826e3bd627 100644 --- a/app/Models/PiggyBankRepetition.php +++ b/app/Models/PiggyBankRepetition.php @@ -24,6 +24,7 @@ declare(strict_types=1); namespace FireflyIII\Models; use Carbon\Carbon; +use FireflyIII\Casts\SeparateTimezoneCaster; use FireflyIII\Support\Models\ReturnsIntegerIdTrait; use Illuminate\Database\Eloquent\Builder as EloquentBuilder; use Illuminate\Database\Eloquent\Casts\Attribute; @@ -41,8 +42,8 @@ class PiggyBankRepetition extends Model = [ 'created_at' => 'datetime', 'updated_at' => 'datetime', - 'startdate' => 'date', - 'targetdate' => 'date', + 'startdate' => SeparateTimezoneCaster::class, + 'targetdate' => SeparateTimezoneCaster::class, ]; protected $fillable = ['piggy_bank_id', 'startdate', 'startdate_tz', 'targetdate', 'targetdate_tz', 'currentamount']; diff --git a/app/Models/Recurrence.php b/app/Models/Recurrence.php index 61b96fe68e..4c9e67f152 100644 --- a/app/Models/Recurrence.php +++ b/app/Models/Recurrence.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace FireflyIII\Models; +use FireflyIII\Casts\SeparateTimezoneCaster; use FireflyIII\Support\Models\ReturnsIntegerIdTrait; use FireflyIII\Support\Models\ReturnsIntegerUserIdTrait; use FireflyIII\User; @@ -51,9 +52,9 @@ class Recurrence extends Model 'title' => 'string', 'id' => 'int', 'description' => 'string', - 'first_date' => 'date', - 'repeat_until' => 'date', - 'latest_date' => 'date', + 'first_date' => SeparateTimezoneCaster::class, + 'repeat_until' => SeparateTimezoneCaster::class, + 'latest_date' => SeparateTimezoneCaster::class, 'repetitions' => 'int', 'active' => 'bool', 'apply_rules' => 'bool', diff --git a/app/Models/Tag.php b/app/Models/Tag.php index f55b989e0b..fa559ac459 100644 --- a/app/Models/Tag.php +++ b/app/Models/Tag.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace FireflyIII\Models; +use FireflyIII\Casts\SeparateTimezoneCaster; use FireflyIII\Support\Models\ReturnsIntegerIdTrait; use FireflyIII\Support\Models\ReturnsIntegerUserIdTrait; use FireflyIII\User; @@ -47,7 +48,7 @@ class Tag extends Model 'created_at' => 'datetime', 'updated_at' => 'datetime', 'deleted_at' => 'datetime', - 'date' => 'date', + 'date' => SeparateTimezoneCaster::class, 'zoomLevel' => 'int', 'latitude' => 'float', 'longitude' => 'float', diff --git a/app/Models/TransactionJournal.php b/app/Models/TransactionJournal.php index 9fe911a8e7..87779dd2b4 100644 --- a/app/Models/TransactionJournal.php +++ b/app/Models/TransactionJournal.php @@ -24,6 +24,7 @@ declare(strict_types=1); namespace FireflyIII\Models; use Carbon\Carbon; +use FireflyIII\Casts\SeparateTimezoneCaster; use FireflyIII\Support\Models\ReturnsIntegerIdTrait; use FireflyIII\Support\Models\ReturnsIntegerUserIdTrait; use FireflyIII\User; @@ -55,7 +56,7 @@ class TransactionJournal extends Model 'created_at' => 'datetime', 'updated_at' => 'datetime', 'deleted_at' => 'datetime', - 'date' => 'datetime', + 'date' => SeparateTimezoneCaster::class, 'interest_date' => 'date', 'book_date' => 'date', 'process_date' => 'date', diff --git a/resources/views/transactions/show.twig b/resources/views/transactions/show.twig index 4336941245..82307440a7 100644 --- a/resources/views/transactions/show.twig +++ b/resources/views/transactions/show.twig @@ -75,6 +75,9 @@ {{ trans('list.date') }} {{ first.date.isoFormat(dateTimeFormat) }} + {% if(first.date_tz != '') %} + ({{ first.date_tz }}) + {% endif %}