From c536de4b248d4614f200924c32c19f42f05cb71f Mon Sep 17 00:00:00 2001 From: Diego Steiner Date: Mon, 2 Dec 2024 20:42:03 +0000 Subject: [PATCH 01/14] fix: error regression when creating invoices --- app/models/payment_infos/text_payment_info.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/payment_infos/text_payment_info.rb b/app/models/payment_infos/text_payment_info.rb index 228e33cd3..ce79a5edd 100644 --- a/app/models/payment_infos/text_payment_info.rb +++ b/app/models/payment_infos/text_payment_info.rb @@ -9,7 +9,7 @@ class TextPaymentInfo < ::PaymentInfo delegate :esr_beneficiary_account, to: :organisation def body - @body ||= rich_text_template&.interpolate(payment_info: self)&.body + @body ||= rich_text_template&.interpolate({ payment_info: self })&.body end def title From ca5ac405fd2cc51b56bf2c2408fd0e32dff97330 Mon Sep 17 00:00:00 2001 From: Diego Steiner Date: Sun, 27 Oct 2024 15:37:18 +0000 Subject: [PATCH 02/14] feature: add taf format from specs --- app/models/accounting.rb | 20 +++ .../accounting_journal_entry.rb | 82 +++++++++++ app/services/taf_block.rb | 135 ++++++++++++++++++ spec/services/taf_block_spec.rb | 64 +++++++++ 4 files changed, 301 insertions(+) create mode 100644 app/models/accounting.rb create mode 100644 app/models/data_digest_templates/accounting_journal_entry.rb create mode 100644 app/services/taf_block.rb create mode 100644 spec/services/taf_block_spec.rb diff --git a/app/models/accounting.rb b/app/models/accounting.rb new file mode 100644 index 000000000..69ed7ece7 --- /dev/null +++ b/app/models/accounting.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Accounting + class Factory + def initialize(invoice) + @invoice = invoice + end + + def call; end + end + + JournalEntry = Data.define(:account_id, :b_type, :cost_account_id, :cost_index, :code, :date, + :flags, :tax_id, :text, :tax_index, :type, :amount_netto, :amount_brutto, :amount_tax, + :op_id, :pk_id) do + def initialize(**args) + defaults = members.index_with(nil).merge({}) + super(defaults.merge(args)) + end + end +end diff --git a/app/models/data_digest_templates/accounting_journal_entry.rb b/app/models/data_digest_templates/accounting_journal_entry.rb new file mode 100644 index 000000000..90e8cff5f --- /dev/null +++ b/app/models/data_digest_templates/accounting_journal_entry.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: data_digest_templates +# +# id :bigint not null, primary key +# columns_config :jsonb +# group :string +# label :string +# prefilter_params :jsonb +# type :string +# created_at :datetime not null +# updated_at :datetime not null +# organisation_id :bigint not null +# +# Indexes +# +# index_data_digest_templates_on_organisation_id (organisation_id) +# +# Foreign Keys +# +# fk_rails_... (organisation_id => organisations.id) +# + +module DataDigestTemplates + class AccountingJournalEntry < DataDigestTemplate + ::DataDigestTemplate.register_subtype self + + DEFAULT_COLUMN_CONFIG = [ + { + header: ::Invoice.human_attribute_name(:ref), + body: '{{ invoice.ref }}' + }, + { + header: ::Booking.human_attribute_name(:ref), + body: '{{ booking.ref }}' + }, + { + header: ::Invoice.human_attribute_name(:issued_at), + body: '{{ invoice.issued_at | datetime_format }}' + }, + { + header: ::Invoice.human_attribute_name(:payable_until), + body: '{{ invoice.payable_until | datetime_format }}' + }, + { + header: ::Invoice.human_attribute_name(:amount), + body: '{{ invoice.amount }}' + }, + { + header: ::Invoice.human_attribute_name(:amount_paid), + body: '{{ invoice.amount_paid }}' + } + # { + # header: ::Invoice.human_attribute_name(:amount_paid), + # body: '{{ invoice.percentage_paid | times: 100 }}%' + # }, + ].freeze + + column_type :default do + body do |invoice, template_context_cache| + booking = invoice.booking + context = template_context_cache[cache_key(invoice)] ||= + TemplateContext.new(booking:, invoice:, organisation: booking.organisation).to_h + @templates[:body]&.render!(context) + end + end + + def periodfilter(period = nil) + filter_class.new(issued_at_after: period&.begin, issued_at_before: period&.end) + end + + def filter_class + ::Invoice::Filter + end + + def base_scope + @base_scope ||= ::Invoice.joins(:booking).where(bookings: { organisation_id: organisation }).kept + end + end +end diff --git a/app/services/taf_block.rb b/app/services/taf_block.rb new file mode 100644 index 000000000..8b3cae76e --- /dev/null +++ b/app/services/taf_block.rb @@ -0,0 +1,135 @@ +# frozen_string_literal: true + +class TafBlock + INDENTOR = ' ' + SEPARATOR = "\n" + attr_reader :type, :properties, :children + + def initialize(type, *children, **properties, &) + @type = type + @children = children.select { _1.is_a?(TafBlock) } + @properties = properties + + yield self if block_given? + end + + def property(**new_properties) + @properties.merge!(new_properties) + end + + def child(*new_children) + @children += new_children.select { _1.is_a?(TafBlock) } + end + + def serialize(indent_level: 0, indent_with: ' ', separate_with: "\n") # rubocop:disable Metrics/MethodLength + indent = [indent_with * indent_level].join + separate_and_indent = [separate_with, indent, indent_with].join + serialized_children = children.map do |child| + child.serialize(indent_level: indent_level + 1, indent_with:, separate_with:) + end + + [ # tag_start + indent, "{#{type}", + # properties + separate_and_indent, self.class.serialize_properies(properties, join_with: separate_and_indent), + # children + (children.present? && separate_with) || nil, serialized_children&.join(separate_with), + # tag end + separate_with, indent, '}' + ].compact.join + end + + def to_s + serialize + end + + def self.serialize_value(value) # rubocop:disable Metrics/MethodLength + case value + when ::FalseClass, ::TrueClass + value ? '1' : '0' + when ::BigDecimal, ::Float + format('%.2f', value) + when ::Numeric + value.to_s + when ::Date + value.strftime('%d.%m.%Y') + else + "\"#{value.to_s.gsub('"', '""')}\"" + end + end + + def self.serialize_properies(properties, join_with: ' ') + return '' unless properties.is_a?(Hash) + + properties.compact.flat_map { |key, value| "#{key}=#{serialize_value(value)}" }.join(join_with) + end + + def self.converters + @converters ||= {} + end + + def self.register_converter(klass, &conversion_block) + converters[klass] = conversion_block + end + + def self.convert(value, **options) + conversion_block = converters[converters.keys.find { |klass| value.is_a?(klass) }] + instance_exec(value, options, &conversion_block) if conversion_block.present? + end + + register_converter Accounting::JournalEntry do |value, _options| + new(:Bk, **{ + # The Id of a book keeping account. [Fibu-Konto] + AccId: value.account_id, + # Integer; Booking type: 1=cost booking, 2=tax booking + BType: value.b_type.presence || 1, + # String[13], This is the cost type account + CAcc: value.cost_account_id, + # Integer; This is the index of the booking that represents the cost booking which is attached to this booking + CIdx: value.cost_index, + # String[9]; A user definable code. + Code: value.code&.slice(0..8), + # Date; The date of the booking. + Date: value.date, + # IntegerAuxilliary flags. This value consists of the sum of one or more of + # the following biases: + # 1 - The booking is the first one into the specified OP. + # 16 - This is a hidden booking. [Transitorische] + # 32 - This booking is the exit booking, as oposed to the return booking. + # Only valid if the hidden flag is set. + Flags: value.flags, + # String[5]; The Id of the tax. [MWSt-Kürzel] + TaxId: value.tax_id, + # String[61*]; This string specifies the first line of the booking text. + Text: value.text&.slice(0..59)&.lines&.first&.strip || '-', + # String[*]; This string specifies the second line of the booking text. + # (*)Both fields Text and Text2 are stored in the same memory location, + # which means their total length may not exceed 60 characters (1 char is + # required internally). + # Be careful not to put too many characters onto one single line, because + # most Reports are not designed to display a full string containing 60 + # characters. + Text2: value.text&.slice(0..59)&.lines&.[](1..-1)&.join("\n"), + # Integer; This is the index of the booking that represents the tax booking + # which is attached to this booking. + TIdx: value.tax_index, + # BooleanBooking type. + # 0 a debit booking [Soll] + # 1 a credit booking [Haben] + Type: value.type.to_i, + # Currency; The net amount for this booking. [Netto-Betrag] + ValNt: value.amount_netto, + # Currency; The tax amount for this booking. [Brutto-Betrag] + ValBt: value.amount_brutto, + # Currency; The tax amount for this booking. [Steuer-Betrag] + ValTx: value.amount_tax, + # Currency; The gross amount for this booking in the foreign currency specified + # by currency of the account AccId. [FW-Betrag] + # ValFW : + # String[13]The OP id of this booking. + OpId: value.op_id, + # The PK number of this booking. + PkKey: value.pk_id + }) + end +end diff --git a/spec/services/taf_block_spec.rb b/spec/services/taf_block_spec.rb new file mode 100644 index 000000000..ddafde26b --- /dev/null +++ b/spec/services/taf_block_spec.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe TafBlock, type: :model do + subject(:taf_block) do + described_class.new(:Blg, text: 'TAF is "great"') do |taf| + taf.property test: 1 + taf.child described_class.new(:Bk, test: 2) + end + end + + describe '#initialize' do + it 'works as DSL' do + expect(taf_block.type).to eq(:Blg) + expect(taf_block.properties).to eq({ test: 1, text: 'TAF is "great"' }) + expect(taf_block.children.count).to eq(1) + expect(taf_block.children.first.type).to eq(:Bk) + expect(taf_block.children.first.properties).to eq({ test: 2 }) + expect(taf_block.children.first.children.count).to eq(0) + end + end + + describe '#serialize' do + subject(:serialize) { taf_block.serialize.chomp } + + it 'returns serialized string' do + is_expected.to eq(<<~TAF.chomp) + {Blg + text="TAF is ""great""" + test=1 + {Bk + test=2 + } + } + TAF + end + end + + context '::convert' do + describe 'Accounting::JournalEntry' do + subject(:converted) { described_class.convert(conversion_subject) } + let(:conversion_subject) do + Accounting::JournalEntry.new(cost_account_id: 1050, amount_netto: 2091.75, date: Date.new(2024, 10, 5), + text: "Lorem ipsum\nSecond Line, but its longer than sixty \"chars\", so nope!") + end + + it 'converts correctly' do + is_expected.to be_a described_class + expect(converted.to_s).to eq(<<~TAF.chomp) + {Bk + BType=1 + CAcc=1050 + Date=05.10.2024 + Text="Lorem ipsum" + Text2="Second Line, but its longer than sixty ""chars"", " + Type=0 + ValNt=2091.75 + } + TAF + end + end + end +end From d5b201da56336a71fae4093d53c07972c372429d Mon Sep 17 00:00:00 2001 From: Diego Steiner Date: Thu, 31 Oct 2024 10:07:26 +0000 Subject: [PATCH 03/14] feat: add journal entries --- app/models/accounting.rb | 18 ++-- app/models/application_record.rb | 1 + app/models/invoice.rb | 11 +++ app/models/invoice_part.rb | 4 + app/models/invoice_parts/add.rb | 8 ++ app/models/tarif.rb | 2 +- app/models/tarifs/amount.rb | 2 +- app/models/tarifs/flat.rb | 2 +- app/models/tarifs/group_minimum.rb | 2 +- app/models/tarifs/metered.rb | 2 +- app/models/tarifs/overnight_stay.rb | 2 +- app/models/tarifs/price.rb | 2 +- app/models/tenant.rb | 1 + app/params/manage/tarif_params.rb | 2 +- app/serializers/manage/tarif_serializer.rb | 2 +- app/services/taf_block.rb | 96 ++++++++++++------- app/views/manage/tarifs/_form.html.slim | 2 +- config/locales/de.yml | 2 +- config/locales/en.yml | 2 +- config/locales/fr.yml | 2 +- config/locales/it.yml | 2 +- ...241030163341_add_accounting_information.rb | 6 ++ db/schema.rb | 3 +- spec/factories/tarifs.rb | 2 +- spec/factories/tenants.rb | 1 + spec/models/tarif_spec.rb | 2 +- spec/models/tarifs/group_minimum_spec.rb | 2 +- spec/models/tenant_spec.rb | 1 + spec/services/taf_block_spec.rb | 35 ++++++- 29 files changed, 155 insertions(+), 64 deletions(-) create mode 100644 db/migrate/20241030163341_add_accounting_information.rb diff --git a/app/models/accounting.rb b/app/models/accounting.rb index 69ed7ece7..46438fd90 100644 --- a/app/models/accounting.rb +++ b/app/models/accounting.rb @@ -1,19 +1,19 @@ # frozen_string_literal: true module Accounting - class Factory - def initialize(invoice) - @invoice = invoice + JournalEntry = Data.define(:date, :items) do + def initialize(**args) + defaults = { items: [] } + super(defaults.merge(args)) end - - def call; end end - JournalEntry = Data.define(:account_id, :b_type, :cost_account_id, :cost_index, :code, :date, - :flags, :tax_id, :text, :tax_index, :type, :amount_netto, :amount_brutto, :amount_tax, - :op_id, :pk_id) do + JournalEntryItem = Data.define(:account, :date, :tax_code, :text, :amount, :side, :cost_center, + :index, :amount_type, :source) do + delegate :invoice, to: :invoice_part + def initialize(**args) - defaults = members.index_with(nil).merge({}) + defaults = { index: nil, tax_code: nil, text: '', cost_center:, source: nil } super(defaults.merge(args)) end end diff --git a/app/models/application_record.rb b/app/models/application_record.rb index 0c450deef..dca40d63f 100644 --- a/app/models/application_record.rb +++ b/app/models/application_record.rb @@ -2,6 +2,7 @@ class ApplicationRecord < ActiveRecord::Base self.abstract_class = true + self.attributes_for_inspect = :all def self.human_enum(enum, value, default: '-', **) return default if value.blank? diff --git a/app/models/invoice.rb b/app/models/invoice.rb index 9de427713..85f6ebbe9 100644 --- a/app/models/invoice.rb +++ b/app/models/invoice.rb @@ -190,4 +190,15 @@ def vat [vat, { tax:, total: }] end end + + def journal_entry_item + Accounting::JournalEntryItem.new(account: booking.tenant.accounting_account_nr, date: sent_at, + amount: amount, amount_type: :brutto, side: 1, + text: ref, source: self) + end + + def journal_entry + Accounting::JournalEntry.new(date: sent_at, + items: [journal_entry_item] + invoice_parts.flat_map(&:journal_entry_items)) + end end diff --git a/app/models/invoice_part.rb b/app/models/invoice_part.rb index 219b3bd65..dbb43c535 100644 --- a/app/models/invoice_part.rb +++ b/app/models/invoice_part.rb @@ -62,6 +62,10 @@ def to_sum(sum) sum + calculated_amount end + def journal_entry_items + nil + end + def self.from_usage(usage, **attributes) return unless usage diff --git a/app/models/invoice_parts/add.rb b/app/models/invoice_parts/add.rb index e5808c556..53beb0fc3 100644 --- a/app/models/invoice_parts/add.rb +++ b/app/models/invoice_parts/add.rb @@ -29,5 +29,13 @@ class Add < InvoicePart def calculated_amount amount end + + def journal_entry_items + [ + Accounting::JournalEntryItem.new(account: usage&.tarif&.accounting_account_nr, date: invoice.sent_at, + amount: (amount / ((100 + (vat || 0))) * 100), amount_type: :netto, + side: -1, tax_code: vat.to_s, text: invoice.ref, source: self) + ] + end end end diff --git a/app/models/tarif.rb b/app/models/tarif.rb index df97ea55e..1ebb8cbc3 100644 --- a/app/models/tarif.rb +++ b/app/models/tarif.rb @@ -5,7 +5,7 @@ # Table name: tarifs # # id :bigint not null, primary key -# accountancy_account :string +# accounting_account_nr :string # associated_types :integer default(0), not null # discarded_at :datetime # label_i18n :jsonb diff --git a/app/models/tarifs/amount.rb b/app/models/tarifs/amount.rb index a8311d415..b989d8b1f 100644 --- a/app/models/tarifs/amount.rb +++ b/app/models/tarifs/amount.rb @@ -5,7 +5,7 @@ # Table name: tarifs # # id :bigint not null, primary key -# accountancy_account :string +# accounting_account_nr :string # associated_types :integer default(0), not null # discarded_at :datetime # label_i18n :jsonb diff --git a/app/models/tarifs/flat.rb b/app/models/tarifs/flat.rb index 51dcbd8c4..266f5a471 100644 --- a/app/models/tarifs/flat.rb +++ b/app/models/tarifs/flat.rb @@ -5,7 +5,7 @@ # Table name: tarifs # # id :bigint not null, primary key -# accountancy_account :string +# accounting_account_nr :string # associated_types :integer default(0), not null # discarded_at :datetime # label_i18n :jsonb diff --git a/app/models/tarifs/group_minimum.rb b/app/models/tarifs/group_minimum.rb index 0ed4ff76f..1195ffc0a 100644 --- a/app/models/tarifs/group_minimum.rb +++ b/app/models/tarifs/group_minimum.rb @@ -5,7 +5,7 @@ # Table name: tarifs # # id :bigint not null, primary key -# accountancy_account :string +# accounting_account_nr :string # associated_types :integer default(0), not null # discarded_at :datetime # label_i18n :jsonb diff --git a/app/models/tarifs/metered.rb b/app/models/tarifs/metered.rb index 5fcff9bae..878ecf941 100644 --- a/app/models/tarifs/metered.rb +++ b/app/models/tarifs/metered.rb @@ -5,7 +5,7 @@ # Table name: tarifs # # id :bigint not null, primary key -# accountancy_account :string +# accounting_account_nr :string # associated_types :integer default(0), not null # discarded_at :datetime # label_i18n :jsonb diff --git a/app/models/tarifs/overnight_stay.rb b/app/models/tarifs/overnight_stay.rb index 6208be456..6171c7009 100644 --- a/app/models/tarifs/overnight_stay.rb +++ b/app/models/tarifs/overnight_stay.rb @@ -5,7 +5,7 @@ # Table name: tarifs # # id :bigint not null, primary key -# accountancy_account :string +# accounting_account_nr :string # associated_types :integer default(0), not null # discarded_at :datetime # label_i18n :jsonb diff --git a/app/models/tarifs/price.rb b/app/models/tarifs/price.rb index 3415633b1..8360cecb2 100644 --- a/app/models/tarifs/price.rb +++ b/app/models/tarifs/price.rb @@ -5,7 +5,7 @@ # Table name: tarifs # # id :bigint not null, primary key -# accountancy_account :string +# accounting_account_nr :string # associated_types :integer default(0), not null # discarded_at :datetime # label_i18n :jsonb diff --git a/app/models/tenant.rb b/app/models/tenant.rb index 9df5b0dd4..75b77c910 100644 --- a/app/models/tenant.rb +++ b/app/models/tenant.rb @@ -5,6 +5,7 @@ # Table name: tenants # # id :bigint not null, primary key +# accounting_account_nr :string # address_addon :string # birth_date :date # bookings_without_contract :boolean default(FALSE) diff --git a/app/params/manage/tarif_params.rb b/app/params/manage/tarif_params.rb index fbdd8aa0b..bf2e2a1a9 100644 --- a/app/params/manage/tarif_params.rb +++ b/app/params/manage/tarif_params.rb @@ -3,7 +3,7 @@ module Manage class TarifParams < ApplicationParams def self.permitted_keys - %i[type label unit price_per_unit ordinal tarif_group accountancy_account pin + %i[type label unit price_per_unit ordinal tarif_group accounting_account_nr pin prefill_usage_method prefill_usage_booking_question_id vat minimum_usage_per_night minimum_usage_total minimum_price_per_night minimum_price_total] + I18n.available_locales.map { |locale| ["label_#{locale}", "unit_#{locale}"] }.flatten + diff --git a/app/serializers/manage/tarif_serializer.rb b/app/serializers/manage/tarif_serializer.rb index fd1d5b888..cc1fd8c02 100644 --- a/app/serializers/manage/tarif_serializer.rb +++ b/app/serializers/manage/tarif_serializer.rb @@ -3,7 +3,7 @@ module Manage class TarifSerializer < ApplicationSerializer fields :label, :pin, :prefill_usage_method, :price_per_unit, :tarif_group, :type, :unit, :ordinal, - :label_i18n, :unit_i18n, :valid_from, :valid_until, :accountancy_account, + :label_i18n, :unit_i18n, :valid_from, :valid_until, :accounting_account_nr, :minimum_usage_per_night, :minimum_usage_total field :associated_types do |tarif| diff --git a/app/services/taf_block.rb b/app/services/taf_block.rb index 8b3cae76e..11cae29bc 100644 --- a/app/services/taf_block.rb +++ b/app/services/taf_block.rb @@ -21,12 +21,10 @@ def child(*new_children) @children += new_children.select { _1.is_a?(TafBlock) } end - def serialize(indent_level: 0, indent_with: ' ', separate_with: "\n") # rubocop:disable Metrics/MethodLength + def serialize(indent_level: 0, indent_with: ' ', separate_with: "\n") indent = [indent_with * indent_level].join separate_and_indent = [separate_with, indent, indent_with].join - serialized_children = children.map do |child| - child.serialize(indent_level: indent_level + 1, indent_with:, separate_with:) - end + serialized_children = serialize_children(indent_level:, indent_with:, separate_with:) [ # tag_start indent, "{#{type}", @@ -51,10 +49,16 @@ def self.serialize_value(value) # rubocop:disable Metrics/MethodLength format('%.2f', value) when ::Numeric value.to_s - when ::Date + when ::Date, ::DateTime value.strftime('%d.%m.%Y') else - "\"#{value.to_s.gsub('"', '""')}\"" + "\"#{value.to_s.gsub('"', '""')}\"".presence + end + end + + def serialize_children(indent_level:, indent_with:, separate_with:) + children.map do |child| + child.serialize(indent_level: indent_level + 1, indent_with:, separate_with:) end end @@ -64,44 +68,62 @@ def self.serialize_properies(properties, join_with: ' ') properties.compact.flat_map { |key, value| "#{key}=#{serialize_value(value)}" }.join(join_with) end - def self.converters - @converters ||= {} + def self.derivers + @derivers ||= {} + end + + def self.register_deriver(klass, &derive_block) + derivers[klass] = derive_block end - def self.register_converter(klass, &conversion_block) - converters[klass] = conversion_block + def self.derive(value, **options) + derive_block = derivers[derivers.keys.find { |klass| value.is_a?(klass) }] + instance_exec(value, options, &derive_block) if derive_block.present? end - def self.convert(value, **options) - conversion_block = converters[converters.keys.find { |klass| value.is_a?(klass) }] - instance_exec(value, options, &conversion_block) if conversion_block.present? + register_deriver Accounting::JournalEntry do |value, **options| + new(:Blg, *value.items.map { TafBlock.serialize(_1) }, **{ + # Date; The date of the booking. + Date: options.fetch(:Date, value.date), + + Orig: true + }) end - register_converter Accounting::JournalEntry do |value, _options| + register_deriver Accounting::JournalEntryItem do |value, **options| new(:Bk, **{ # The Id of a book keeping account. [Fibu-Konto] - AccId: value.account_id, + AccId: options.fetch(:AccId, value.account), + # Integer; Booking type: 1=cost booking, 2=tax booking - BType: value.b_type.presence || 1, + BType: options.fetch(:BType, value.amount_type == :tax || 1), + # String[13], This is the cost type account - CAcc: value.cost_account_id, + CAcc: options.fetch(:CAcc, value.cost_center), + # Integer; This is the index of the booking that represents the cost booking which is attached to this booking - CIdx: value.cost_index, + CIdx: options.fetch(:CIdx, value.index), + # String[9]; A user definable code. - Code: value.code&.slice(0..8), + Code: options.fetch(:Code, nil)&.slice(0..8), + # Date; The date of the booking. - Date: value.date, + Date: options.fetch(:Date, value.date), + # IntegerAuxilliary flags. This value consists of the sum of one or more of # the following biases: # 1 - The booking is the first one into the specified OP. # 16 - This is a hidden booking. [Transitorische] # 32 - This booking is the exit booking, as oposed to the return booking. # Only valid if the hidden flag is set. - Flags: value.flags, + Flags: options.fetch(:Flags, nil), + # String[5]; The Id of the tax. [MWSt-Kürzel] - TaxId: value.tax_id, + TaxId: options.fetch(:TaxId, value.tax_code), + # String[61*]; This string specifies the first line of the booking text. - Text: value.text&.slice(0..59)&.lines&.first&.strip || '-', + Text: options.fetch(:Text, value.text&.slice(0..59)&.lines&.first&.strip || '-'), + # String[*]; This string specifies the second line of the booking text. # (*)Both fields Text and Text2 are stored in the same memory location, # which means their total length may not exceed 60 characters (1 char is @@ -109,27 +131,35 @@ def self.convert(value, **options) # Be careful not to put too many characters onto one single line, because # most Reports are not designed to display a full string containing 60 # characters. - Text2: value.text&.slice(0..59)&.lines&.[](1..-1)&.join("\n"), + Text2: options.fetch(:Text2, value.text&.slice(0..59)&.lines&.[](1..-1)&.join("\n")), + # Integer; This is the index of the booking that represents the tax booking # which is attached to this booking. - TIdx: value.tax_index, - # BooleanBooking type. + TIdx: options.fetch(:TIdx, (value.amount_type == :tax && value.index) || nil), + + # Boolean; Booking type. # 0 a debit booking [Soll] # 1 a credit booking [Haben] - Type: value.type.to_i, + Type: options.fetch(:Type, { 1 => 0, -1 => 1 }[value.side]), + # Currency; The net amount for this booking. [Netto-Betrag] - ValNt: value.amount_netto, + ValNt: options.fetch(:ValNt, value.amount_type == :netto ? value.amount : nil), + # Currency; The tax amount for this booking. [Brutto-Betrag] - ValBt: value.amount_brutto, + ValBt: options.fetch(:ValBt, value.amount_type == :brutto ? value.amount : nil), + # Currency; The tax amount for this booking. [Steuer-Betrag] - ValTx: value.amount_tax, + ValTx: options.fetch(:ValTx, value.amount_type == :tax ? value.amount : nil), + # Currency; The gross amount for this booking in the foreign currency specified # by currency of the account AccId. [FW-Betrag] - # ValFW : + # ValFW : not implemented + # String[13]The OP id of this booking. - OpId: value.op_id, + OpId: options.fetch(:OpId, nil), + # The PK number of this booking. - PkKey: value.pk_id + PkKey: options.fetch(:PkKey, nil) }) end end diff --git a/app/views/manage/tarifs/_form.html.slim b/app/views/manage/tarifs/_form.html.slim index e4ae5c4e9..ac4127134 100644 --- a/app/views/manage/tarifs/_form.html.slim +++ b/app/views/manage/tarifs/_form.html.slim @@ -47,7 +47,7 @@ .row .col-md-6= f.collection_select :prefill_usage_booking_question_id, @tarif.prefill_usage_booking_questions, :id, :label, include_blank: true .col-md-6= f.select :prefill_usage_method, enum_options_for_select(Tarif, :prefill_usage_methods, @tarif.prefill_usage_method), include_blank: true - = f.text_field :accountancy_account + = f.text_field :accounting_account_nr = f.text_field :vat, step: 0.1, inputmode: "numeric" fieldset.mt-4 diff --git a/config/locales/de.yml b/config/locales/de.yml index 30d17e501..115b75bab 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -332,7 +332,7 @@ de: namespace: Bereich title: Titel tarif: - accountancy_account: Buchhaltungskonto + accounting_account_nr: Buchhaltungskonto associated_types: Ausgewiesen in Dokumenten enabling_conditions: Bedingungen für erlaubte Auswahl label: Tarifbezeichnung diff --git a/config/locales/en.yml b/config/locales/en.yml index 2bf2412ab..002801f2f 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -268,7 +268,7 @@ en: namespace: Bereich title: Titel tarif: - accountancy_account: zugewiesenes Buchhaltungskonto + accounting_account_nr: zugewiesenes Buchhaltungskonto associated_types: Ausgewiesen in Rechnungsarten enabling_conditions: Bedingungen für erlaubte Auswahl label: Tarifbezeichnung diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 307240bf2..d397fca35 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -330,7 +330,7 @@ fr: namespace: Domaine title: Titre tarif: - accountancy_account: Compte comptable + accounting_account_nr: Compte comptable associated_types: Affiché dans les documents enabling_conditions: Conditions de sélection autorisée label: Désignation du tarif diff --git a/config/locales/it.yml b/config/locales/it.yml index 9fb672da8..cb10948f2 100644 --- a/config/locales/it.yml +++ b/config/locales/it.yml @@ -330,7 +330,7 @@ it: namespace: title: Titolo tarif: - accountancy_account: Conto contabile + accounting_account_nr: Conto contabile associated_types: Indicato nei documenti enabling_conditions: Condizioni per la selezione consentita label: Designazione tariffa diff --git a/db/migrate/20241030163341_add_accounting_information.rb b/db/migrate/20241030163341_add_accounting_information.rb new file mode 100644 index 000000000..a1afa0c8f --- /dev/null +++ b/db/migrate/20241030163341_add_accounting_information.rb @@ -0,0 +1,6 @@ +class AddAccountingInformation < ActiveRecord::Migration[7.2] + def change + add_column :tenants, :accounting_account_nr, :string, null: true + rename_column :tarifs, :accountancy_account, :accounting_account_nr + end +end diff --git a/db/schema.rb b/db/schema.rb index 996b6f6af..4af65c04b 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -532,7 +532,7 @@ t.datetime "updated_at", precision: nil, null: false t.jsonb "label_i18n", default: {} t.jsonb "unit_i18n", default: {} - t.string "accountancy_account" + t.string "accounting_account_nr" t.integer "associated_types", default: 0, null: false t.decimal "minimum_usage_per_night" t.decimal "minimum_usage_total" @@ -572,6 +572,7 @@ t.string "locale" t.boolean "bookings_without_invoice", default: false t.integer "salutation_form" + t.string "accounting_account_nr" t.index ["email", "organisation_id"], name: "index_tenants_on_email_and_organisation_id", unique: true t.index ["email"], name: "index_tenants_on_email" t.index ["organisation_id"], name: "index_tenants_on_organisation_id" diff --git a/spec/factories/tarifs.rb b/spec/factories/tarifs.rb index ad8ccb11a..c188f39c8 100644 --- a/spec/factories/tarifs.rb +++ b/spec/factories/tarifs.rb @@ -5,7 +5,7 @@ # Table name: tarifs # # id :bigint not null, primary key -# accountancy_account :string +# accounting_account_nr :string # associated_types :integer default(0), not null # discarded_at :datetime # label_i18n :jsonb diff --git a/spec/factories/tenants.rb b/spec/factories/tenants.rb index cb99ed8aa..9068f4ee8 100644 --- a/spec/factories/tenants.rb +++ b/spec/factories/tenants.rb @@ -5,6 +5,7 @@ # Table name: tenants # # id :bigint not null, primary key +# accounting_account_nr :string # address_addon :string # birth_date :date # bookings_without_contract :boolean default(FALSE) diff --git a/spec/models/tarif_spec.rb b/spec/models/tarif_spec.rb index aa72db44e..54d0e78a0 100644 --- a/spec/models/tarif_spec.rb +++ b/spec/models/tarif_spec.rb @@ -5,7 +5,7 @@ # Table name: tarifs # # id :bigint not null, primary key -# accountancy_account :string +# accounting_account_nr :string # associated_types :integer default(0), not null # discarded_at :datetime # label_i18n :jsonb diff --git a/spec/models/tarifs/group_minimum_spec.rb b/spec/models/tarifs/group_minimum_spec.rb index 768160a40..4d6cbaef1 100644 --- a/spec/models/tarifs/group_minimum_spec.rb +++ b/spec/models/tarifs/group_minimum_spec.rb @@ -5,7 +5,7 @@ # Table name: tarifs # # id :bigint not null, primary key -# accountancy_account :string +# accounting_account_nr :string # associated_types :integer default(0), not null # discarded_at :datetime # label_i18n :jsonb diff --git a/spec/models/tenant_spec.rb b/spec/models/tenant_spec.rb index 4b9d6776a..c24f7b56d 100644 --- a/spec/models/tenant_spec.rb +++ b/spec/models/tenant_spec.rb @@ -5,6 +5,7 @@ # Table name: tenants # # id :bigint not null, primary key +# accounting_account_nr :string # address_addon :string # birth_date :date # bookings_without_contract :boolean default(FALSE) diff --git a/spec/services/taf_block_spec.rb b/spec/services/taf_block_spec.rb index ddafde26b..6c0f75b63 100644 --- a/spec/services/taf_block_spec.rb +++ b/spec/services/taf_block_spec.rb @@ -38,20 +38,47 @@ end context '::convert' do - describe 'Accounting::JournalEntry' do + describe 'Accounting::JournalEntryItem' do subject(:converted) { described_class.convert(conversion_subject) } let(:conversion_subject) do - Accounting::JournalEntry.new(cost_account_id: 1050, amount_netto: 2091.75, date: Date.new(2024, 10, 5), - text: "Lorem ipsum\nSecond Line, but its longer than sixty \"chars\", so nope!") + Accounting::JournalEntryItem.new(account: 1050, amount: 2091.75, date: Date.new(2024, 10, 5), + amount_type: :netto, side: 1, tax_code: 'MwSt38', + text: "Lorem ipsum\nSecond Line, but its longer than sixty \"chars\", OMG!") end it 'converts correctly' do is_expected.to be_a described_class expect(converted.to_s).to eq(<<~TAF.chomp) {Bk + AccId=1050 BType=1 - CAcc=1050 Date=05.10.2024 + TaxId="MwSt38" + Text="Lorem ipsum" + Text2="Second Line, but its longer than sixty ""chars"", " + Type=0 + ValNt=2091.75 + } + TAF + end + end + + describe 'Accounting::JournalEntryItem' do + subject(:converted) { described_class.convert(conversion_subject) } + let(:conversion_subject) do + Accounting::JournalEntryItem.new(account: 1050, amount: 2091.75, date: Date.new(2024, 10, 5), + amount_type: :netto, side: 1, tax_code: 'USt38', + text: "Lorem ipsum\nSecond Line, but its longer than sixty \"chars\", OMG!") + end + + it 'converts correctly' do + is_expected.to be_a described_class + expect(converted.to_s).to eq(<<~TAF.chomp) + {Bk + AccId=1050 + BType=1 + Date=05.10.2024 + TaxId="MwSt38" Text="Lorem ipsum" Text2="Second Line, but its longer than sixty ""chars"", " Type=0 From f8e6d9e7f265dc15c29e10ee93ab4fed674c11f7 Mon Sep 17 00:00:00 2001 From: Diego Steiner Date: Wed, 6 Nov 2024 16:36:36 +0100 Subject: [PATCH 04/14] feat: remodel data_digests to support non-tabular datastructures --- .../manage/data_digests_controller.rb | 1 + app/models/accounting.rb | 34 +++- app/models/data_digest.rb | 48 +----- app/models/data_digest_template.rb | 92 +---------- .../accounting_journal_entry.rb | 58 ++----- app/models/data_digest_templates/booking.rb | 2 +- app/models/data_digest_templates/invoice.rb | 2 +- .../data_digest_templates/invoice_part.rb | 2 +- .../meter_reading_period.rb | 2 +- app/models/data_digest_templates/payment.rb | 2 +- app/models/data_digest_templates/tabular.rb | 154 ++++++++++++++++++ app/models/data_digest_templates/tenant.rb | 2 +- app/models/invoice.rb | 11 +- app/models/invoice_parts/add.rb | 3 +- app/services/taf_block.rb | 34 ++-- .../data_digest_templates/_form.html.slim | 5 +- app/views/manage/data_digests/show.html.slim | 18 +- .../_form_fields.html.slim | 0 .../_show_data_digest.html.slim | 18 ++ .../booking/_form_fields.html.slim | 1 + .../booking/_show_data_digest.html.slim | 1 + .../tabular/_form_fields.html.slim | 4 + config/initializers/mime_types.rb | 2 + 23 files changed, 263 insertions(+), 233 deletions(-) create mode 100644 app/models/data_digest_templates/tabular.rb create mode 100644 app/views/renderables/data_digest_templates/accounting_journal_entry/_form_fields.html.slim create mode 100644 app/views/renderables/data_digest_templates/accounting_journal_entry/_show_data_digest.html.slim create mode 100644 app/views/renderables/data_digest_templates/booking/_form_fields.html.slim create mode 100644 app/views/renderables/data_digest_templates/booking/_show_data_digest.html.slim create mode 100644 app/views/renderables/data_digest_templates/tabular/_form_fields.html.slim diff --git a/app/controllers/manage/data_digests_controller.rb b/app/controllers/manage/data_digests_controller.rb index f861617db..0c6092946 100644 --- a/app/controllers/manage/data_digests_controller.rb +++ b/app/controllers/manage/data_digests_controller.rb @@ -15,6 +15,7 @@ def show format.html format.csv { send_data @data_digest.format(:csv), filename: "#{@data_digest.label}.csv" } format.pdf { send_data @data_digest.format(:pdf), filename: "#{@data_digest.label}.pdf" } + format.taf { send_data @data_digest.format(:taf), filename: "#{@data_digest.label}.taf" } end rescue Prawn::Errors::CannotFit redirect_to manage_data_digests_path, alert: t('.pdf_error') diff --git a/app/models/accounting.rb b/app/models/accounting.rb index 46438fd90..2a82a29d8 100644 --- a/app/models/accounting.rb +++ b/app/models/accounting.rb @@ -1,20 +1,38 @@ # frozen_string_literal: true module Accounting - JournalEntry = Data.define(:date, :items) do + JournalEntry = Data.define(:id, :date, :invoice_id, :items) do + extend ActiveModel::Translation + extend ActiveModel::Naming + def initialize(**args) - defaults = { items: [] } - super(defaults.merge(args)) + args.symbolize_keys! + date = args.delete(:date)&.then { _1.try(:to_date) || Date.parse(_1).to_date } + items = Array.wrap(args.delete(:items)).map do |item| + case item + when Hash, JournalEntryItem + JournalEntryItem.new(**item.to_h.merge(journal_entry: self)) + end + end.compact + super(id: nil, **args, items:, date:) + end + + def to_h + super.merge(items: items.map(&:to_h)) end end - JournalEntryItem = Data.define(:account, :date, :tax_code, :text, :amount, :side, :cost_center, - :index, :amount_type, :source) do - delegate :invoice, to: :invoice_part + JournalEntryItem = Data.define(:id, :account, :date, :tax_code, :text, :amount, :side, :cost_center, + :index, :amount_type, :source, :invoice_id) do + extend ActiveModel::Translation + extend ActiveModel::Naming def initialize(**args) - defaults = { index: nil, tax_code: nil, text: '', cost_center:, source: nil } - super(defaults.merge(args)) + args.symbolize_keys! + @journal_entry = args.delete(:journal_entry) + defaults = { id: nil, index: nil, tax_code: nil, text: '', cost_center: nil } + date = args.delete(:date)&.then { _1.try(:to_date) || Date.parse(_1).to_date } + super(**defaults, **args, date:) end end end diff --git a/app/models/data_digest.rb b/app/models/data_digest.rb index c463e1656..8b9dcda87 100644 --- a/app/models/data_digest.rb +++ b/app/models/data_digest.rb @@ -21,10 +21,7 @@ # index_data_digests_on_organisation_id (organisation_id) # -require 'csv' - class DataDigest < ApplicationRecord - Formatter = Struct.new(:default_options, :block) PERIODS = { ever: ->(_at) { Range.new(nil, nil) }, last_year: ->(at) { ((at - 1.year).beginning_of_year)..((at - 1.year).end_of_year) }, @@ -53,14 +50,6 @@ class DataDigest < ApplicationRecord delegate :label, to: :data_digest_template attr_reader :period - def self.formatters - @formatters ||= (superclass.respond_to?(:formatters) && superclass.formatters&.dup) || {} - end - - def self.formatter(format, default_options: {}, &block) - formatters[format.to_sym] = Formatter.new(default_options, block) - end - def period=(period_key) @period = period_key&.to_sym period_range = PERIODS[@period]&.call(Time.zone.now) @@ -78,15 +67,7 @@ def records end def crunch - self.data = [] - records.pluck(:id).uniq.each do |record_id| - record = data_digest_template.base_scope.find(record_id) - template_context_cache = {} - data << data_digest_template.columns.map do |column| - column.body(record, template_context_cache) - end - end - data + self.data = data_digest_template.crunch(records) end def crunch! @@ -99,14 +80,6 @@ def crunching_finished? crunching_finished_at.present? end - def header - data_digest_template.columns.map(&:header) - end - - def footer - data_digest_template.columns.map(&:footer) - end - def format(format, **options) data || crunch formatter = formatters[format&.to_sym] @@ -115,7 +88,7 @@ def format(format, **options) end def formatters - self.class.formatters + data_digest_template.class.formatters end def localized_period @@ -123,21 +96,4 @@ def localized_period period_from: (period_from && I18n.l(period_from)) || '', period_to: (period_to && I18n.l(period_to)) || '') end - - formatter(:csv) do |options = {}| - options.reverse_merge!({ col_sep: ';', write_headers: true, skip_blanks: true, - force_quotes: true, encoding: 'utf-8' }) - - bom = "\uFEFF" - bom + CSV.generate(**options) do |csv| - csv << header - data&.each { |row| csv << row } - csv << footer if footer.any?(&:present?) - end - end - - formatter(:pdf) do |options = {}| - options.reverse_merge!({ document_options: { page_layout: :landscape } }) - Export::Pdf::DataDigestPdf.new(self, **options).render_document - end end diff --git a/app/models/data_digest_template.rb b/app/models/data_digest_template.rb index 3b6f6b9bd..111fd9169 100644 --- a/app/models/data_digest_template.rb +++ b/app/models/data_digest_template.rb @@ -25,9 +25,10 @@ require 'json' class DataDigestTemplate < ApplicationRecord - DEFAULT_COLUMN_CONFIG = [].freeze + Formatter = Struct.new(:default_options, :block) include Subtypeable + include TemplateRenderable belongs_to :organisation, inverse_of: :data_digest_templates has_many :data_digests, inverse_of: :data_digest_template, dependent: :destroy @@ -35,24 +36,9 @@ class DataDigestTemplate < ApplicationRecord validates :type, inclusion: { in: ->(_) { DataDigestTemplate.subtypes.keys.map(&:to_s) } } class << self - def column_types - @column_types ||= (superclass.respond_to?(:column_types) && superclass.column_types&.dup) || {} - end - - def column_type(name, column_type = nil, &) - column_types[name] = column_type || ColumnType.new(&) - end - def period(period_sym, at: Time.zone.now) PERIODS[period_sym&.to_sym]&.call(at) end - - def replace_in_columns_config!(search, replace, scope: DataDigestTemplate.all) - scope.where.not(columns_config: nil).find_each do |template| - json = JSON.generate(template.columns_config).gsub(search, replace) - template.update!(columns_config: json) - end - end end def group @@ -70,6 +56,7 @@ def prefilter end def filter_class; end + def crunch(_records); end def records(period) prefiltered = prefilter&.apply(base_scope) || base_scope @@ -80,76 +67,11 @@ def digest(period = nil) DataDigest.new(data_digest_template: self, period:) end - def columns - @columns ||= (columns_config.presence || self.class::DEFAULT_COLUMN_CONFIG).map do |config| - config.symbolize_keys! - column_type = config.fetch(:type, :default)&.to_sym - self.class.column_types.fetch(column_type, ColumnType.new).column_from_config(config) - end - end - - def columns_config=(value) - value = value.presence - value = JSON.parse(value) if value.is_a?(String) - value = Array.wrap(value) - value = nil if value == self.class::DEFAULT_COLUMN_CONFIG - super - end - - def eject_columns_config - self.columns_config = self.class::DEFAULT_COLUMN_CONFIG if columns_config.blank? - end - - class ColumnType - def initialize(&) - instance_exec(&) if block_given? - end - - def header(&block) - @header = block - end - - def footer(&block) - @footer = block - end - - def body(&block) - @body = block - end - - def column_from_config(config) - Column.new(config, header: @header, footer: @footer, body: @body) - end + def self.formatters + @formatters ||= (superclass.respond_to?(:formatters) && superclass.formatters&.dup) || {} end - class Column - attr_accessor :config - - def initialize(config, header: nil, footer: nil, body: nil) - @config = config.symbolize_keys - @blocks = { header:, footer:, body: } - @templates = @config.slice(*@blocks.keys).transform_values { |template| Liquid::Template.parse(template) } - end - - def column_type - @config.fetch(:type, :default) - end - - def header - @header ||= instance_exec(&@blocks[:header] || -> { @templates[:header]&.render! }) - end - - def footer - @footer ||= instance_exec(&@blocks[:footer] || -> { @templates[:footer]&.render! }) - end - - def body(record, template_context_cache = {}) - instance_exec(record, template_context_cache, &@blocks[:body] || - ->(_record, _template_context_cache) { @templates[:body]&.render! }) - end - - def cache_key(record, *parts) - [column_type, record.class, record.id].concat(parts).join(':') - end + def self.formatter(format, default_options: {}, &block) + formatters[format.to_sym] = Formatter.new(default_options, block) end end diff --git a/app/models/data_digest_templates/accounting_journal_entry.rb b/app/models/data_digest_templates/accounting_journal_entry.rb index 90e8cff5f..680bdfbe2 100644 --- a/app/models/data_digest_templates/accounting_journal_entry.rb +++ b/app/models/data_digest_templates/accounting_journal_entry.rb @@ -24,49 +24,9 @@ # module DataDigestTemplates - class AccountingJournalEntry < DataDigestTemplate + class AccountingJournalEntry < ::DataDigestTemplate ::DataDigestTemplate.register_subtype self - DEFAULT_COLUMN_CONFIG = [ - { - header: ::Invoice.human_attribute_name(:ref), - body: '{{ invoice.ref }}' - }, - { - header: ::Booking.human_attribute_name(:ref), - body: '{{ booking.ref }}' - }, - { - header: ::Invoice.human_attribute_name(:issued_at), - body: '{{ invoice.issued_at | datetime_format }}' - }, - { - header: ::Invoice.human_attribute_name(:payable_until), - body: '{{ invoice.payable_until | datetime_format }}' - }, - { - header: ::Invoice.human_attribute_name(:amount), - body: '{{ invoice.amount }}' - }, - { - header: ::Invoice.human_attribute_name(:amount_paid), - body: '{{ invoice.amount_paid }}' - } - # { - # header: ::Invoice.human_attribute_name(:amount_paid), - # body: '{{ invoice.percentage_paid | times: 100 }}%' - # }, - ].freeze - - column_type :default do - body do |invoice, template_context_cache| - booking = invoice.booking - context = template_context_cache[cache_key(invoice)] ||= - TemplateContext.new(booking:, invoice:, organisation: booking.organisation).to_h - @templates[:body]&.render!(context) - end - end - def periodfilter(period = nil) filter_class.new(issued_at_after: period&.begin, issued_at_before: period&.end) end @@ -78,5 +38,21 @@ def filter_class def base_scope @base_scope ||= ::Invoice.joins(:booking).where(bookings: { organisation_id: organisation }).kept end + + def crunch(records) + invoice_ids = records.pluck(:id).uniq + base_scope.where(id: invoice_ids).find_each.flat_map do |invoice| + invoice.journal_entry + end + end + + formatter(:taf) do |_options = {}| + data.flat_map do |record| + journal_entry = ::Accounting::JournalEntry.new(**record) + [ + TafBlock.build_from(journal_entry) + ] + end.join("\n") + end end end diff --git a/app/models/data_digest_templates/booking.rb b/app/models/data_digest_templates/booking.rb index ca6d75594..163db6aca 100644 --- a/app/models/data_digest_templates/booking.rb +++ b/app/models/data_digest_templates/booking.rb @@ -24,7 +24,7 @@ # module DataDigestTemplates - class Booking < ::DataDigestTemplate + class Booking < Tabular ::DataDigestTemplate.register_subtype self DEFAULT_COLUMN_CONFIG = [ diff --git a/app/models/data_digest_templates/invoice.rb b/app/models/data_digest_templates/invoice.rb index 310ae794b..849a8608f 100644 --- a/app/models/data_digest_templates/invoice.rb +++ b/app/models/data_digest_templates/invoice.rb @@ -24,7 +24,7 @@ # module DataDigestTemplates - class Invoice < DataDigestTemplate + class Invoice < Tabular ::DataDigestTemplate.register_subtype self DEFAULT_COLUMN_CONFIG = [ diff --git a/app/models/data_digest_templates/invoice_part.rb b/app/models/data_digest_templates/invoice_part.rb index f46ca9495..4493d7f22 100644 --- a/app/models/data_digest_templates/invoice_part.rb +++ b/app/models/data_digest_templates/invoice_part.rb @@ -24,7 +24,7 @@ # module DataDigestTemplates - class InvoicePart < DataDigestTemplate + class InvoicePart < Tabular ::DataDigestTemplate.register_subtype self DEFAULT_COLUMN_CONFIG = [ diff --git a/app/models/data_digest_templates/meter_reading_period.rb b/app/models/data_digest_templates/meter_reading_period.rb index d5cc067b4..53373cb57 100644 --- a/app/models/data_digest_templates/meter_reading_period.rb +++ b/app/models/data_digest_templates/meter_reading_period.rb @@ -24,7 +24,7 @@ # module DataDigestTemplates - class MeterReadingPeriod < ::DataDigestTemplate + class MeterReadingPeriod < Tabular ::DataDigestTemplate.register_subtype self DEFAULT_COLUMN_CONFIG = [ { diff --git a/app/models/data_digest_templates/payment.rb b/app/models/data_digest_templates/payment.rb index f5928ed05..e3b4e1262 100644 --- a/app/models/data_digest_templates/payment.rb +++ b/app/models/data_digest_templates/payment.rb @@ -24,7 +24,7 @@ # module DataDigestTemplates - class Payment < DataDigestTemplate + class Payment < Tabular ::DataDigestTemplate.register_subtype self DEFAULT_COLUMN_CONFIG = [ diff --git a/app/models/data_digest_templates/tabular.rb b/app/models/data_digest_templates/tabular.rb new file mode 100644 index 000000000..236eb5756 --- /dev/null +++ b/app/models/data_digest_templates/tabular.rb @@ -0,0 +1,154 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: data_digest_templates +# +# id :bigint not null, primary key +# columns_config :jsonb +# group :string +# label :string +# prefilter_params :jsonb +# type :string +# created_at :datetime not null +# updated_at :datetime not null +# organisation_id :bigint not null +# +# Indexes +# +# index_data_digest_templates_on_organisation_id (organisation_id) +# +# Foreign Keys +# +# fk_rails_... (organisation_id => organisations.id) +# + +module DataDigestTemplates + class Tabular < DataDigestTemplate + DEFAULT_COLUMN_CONFIG = [].freeze + + class << self + def column_types + @column_types ||= (superclass.respond_to?(:column_types) && superclass.column_types&.dup) || {} + end + + def column_type(name, column_type = nil, &) + column_types[name] = column_type || ColumnType.new(&) + end + + def replace_in_columns_config!(search, replace, scope: DataDigestTemplate.all) + scope.where.not(columns_config: nil).find_each do |template| + json = JSON.generate(template.columns_config).gsub(search, replace) + template.update!(columns_config: json) + end + end + end + + def columns + @columns ||= (columns_config.presence || self.class::DEFAULT_COLUMN_CONFIG).map do |config| + config.symbolize_keys! + column_type = config.fetch(:type, :default)&.to_sym + self.class.column_types.fetch(column_type, ColumnType.new).column_from_config(config) + end + end + + def columns_config=(value) + value = value.presence + value = JSON.parse(value) if value.is_a?(String) + value = Array.wrap(value) + value = nil if value == self.class::DEFAULT_COLUMN_CONFIG + super + end + + def crunch(records) + record_ids = records.pluck(:id).uniq + base_scope.where(id: record_ids).find_each.map do |record| + template_context_cache = {} + columns.map { |column| column.body(record, template_context_cache) } + end + end + + def headers + columns.map(&:header) + end + + def footers + columns.map(&:footer) + end + + def eject_columns_config + self.columns_config = self.class::DEFAULT_COLUMN_CONFIG if columns_config.blank? + end + + class ColumnType + def initialize(&) + instance_exec(&) if block_given? + end + + def header(&block) + @header = block + end + + def footer(&block) + @footer = block + end + + def body(&block) + @body = block + end + + def column_from_config(config) + Column.new(config, header: @header, footer: @footer, body: @body) + end + end + + class Column + attr_accessor :config + + def initialize(config, header: nil, footer: nil, body: nil) + @config = config.symbolize_keys + @blocks = { header:, footer:, body: } + @templates = @config.slice(*@blocks.keys).transform_values { |template| Liquid::Template.parse(template) } + end + + def column_type + @config.fetch(:type, :default) + end + + def header + @header ||= instance_exec(&@blocks[:header] || -> { @templates[:header]&.render! }) + end + + def footer + @footer ||= instance_exec(&@blocks[:footer] || -> { @templates[:footer]&.render! }) + end + + def body(record, template_context_cache = {}) + instance_exec(record, template_context_cache, &@blocks[:body] || + ->(_record, _template_context_cache) { @templates[:body]&.render! }) + end + + def cache_key(record, *parts) + [column_type, record.class, record.try(:id) || record.object_id].concat(parts).join(':') + end + end + + formatter(:csv) do |options = {}| + require 'csv' + options.reverse_merge!({ col_sep: ';', write_headers: true, skip_blanks: true, + force_quotes: true, encoding: 'utf-8' }) + + bom = "\uFEFF" + bom + CSV.generate(**options) do |csv| + csv << header + data&.each { |row| csv << row } + csv << footer if footer.any?(&:present?) + end + end + + formatter(:pdf) do |options = {}| + options.reverse_merge!({ document_options: { page_layout: :landscape } }) + Export::Pdf::DataDigestPdf.new(self, **options).render_document + end + end +end diff --git a/app/models/data_digest_templates/tenant.rb b/app/models/data_digest_templates/tenant.rb index 733b4143f..65bef1f8d 100644 --- a/app/models/data_digest_templates/tenant.rb +++ b/app/models/data_digest_templates/tenant.rb @@ -24,7 +24,7 @@ # module DataDigestTemplates - class Tenant < ::DataDigestTemplate + class Tenant < Tabular ::DataDigestTemplate.register_subtype self DEFAULT_COLUMN_CONFIG = [ { diff --git a/app/models/invoice.rb b/app/models/invoice.rb index 85f6ebbe9..2b587e684 100644 --- a/app/models/invoice.rb +++ b/app/models/invoice.rb @@ -191,14 +191,9 @@ def vat end end - def journal_entry_item - Accounting::JournalEntryItem.new(account: booking.tenant.accounting_account_nr, date: sent_at, - amount: amount, amount_type: :brutto, side: 1, - text: ref, source: self) - end - def journal_entry - Accounting::JournalEntry.new(date: sent_at, - items: [journal_entry_item] + invoice_parts.flat_map(&:journal_entry_items)) + items = [{ account: booking.tenant.accounting_account_nr, date: sent_at, amount: amount, amount_type: :brutto, + side: 1, text: ref, source: :invoice, invoice_id: id }] + invoice_parts.map(&:journal_entry_items) + Accounting::JournalEntry.new(date: sent_at, invoice_id: id, items:) end end diff --git a/app/models/invoice_parts/add.rb b/app/models/invoice_parts/add.rb index 53beb0fc3..18a138ab6 100644 --- a/app/models/invoice_parts/add.rb +++ b/app/models/invoice_parts/add.rb @@ -34,7 +34,8 @@ def journal_entry_items [ Accounting::JournalEntryItem.new(account: usage&.tarif&.accounting_account_nr, date: invoice.sent_at, amount: (amount / ((100 + (vat || 0))) * 100), amount_type: :netto, - side: -1, tax_code: vat.to_s, text: invoice.ref, source: self) + side: -1, tax_code: vat.to_s, text: invoice.ref, source: :invoice_part, + invoice_id: invoice.id) ] end end diff --git a/app/services/taf_block.rb b/app/services/taf_block.rb index 11cae29bc..5a8f671a6 100644 --- a/app/services/taf_block.rb +++ b/app/services/taf_block.rb @@ -49,7 +49,7 @@ def self.serialize_value(value) # rubocop:disable Metrics/MethodLength format('%.2f', value) when ::Numeric value.to_s - when ::Date, ::DateTime + when ::Date, ::DateTime, ::ActiveSupport::TimeWithZone value.strftime('%d.%m.%Y') else "\"#{value.to_s.gsub('"', '""')}\"".presence @@ -68,21 +68,21 @@ def self.serialize_properies(properties, join_with: ' ') properties.compact.flat_map { |key, value| "#{key}=#{serialize_value(value)}" }.join(join_with) end - def self.derivers - @derivers ||= {} + def self.builders + @builders ||= {} end - def self.register_deriver(klass, &derive_block) - derivers[klass] = derive_block + def self.register_builder(klass, &build_block) + builders[klass] = build_block end - def self.derive(value, **options) - derive_block = derivers[derivers.keys.find { |klass| value.is_a?(klass) }] - instance_exec(value, options, &derive_block) if derive_block.present? + def self.build_from(value, **options) + build_block = builders[builders.keys.find { |klass| value.is_a?(klass) }] + instance_exec(value, options, &build_block) if build_block.present? end - register_deriver Accounting::JournalEntry do |value, **options| - new(:Blg, *value.items.map { TafBlock.serialize(_1) }, **{ + register_builder Accounting::JournalEntry do |value, **options| + new(:Blg, *value.items.map { TafBlock.build_from(_1) }, **{ # Date; The date of the booking. Date: options.fetch(:Date, value.date), @@ -90,13 +90,13 @@ def self.derive(value, **options) }) end - register_deriver Accounting::JournalEntryItem do |value, **options| + register_builder Accounting::JournalEntryItem do |value, **options| new(:Bk, **{ # The Id of a book keeping account. [Fibu-Konto] AccId: options.fetch(:AccId, value.account), # Integer; Booking type: 1=cost booking, 2=tax booking - BType: options.fetch(:BType, value.amount_type == :tax || 1), + BType: options.fetch(:BType, value.amount_type&.to_sym == :tax || 1), # String[13], This is the cost type account CAcc: options.fetch(:CAcc, value.cost_center), @@ -131,11 +131,11 @@ def self.derive(value, **options) # Be careful not to put too many characters onto one single line, because # most Reports are not designed to display a full string containing 60 # characters. - Text2: options.fetch(:Text2, value.text&.slice(0..59)&.lines&.[](1..-1)&.join("\n")), + Text2: options.fetch(:Text2, value.text&.slice(0..59)&.lines&.[](1..-1)&.join("\n")).presence, # Integer; This is the index of the booking that represents the tax booking # which is attached to this booking. - TIdx: options.fetch(:TIdx, (value.amount_type == :tax && value.index) || nil), + TIdx: options.fetch(:TIdx, (value.amount_type&.to_sym == :tax && value.index) || nil), # Boolean; Booking type. # 0 a debit booking [Soll] @@ -143,13 +143,13 @@ def self.derive(value, **options) Type: options.fetch(:Type, { 1 => 0, -1 => 1 }[value.side]), # Currency; The net amount for this booking. [Netto-Betrag] - ValNt: options.fetch(:ValNt, value.amount_type == :netto ? value.amount : nil), + ValNt: options.fetch(:ValNt, value.amount_type&.to_sym == :netto ? value.amount : nil), # Currency; The tax amount for this booking. [Brutto-Betrag] - ValBt: options.fetch(:ValBt, value.amount_type == :brutto ? value.amount : nil), + ValBt: options.fetch(:ValBt, value.amount_type&.to_sym == :brutto ? value.amount : nil), # Currency; The tax amount for this booking. [Steuer-Betrag] - ValTx: options.fetch(:ValTx, value.amount_type == :tax ? value.amount : nil), + ValTx: options.fetch(:ValTx, value.amount_type&.to_sym == :tax ? value.amount : nil), # Currency; The gross amount for this booking in the foreign currency specified # by currency of the account AccId. [FW-Betrag] diff --git a/app/views/manage/data_digest_templates/_form.html.slim b/app/views/manage/data_digest_templates/_form.html.slim index 63e0d8c0c..4e11d9ed5 100644 --- a/app/views/manage/data_digest_templates/_form.html.slim +++ b/app/views/manage/data_digest_templates/_form.html.slim @@ -4,10 +4,7 @@ = f.text_field :label = f.text_field :group - = react_component('data_digest_templates/ColumnsConfigForm', { \ - name: "data_digest_template[columns_config]", \ - json: JSON.generate(@data_digest_template.columns_config.presence || @data_digest_template.class::DEFAULT_COLUMN_CONFIG) \ - }) + = render partial: @data_digest_template.to_partial_path('form_fields'), locals: { data_digest_template: @data_digest_template } - if @data_digest_template.filter_class h5= @data_digest_template.filter_class.model_name.human diff --git a/app/views/manage/data_digests/show.html.slim b/app/views/manage/data_digests/show.html.slim index 19405c4d1..91a421d58 100644 --- a/app/views/manage/data_digests/show.html.slim +++ b/app/views/manage/data_digests/show.html.slim @@ -1,23 +1,7 @@ h1.mt-0= @data_digest.label p= @data_digest.localized_period -.table-responsive - table.table-striped.table.table-responsive - - if @data_digest.header - thead - - @data_digest.header.each do |header| - th= header - - tbody.shadow-sm - - @data_digest.data&.each do |row| - tr - - row.each do |cell| - td= cell - - - if @data_digest.footer&.any?(&:present?) - tfooter - - @data_digest.footer.each do |footer| - td= footer += render partial: @data_digest.data_digest_template.to_partial_path('show_data_digest'), locals: { data_digest: @data_digest } br .btn-group diff --git a/app/views/renderables/data_digest_templates/accounting_journal_entry/_form_fields.html.slim b/app/views/renderables/data_digest_templates/accounting_journal_entry/_form_fields.html.slim new file mode 100644 index 000000000..e69de29bb diff --git a/app/views/renderables/data_digest_templates/accounting_journal_entry/_show_data_digest.html.slim b/app/views/renderables/data_digest_templates/accounting_journal_entry/_show_data_digest.html.slim new file mode 100644 index 000000000..874193219 --- /dev/null +++ b/app/views/renderables/data_digest_templates/accounting_journal_entry/_show_data_digest.html.slim @@ -0,0 +1,18 @@ +ol + - @data_digest.data.each do |data_item| + - journal_entry = Accounting::JournalEntry.new(**data_item) + li + = Accounting::JournalEntry.model_name.human + dl + dt= Accounting::JournalEntry.human_attribute_name(:date) + dd= I18n.l(journal_entry.date) if journal_entry.date.present? + ol + - journal_entry.items.each do |item| + li + dl + dt= Accounting::JournalEntryItem.human_attribute_name(:account) + dd= item.account + dt= Accounting::JournalEntryItem.human_attribute_name(:amount) + dd= number_to_currency(item.amount) + dt= Accounting::JournalEntryItem.human_attribute_name(:side) + dd= item.side.positive? ? "Haben" : "Soll" diff --git a/app/views/renderables/data_digest_templates/booking/_form_fields.html.slim b/app/views/renderables/data_digest_templates/booking/_form_fields.html.slim new file mode 100644 index 000000000..0d2a7b50c --- /dev/null +++ b/app/views/renderables/data_digest_templates/booking/_form_fields.html.slim @@ -0,0 +1 @@ += render partial: 'renderabes/data_digest_templates/tabular/form_fields', locals: { data_digest_template: } diff --git a/app/views/renderables/data_digest_templates/booking/_show_data_digest.html.slim b/app/views/renderables/data_digest_templates/booking/_show_data_digest.html.slim new file mode 100644 index 000000000..12593818b --- /dev/null +++ b/app/views/renderables/data_digest_templates/booking/_show_data_digest.html.slim @@ -0,0 +1 @@ += render partial: 'renderables/data_digest_templates/tabular/show_data_digest', locals: { data_digest: } diff --git a/app/views/renderables/data_digest_templates/tabular/_form_fields.html.slim b/app/views/renderables/data_digest_templates/tabular/_form_fields.html.slim new file mode 100644 index 000000000..f841597e0 --- /dev/null +++ b/app/views/renderables/data_digest_templates/tabular/_form_fields.html.slim @@ -0,0 +1,4 @@ += react_component('data_digest_templates/ColumnsConfigForm', { \ + name: "data_digest_template[columns_config]", \ + json: JSON.generate(data_digest_template.columns_config.presence || data_digest_template.class::DEFAULT_COLUMN_CONFIG) \ + }) diff --git a/config/initializers/mime_types.rb b/config/initializers/mime_types.rb index 7b73d1680..66684fd20 100644 --- a/config/initializers/mime_types.rb +++ b/config/initializers/mime_types.rb @@ -6,3 +6,5 @@ # Mime::Type.register "text/richtext", :rtf # Mime::Type.register "application/pdf", :pdf +# Mime::Type.register 'application/x.taf', :taf +Mime::Type.register 'text/plain', :taf From 01e99952405a64afdb52b9a57369150b03fb1ef6 Mon Sep 17 00:00:00 2001 From: Diego Steiner Date: Wed, 6 Nov 2024 16:36:41 +0100 Subject: [PATCH 05/14] feat: remodel data_digests to support non-tabular datastructures --- .../tabular/_show_data_digest.html.slim | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 app/views/renderables/data_digest_templates/tabular/_show_data_digest.html.slim diff --git a/app/views/renderables/data_digest_templates/tabular/_show_data_digest.html.slim b/app/views/renderables/data_digest_templates/tabular/_show_data_digest.html.slim new file mode 100644 index 000000000..2e878102d --- /dev/null +++ b/app/views/renderables/data_digest_templates/tabular/_show_data_digest.html.slim @@ -0,0 +1,19 @@ +.table-responsive + table.table-striped.table.table-responsive + - headers = data_digest.data_digest_template.headers + - footers = data_digest.data_digest_template.footers + - if headers + thead + - headers.each do |header| + th= header + + tbody.shadow-sm + - data_digest.data&.each do |row| + tr + - row.each do |cell| + td= cell + + - if footers&.any?(&:present?) + tfooter + - footers.each do |footer| + td= footer From e2639ad9ec86c364dfbca2dd9babb1e0cb1c316a Mon Sep 17 00:00:00 2001 From: Diego Steiner Date: Tue, 3 Dec 2024 13:11:16 +0000 Subject: [PATCH 06/14] feat: vat_categories --- .../manage/vat_categories_controller.rb | 39 +++++++++++++++ app/models/ability.rb | 50 ++++++++++--------- app/models/invoice.rb | 12 ++--- app/models/invoice_part.rb | 30 +++++------ app/models/invoice_parts/add.rb | 35 ++++++------- app/models/invoice_parts/percentage.rb | 27 +++++----- app/models/invoice_parts/text.rb | 27 +++++----- app/models/organisation.rb | 1 + app/models/tarif.rb | 1 + app/models/vat_category.rb | 28 +++++++++++ app/params/manage/invoice_part_params.rb | 2 +- app/params/manage/tarif_params.rb | 4 +- .../manage/invoice_part_serializer.rb | 1 + app/serializers/manage/invoice_serializer.rb | 2 +- app/serializers/manage/tarif_serializer.rb | 2 + .../public/vat_category_serializer.rb | 7 +++ .../invoice/invoice_parts_table.rb | 12 +++-- app/services/template_context.rb | 3 +- .../manage/invoice_parts/_form.html.slim | 4 +- app/views/manage/tarifs/_form.html.slim | 14 ++++-- .../manage/vat_categories/_form.html.slim | 23 +++++++++ .../manage/vat_categories/edit.html.slim | 5 ++ .../manage/vat_categories/index.html.slim | 24 +++++++++ app/views/manage/vat_categories/new.html.slim | 5 ++ .../_show_data_digest.html.slim | 12 ++--- .../invoice_parts/add/_form_fields.html.slim | 2 +- .../percentage/_form_fields.html.slim | 2 +- config/locales/de.yml | 13 +++-- config/locales/en.yml | 2 +- config/locales/fr.yml | 2 - config/locales/it.yml | 4 +- config/routes.rb | 1 + ...241030163341_add_accounting_information.rb | 1 + .../20241203100339_create_vat_categories.rb | 13 +++++ ...203101328_add_vat_category_id_to_tarifs.rb | 27 ++++++++++ db/schema.rb | 24 +++++++-- spec/factories/invoice_parts.rb | 27 +++++----- spec/factories/vat_categories.rb | 10 ++++ spec/models/invoice_part_spec.rb | 27 +++++----- spec/models/vat_category_spec.rb | 7 +++ 40 files changed, 381 insertions(+), 151 deletions(-) create mode 100644 app/controllers/manage/vat_categories_controller.rb create mode 100644 app/models/vat_category.rb create mode 100644 app/serializers/public/vat_category_serializer.rb create mode 100644 app/views/manage/vat_categories/_form.html.slim create mode 100644 app/views/manage/vat_categories/edit.html.slim create mode 100644 app/views/manage/vat_categories/index.html.slim create mode 100644 app/views/manage/vat_categories/new.html.slim create mode 100644 db/migrate/20241203100339_create_vat_categories.rb create mode 100644 db/migrate/20241203101328_add_vat_category_id_to_tarifs.rb create mode 100644 spec/factories/vat_categories.rb create mode 100644 spec/models/vat_category_spec.rb diff --git a/app/controllers/manage/vat_categories_controller.rb b/app/controllers/manage/vat_categories_controller.rb new file mode 100644 index 000000000..a8dce9682 --- /dev/null +++ b/app/controllers/manage/vat_categories_controller.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module Manage + class VatCategoriesController < BaseController + load_and_authorize_resource :vat_category + + def index + @vat_categories = @vat_categories.where(organisation: current_organisation).ordered + respond_with :manage, @vat_categories + end + + def edit + respond_with :manage, @vat_category + end + + def create + @vat_category.organisation = current_organisation + @vat_category.save + respond_with :manage, location: manage_vat_categories_path + end + + def update + @vat_category.update(vat_category_params) + respond_with :manage, location: manage_vat_categories_path + end + + def destroy + @vat_category.discarded? ? @vat_category.destroy : @vat_category.discard! + respond_with :manage, @vat_category, location: manage_vat_categories_path + end + + private + + def vat_category_params + locale_params = I18n.available_locales.map { |locale| ["label_#{locale}"] } + params.require(:vat_category).permit(:percentage, :accouting_vat_code, locale_params.flatten) + end + end +end diff --git a/app/models/ability.rb b/app/models/ability.rb index b449e2e0e..871393e4c 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -33,15 +33,16 @@ class Manage < Base can(:manage, BookingAgent, organisation:) can(:manage, BookingCategory, organisation:) - can :manage, BookingCondition, tarif: { organisation: } + can(:manage, BookingCondition, tarif: { organisation: }) can(:manage, BookingQuestion, organisation:) can(:manage, BookingValidation, organisation:) can(:manage, DesignatedDocument, organisation:) can(:manage, Occupiable, organisation:) can(:manage, Operator, organisation:) - can %i[read edit update], Organisation, id: organisation.id + can(%i[read edit update], Organisation, id: organisation.id) can(:manage, OrganisationUser, organisation:) - can :manage, Tarif, organisation: + can(:manage, Tarif, organisation:) + can(:manage, VatCategory, organisation:) end role :manager do |user, organisation| @@ -50,21 +51,21 @@ class Manage < Base abilities_for_role(:readonly, user, organisation) can(:manage, Booking, organisation:) - can :manage, Contract, booking: { organisation: } + can(:manage, Contract, booking: { organisation: }) can(:manage, DataDigest, organisation:) can(:manage, DataDigestTemplate, organisation:) - can :manage, Deadline, booking: { organisation: } - can :manage, Invoice, booking: { organisation: } - can :manage, InvoicePart, invoice: { booking: { organisation: } } - can :manage, Notification, booking: { organisation: } - can :new, Occupancy - can :manage, Occupancy, occupiable: { organisation: } + can(:manage, Deadline, booking: { organisation: }) + can(:manage, Invoice, booking: { organisation: }) + can(:manage, InvoicePart, invoice: { booking: { organisation: } }) + can(:manage, Notification, booking: { organisation: }) + can(:new, Occupancy) + can(:manage, Occupancy, occupiable: { organisation: }) can(:manage, OperatorResponsibility, organisation:) - can :manage, Payment, booking: { organisation: } + can(:manage, Payment, booking: { organisation: }) can(:manage, Tenant, organisation:) - can :manage, Usage, booking: { organisation: } + can(:manage, Usage, booking: { organisation: }) can(:manage, RichTextTemplate, organisation:) - can :read, PlanBBackup, organisation: + can(:read, PlanBBackup, organisation:) end role :readonly do |user, organisation| @@ -73,28 +74,29 @@ class Manage < Base can(%i[read calendar], Booking, organisation:) can(:read, BookingAgent, organisation:) can(:read, BookingCategory, organisation:) - can :read, BookingCondition, tarif: { organisation: } + can(:read, BookingCondition, tarif: { organisation: }) can(:read, BookingQuestion, organisation:) can(:read, BookingValidation, organisation:) - can :read, Contract, booking: { organisation: } + can(:read, Contract, booking: { organisation: }) can(%i[read new create], DataDigest, organisation:) can(:read, DataDigestTemplate, organisation:) - can :read, Deadline, booking: { organisation: } + can(:read, Deadline, booking: { organisation: }) can(%i[read calendar at embed], Home, organisation:) - can :read, Invoice, booking: { organisation: } - can :read, InvoicePart, invoice: { booking: { organisation: } } - can :read, Notification, booking: { organisation: } - can %i[read calendar at embed], Occupancy, occupiable: { organisation: } + can(:read, Invoice, booking: { organisation: }) + can(:read, InvoicePart, invoice: { booking: { organisation: } }) + can(:read, Notification, booking: { organisation: }) + can(%i[read calendar at embed], Occupancy, occupiable: { organisation: }) can(%i[read calendar], Occupiable, organisation:) can(:read, Operator, organisation:) can(:read, OperatorResponsibility, organisation:) - can %i[read edit], Organisation, id: organisation.id - can :read, Payment, booking: { organisation: } + can(%i[read edit], Organisation, id: organisation.id) + can(:read, Payment, booking: { organisation: }) can(:read, RichTextTemplate, organisation:) can(:read, Tarif, organisation:) can(:read, Tenant, organisation:) - can :read, Usage, booking: { organisation: } - can :read, User, organisation: + can(:read, Usage, booking: { organisation: }) + can(:read, User, organisation:) + can(:read, VatCategory, organisation:) end end diff --git a/app/models/invoice.rb b/app/models/invoice.rb index 2b587e684..d648b5100 100644 --- a/app/models/invoice.rb +++ b/app/models/invoice.rb @@ -181,19 +181,13 @@ def to_attachable { io: StringIO.new(pdf.blob.download), filename:, content_type: pdf.content_type } if pdf&.blob.present? end - def vat - invoice_parts.filter { |invoice_part| invoice_part.vat.present? && invoice_part.vat.positive? } - .group_by(&:vat) - .to_h do |vat, vat_invoice_parts| - total = vat_invoice_parts.sum(&:calculated_amount) - tax = total / (100 + vat) * vat - [vat, { tax:, total: }] - end + def vat_amounts + invoice_parts.group_by(&:vat_category).except(nil).transform_values { _1.sum(&:calculated_amount) } end def journal_entry items = [{ account: booking.tenant.accounting_account_nr, date: sent_at, amount: amount, amount_type: :brutto, side: 1, text: ref, source: :invoice, invoice_id: id }] + invoice_parts.map(&:journal_entry_items) - Accounting::JournalEntry.new(date: sent_at, invoice_id: id, items:) + Accounting::JournalEntryGroup.new(date: sent_at, invoice_id: id, items:) end end diff --git a/app/models/invoice_part.rb b/app/models/invoice_part.rb index dbb43c535..435d3b536 100644 --- a/app/models/invoice_part.rb +++ b/app/models/invoice_part.rb @@ -4,22 +4,23 @@ # # Table name: invoice_parts # -# id :integer not null, primary key -# invoice_id :integer -# usage_id :integer -# type :string -# amount :decimal(, ) -# label :string -# breakdown :string -# ordinal :integer -# created_at :datetime not null -# updated_at :datetime not null -# vat :decimal(, ) +# id :integer not null, primary key +# invoice_id :integer +# usage_id :integer +# type :string +# amount :decimal(, ) +# label :string +# breakdown :string +# ordinal :integer +# created_at :datetime not null +# updated_at :datetime not null +# vat_category_id :integer # # Indexes # -# index_invoice_parts_on_invoice_id (invoice_id) -# index_invoice_parts_on_usage_id (usage_id) +# index_invoice_parts_on_invoice_id (invoice_id) +# index_invoice_parts_on_usage_id (usage_id) +# index_invoice_parts_on_vat_category_id (vat_category_id) # class InvoicePart < ApplicationRecord @@ -30,6 +31,7 @@ class InvoicePart < ApplicationRecord belongs_to :invoice, inverse_of: :invoice_parts, touch: true belongs_to :usage, inverse_of: :invoice_parts, optional: true + belongs_to :vat_category, optional: true has_one :tarif, through: :usage has_one :booking, through: :usage @@ -70,7 +72,7 @@ def self.from_usage(usage, **attributes) return unless usage new(attributes.reverse_merge( - usage:, label: usage.tarif.label, ordinal: usage.tarif.ordinal, vat: usage.tarif.vat, + usage:, label: usage.tarif.label, ordinal: usage.tarif.ordinal, vat_category: usage.tarif.vat_category, breakdown: usage.remarks.presence || usage.breakdown, amount: usage.price )) end diff --git a/app/models/invoice_parts/add.rb b/app/models/invoice_parts/add.rb index 18a138ab6..d18cef05b 100644 --- a/app/models/invoice_parts/add.rb +++ b/app/models/invoice_parts/add.rb @@ -4,22 +4,23 @@ # # Table name: invoice_parts # -# id :integer not null, primary key -# invoice_id :integer -# usage_id :integer -# type :string -# amount :decimal(, ) -# label :string -# breakdown :string -# ordinal :integer -# created_at :datetime not null -# updated_at :datetime not null -# vat :decimal(, ) +# id :integer not null, primary key +# invoice_id :integer +# usage_id :integer +# type :string +# amount :decimal(, ) +# label :string +# breakdown :string +# ordinal :integer +# created_at :datetime not null +# updated_at :datetime not null +# vat_category_id :integer # # Indexes # -# index_invoice_parts_on_invoice_id (invoice_id) -# index_invoice_parts_on_usage_id (usage_id) +# index_invoice_parts_on_invoice_id (invoice_id) +# index_invoice_parts_on_usage_id (usage_id) +# index_invoice_parts_on_vat_category_id (vat_category_id) # module InvoiceParts @@ -32,10 +33,10 @@ def calculated_amount def journal_entry_items [ - Accounting::JournalEntryItem.new(account: usage&.tarif&.accounting_account_nr, date: invoice.sent_at, - amount: (amount / ((100 + (vat || 0))) * 100), amount_type: :netto, - side: -1, tax_code: vat.to_s, text: invoice.ref, source: :invoice_part, - invoice_id: invoice.id) + Accounting::JournalEntry.new(account: usage&.tarif&.accounting_account_nr, date: invoice.sent_at, + amount: (amount / ((100 + (vat || 0))) * 100), amount_type: :netto, + side: -1, tax_code: vat_category&.accouting_vat_code, + text: invoice.ref, source: :invoice_part, invoice_id: invoice.id) ] end end diff --git a/app/models/invoice_parts/percentage.rb b/app/models/invoice_parts/percentage.rb index 8001d21bc..56f1aafad 100644 --- a/app/models/invoice_parts/percentage.rb +++ b/app/models/invoice_parts/percentage.rb @@ -4,22 +4,23 @@ # # Table name: invoice_parts # -# id :integer not null, primary key -# invoice_id :integer -# usage_id :integer -# type :string -# amount :decimal(, ) -# label :string -# breakdown :string -# ordinal :integer -# created_at :datetime not null -# updated_at :datetime not null -# vat :decimal(, ) +# id :integer not null, primary key +# invoice_id :integer +# usage_id :integer +# type :string +# amount :decimal(, ) +# label :string +# breakdown :string +# ordinal :integer +# created_at :datetime not null +# updated_at :datetime not null +# vat_category_id :integer # # Indexes # -# index_invoice_parts_on_invoice_id (invoice_id) -# index_invoice_parts_on_usage_id (usage_id) +# index_invoice_parts_on_invoice_id (invoice_id) +# index_invoice_parts_on_usage_id (usage_id) +# index_invoice_parts_on_vat_category_id (vat_category_id) # module InvoiceParts diff --git a/app/models/invoice_parts/text.rb b/app/models/invoice_parts/text.rb index d263591af..15d55e24b 100644 --- a/app/models/invoice_parts/text.rb +++ b/app/models/invoice_parts/text.rb @@ -4,22 +4,23 @@ # # Table name: invoice_parts # -# id :integer not null, primary key -# invoice_id :integer -# usage_id :integer -# type :string -# amount :decimal(, ) -# label :string -# breakdown :string -# ordinal :integer -# created_at :datetime not null -# updated_at :datetime not null -# vat :decimal(, ) +# id :integer not null, primary key +# invoice_id :integer +# usage_id :integer +# type :string +# amount :decimal(, ) +# label :string +# breakdown :string +# ordinal :integer +# created_at :datetime not null +# updated_at :datetime not null +# vat_category_id :integer # # Indexes # -# index_invoice_parts_on_invoice_id (invoice_id) -# index_invoice_parts_on_usage_id (usage_id) +# index_invoice_parts_on_invoice_id (invoice_id) +# index_invoice_parts_on_usage_id (usage_id) +# index_invoice_parts_on_vat_category_id (vat_category_id) # module InvoiceParts diff --git a/app/models/organisation.rb b/app/models/organisation.rb index d10e883b1..e0b5d1889 100644 --- a/app/models/organisation.rb +++ b/app/models/organisation.rb @@ -63,6 +63,7 @@ class Organisation < ApplicationRecord has_many :users, through: :organisation_users has_many :tarifs, dependent: :destroy, inverse_of: :organisation has_many :plan_b_backups, dependent: :destroy, inverse_of: :organisation + has_many :vat_categories, dependent: :destroy, inverse_of: :organisation has_one_attached :logo has_one_attached :contract_signature diff --git a/app/models/tarif.rb b/app/models/tarif.rb index 1ebb8cbc3..fd70bfb1e 100644 --- a/app/models/tarif.rb +++ b/app/models/tarif.rb @@ -62,6 +62,7 @@ class Tarif < ApplicationRecord flag :associated_types, ASSOCIATED_TYPES.keys belongs_to :organisation, inverse_of: :tarifs + belongs_to :vat_category, inverse_of: :tarifs, optional: true belongs_to :prefill_usage_booking_question, class_name: 'BookingQuestion', inverse_of: :tarifs, optional: true has_many :meter_reading_periods, dependent: :destroy, inverse_of: :tarif has_many :bookings, through: :usages, inverse_of: :tarifs diff --git a/app/models/vat_category.rb b/app/models/vat_category.rb new file mode 100644 index 000000000..8508004b5 --- /dev/null +++ b/app/models/vat_category.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +class VatCategory < ApplicationRecord + include Discard::Model + extend Mobility + + belongs_to :organisation, inverse_of: :vat_categories + has_many :tarifs, inverse_of: :vat_category, dependent: :restrict_with_error + + translates :label, column_suffix: '_i18n', locale_accessors: true + + validates :percentage, presence: true, numericality: { gteq: 0.0 } + + scope :ordered, -> { order(percentage: :ASC) } + + def to_s + formatted_percentage = ActiveSupport::NumberHelper.number_to_percentage(percentage, precision: 2) + return formatted_percentage if label.blank? + + "#{label} (#{formatted_percentage})" + end + + def tax_of(amount) + return 0 if percentage.blank? || percentage.zero? + + amount / (100 + percentage) * percentage + end +end diff --git a/app/params/manage/invoice_part_params.rb b/app/params/manage/invoice_part_params.rb index 838030c0f..d5ad00a20 100644 --- a/app/params/manage/invoice_part_params.rb +++ b/app/params/manage/invoice_part_params.rb @@ -3,7 +3,7 @@ module Manage class InvoicePartParams < ApplicationParams def self.permitted_keys - %i[usage_id label breakdown amount type ordinal_position vat] + %i[usage_id label breakdown amount type ordinal_position vat_category_id] end end end diff --git a/app/params/manage/tarif_params.rb b/app/params/manage/tarif_params.rb index bf2e2a1a9..a14feb1a6 100644 --- a/app/params/manage/tarif_params.rb +++ b/app/params/manage/tarif_params.rb @@ -3,8 +3,8 @@ module Manage class TarifParams < ApplicationParams def self.permitted_keys - %i[type label unit price_per_unit ordinal tarif_group accounting_account_nr pin - prefill_usage_method prefill_usage_booking_question_id vat + %i[type label unit price_per_unit ordinal tarif_group accounting_account_nr accounting_profit_center_nr + prefill_usage_method prefill_usage_booking_question_id vat_category_id pin minimum_usage_per_night minimum_usage_total minimum_price_per_night minimum_price_total] + I18n.available_locales.map { |locale| ["label_#{locale}", "unit_#{locale}"] }.flatten + [{ associated_types: [], diff --git a/app/serializers/manage/invoice_part_serializer.rb b/app/serializers/manage/invoice_part_serializer.rb index ebda6f9e5..ac571d8a9 100644 --- a/app/serializers/manage/invoice_part_serializer.rb +++ b/app/serializers/manage/invoice_part_serializer.rb @@ -4,6 +4,7 @@ module Manage class InvoicePartSerializer < ApplicationSerializer association :tarif, blueprint: Manage::TarifSerializer association :usage, blueprint: Manage::UsageSerializer + association :vat_category, blueprint: Public::VatCategorySerializer fields :amount, :label, :breakdown, :usage_id diff --git a/app/serializers/manage/invoice_serializer.rb b/app/serializers/manage/invoice_serializer.rb index c089fcd90..099490a57 100644 --- a/app/serializers/manage/invoice_serializer.rb +++ b/app/serializers/manage/invoice_serializer.rb @@ -4,6 +4,6 @@ module Manage class InvoiceSerializer < ApplicationSerializer identifier :id fields :type, :text, :issued_at, :payable_until, :ref, :sent_at, :booking_id, - :amount_paid, :percentage_paid, :amount, :locale, :vat, :payment_required + :amount_paid, :percentage_paid, :amount, :locale, :payment_required end end diff --git a/app/serializers/manage/tarif_serializer.rb b/app/serializers/manage/tarif_serializer.rb index cc1fd8c02..52742ab1f 100644 --- a/app/serializers/manage/tarif_serializer.rb +++ b/app/serializers/manage/tarif_serializer.rb @@ -2,6 +2,8 @@ module Manage class TarifSerializer < ApplicationSerializer + association :vat_category, blueprint: Public::VatCategorySerializer + fields :label, :pin, :prefill_usage_method, :price_per_unit, :tarif_group, :type, :unit, :ordinal, :label_i18n, :unit_i18n, :valid_from, :valid_until, :accounting_account_nr, :minimum_usage_per_night, :minimum_usage_total diff --git a/app/serializers/public/vat_category_serializer.rb b/app/serializers/public/vat_category_serializer.rb new file mode 100644 index 000000000..e44fe2698 --- /dev/null +++ b/app/serializers/public/vat_category_serializer.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module Public + class VatCategorySerializer < ApplicationSerializer + fields :label_i18n, :percentage, :accounting_vat_code, :to_s + end +end diff --git a/app/services/export/pdf/renderables/invoice/invoice_parts_table.rb b/app/services/export/pdf/renderables/invoice/invoice_parts_table.rb index 0473a751e..27cf77604 100644 --- a/app/services/export/pdf/renderables/invoice/invoice_parts_table.rb +++ b/app/services/export/pdf/renderables/invoice/invoice_parts_table.rb @@ -44,7 +44,7 @@ def render_invoice_total_table end def render_invoice_vat_table - return if invoice.vat.none? + return if invoice.vat_amounts.none? move_down 10 start_new_page if cursor < (vat_table_data.count + 1) * 9 @@ -90,11 +90,13 @@ def invoice_part_table_row_data(invoice_part) end def vat_table_data - invoice.vat.map do |vat_percentage, vat_amounts| + invoice.vat_amounts.map do |vat_category, amount| [ - I18n.t('invoices.vat_label', vat: vat_percentage), organisation.currency, - ActionController::Base.helpers.number_to_currency(vat_amounts[:total], unit: ''), - ActionController::Base.helpers.number_to_currency(vat_amounts[:tax], unit: '') + vat_category.label, + helpers.number_to_percentage(vat_category.percentage, precision: 2), + organisation.currency, + helpers.number_to_currency(amount, unit: ''), + helpers.number_to_currency(vat_category.tax_of(amount), unit: '') ] end end diff --git a/app/services/template_context.rb b/app/services/template_context.rb index ec5456cc1..bc20f0e3e 100644 --- a/app/services/template_context.rb +++ b/app/services/template_context.rb @@ -15,7 +15,8 @@ class TemplateContext CostEstimation => Manage::CostEstimationSerializer, BookingQuestion => Public::BookingQuestionSerializer, BookingQuestionResponse => Public::BookingQuestionResponseSerializer, - MeterReadingPeriod => Manage::MeterReadingPeriodSerializer + MeterReadingPeriod => Manage::MeterReadingPeriodSerializer, + VatCategory => Public::VatCategorySerializer }.freeze def initialize(context) diff --git a/app/views/manage/invoice_parts/_form.html.slim b/app/views/manage/invoice_parts/_form.html.slim index 74d0553aa..147bea9e9 100644 --- a/app/views/manage/invoice_parts/_form.html.slim +++ b/app/views/manage/invoice_parts/_form.html.slim @@ -6,8 +6,10 @@ = f.text_field :ordinal_position = f.text_field :label = f.text_field :breakdown - = f.text_field :vat, inputmode: 'numeric' = f.text_field :amount, inputmode: 'numeric' + - if current_organisation.vat_categories.any? + = f.collection_select :vat_category_id, current_organisation.vat_categories, :id, :to_s, include_blank: true + .form-actions.pt-4.mt-3 = f.submit diff --git a/app/views/manage/tarifs/_form.html.slim b/app/views/manage/tarifs/_form.html.slim index ac4127134..31ba72370 100644 --- a/app/views/manage/tarifs/_form.html.slim +++ b/app/views/manage/tarifs/_form.html.slim @@ -30,9 +30,7 @@ fieldset = f.text_field :price_per_unit, step: 0.01, inputmode: "numeric" - = f.check_box :pin - fieldset details summary.mb-3 =t('.minimum') @@ -41,14 +39,22 @@ = f.text_field :minimum_usage_total, step: 0.01, inputmode: "numeric" = f.text_field :minimum_price_per_night, step: 0.01, inputmode: "numeric" = f.text_field :minimum_price_total, step: 0.01, inputmode: "numeric" + + = f.check_box :pin fieldset = f.collection_check_boxes :associated_types, Tarif.associated_types.keys, :itself, ->(key) { Tarif::ASSOCIATED_TYPES[key]&.model_name&.human } .row .col-md-6= f.collection_select :prefill_usage_booking_question_id, @tarif.prefill_usage_booking_questions, :id, :label, include_blank: true .col-md-6= f.select :prefill_usage_method, enum_options_for_select(Tarif, :prefill_usage_methods, @tarif.prefill_usage_method), include_blank: true - = f.text_field :accounting_account_nr - = f.text_field :vat, step: 0.1, inputmode: "numeric" + + fieldset + .row + .col-md-6= f.text_field :accounting_account_nr + .col-md-6= f.text_field :accounting_profit_center_nr + + - if current_organisation.vat_categories.any? + = f.collection_select :vat_category_id, current_organisation.vat_categories, :id, :to_s, include_blank: true fieldset.mt-4 label.mb-2= Tarif.human_attribute_name(:selecting_conditions) diff --git a/app/views/manage/vat_categories/_form.html.slim b/app/views/manage/vat_categories/_form.html.slim new file mode 100644 index 000000000..6884f38be --- /dev/null +++ b/app/views/manage/vat_categories/_form.html.slim @@ -0,0 +1,23 @@ += form_with(model: [:manage, @vat_category], local: true) do |f| + + fieldset + ul.nav.nav-tabs.mt-4 role="tablist" + - I18n.available_locales.each do |locale| + - current_locale = locale == I18n.locale + li.nav-item + = link_to locale.upcase, "#title-#{locale}-tab", + class: "nav-link #{'active' if current_locale}", + aria: { controls: "title-#{locale}-tab", selected: current_locale }, + data: { "bs-toggle": 'tab' }, role: 'tab' + .tab-content + - I18n.available_locales.each do |locale| + - current_locale = locale == I18n.locale + .tab-pane.pt-3[id="label-#{locale}-tab" class="#{'show active' if current_locale}" aria-labelledby="label-#{locale}-tab" role='tabpanel'] + = f.text_field "label_#{locale.to_s}", label: VatCategory.human_attribute_name(:label) + + = f.text_field :percentage, step: 0.1, inputmode: "numeric" + = f.text_field :accounting_vat_code + + .form-actions.pt-4.mt-3 + = f.submit + = link_to t(:back), manage_vat_categories_path, class: 'btn btn-default' diff --git a/app/views/manage/vat_categories/edit.html.slim b/app/views/manage/vat_categories/edit.html.slim new file mode 100644 index 000000000..f6dd6ddc9 --- /dev/null +++ b/app/views/manage/vat_categories/edit.html.slim @@ -0,0 +1,5 @@ +.row.justify-content-center + .col-lg-8 + .card.shadow-sm + .card-body + == render 'form' diff --git a/app/views/manage/vat_categories/index.html.slim b/app/views/manage/vat_categories/index.html.slim new file mode 100644 index 000000000..4ee4ecc06 --- /dev/null +++ b/app/views/manage/vat_categories/index.html.slim @@ -0,0 +1,24 @@ + +h1.mt-0.mb-5= VatCategory.model_name.human(count: 2) + +.table-responsive + table.table.table-hover.align-middle + tbody.shadow-sm + - @vat_categories.each do |vat_category| + tr.bg-white[class=('disabled' if vat_category.discarded?)] + td + = link_to vat_category.to_s, edit_manage_vat_category_path(vat_category) + .badge.bg-secondary= vat_category.accounting_vat_code if vat_category.accounting_vat_code.present? + td.p-1.text-end + .btn-group + - unless vat_category.discarded? + = link_to edit_manage_vat_category_path(vat_category), class: 'btn btn-default' do + span.fa.fa-edit + = link_to manage_vat_category_path(vat_category), data: { confirm: t(:confirm) }, method: :delete, title: t(:destroy), class: 'btn btn-default' do + span.fa.fa-trash + +br += link_to new_manage_vat_category_path, class: 'btn btn-primary' do + span.fa.fa-file-o + ' + = t(:add_record, model_name: VatCategory.model_name.human) diff --git a/app/views/manage/vat_categories/new.html.slim b/app/views/manage/vat_categories/new.html.slim new file mode 100644 index 000000000..f6dd6ddc9 --- /dev/null +++ b/app/views/manage/vat_categories/new.html.slim @@ -0,0 +1,5 @@ +.row.justify-content-center + .col-lg-8 + .card.shadow-sm + .card-body + == render 'form' diff --git a/app/views/renderables/data_digest_templates/accounting_journal_entry/_show_data_digest.html.slim b/app/views/renderables/data_digest_templates/accounting_journal_entry/_show_data_digest.html.slim index 874193219..74393f333 100644 --- a/app/views/renderables/data_digest_templates/accounting_journal_entry/_show_data_digest.html.slim +++ b/app/views/renderables/data_digest_templates/accounting_journal_entry/_show_data_digest.html.slim @@ -1,18 +1,18 @@ ol - @data_digest.data.each do |data_item| - - journal_entry = Accounting::JournalEntry.new(**data_item) + - journal_entry = Accounting::JournalEntryGroup.new(**data_item) li - = Accounting::JournalEntry.model_name.human + = Accounting::JournalEntryGroup.model_name.human dl - dt= Accounting::JournalEntry.human_attribute_name(:date) + dt= Accounting::JournalEntryGroup.human_attribute_name(:date) dd= I18n.l(journal_entry.date) if journal_entry.date.present? ol - journal_entry.items.each do |item| li dl - dt= Accounting::JournalEntryItem.human_attribute_name(:account) + dt= Accounting::JournalEntry.human_attribute_name(:account) dd= item.account - dt= Accounting::JournalEntryItem.human_attribute_name(:amount) + dt= Accounting::JournalEntry.human_attribute_name(:amount) dd= number_to_currency(item.amount) - dt= Accounting::JournalEntryItem.human_attribute_name(:side) + dt= Accounting::JournalEntry.human_attribute_name(:side) dd= item.side.positive? ? "Haben" : "Soll" diff --git a/app/views/renderables/invoice_parts/add/_form_fields.html.slim b/app/views/renderables/invoice_parts/add/_form_fields.html.slim index 4337b99a6..25fde7e25 100644 --- a/app/views/renderables/invoice_parts/add/_form_fields.html.slim +++ b/app/views/renderables/invoice_parts/add/_form_fields.html.slim @@ -2,7 +2,7 @@ div = f.hidden_field :id = f.hidden_field :usage_id = f.hidden_field :type - = f.hidden_field :vat + = f.hidden_field :vat_category_id .row .col-1.py-2 diff --git a/app/views/renderables/invoice_parts/percentage/_form_fields.html.slim b/app/views/renderables/invoice_parts/percentage/_form_fields.html.slim index 97c7e1213..3686c2793 100644 --- a/app/views/renderables/invoice_parts/percentage/_form_fields.html.slim +++ b/app/views/renderables/invoice_parts/percentage/_form_fields.html.slim @@ -2,7 +2,7 @@ div = f.hidden_field :id = f.hidden_field :usage_id = f.hidden_field :type - = f.hidden_field :vat + = f.hidden_field :vat_category_id .row .col-1.py-2 diff --git a/config/locales/de.yml b/config/locales/de.yml index 115b75bab..ef91b8bb0 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -333,6 +333,7 @@ de: title: Titel tarif: accounting_account_nr: Buchhaltungskonto + accounting_profit_center_nr: Profit-Center associated_types: Ausgewiesen in Dokumenten enabling_conditions: Bedingungen für erlaubte Auswahl label: Tarifbezeichnung @@ -350,7 +351,7 @@ de: tarif_group: Tarifgruppe type: Typ unit: Einheit - vat: inkl. MwSt. in Prozent + vat_category_id: MwSt. Kategorie tenant: address: Adresse address_addon: Adresszusatz / Schule / Firma @@ -400,6 +401,10 @@ de: reset_password_token: Passwort-Zurücksetzen-Token sign_in_count: Anzahl Anmeldungen updated_at: Aktualisiert am + vat_category: + accounting_vat_code: Buchhaltungscode + label: Bezeichnung + percentage: Satz enums: booking: committed_request: @@ -724,6 +729,9 @@ de: user: one: Account other: Account + vat_category: + one: MwSt. Kategorie + other: MwSt. Kategorien add_record: "%{model_name} hinzufügen" back: Zurück booking_actions: @@ -1309,7 +1317,6 @@ de: deposited_amount: Gutschrift aus Anzahlung invoices: total: Total - vat_label: zu MwSt. Satz von %{vat}% vat_title: in den Preisen enhaltene MwSt. layouts: footer: @@ -1493,7 +1500,7 @@ de: format: delimiter: '' format: "%n%" - precision: '1' + precision: 1 precision: format: delimiter: '' diff --git a/config/locales/en.yml b/config/locales/en.yml index 002801f2f..845b10a3b 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1280,7 +1280,7 @@ en: format: delimiter: '' format: "%n%" - precision: '1' + precision: 1 precision: format: delimiter: '' diff --git a/config/locales/fr.yml b/config/locales/fr.yml index d397fca35..4b08c5c38 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -348,7 +348,6 @@ fr: tarif_group: groupe tarifaire type: type unit: unité - vat: TVA incluse en pourcentage tenant: address: Adresse address_addon: Complément d'adresse / école / entreprise @@ -1209,7 +1208,6 @@ fr: deposited_amount: Avoir à partir d'un acompte invoices: total: Total - vat_label: au taux de TVA. Taux de %{vat}% vat_title: TVA incluse dans les prix layouts: footer: diff --git a/config/locales/it.yml b/config/locales/it.yml index cb10948f2..0816a7049 100644 --- a/config/locales/it.yml +++ b/config/locales/it.yml @@ -348,7 +348,6 @@ it: tarif_group: Gruppo tariffario type: Tipo unit: Unità - vat: tenant: address: Indirizzo address_addon: Suffisso indirizzo / scuola / azienda @@ -1174,7 +1173,6 @@ it: deposited_amount: invoices: total: Totale - vat_label: vat_title: layouts: footer: @@ -1361,7 +1359,7 @@ it: format: delimiter: '' format: "%n%" - precision: '1' + precision: 1 precision: format: delimiter: '' diff --git a/config/routes.rb b/config/routes.rb index 16b62aa4d..b79e60c21 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -59,6 +59,7 @@ resources :designated_documents resources :booking_agents resources :booking_categories, except: :show + resources :vat_categories, except: :show resources :notifications, only: %i[index] resources :rich_text_templates do post :create_missing, on: :collection diff --git a/db/migrate/20241030163341_add_accounting_information.rb b/db/migrate/20241030163341_add_accounting_information.rb index a1afa0c8f..0e928db55 100644 --- a/db/migrate/20241030163341_add_accounting_information.rb +++ b/db/migrate/20241030163341_add_accounting_information.rb @@ -2,5 +2,6 @@ class AddAccountingInformation < ActiveRecord::Migration[7.2] def change add_column :tenants, :accounting_account_nr, :string, null: true rename_column :tarifs, :accountancy_account, :accounting_account_nr + add_column :tarifs, :accounting_profit_center_nr, :string, null: true end end diff --git a/db/migrate/20241203100339_create_vat_categories.rb b/db/migrate/20241203100339_create_vat_categories.rb new file mode 100644 index 000000000..ec77aa8a9 --- /dev/null +++ b/db/migrate/20241203100339_create_vat_categories.rb @@ -0,0 +1,13 @@ +class CreateVatCategories < ActiveRecord::Migration[8.0] + def change + create_table :vat_categories do |t| + t.decimal :percentage, null: false, default: 0.0 + t.jsonb :label_i18n, null: true + t.references :organisation, null: false, foreign_key: true + t.string :accounting_vat_code + t.datetime :discarded_at, index: true + + t.timestamps + end + end +end diff --git a/db/migrate/20241203101328_add_vat_category_id_to_tarifs.rb b/db/migrate/20241203101328_add_vat_category_id_to_tarifs.rb new file mode 100644 index 000000000..fc5bed859 --- /dev/null +++ b/db/migrate/20241203101328_add_vat_category_id_to_tarifs.rb @@ -0,0 +1,27 @@ +class AddVatCategoryIdToTarifs < ActiveRecord::Migration[8.0] + def change + add_reference :tarifs, :vat_category, null: true, foreign_key: true + add_reference :invoice_parts, :vat_category, null: true, foreign_key: true + + reversible do |direction| + direction.up do + Organisation.find_each do |organisation| + organisation.tarifs.find_each { migrate_tarif(_1) } + end + end + end + + remove_column :tarifs, :vat, :decimal + remove_column :invoice_parts, :vat, :decimal + end + + protected + + def migrate_tarif(tarif) + return if !tarif.respond_to?(:vat) || tarif.vat.blank? + + vat_category = organisation.vat_categories.find_or_create_by!(percentage: tarif.vat) + tarif.update(vat_category:) + tarif.invoice_parts.find_each { |invoice_part| invoice_part.update(vat_category:) } + end +end diff --git a/db/schema.rb b/db/schema.rb index 4af65c04b..531fe1748 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2024_11_29_161448) do +ActiveRecord::Schema[8.0].define(version: 2024_12_03_101328) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" enable_extension "pgcrypto" @@ -307,9 +307,10 @@ t.integer "ordinal" t.datetime "created_at", precision: nil, null: false t.datetime "updated_at", precision: nil, null: false - t.decimal "vat" + t.bigint "vat_category_id" t.index ["invoice_id"], name: "index_invoice_parts_on_invoice_id" t.index ["usage_id"], name: "index_invoice_parts_on_usage_id" + t.index ["vat_category_id"], name: "index_invoice_parts_on_vat_category_id" end create_table "invoices", force: :cascade do |t| @@ -538,14 +539,16 @@ t.decimal "minimum_usage_total" t.bigint "organisation_id", null: false t.datetime "discarded_at" - t.decimal "vat" t.bigint "prefill_usage_booking_question_id" t.decimal "minimum_price_per_night" t.decimal "minimum_price_total" + t.string "accounting_profit_center_nr" + t.bigint "vat_category_id" t.index ["discarded_at"], name: "index_tarifs_on_discarded_at" t.index ["organisation_id"], name: "index_tarifs_on_organisation_id" t.index ["prefill_usage_booking_question_id"], name: "index_tarifs_on_prefill_usage_booking_question_id" t.index ["type"], name: "index_tarifs_on_type" + t.index ["vat_category_id"], name: "index_tarifs_on_vat_category_id" end create_table "tenants", force: :cascade do |t| @@ -626,6 +629,18 @@ t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true end + create_table "vat_categories", force: :cascade do |t| + t.decimal "percentage", default: "0.0", null: false + t.jsonb "label_i18n" + t.bigint "organisation_id", null: false + t.string "accounting_vat_code" + t.datetime "discarded_at" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["discarded_at"], name: "index_vat_categories_on_discarded_at" + t.index ["organisation_id"], name: "index_vat_categories_on_organisation_id" + end + add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id" add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id" add_foreign_key "agent_bookings", "booking_agents" @@ -649,6 +664,7 @@ add_foreign_key "deadlines", "bookings" add_foreign_key "invoice_parts", "invoices" add_foreign_key "invoice_parts", "usages" + add_foreign_key "invoice_parts", "vat_categories" add_foreign_key "invoices", "bookings" add_foreign_key "invoices", "invoices", column: "supersede_invoice_id" add_foreign_key "mail_template_designated_documents", "designated_documents" @@ -671,8 +687,10 @@ add_foreign_key "rich_text_templates", "organisations" add_foreign_key "tarifs", "booking_questions", column: "prefill_usage_booking_question_id" add_foreign_key "tarifs", "organisations" + add_foreign_key "tarifs", "vat_categories" add_foreign_key "tenants", "organisations" add_foreign_key "usages", "bookings" add_foreign_key "usages", "tarifs" add_foreign_key "users", "organisations", column: "default_organisation_id" + add_foreign_key "vat_categories", "organisations" end diff --git a/spec/factories/invoice_parts.rb b/spec/factories/invoice_parts.rb index d421bff22..d355f5dda 100644 --- a/spec/factories/invoice_parts.rb +++ b/spec/factories/invoice_parts.rb @@ -4,22 +4,23 @@ # # Table name: invoice_parts # -# id :integer not null, primary key -# invoice_id :integer -# usage_id :integer -# type :string -# amount :decimal(, ) -# label :string -# breakdown :string -# ordinal :integer -# created_at :datetime not null -# updated_at :datetime not null -# vat :decimal(, ) +# id :integer not null, primary key +# invoice_id :integer +# usage_id :integer +# type :string +# amount :decimal(, ) +# label :string +# breakdown :string +# ordinal :integer +# created_at :datetime not null +# updated_at :datetime not null +# vat_category_id :integer # # Indexes # -# index_invoice_parts_on_invoice_id (invoice_id) -# index_invoice_parts_on_usage_id (usage_id) +# index_invoice_parts_on_invoice_id (invoice_id) +# index_invoice_parts_on_usage_id (usage_id) +# index_invoice_parts_on_vat_category_id (vat_category_id) # FactoryBot.define do diff --git a/spec/factories/vat_categories.rb b/spec/factories/vat_categories.rb new file mode 100644 index 000000000..81c4a42ca --- /dev/null +++ b/spec/factories/vat_categories.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :vat_category do + percentage { 8.1 } + label { 'Normalsatz' } + organisation + accounting_vat_code { '' } + end +end diff --git a/spec/models/invoice_part_spec.rb b/spec/models/invoice_part_spec.rb index f24412e7a..c3600b75a 100644 --- a/spec/models/invoice_part_spec.rb +++ b/spec/models/invoice_part_spec.rb @@ -4,22 +4,23 @@ # # Table name: invoice_parts # -# id :integer not null, primary key -# invoice_id :integer -# usage_id :integer -# type :string -# amount :decimal(, ) -# label :string -# breakdown :string -# ordinal :integer -# created_at :datetime not null -# updated_at :datetime not null -# vat :decimal(, ) +# id :integer not null, primary key +# invoice_id :integer +# usage_id :integer +# type :string +# amount :decimal(, ) +# label :string +# breakdown :string +# ordinal :integer +# created_at :datetime not null +# updated_at :datetime not null +# vat_category_id :integer # # Indexes # -# index_invoice_parts_on_invoice_id (invoice_id) -# index_invoice_parts_on_usage_id (usage_id) +# index_invoice_parts_on_invoice_id (invoice_id) +# index_invoice_parts_on_usage_id (usage_id) +# index_invoice_parts_on_vat_category_id (vat_category_id) # require 'rails_helper' diff --git a/spec/models/vat_category_spec.rb b/spec/models/vat_category_spec.rb new file mode 100644 index 000000000..df6dfbb3f --- /dev/null +++ b/spec/models/vat_category_spec.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe VatCategory, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end From f4f8db61313e612d56a5102592e83b9f9ca6a3bf Mon Sep 17 00:00:00 2001 From: Diego Steiner Date: Tue, 3 Dec 2024 14:47:23 +0000 Subject: [PATCH 07/14] fix: specs --- app/models/accounting.rb | 12 ++--- .../accounting_journal_entry.rb | 4 +- app/models/data_digest_templates/booking.rb | 6 ++- app/models/data_digest_templates/payment.rb | 6 ++- app/models/data_digest_templates/tabular.rb | 14 +++-- app/services/export/pdf/data_digest_pdf.rb | 2 +- app/services/taf_block.rb | 18 +++---- .../booking/_form_fields.html.slim | 2 +- .../tabular/_show_data_digest.html.slim | 16 +++--- spec/models/data_digest_template_spec.rb | 22 +------- .../booking_spec.rb | 14 ++--- .../invoice_part_spec.rb | 6 +-- .../payment_spec.rb | 12 +++-- .../data_digest_templates/tabular_spec.rb | 52 +++++++++++++++++++ spec/services/taf_block_spec.rb | 34 ++++++------ 15 files changed, 134 insertions(+), 86 deletions(-) rename spec/models/{data_digests => data_digest_templates}/booking_spec.rb (85%) rename spec/models/{data_digests => data_digest_templates}/invoice_part_spec.rb (93%) rename spec/models/{data_digests => data_digest_templates}/payment_spec.rb (74%) create mode 100644 spec/models/data_digest_templates/tabular_spec.rb diff --git a/app/models/accounting.rb b/app/models/accounting.rb index 2a82a29d8..0d6ca4a1b 100644 --- a/app/models/accounting.rb +++ b/app/models/accounting.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Accounting - JournalEntry = Data.define(:id, :date, :invoice_id, :items) do + JournalEntryGroup = Data.define(:id, :date, :items) do extend ActiveModel::Translation extend ActiveModel::Naming @@ -10,8 +10,8 @@ def initialize(**args) date = args.delete(:date)&.then { _1.try(:to_date) || Date.parse(_1).to_date } items = Array.wrap(args.delete(:items)).map do |item| case item - when Hash, JournalEntryItem - JournalEntryItem.new(**item.to_h.merge(journal_entry: self)) + when Hash, JournalEntry + JournalEntry.new(**item.to_h, journal_entry: self) end end.compact super(id: nil, **args, items:, date:) @@ -22,15 +22,15 @@ def to_h end end - JournalEntryItem = Data.define(:id, :account, :date, :tax_code, :text, :amount, :side, :cost_center, - :index, :amount_type, :source, :invoice_id) do + JournalEntry = Data.define(:id, :account, :date, :tax_code, :text, :amount, :side, :cost_center, + :index, :amount_type, :source) do extend ActiveModel::Translation extend ActiveModel::Naming def initialize(**args) args.symbolize_keys! @journal_entry = args.delete(:journal_entry) - defaults = { id: nil, index: nil, tax_code: nil, text: '', cost_center: nil } + defaults = { id: nil, index: nil, tax_code: nil, text: nil, cost_center: nil, source: nil } date = args.delete(:date)&.then { _1.try(:to_date) || Date.parse(_1).to_date } super(**defaults, **args, date:) end diff --git a/app/models/data_digest_templates/accounting_journal_entry.rb b/app/models/data_digest_templates/accounting_journal_entry.rb index 680bdfbe2..287c62b0f 100644 --- a/app/models/data_digest_templates/accounting_journal_entry.rb +++ b/app/models/data_digest_templates/accounting_journal_entry.rb @@ -41,14 +41,14 @@ def base_scope def crunch(records) invoice_ids = records.pluck(:id).uniq - base_scope.where(id: invoice_ids).find_each.flat_map do |invoice| + base_scope.where(id: invoice_ids).find_each(cursor: []).flat_map do |invoice| invoice.journal_entry end end formatter(:taf) do |_options = {}| data.flat_map do |record| - journal_entry = ::Accounting::JournalEntry.new(**record) + journal_entry = ::Accounting::JournalEntryGroup.new(**record) [ TafBlock.build_from(journal_entry) ] diff --git a/app/models/data_digest_templates/booking.rb b/app/models/data_digest_templates/booking.rb index 163db6aca..235997858 100644 --- a/app/models/data_digest_templates/booking.rb +++ b/app/models/data_digest_templates/booking.rb @@ -113,8 +113,12 @@ def filter_class ::Booking::Filter end + def record_order + { begins_at: :asc, id: :asc } + end + def base_scope - @base_scope ||= organisation.bookings.ordered.with_default_includes + @base_scope ||= organisation.bookings.with_default_includes end end end diff --git a/app/models/data_digest_templates/payment.rb b/app/models/data_digest_templates/payment.rb index e3b4e1262..89800ddd3 100644 --- a/app/models/data_digest_templates/payment.rb +++ b/app/models/data_digest_templates/payment.rb @@ -71,8 +71,12 @@ def filter_class ::Payment::Filter end + def record_order + { created_at: :asc, id: :asc } + end + def base_scope - @base_scope ||= organisation.payments.ordered + @base_scope ||= organisation.payments end end end diff --git a/app/models/data_digest_templates/tabular.rb b/app/models/data_digest_templates/tabular.rb index 236eb5756..9115c09ed 100644 --- a/app/models/data_digest_templates/tabular.rb +++ b/app/models/data_digest_templates/tabular.rb @@ -62,17 +62,21 @@ def columns_config=(value) def crunch(records) record_ids = records.pluck(:id).uniq - base_scope.where(id: record_ids).find_each.map do |record| + base_scope.where(id: record_ids).find_each(cursor: record_order.keys, order: record_order.values).map do |record| template_context_cache = {} columns.map { |column| column.body(record, template_context_cache) } end end - def headers + def record_order + { id: :asc } + end + + def header columns.map(&:header) end - def footers + def footer columns.map(&:footer) end @@ -140,9 +144,9 @@ def cache_key(record, *parts) bom = "\uFEFF" bom + CSV.generate(**options) do |csv| - csv << header + csv << data_digest_template.header data&.each { |row| csv << row } - csv << footer if footer.any?(&:present?) + csv << data_digest_template.footer if data_digest_template.footer.any?(&:present?) end end diff --git a/app/services/export/pdf/data_digest_pdf.rb b/app/services/export/pdf/data_digest_pdf.rb index 219018683..171467cdf 100644 --- a/app/services/export/pdf/data_digest_pdf.rb +++ b/app/services/export/pdf/data_digest_pdf.rb @@ -29,7 +29,7 @@ def document_options end to_render do - table_data = [data_digest.header] + data_digest.data + table_data = [data_digest.data_digest_template.header] + data_digest.data table(table_data, width: bounds.width) do cells.style(size: 6, borders: []) row(0).font_style = :bold diff --git a/app/services/taf_block.rb b/app/services/taf_block.rb index 5a8f671a6..7398bd79d 100644 --- a/app/services/taf_block.rb +++ b/app/services/taf_block.rb @@ -68,20 +68,20 @@ def self.serialize_properies(properties, join_with: ' ') properties.compact.flat_map { |key, value| "#{key}=#{serialize_value(value)}" }.join(join_with) end - def self.builders - @builders ||= {} + def self.factories + @factories ||= {} end - def self.register_builder(klass, &build_block) - builders[klass] = build_block + def self.register_factory(klass, &build_block) + factories[klass] = build_block end def self.build_from(value, **options) - build_block = builders[builders.keys.find { |klass| value.is_a?(klass) }] + build_block = factories[factories.keys.find { |klass| value.is_a?(klass) }] instance_exec(value, options, &build_block) if build_block.present? end - register_builder Accounting::JournalEntry do |value, **options| + register_factory Accounting::JournalEntryGroup do |value, **options| new(:Blg, *value.items.map { TafBlock.build_from(_1) }, **{ # Date; The date of the booking. Date: options.fetch(:Date, value.date), @@ -90,7 +90,7 @@ def self.build_from(value, **options) }) end - register_builder Accounting::JournalEntryItem do |value, **options| + register_factory Accounting::JournalEntry do |value, **options| new(:Bk, **{ # The Id of a book keeping account. [Fibu-Konto] AccId: options.fetch(:AccId, value.account), @@ -122,7 +122,7 @@ def self.build_from(value, **options) TaxId: options.fetch(:TaxId, value.tax_code), # String[61*]; This string specifies the first line of the booking text. - Text: options.fetch(:Text, value.text&.slice(0..59)&.lines&.first&.strip || '-'), + Text: options.fetch(:Text, value.text&.slice(0..59)&.lines&.first&.strip || '-'), # rubocop:disable Style/SafeNavigationChainLength # String[*]; This string specifies the second line of the booking text. # (*)Both fields Text and Text2 are stored in the same memory location, @@ -131,7 +131,7 @@ def self.build_from(value, **options) # Be careful not to put too many characters onto one single line, because # most Reports are not designed to display a full string containing 60 # characters. - Text2: options.fetch(:Text2, value.text&.slice(0..59)&.lines&.[](1..-1)&.join("\n")).presence, + Text2: options.fetch(:Text2, value.text&.slice(0..59)&.lines&.[](1..-1)&.join("\n")).presence, # rubocop:disable Style/SafeNavigationChainLength # Integer; This is the index of the booking that represents the tax booking # which is attached to this booking. diff --git a/app/views/renderables/data_digest_templates/booking/_form_fields.html.slim b/app/views/renderables/data_digest_templates/booking/_form_fields.html.slim index 0d2a7b50c..004418319 100644 --- a/app/views/renderables/data_digest_templates/booking/_form_fields.html.slim +++ b/app/views/renderables/data_digest_templates/booking/_form_fields.html.slim @@ -1 +1 @@ -= render partial: 'renderabes/data_digest_templates/tabular/form_fields', locals: { data_digest_template: } += render partial: 'renderables/data_digest_templates/tabular/form_fields', locals: { data_digest_template: } diff --git a/app/views/renderables/data_digest_templates/tabular/_show_data_digest.html.slim b/app/views/renderables/data_digest_templates/tabular/_show_data_digest.html.slim index 2e878102d..fc12f4500 100644 --- a/app/views/renderables/data_digest_templates/tabular/_show_data_digest.html.slim +++ b/app/views/renderables/data_digest_templates/tabular/_show_data_digest.html.slim @@ -1,11 +1,11 @@ .table-responsive table.table-striped.table.table-responsive - - headers = data_digest.data_digest_template.headers - - footers = data_digest.data_digest_template.footers - - if headers + - header = data_digest.data_digest_template.header + - footer = data_digest.data_digest_template.footer + - if header thead - - headers.each do |header| - th= header + - header.each do |header_column| + th= header_column tbody.shadow-sm - data_digest.data&.each do |row| @@ -13,7 +13,7 @@ - row.each do |cell| td= cell - - if footers&.any?(&:present?) + - if footer&.any?(&:present?) tfooter - - footers.each do |footer| - td= footer + - footer.each do |footer_column| + td= footer_column diff --git a/spec/models/data_digest_template_spec.rb b/spec/models/data_digest_template_spec.rb index b9b9abbcf..f46d2cec5 100644 --- a/spec/models/data_digest_template_spec.rb +++ b/spec/models/data_digest_template_spec.rb @@ -26,25 +26,5 @@ require 'rails_helper' RSpec.describe DataDigestTemplate, type: :model do - describe '#columns' do - subject(:columns) { data_digest_template.columns } - - let(:columns_config) do - [ - { - header: 'Test Header 1', - body: '{{ booking.ref }}' - }, - { - header: 'Test Header 2', - body: '{{ booking.name }}' - } - ] - end - - let(:data_digest_template) { create(:data_digest_template, columns_config:) } - - it { expect(columns.count).to eq(2) } - it { is_expected.to all(be_a DataDigestTemplate::Column) } - end + subject(:data_digest_template) { described_class.new } end diff --git a/spec/models/data_digests/booking_spec.rb b/spec/models/data_digest_templates/booking_spec.rb similarity index 85% rename from spec/models/data_digests/booking_spec.rb rename to spec/models/data_digest_templates/booking_spec.rb index e6f3f85cd..00efee71b 100644 --- a/spec/models/data_digests/booking_spec.rb +++ b/spec/models/data_digest_templates/booking_spec.rb @@ -24,12 +24,14 @@ require 'rails_helper' RSpec.describe DataDigestTemplates::Booking, type: :model do + subject(:data_digest_template) do + build(:data_digest_template, columns_config:, organisation:).becomes(described_class).tap(&:save) + end subject(:data_digest) { data_digest_template.data_digests.create } let(:home) { create(:home) } let(:organisation) { home.organisation } let(:columns_config) { nil } - let(:data_digest_template) { create(:booking_data_digest_template, columns_config:, organisation:) } let!(:bookings) { create_list(:booking, 3, organisation:, home:) } describe '#records' do @@ -69,10 +71,10 @@ it { expect(data_digest.data.count).to be(3) } it do - expect(data_digest.header).to eq ['Buchungsreferenz', 'Hauptmietobjekt', - 'Beginn der Belegung', 'Ende der Belegung', - 'Beschreibung des Mietzwecks', 'Nächte', 'Mieter', 'Adresse', 'Email', - 'Telefon'] + expect(data_digest_template.header).to eq ['Buchungsreferenz', 'Hauptmietobjekt', + 'Beginn der Belegung', 'Ende der Belegung', + 'Beschreibung des Mietzwecks', 'Nächte', 'Mieter', + 'Adresse', 'Email', 'Telefon'] end end @@ -103,7 +105,7 @@ it { expect(data_digest.data.count).to be(3) } it do - expect(data_digest.header).to eq(['Ref', 'Usage Price']) + expect(data_digest_template.header).to eq(['Ref', 'Usage Price']) expect(data_digest.data).to include(*bookings.map { |booking| [booking.ref, 'CHF 15.00'] }) end end diff --git a/spec/models/data_digests/invoice_part_spec.rb b/spec/models/data_digest_templates/invoice_part_spec.rb similarity index 93% rename from spec/models/data_digests/invoice_part_spec.rb rename to spec/models/data_digest_templates/invoice_part_spec.rb index 77a981c02..b0e0216da 100644 --- a/spec/models/data_digests/invoice_part_spec.rb +++ b/spec/models/data_digest_templates/invoice_part_spec.rb @@ -24,6 +24,9 @@ require 'rails_helper' RSpec.describe DataDigestTemplates::InvoicePart, type: :model do + subject(:data_digest_template) do + build(:data_digest_template, columns_config:, organisation:).becomes(described_class).tap(&:save) + end subject(:data_digest) { data_digest_template.data_digests.create } let(:columns_config) { nil } @@ -36,9 +39,6 @@ end end.flatten end - let(:data_digest_template) do - create(:invoice_part_data_digest_template, columns_config:, organisation:) - end let(:home) { create(:home, organisation:) } let(:organisation) { create(:organisation) } let(:tarifs) { create_list(:tarif, 4, organisation:) } diff --git a/spec/models/data_digests/payment_spec.rb b/spec/models/data_digest_templates/payment_spec.rb similarity index 74% rename from spec/models/data_digests/payment_spec.rb rename to spec/models/data_digest_templates/payment_spec.rb index 37313820c..8193cdf40 100644 --- a/spec/models/data_digests/payment_spec.rb +++ b/spec/models/data_digest_templates/payment_spec.rb @@ -24,15 +24,16 @@ require 'rails_helper' RSpec.describe DataDigestTemplates::Payment, type: :model do + subject(:data_digest_template) do + build(:data_digest_template, columns_config:, organisation:).becomes(described_class).tap(&:save) + end subject(:data_digest) { data_digest_template.data_digests.create } + let(:organisation) { create(:organisation) } let(:columns_config) { nil } - let(:data_digest_template) do - create(:payment_data_digest_template, columns_config:) - end before do - create_list(:booking, 3, organisation: data_digest.organisation).map do |booking| + create_list(:booking, 3, organisation:).map do |booking| invoice = create(:invoice, booking:) create(:payment, invoice:, amount: invoice.amount) end @@ -43,7 +44,8 @@ it { is_expected.to be_a(DataDigest) } it do - expect(data_digest.header).to eq ['Ref', 'Buchungsreferenz', 'Bezahlt am', 'Betrag', 'Mieter', 'Bemerkungen'] + expect(data_digest.data_digest_template.header).to eq ['Ref', 'Buchungsreferenz', 'Bezahlt am', 'Betrag', + 'Mieter', 'Bemerkungen'] end it { expect(data_digest.data.count).to be(3) } diff --git a/spec/models/data_digest_templates/tabular_spec.rb b/spec/models/data_digest_templates/tabular_spec.rb new file mode 100644 index 000000000..f3e52913d --- /dev/null +++ b/spec/models/data_digest_templates/tabular_spec.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: data_digest_templates +# +# id :bigint not null, primary key +# columns_config :jsonb +# group :string +# label :string +# prefilter_params :jsonb +# type :string +# created_at :datetime not null +# updated_at :datetime not null +# organisation_id :bigint not null +# +# Indexes +# +# index_data_digest_templates_on_organisation_id (organisation_id) +# +# Foreign Keys +# +# fk_rails_... (organisation_id => organisations.id) +# + +require 'rails_helper' + +RSpec.describe DataDigestTemplates::Tabular, type: :model do + subject(:data_digest_template) do + build(:data_digest_template, columns_config:, organisation:).becomes(described_class).tap(&:save) + end + subject(:columns) { data_digest_template.columns } + let(:organisation) { create(:organisation) } + + describe '#columns' do + let(:columns_config) do + [ + { + header: 'Test Header 1', + body: '{{ booking.ref }}' + }, + { + header: 'Test Header 2', + body: '{{ booking.name }}' + } + ] + end + + it { expect(columns.count).to eq(2) } + it { is_expected.to all(be_a DataDigestTemplates::Tabular::Column) } + end +end diff --git a/spec/services/taf_block_spec.rb b/spec/services/taf_block_spec.rb index 6c0f75b63..22f3489ea 100644 --- a/spec/services/taf_block_spec.rb +++ b/spec/services/taf_block_spec.rb @@ -37,18 +37,18 @@ end end - context '::convert' do - describe 'Accounting::JournalEntryItem' do - subject(:converted) { described_class.convert(conversion_subject) } - let(:conversion_subject) do - Accounting::JournalEntryItem.new(account: 1050, amount: 2091.75, date: Date.new(2024, 10, 5), - amount_type: :netto, side: 1, tax_code: 'MwSt38', - text: "Lorem ipsum\nSecond Line, but its longer than sixty \"chars\", OMG!") + context '::build_from' do + describe 'Accounting::JournalEntry' do + subject(:taf_block) { described_class.build_from(journal_entry) } + let(:journal_entry) do + Accounting::JournalEntry.new(account: 1050, amount: 2091.75, date: Date.new(2024, 10, 5), + amount_type: :netto, side: 1, tax_code: 'MwSt38', + text: "Lorem ipsum\nSecond Line, but its longer than sixty \"chars\", OMG!") end - it 'converts correctly' do + it 'builds correctly' do is_expected.to be_a described_class - expect(converted.to_s).to eq(<<~TAF.chomp) + expect(taf_block.to_s).to eq(<<~TAF.chomp) {Bk AccId=1050 BType=1 @@ -63,17 +63,17 @@ end end - describe 'Accounting::JournalEntryItem' do - subject(:converted) { described_class.convert(conversion_subject) } - let(:conversion_subject) do - Accounting::JournalEntryItem.new(account: 1050, amount: 2091.75, date: Date.new(2024, 10, 5), - amount_type: :netto, side: 1, tax_code: 'USt38', - text: "Lorem ipsum\nSecond Line, but its longer than sixty \"chars\", OMG!") + describe 'Accounting::JournalEntry' do + subject(:taf_block) { described_class.build_from(journal_entry) } + let(:journal_entry) do + Accounting::JournalEntry.new(account: 1050, amount: 2091.75, date: Date.new(2024, 10, 5), + amount_type: :netto, side: 1, tax_code: 'MwSt38', + text: "Lorem ipsum\nSecond Line, but its longer than sixty \"chars\", OMG!") end - it 'converts correctly' do + it 'builds correctly' do is_expected.to be_a described_class - expect(converted.to_s).to eq(<<~TAF.chomp) + expect(taf_block.to_s).to eq(<<~TAF.chomp) {Bk AccId=1050 BType=1 From 78d4f684519892cc0f11ecf78500c98dc0c80641 Mon Sep 17 00:00:00 2001 From: Diego Steiner Date: Tue, 3 Dec 2024 14:49:00 +0000 Subject: [PATCH 08/14] feat: add invoice_part for deposit --- app/models/invoice_part/factory.rb | 4 ++-- app/models/invoice_parts/deposit.rb | 34 +++++++++++++++++++++++++++++ config/locales/de.yml | 3 +++ 3 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 app/models/invoice_parts/deposit.rb diff --git a/app/models/invoice_part/factory.rb b/app/models/invoice_part/factory.rb index 6de533c63..37ea4034f 100644 --- a/app/models/invoice_part/factory.rb +++ b/app/models/invoice_part/factory.rb @@ -54,8 +54,8 @@ def from_deposits [ InvoiceParts::Text.new(apply:, label: Invoices::Deposit.model_name.human), - InvoiceParts::Add.new(apply:, label: I18n.t('invoice_parts.deposited_amount'), - amount: - deposited_amount) + InvoiceParts::Deposit.new(apply:, label: I18n.t('invoice_parts.deposited_amount'), + amount: - deposited_amount) ] end diff --git a/app/models/invoice_parts/deposit.rb b/app/models/invoice_parts/deposit.rb new file mode 100644 index 000000000..b5d610349 --- /dev/null +++ b/app/models/invoice_parts/deposit.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +# == Schema Information +# +# Table name: invoice_parts +# +# id :integer not null, primary key +# invoice_id :integer +# usage_id :integer +# type :string +# amount :decimal(, ) +# label :string +# breakdown :string +# ordinal :integer +# created_at :datetime not null +# updated_at :datetime not null +# vat_category_id :integer +# +# Indexes +# +# index_invoice_parts_on_invoice_id (invoice_id) +# index_invoice_parts_on_usage_id (usage_id) +# index_invoice_parts_on_vat_category_id (vat_category_id) +# + +module InvoiceParts + class Deposit < InvoicePart + InvoicePart.register_subtype self + + def calculated_amount + amount + end + end +end diff --git a/config/locales/de.yml b/config/locales/de.yml index ef91b8bb0..9154483b4 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -651,6 +651,9 @@ de: invoice_parts/add: one: Normale Rechungsposition other: Normale Rechnungspositionen + invoice_parts/deposit: + one: Anzahlung Rechungsposition + other: Anzahlung Rechnungspositionen invoice_parts/percentage: one: Rabatt other: Rabatt From 143ee27280bb7799be6381b01fee81a9e474c3c9 Mon Sep 17 00:00:00 2001 From: Diego Steiner Date: Tue, 3 Dec 2024 21:06:42 +0000 Subject: [PATCH 09/14] fix: accounting journal entries --- .rubocop.yml | 2 +- .../manage/vat_categories_controller.rb | 2 +- app/models/accounting.rb | 62 ++++++++++---- app/models/booking.rb | 2 +- app/models/data_digest_template.rb | 32 ++++--- .../accounting_journal_entry.rb | 84 ++++++++++++++++--- app/models/invoice.rb | 14 +++- app/models/invoice_part.rb | 8 +- app/models/invoice_parts/add.rb | 12 +-- app/models/invoice_parts/deposit.rb | 6 +- app/models/mail_template.rb | 2 +- app/models/rich_text_template.rb | 1 - app/models/vat_category.rb | 4 +- .../manage/journal_entry_serializer.rb | 19 +++++ .../invoice/invoice_parts_table.rb | 12 +-- app/services/taf_block.rb | 2 +- app/services/template_context.rb | 1 + .../_form_fields.html.slim | 1 + .../_show_data_digest.html.slim | 19 +---- spec/services/taf_block_spec.rb | 10 ++- 20 files changed, 200 insertions(+), 95 deletions(-) create mode 100644 app/serializers/manage/journal_entry_serializer.rb diff --git a/.rubocop.yml b/.rubocop.yml index 44ae283b4..356a3badf 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -27,7 +27,7 @@ Layout/LineLength: Max: 120 Metrics/ClassLength: - Max: 120 + Max: 150 Metrics/MethodLength: Exclude: diff --git a/app/controllers/manage/vat_categories_controller.rb b/app/controllers/manage/vat_categories_controller.rb index a8dce9682..7c31fc7e9 100644 --- a/app/controllers/manage/vat_categories_controller.rb +++ b/app/controllers/manage/vat_categories_controller.rb @@ -33,7 +33,7 @@ def destroy def vat_category_params locale_params = I18n.available_locales.map { |locale| ["label_#{locale}"] } - params.require(:vat_category).permit(:percentage, :accouting_vat_code, locale_params.flatten) + params.require(:vat_category).permit(:percentage, :accounting_vat_code, locale_params.flatten) end end end diff --git a/app/models/accounting.rb b/app/models/accounting.rb index 0d6ca4a1b..3578ca9ed 100644 --- a/app/models/accounting.rb +++ b/app/models/accounting.rb @@ -1,38 +1,70 @@ # frozen_string_literal: true module Accounting - JournalEntryGroup = Data.define(:id, :date, :items) do + JournalEntry = Data.define(:id, :account, :date, :tax_code, :text, :amount, :side, :cost_center, + :index, :amount_type, :source, :reference, :currency, :booking) do extend ActiveModel::Translation extend ActiveModel::Naming def initialize(**args) args.symbolize_keys! + defaults = { id: nil, index: nil, tax_code: nil, text: nil, cost_center: nil, source: nil } + side = args.delete(:side) if %i[soll haben].include?(args[:side]) date = args.delete(:date)&.then { _1.try(:to_date) || Date.parse(_1).to_date } - items = Array.wrap(args.delete(:items)).map do |item| - case item - when Hash, JournalEntry - JournalEntry.new(**item.to_h, journal_entry: self) - end - end.compact - super(id: nil, **args, items:, date:) + super(**defaults, **args, side:, date:) end - def to_h - super.merge(items: items.map(&:to_h)) + def soll? + side == :soll + end + + def haben? + side == :haben + end + + def soll_account + account if soll? + end + + def haben_account + account if haben? + end + + def valid? + (soll_account.present? || haben_account.present?) && amount.present? + end + + def to_s + [ + (id || index).presence&.then { "[#{_1}]" }, + soll_account, + '->', + haben_account, + ActiveSupport::NumberHelper.number_to_currency(amount, unit: currency), + ':', + text + ].compact.join(' ') end end - JournalEntry = Data.define(:id, :account, :date, :tax_code, :text, :amount, :side, :cost_center, - :index, :amount_type, :source) do + JournalEntryGroup = Data.define(:id, :date, :items) do extend ActiveModel::Translation extend ActiveModel::Naming def initialize(**args) args.symbolize_keys! - @journal_entry = args.delete(:journal_entry) - defaults = { id: nil, index: nil, tax_code: nil, text: nil, cost_center: nil, source: nil } date = args.delete(:date)&.then { _1.try(:to_date) || Date.parse(_1).to_date } - super(**defaults, **args, date:) + items = Array.wrap(args.delete(:items)).map do |item| + case item + when Hash, JournalEntry + JournalEntry.new(**item.to_h) + end + end.compact + super(id: nil, **args, items:, date:) + end + + def to_h + super.merge(items: items.map(&:to_h)) end end end diff --git a/app/models/booking.rb b/app/models/booking.rb index 049148e25..6f49e16be 100644 --- a/app/models/booking.rb +++ b/app/models/booking.rb @@ -52,7 +52,7 @@ # fk_rails_... (organisation_id => organisations.id) # -class Booking < ApplicationRecord # rubocop:disable Metrics/ClassLength +class Booking < ApplicationRecord include BookingStateConcern include Timespanable diff --git a/app/models/data_digest_template.rb b/app/models/data_digest_template.rb index 111fd9169..fd7559003 100644 --- a/app/models/data_digest_template.rb +++ b/app/models/data_digest_template.rb @@ -39,25 +39,20 @@ class << self def period(period_sym, at: Time.zone.now) PERIODS[period_sym&.to_sym]&.call(at) end - end - def group - super.presence - end + def formatters + @formatters ||= (superclass.respond_to?(:formatters) && superclass.formatters&.dup) || {} + end - def base_scope - raise NotImplementedError + def formatter(format, default_options: {}, &block) + formatters[format.to_sym] = Formatter.new(default_options, block) + end end - def periodfilter(period); end - - def prefilter - @prefilter ||= filter_class&.new(prefilter_params.presence || {}) + def group + super.presence end - def filter_class; end - def crunch(_records); end - def records(period) prefiltered = prefilter&.apply(base_scope) || base_scope periodfilter(period)&.apply(prefiltered) || prefiltered @@ -67,11 +62,12 @@ def digest(period = nil) DataDigest.new(data_digest_template: self, period:) end - def self.formatters - @formatters ||= (superclass.respond_to?(:formatters) && superclass.formatters&.dup) || {} - end + def filter_class; end + def base_scope; end + def periodfilter(_period); end + def crunch(_records); end - def self.formatter(format, default_options: {}, &block) - formatters[format.to_sym] = Formatter.new(default_options, block) + def prefilter + @prefilter ||= filter_class&.new(prefilter_params.presence || {}) end end diff --git a/app/models/data_digest_templates/accounting_journal_entry.rb b/app/models/data_digest_templates/accounting_journal_entry.rb index 287c62b0f..ba0d988af 100644 --- a/app/models/data_digest_templates/accounting_journal_entry.rb +++ b/app/models/data_digest_templates/accounting_journal_entry.rb @@ -24,25 +24,71 @@ # module DataDigestTemplates - class AccountingJournalEntry < ::DataDigestTemplate + class AccountingJournalEntry < Tabular ::DataDigestTemplate.register_subtype self - def periodfilter(period = nil) - filter_class.new(issued_at_after: period&.begin, issued_at_before: period&.end) - end + DEFAULT_COLUMN_CONFIG = [ + { + header: ::Accounting::JournalEntry.human_attribute_name(:date), + body: '{{ journal_entry.date | date_format }}' + }, + { + header: ::Accounting::JournalEntry.human_attribute_name(:reference), + body: '{{ journal_entry.reference }}' + }, + { + header: ::Accounting::JournalEntry.human_attribute_name(:text), + body: '{{ journal_entry.text }}' + }, + { + header: ::Accounting::JournalEntry.human_attribute_name(:soll_account), + body: '{{ journal_entry.soll_account }}' + }, + { + header: ::Accounting::JournalEntry.human_attribute_name(:haben_account), + body: '{{ journal_entry.haben_account }}' + }, + { + header: ::Accounting::JournalEntry.human_attribute_name(:amount), + body: '{{ journal_entry.amount | round: 2 }}' + }, + { + header: ::Accounting::JournalEntry.human_attribute_name(:tax_code), + body: '{{ journal_entry.tax_code }}' + }, + { + header: ::Accounting::JournalEntry.human_attribute_name(:cost_center), + body: '{{ journal_entry.cost_center }}' + }, + { + header: ::Accounting::JournalEntry.model_name.human, + body: '{{ journal_entry.to_s }}' + }, + { + header: ::Booking.human_attribute_name(:ref), + body: '{{ booking.ref }}' + } + ].freeze - def filter_class - ::Invoice::Filter + column_type :default do + body do |journal_entry, tempalte_context_cache| + booking = journal_entry.booking + context = tempalte_context_cache[cache_key(journal_entry)] ||= + TemplateContext.new(booking:, organisation: booking.organisation, journal_entry:).to_h + @templates[:body]&.render!(context) + end end - def base_scope - @base_scope ||= ::Invoice.joins(:booking).where(bookings: { organisation_id: organisation }).kept + def records(period) + invoice_filter = ::Invoice::Filter.new(issued_at_after: period&.begin, issued_at_before: period&.end) + invoices = invoice_filter.apply(::Invoice.joins(:booking).where(bookings: { organisation: organisation }).kept) + invoices.map(&:journal_entries) end def crunch(records) - invoice_ids = records.pluck(:id).uniq - base_scope.where(id: invoice_ids).find_each(cursor: []).flat_map do |invoice| - invoice.journal_entry + records.flatten.compact.map do |record| + template_context_cache = {} + columns.map { |column| column.body(record, template_context_cache) } end end @@ -54,5 +100,21 @@ def crunch(records) ] end.join("\n") end + + protected + + def periodfilter(period = nil) + raise NotImplementedError + # filter_class.new(issued_at_after: period&.begin, issued_at_before: period&.end) + end + + def base_scope + raise NotImplementedError + # @base_scope ||= ::Invoice.joins(:booking).where(bookings: { organisation_id: organisation }).kept + end + + def prefilter + raise NotImplementedError + end end end diff --git a/app/models/invoice.rb b/app/models/invoice.rb index d648b5100..2473ce2fa 100644 --- a/app/models/invoice.rb +++ b/app/models/invoice.rb @@ -73,6 +73,8 @@ class Invoice < ApplicationRecord after_save :recalculate! after_create { generate_ref? && generate_ref && save } + delegate :currency, to: :organisation + validates :type, inclusion: { in: ->(_) { Invoice.subtypes.keys.map(&:to_s) } } validate do errors.add(:supersede_invoice_id, :invalid) if supersede_invoice && supersede_invoice.organisation != organisation @@ -185,9 +187,13 @@ def vat_amounts invoice_parts.group_by(&:vat_category).except(nil).transform_values { _1.sum(&:calculated_amount) } end - def journal_entry - items = [{ account: booking.tenant.accounting_account_nr, date: sent_at, amount: amount, amount_type: :brutto, - side: 1, text: ref, source: :invoice, invoice_id: id }] + invoice_parts.map(&:journal_entry_items) - Accounting::JournalEntryGroup.new(date: sent_at, invoice_id: id, items:) + def journal_entries + [ + Accounting::JournalEntry.new( + account: booking.tenant.accounting_account_nr, date: issued_at, amount:, amount_type: :brutto, side: :soll, + reference: ref, source: self, currency:, booking:, + text: [self.class.model_name.human, ref].join(' ') + ) + ] + invoice_parts.map(&:journal_entries) end end diff --git a/app/models/invoice_part.rb b/app/models/invoice_part.rb index 435d3b536..f6c2718a0 100644 --- a/app/models/invoice_part.rb +++ b/app/models/invoice_part.rb @@ -38,6 +38,8 @@ class InvoicePart < ApplicationRecord attribute :apply, :boolean, default: true + delegate :booking, :organisation, to: :invoice + ranks :ordinal, with_same: :invoice_id, class_name: 'InvoicePart' scope :ordered, -> { rank(:ordinal) } @@ -64,10 +66,14 @@ def to_sum(sum) sum + calculated_amount end - def journal_entry_items + def journal_entries nil end + def amount_netto + amount - (vat_category&.amount_tax(amount) || 0) + end + def self.from_usage(usage, **attributes) return unless usage diff --git a/app/models/invoice_parts/add.rb b/app/models/invoice_parts/add.rb index d18cef05b..04ad7a7a5 100644 --- a/app/models/invoice_parts/add.rb +++ b/app/models/invoice_parts/add.rb @@ -31,12 +31,14 @@ def calculated_amount amount end - def journal_entry_items + def journal_entries # rubocop:disable Metrics/AbcSize [ - Accounting::JournalEntry.new(account: usage&.tarif&.accounting_account_nr, date: invoice.sent_at, - amount: (amount / ((100 + (vat || 0))) * 100), amount_type: :netto, - side: -1, tax_code: vat_category&.accouting_vat_code, - text: invoice.ref, source: :invoice_part, invoice_id: invoice.id) + Accounting::JournalEntry.new( + account: tarif&.accounting_account_nr, date: invoice.issued_at, amount: brutto.abs, amount_type: :brutto, + side: :haben, tax_code: vat_category&.accounting_vat_code, reference: invoice.ref, source: self, + currency: organisation.currency, booking:, cost_center: tarif&.accounting_profit_center_nr, + text: [invoice.class.model_name.human, invoice.ref, self.class.model_name.human, label].join(' ') + ) ] end end diff --git a/app/models/invoice_parts/deposit.rb b/app/models/invoice_parts/deposit.rb index b5d610349..8fdd905f8 100644 --- a/app/models/invoice_parts/deposit.rb +++ b/app/models/invoice_parts/deposit.rb @@ -24,11 +24,7 @@ # module InvoiceParts - class Deposit < InvoicePart + class Deposit < Add InvoicePart.register_subtype self - - def calculated_amount - amount - end end end diff --git a/app/models/mail_template.rb b/app/models/mail_template.rb index 276a0176a..5f32284c7 100644 --- a/app/models/mail_template.rb +++ b/app/models/mail_template.rb @@ -29,7 +29,7 @@ class MailTemplate < RichTextTemplate has_many :mail_template_designated_documents, dependent: :destroy has_many :designated_documents, through: :mail_template_designated_documents - has_many :notifications, dependent: :nullify + has_many :notifications, inverse_of: :mail_template, dependent: :nullify def use(booking, to: nil, attach: nil, context: {}, **args, &callback) return nil unless enabled diff --git a/app/models/rich_text_template.rb b/app/models/rich_text_template.rb index 5aac8f32b..bc986fd3d 100644 --- a/app/models/rich_text_template.rb +++ b/app/models/rich_text_template.rb @@ -75,7 +75,6 @@ def define(key, **definition) translates :title, :body, column_suffix: '_i18n', locale_accessors: true belongs_to :organisation, inverse_of: :rich_text_templates - has_many :notifications, inverse_of: :rich_text_template, dependent: :nullify scope :ordered, -> { order(key: :ASC) } scope :enabled, -> { where(enabled: true) } diff --git a/app/models/vat_category.rb b/app/models/vat_category.rb index 8508004b5..5491431e7 100644 --- a/app/models/vat_category.rb +++ b/app/models/vat_category.rb @@ -20,9 +20,9 @@ def to_s "#{label} (#{formatted_percentage})" end - def tax_of(amount) + def amount_tax(amount) return 0 if percentage.blank? || percentage.zero? - amount / (100 + percentage) * percentage + (amount / (100 + percentage)) * percentage end end diff --git a/app/serializers/manage/journal_entry_serializer.rb b/app/serializers/manage/journal_entry_serializer.rb new file mode 100644 index 000000000..5a86b72ce --- /dev/null +++ b/app/serializers/manage/journal_entry_serializer.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Manage + class JournalEntrySerializer < ApplicationSerializer + fields :id, :account, :date, :tax_code, :text, :amount, :side, :cost_center, + :index, :amount_type, :reference, :currency, :to_s, :soll_account, :haben_account + + field :booking_id do |journal_entry| + journal_entry.booking&.id + end + + field :source do |journal_entry| + { + type: journal_entry.source&.class&.sti_name, + id: journal_entry.source&.id + } + end + end +end diff --git a/app/services/export/pdf/renderables/invoice/invoice_parts_table.rb b/app/services/export/pdf/renderables/invoice/invoice_parts_table.rb index 27cf77604..b80dc4d24 100644 --- a/app/services/export/pdf/renderables/invoice/invoice_parts_table.rb +++ b/app/services/export/pdf/renderables/invoice/invoice_parts_table.rb @@ -8,7 +8,7 @@ class InvoicePartsTable < Renderable attr_reader :invoice delegate :organisation, :invoice_parts, to: :invoice - delegate :helpers, to: ActionController::Base + delegate :number_to_currency, :number_to_percentage, to: ActiveSupport::NumberHelper def initialize(invoice) @invoice = invoice @@ -35,7 +35,7 @@ def render_invoice_parts_table(invoice_parts_table_data) def render_invoice_total_table move_down 10 total_data = [[I18n.t('invoices.total'), '', organisation.currency, - helpers.number_to_currency(invoice.amount, unit: '')]] + number_to_currency(invoice.amount, unit: '')]] table total_data, **table_options(borders: [:top], font_style: :bold, padding: [4, 4, 4, 0]) do column(2).style(align: :right) @@ -85,7 +85,7 @@ def invoice_part_table_row_data(invoice_part) [{ content: invoice_part.label, font_style: :bold }, '', '', ''] else [invoice_part.label, invoice_part.breakdown, organisation.currency, - helpers.number_to_currency(invoice_part.calculated_amount, unit: '')] + number_to_currency(invoice_part.calculated_amount, unit: '')] end end @@ -93,10 +93,10 @@ def vat_table_data invoice.vat_amounts.map do |vat_category, amount| [ vat_category.label, - helpers.number_to_percentage(vat_category.percentage, precision: 2), + number_to_percentage(vat_category.percentage, precision: 2), organisation.currency, - helpers.number_to_currency(amount, unit: ''), - helpers.number_to_currency(vat_category.tax_of(amount), unit: '') + number_to_currency(amount, unit: ''), + number_to_currency(vat_category.amount_tax(amount), unit: '') ] end end diff --git a/app/services/taf_block.rb b/app/services/taf_block.rb index 7398bd79d..08926341d 100644 --- a/app/services/taf_block.rb +++ b/app/services/taf_block.rb @@ -140,7 +140,7 @@ def self.build_from(value, **options) # Boolean; Booking type. # 0 a debit booking [Soll] # 1 a credit booking [Haben] - Type: options.fetch(:Type, { 1 => 0, -1 => 1 }[value.side]), + Type: options.fetch(:Type, { soll: 0, haben: 1 }[value.side]), # Currency; The net amount for this booking. [Netto-Betrag] ValNt: options.fetch(:ValNt, value.amount_type&.to_sym == :netto ? value.amount : nil), diff --git a/app/services/template_context.rb b/app/services/template_context.rb index bc20f0e3e..57c5fecd0 100644 --- a/app/services/template_context.rb +++ b/app/services/template_context.rb @@ -8,6 +8,7 @@ class TemplateContext Payment => Manage::PaymentSerializer, Invoice => Manage::InvoiceSerializer, InvoicePart => Manage::InvoicePartSerializer, + Accounting::JournalEntry => Manage::JournalEntrySerializer, Tenant => Manage::TenantSerializer, Usage => Manage::UsageSerializer, PaymentInfo => Manage::PaymentInfoSerializer, diff --git a/app/views/renderables/data_digest_templates/accounting_journal_entry/_form_fields.html.slim b/app/views/renderables/data_digest_templates/accounting_journal_entry/_form_fields.html.slim index e69de29bb..004418319 100644 --- a/app/views/renderables/data_digest_templates/accounting_journal_entry/_form_fields.html.slim +++ b/app/views/renderables/data_digest_templates/accounting_journal_entry/_form_fields.html.slim @@ -0,0 +1 @@ += render partial: 'renderables/data_digest_templates/tabular/form_fields', locals: { data_digest_template: } diff --git a/app/views/renderables/data_digest_templates/accounting_journal_entry/_show_data_digest.html.slim b/app/views/renderables/data_digest_templates/accounting_journal_entry/_show_data_digest.html.slim index 74393f333..12593818b 100644 --- a/app/views/renderables/data_digest_templates/accounting_journal_entry/_show_data_digest.html.slim +++ b/app/views/renderables/data_digest_templates/accounting_journal_entry/_show_data_digest.html.slim @@ -1,18 +1 @@ -ol - - @data_digest.data.each do |data_item| - - journal_entry = Accounting::JournalEntryGroup.new(**data_item) - li - = Accounting::JournalEntryGroup.model_name.human - dl - dt= Accounting::JournalEntryGroup.human_attribute_name(:date) - dd= I18n.l(journal_entry.date) if journal_entry.date.present? - ol - - journal_entry.items.each do |item| - li - dl - dt= Accounting::JournalEntry.human_attribute_name(:account) - dd= item.account - dt= Accounting::JournalEntry.human_attribute_name(:amount) - dd= number_to_currency(item.amount) - dt= Accounting::JournalEntry.human_attribute_name(:side) - dd= item.side.positive? ? "Haben" : "Soll" += render partial: 'renderables/data_digest_templates/tabular/show_data_digest', locals: { data_digest: } diff --git a/spec/services/taf_block_spec.rb b/spec/services/taf_block_spec.rb index 22f3489ea..b32e75a1f 100644 --- a/spec/services/taf_block_spec.rb +++ b/spec/services/taf_block_spec.rb @@ -38,11 +38,13 @@ end context '::build_from' do + let(:booking) { create(:booking) } + let(:currency) { booking.organisation.currency } describe 'Accounting::JournalEntry' do subject(:taf_block) { described_class.build_from(journal_entry) } let(:journal_entry) do - Accounting::JournalEntry.new(account: 1050, amount: 2091.75, date: Date.new(2024, 10, 5), - amount_type: :netto, side: 1, tax_code: 'MwSt38', + Accounting::JournalEntry.new(account: 1050, amount: 2091.75, date: Date.new(2024, 10, 5), reference: '1234', + amount_type: :netto, side: :soll, tax_code: 'MwSt38', booking:, currency:, text: "Lorem ipsum\nSecond Line, but its longer than sixty \"chars\", OMG!") end @@ -66,8 +68,8 @@ describe 'Accounting::JournalEntry' do subject(:taf_block) { described_class.build_from(journal_entry) } let(:journal_entry) do - Accounting::JournalEntry.new(account: 1050, amount: 2091.75, date: Date.new(2024, 10, 5), - amount_type: :netto, side: 1, tax_code: 'MwSt38', + Accounting::JournalEntry.new(account: 1050, amount: 2091.75, date: Date.new(2024, 10, 5), reference: '1234', + amount_type: :netto, side: :soll, tax_code: 'MwSt38', booking:, currency:, text: "Lorem ipsum\nSecond Line, but its longer than sixty \"chars\", OMG!") end From 3593f7c3d09f0c2adb259dd7612bd9fefe9caadd Mon Sep 17 00:00:00 2001 From: Diego Steiner Date: Tue, 3 Dec 2024 21:06:52 +0000 Subject: [PATCH 10/14] fix: admin homepage --- app/views/manage/vat_categories/index.html.slim | 5 ++++- app/views/public/pages/home.html.slim | 6 +++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/app/views/manage/vat_categories/index.html.slim b/app/views/manage/vat_categories/index.html.slim index 4ee4ecc06..56790f278 100644 --- a/app/views/manage/vat_categories/index.html.slim +++ b/app/views/manage/vat_categories/index.html.slim @@ -8,7 +8,10 @@ h1.mt-0.mb-5= VatCategory.model_name.human(count: 2) tr.bg-white[class=('disabled' if vat_category.discarded?)] td = link_to vat_category.to_s, edit_manage_vat_category_path(vat_category) - .badge.bg-secondary= vat_category.accounting_vat_code if vat_category.accounting_vat_code.present? + td + = number_to_percentage(vat_category.percentage, precision: 2) + td + = vat_category.accounting_vat_code td.p-1.text-end .btn-group - unless vat_category.discarded? diff --git a/app/views/public/pages/home.html.slim b/app/views/public/pages/home.html.slim index 4a681c15c..5e3d58fd9 100644 --- a/app/views/public/pages/home.html.slim +++ b/app/views/public/pages/home.html.slim @@ -22,12 +22,12 @@ = organisation.bookings.count td.py-1.text-end .btn-group - = link_to edit_manage_organisation_path, class: 'btn btn-default' + = link_to edit_manage_organisation_path(org: organisation.slug), class: 'btn btn-default' span.fa.fa-cog.pe-2 = Organisation.model_name.human - = link_to manage_occupiables_path, class: 'btn btn-default' + = link_to manage_occupiables_path(org: organisation.slug), class: 'btn btn-default' span.fa.fa-home.pe-2 = Occupiable.model_name.human(count: 2) - = link_to manage_tarifs_path, class: 'btn btn-default' + = link_to manage_tarifs_path(org: organisation.slug), class: 'btn btn-default' span.fa.fa-usd.pe-2 = Tarif.model_name.human(count: 2) From c117e1fc92c0521a422856db806e66db840f6d5e Mon Sep 17 00:00:00 2001 From: Diego Steiner Date: Thu, 5 Dec 2024 13:25:29 +0000 Subject: [PATCH 11/14] fix: upgrade mobility --- Gemfile.lock | 45 +- config/initializers/mobility.rb | 16 +- ...20241205130304_add_i18n_columns_default.rb | 25 + db/schema.rb | 18 +- spec/factories/booking_categories.rb | 2 +- spec/factories/booking_validations.rb | 2 +- yarn.lock | 504 +++++++++--------- 7 files changed, 330 insertions(+), 282 deletions(-) create mode 100644 db/migrate/20241205130304_add_i18n_columns_default.rb diff --git a/Gemfile.lock b/Gemfile.lock index 57648614c..313a63f73 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -84,7 +84,7 @@ GEM rake (>= 0.8.7) ast (2.4.2) aws-eventstream (1.3.0) - aws-partitions (1.1014.0) + aws-partitions (1.1018.0) aws-sdk-core (3.214.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) @@ -93,7 +93,7 @@ GEM aws-sdk-kms (1.96.0) aws-sdk-core (~> 3, >= 3.210.0) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.174.0) + aws-sdk-s3 (1.176.0) aws-sdk-core (~> 3, >= 3.210.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) @@ -158,7 +158,7 @@ GEM activerecord (>= 5.a) database_cleaner-core (~> 2.0.0) database_cleaner-core (2.0.1) - date (3.4.0) + date (3.4.1) debug (1.9.2) irb (~> 1.10) reline (>= 0.3.8) @@ -266,12 +266,12 @@ GEM ostruct ice_cube (0.17.0) interception (0.5) - io-console (0.7.2) + io-console (0.8.0) irb (1.14.1) rdoc (>= 4.0.0) reline (>= 0.4.2) jmespath (1.6.2) - json (2.8.2) + json (2.9.0) kramdown (2.5.1) rexml (>= 3.3.9) language_server-protocol (3.17.0.3) @@ -283,7 +283,7 @@ GEM rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) locale (2.1.4) - logger (1.6.1) + logger (1.6.2) loofah (2.23.1) crass (~> 1.0.2) nokogiri (>= 1.12.0) @@ -299,14 +299,14 @@ GEM actionpack (>= 6.0.0, < 8.1) method_source (1.1.0) mini_mime (1.1.5) - minitest (5.25.2) - mobility (1.2.9) + minitest (5.25.4) + mobility (1.3.1) i18n (>= 0.6.10, < 2) request_store (~> 1.0) msgpack (1.7.5) multi_json (1.15.0) mutex_m (0.3.0) - net-http (0.5.0) + net-http (0.6.0) uri net-imap (0.5.1) date @@ -318,7 +318,7 @@ GEM net-smtp (0.5.0) net-protocol nio4r (2.7.4) - nokogiri (1.16.7-x86_64-linux) + nokogiri (1.16.8-x86_64-linux) racc (~> 1.4) orm_adapter (0.5.0) ostruct (0.6.1) @@ -349,7 +349,8 @@ GEM pry-rescue (1.6.0) interception (>= 0.5) pry (>= 0.12.0) - psych (5.2.0) + psych (5.2.1) + date stringio public_suffix (6.0.1) puma (6.5.0) @@ -385,9 +386,9 @@ GEM activesupport (>= 5.0.0) minitest nokogiri (>= 1.6) - rails-html-sanitizer (1.6.0) + rails-html-sanitizer (1.6.1) loofah (~> 2.21) - nokogiri (~> 1.14) + nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) rails-i18n (8.0.1) i18n (>= 0.7, < 2) railties (>= 8.0.0, < 9) @@ -406,7 +407,7 @@ GEM rb-fsevent (0.11.2) rb-inotify (0.11.1) ffi (~> 1.0) - rbs (3.6.1) + rbs (3.7.0) logger rdoc (6.8.1) psych (>= 4.0.0) @@ -420,7 +421,7 @@ GEM redis-client (>= 0.22.0) redis-client (0.22.2) connection_pool - regexp_parser (2.9.2) + regexp_parser (2.9.3) reline (0.5.12) io-console (~> 0.5) request_store (1.7.0) @@ -449,15 +450,15 @@ GEM rspec-expectations (~> 3.13) rspec-mocks (~> 3.13) rspec-support (~> 3.13) - rspec-support (3.13.1) - rubocop (1.69.0) + rspec-support (3.13.2) + rubocop (1.69.1) json (~> 2.3) language_server-protocol (>= 3.17.0) parallel (~> 1.10) parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) - regexp_parser (>= 2.4, < 3.0) - rubocop-ast (>= 1.36.1, < 2.0) + regexp_parser (>= 2.9.3, < 3.0) + rubocop-ast (>= 1.36.2, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 4.0) rubocop-ast (1.36.2) @@ -481,7 +482,7 @@ GEM ruby-lsp (>= 0.22.0, < 0.23.0) ruby-progressbar (1.13.0) rubyzip (2.3.2) - securerandom (0.3.2) + securerandom (0.4.0) selenium-webdriver (4.27.0) base64 (~> 0.2) logger (~> 1.4) @@ -513,7 +514,7 @@ GEM actionpack (>= 3.1) railties (>= 3.1) slim (>= 3.0, < 6.0, != 5.0.0) - sorbet-runtime (0.5.11670) + sorbet-runtime (0.5.11681) squasher (0.8.0) statesman (12.1.0) statsd-ruby (1.5.0) @@ -535,7 +536,7 @@ GEM unicode (0.4.4.5) unicode-display_width (2.6.0) uri (1.0.2) - useragent (0.16.10) + useragent (0.16.11) vite_rails (3.0.19) railties (>= 5.1, < 9) vite_ruby (~> 3.0, >= 3.2.2) diff --git a/config/initializers/mobility.rb b/config/initializers/mobility.rb index 724751253..bc032a2d2 100644 --- a/config/initializers/mobility.rb +++ b/config/initializers/mobility.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true Mobility.configure do + # PLUGINS plugins do # Backend # @@ -60,6 +61,19 @@ # per model by passing +dirty: true+ to +translates+. # dirty false + # Column Fallback + # + # Uncomment line below to fallback to original column. You can pass + # +column_fallback: true+ to +translates+ to return original column on + # default locale, or pass +column_fallback: [:en, :de]+ to +translates+ + # to return original column for those locales or pass + # +column_fallback: ->(locale) { ... }+ to +translates to evaluate which + # locales to return original column for. + # column_fallback + # + # Or uncomment this line to enable column fallback with a global default. + # column_fallback true + # Fallbacks # # Uncomment line below to enable fallbacks, using +I18n.fallbacks+. @@ -110,7 +124,7 @@ # # Adds translated attributes to +attributes+ hash, and defines methods # +translated_attributes+ and +untranslated_attributes+ which return hashes - # with translatd and untranslated attributes, respectively. Be aware that + # with translated and untranslated attributes, respectively. Be aware that # this plugin can create conflicts with other gems. # # attribute_methods diff --git a/db/migrate/20241205130304_add_i18n_columns_default.rb b/db/migrate/20241205130304_add_i18n_columns_default.rb new file mode 100644 index 000000000..e822d5914 --- /dev/null +++ b/db/migrate/20241205130304_add_i18n_columns_default.rb @@ -0,0 +1,25 @@ +class AddI18nColumnsDefault < ActiveRecord::Migration[8.0] + def change + # BookableExtra.where(title_i18n: nil).update_all(title_i18n: {}) + # change_column :bookable_extras, :title_i18n, :jsonb, default: {}, null: false + # BookableExtra.where(description_i18n: nil).update_all(description_i18n: {}) + # change_column :bookable_extras, :description_i18n, :jsonb, default: {}, null: false + + BookingCategory.where(title_i18n: nil).update_all(title_i18n: {}) + change_column :booking_categories, :title_i18n, :jsonb, default: {}, null: false + BookingCategory.where(description_i18n: nil).update_all(description_i18n: {}) + change_column :booking_categories, :description_i18n, :jsonb, default: {}, null: false + BookingQuestion.where(label_i18n: nil).update_all(label_i18n: {}) + change_column :booking_questions, :label_i18n, :jsonb, default: {}, null: false + BookingQuestion.where(description_i18n: nil).update_all(description_i18n: {}) + change_column :booking_questions, :description_i18n, :jsonb, default: {}, null: false + BookingValidation.where(error_message_i18n: nil).update_all(error_message_i18n: {}) + change_column :booking_validations, :error_message_i18n, :jsonb, default: {}, null: false + Occupiable.where(name_i18n: nil).update_all(name_i18n: {}) + change_column :occupiables, :name_i18n, :jsonb, default: {}, null: false + Occupiable.where(description_i18n: nil).update_all(description_i18n: {}) + change_column :occupiables, :description_i18n, :jsonb, default: {}, null: false + VatCategory.where(label_i18n: nil).update_all(label_i18n: {}) + change_column :vat_categories, :label_i18n, :jsonb, default: {}, null: false + end +end diff --git a/db/schema.rb b/db/schema.rb index 531fe1748..9de500b42 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2024_12_03_101328) do +ActiveRecord::Schema[8.0].define(version: 2024_12_05_130304) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" enable_extension "pgcrypto" @@ -98,11 +98,11 @@ create_table "booking_categories", force: :cascade do |t| t.bigint "organisation_id", null: false t.string "key" - t.jsonb "title_i18n" + t.jsonb "title_i18n", default: {}, null: false t.datetime "created_at", precision: nil, null: false t.datetime "updated_at", precision: nil, null: false t.integer "ordinal" - t.jsonb "description_i18n" + t.jsonb "description_i18n", default: {}, null: false t.datetime "discarded_at" t.index ["discarded_at"], name: "index_booking_categories_on_discarded_at" t.index ["key", "organisation_id"], name: "index_booking_categories_on_key_and_organisation_id", unique: true @@ -149,8 +149,8 @@ create_table "booking_questions", force: :cascade do |t| t.bigint "organisation_id", null: false t.datetime "discarded_at" - t.jsonb "label_i18n" - t.jsonb "description_i18n" + t.jsonb "label_i18n", default: {}, null: false + t.jsonb "description_i18n", default: {}, null: false t.string "type" t.integer "ordinal" t.string "key" @@ -181,7 +181,7 @@ create_table "booking_validations", force: :cascade do |t| t.bigint "organisation_id", null: false - t.jsonb "error_message_i18n" + t.jsonb "error_message_i18n", default: {}, null: false t.integer "ordinal" t.datetime "created_at", null: false t.datetime "updated_at", null: false @@ -401,8 +401,8 @@ t.bigint "home_id" t.jsonb "settings" t.integer "ordinal" - t.jsonb "name_i18n" - t.jsonb "description_i18n" + t.jsonb "name_i18n", default: {}, null: false + t.jsonb "description_i18n", default: {}, null: false t.datetime "discarded_at" t.index ["discarded_at"], name: "index_occupiables_on_discarded_at" t.index ["home_id"], name: "index_occupiables_on_home_id" @@ -631,7 +631,7 @@ create_table "vat_categories", force: :cascade do |t| t.decimal "percentage", default: "0.0", null: false - t.jsonb "label_i18n" + t.jsonb "label_i18n", default: {}, null: false t.bigint "organisation_id", null: false t.string "accounting_vat_code" t.datetime "discarded_at" diff --git a/spec/factories/booking_categories.rb b/spec/factories/booking_categories.rb index ea83547bd..f29a9e465 100644 --- a/spec/factories/booking_categories.rb +++ b/spec/factories/booking_categories.rb @@ -26,6 +26,6 @@ factory :booking_category do organisation { nil } sequence(:key) { |i| "category_#{i}" } - title_i18n { Faker::Commerce.department } + title { Faker::Commerce.department } end end diff --git a/spec/factories/booking_validations.rb b/spec/factories/booking_validations.rb index afb44a018..1cf25b6df 100644 --- a/spec/factories/booking_validations.rb +++ b/spec/factories/booking_validations.rb @@ -20,6 +20,6 @@ factory :booking_validation do organisation { nil } check_on { Booking::VALIDATION_CONTEXTS } - error_message_i18n { 'Validation failed' } + error_message { 'Validation failed' } end end diff --git a/yarn.lock b/yarn.lock index 4bb06604b..cf97d0ec7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1544,10 +1544,10 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@eslint/js@9.15.0": - version "9.15.0" - resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.15.0.tgz#df0e24fe869143b59731942128c19938fdbadfb5" - integrity sha512-tMTqrY+EzbXmKJR5ToI8lxu7jaN5EdmrBFJpQk5JmSlyLsx6o4t27r883K5xsLuCYCpfKBCGswMSWXsM+jB7lg== +"@eslint/js@9.16.0": + version "9.16.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.16.0.tgz#3df2b2dd3b9163056616886c86e4082f45dbf3f4" + integrity sha512-tw2HxzQkrbeuvyj1tG2Yqq+0H9wGoI2IMk4EOsQeX+vmd75FtJAzf+gTA69WF+baUKRYQ3x2kbLE08js5OsTVg== "@eslint/object-schema@^2.1.4": version "2.1.4" @@ -1819,95 +1819,95 @@ estree-walker "^2.0.2" picomatch "^2.3.1" -"@rollup/rollup-android-arm-eabi@4.27.4": - version "4.27.4" - resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.27.4.tgz#e3c9cc13f144ba033df4d2c3130a214dc8e3473e" - integrity sha512-2Y3JT6f5MrQkICUyRVCw4oa0sutfAsgaSsb0Lmmy1Wi2y7X5vT9Euqw4gOsCyy0YfKURBg35nhUKZS4mDcfULw== - -"@rollup/rollup-android-arm64@4.27.4": - version "4.27.4" - resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.27.4.tgz#0474250fcb5871aca952e249a0c3270fc4310b55" - integrity sha512-wzKRQXISyi9UdCVRqEd0H4cMpzvHYt1f/C3CoIjES6cG++RHKhrBj2+29nPF0IB5kpy9MS71vs07fvrNGAl/iA== - -"@rollup/rollup-darwin-arm64@4.27.4": - version "4.27.4" - resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.27.4.tgz#77c29b4f9c430c1624f1a6835f2a7e82be3d16f2" - integrity sha512-PlNiRQapift4LNS8DPUHuDX/IdXiLjf8mc5vdEmUR0fF/pyy2qWwzdLjB+iZquGr8LuN4LnUoSEvKRwjSVYz3Q== - -"@rollup/rollup-darwin-x64@4.27.4": - version "4.27.4" - resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.27.4.tgz#7d87711f641a458868758cbf110fb32eabd6a25a" - integrity sha512-o9bH2dbdgBDJaXWJCDTNDYa171ACUdzpxSZt+u/AAeQ20Nk5x+IhA+zsGmrQtpkLiumRJEYef68gcpn2ooXhSQ== - -"@rollup/rollup-freebsd-arm64@4.27.4": - version "4.27.4" - resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.27.4.tgz#662f808d2780e4e91021ac9ee7ed800862bb9a57" - integrity sha512-NBI2/i2hT9Q+HySSHTBh52da7isru4aAAo6qC3I7QFVsuhxi2gM8t/EI9EVcILiHLj1vfi+VGGPaLOUENn7pmw== - -"@rollup/rollup-freebsd-x64@4.27.4": - version "4.27.4" - resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.27.4.tgz#71e5a7bcfcbe51d8b65d158675acec1307edea79" - integrity sha512-wYcC5ycW2zvqtDYrE7deary2P2UFmSh85PUpAx+dwTCO9uw3sgzD6Gv9n5X4vLaQKsrfTSZZ7Z7uynQozPVvWA== - -"@rollup/rollup-linux-arm-gnueabihf@4.27.4": - version "4.27.4" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.27.4.tgz#08f67fcec61ee18f8b33b3f403a834ab8f3aa75d" - integrity sha512-9OwUnK/xKw6DyRlgx8UizeqRFOfi9mf5TYCw1uolDaJSbUmBxP85DE6T4ouCMoN6pXw8ZoTeZCSEfSaYo+/s1w== - -"@rollup/rollup-linux-arm-musleabihf@4.27.4": - version "4.27.4" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.27.4.tgz#2e1ad4607f86475b1731556359c6070eb8f4b109" - integrity sha512-Vgdo4fpuphS9V24WOV+KwkCVJ72u7idTgQaBoLRD0UxBAWTF9GWurJO9YD9yh00BzbkhpeXtm6na+MvJU7Z73A== - -"@rollup/rollup-linux-arm64-gnu@4.27.4": - version "4.27.4" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.27.4.tgz#c65d559dcb0d3dabea500cf7b8215959ae6cccf8" - integrity sha512-pleyNgyd1kkBkw2kOqlBx+0atfIIkkExOTiifoODo6qKDSpnc6WzUY5RhHdmTdIJXBdSnh6JknnYTtmQyobrVg== - -"@rollup/rollup-linux-arm64-musl@4.27.4": - version "4.27.4" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.27.4.tgz#6739f7eb33e20466bb88748519c98ce8dee23922" - integrity sha512-caluiUXvUuVyCHr5DxL8ohaaFFzPGmgmMvwmqAITMpV/Q+tPoaHZ/PWa3t8B2WyoRcIIuu1hkaW5KkeTDNSnMA== - -"@rollup/rollup-linux-powerpc64le-gnu@4.27.4": - version "4.27.4" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.27.4.tgz#8d9fe9471c256e55278cb1f7b1c977cd8fe6df20" - integrity sha512-FScrpHrO60hARyHh7s1zHE97u0KlT/RECzCKAdmI+LEoC1eDh/RDji9JgFqyO+wPDb86Oa/sXkily1+oi4FzJQ== - -"@rollup/rollup-linux-riscv64-gnu@4.27.4": - version "4.27.4" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.27.4.tgz#9a467f7ad5b61c9d66b24e79a3c57cb755d02c35" - integrity sha512-qyyprhyGb7+RBfMPeww9FlHwKkCXdKHeGgSqmIXw9VSUtvyFZ6WZRtnxgbuz76FK7LyoN8t/eINRbPUcvXB5fw== - -"@rollup/rollup-linux-s390x-gnu@4.27.4": - version "4.27.4" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.27.4.tgz#efaddf22df27b87a267a731fbeb9539e92cd4527" - integrity sha512-PFz+y2kb6tbh7m3A7nA9++eInGcDVZUACulf/KzDtovvdTizHpZaJty7Gp0lFwSQcrnebHOqxF1MaKZd7psVRg== - -"@rollup/rollup-linux-x64-gnu@4.27.4": - version "4.27.4" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.27.4.tgz#a959eccb04b07fd1591d7ff745a6865faa7042cd" - integrity sha512-Ni8mMtfo+o/G7DVtweXXV/Ol2TFf63KYjTtoZ5f078AUgJTmaIJnj4JFU7TK/9SVWTaSJGxPi5zMDgK4w+Ez7Q== - -"@rollup/rollup-linux-x64-musl@4.27.4": - version "4.27.4" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.27.4.tgz#927764f1da1f2dd50943716dec93796d10cb6e99" - integrity sha512-5AeeAF1PB9TUzD+3cROzFTnAJAcVUGLuR8ng0E0WXGkYhp6RD6L+6szYVX+64Rs0r72019KHZS1ka1q+zU/wUw== - -"@rollup/rollup-win32-arm64-msvc@4.27.4": - version "4.27.4" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.27.4.tgz#030b6cc607d845da23dced624e47fb45de105840" - integrity sha512-yOpVsA4K5qVwu2CaS3hHxluWIK5HQTjNV4tWjQXluMiiiu4pJj4BN98CvxohNCpcjMeTXk/ZMJBRbgRg8HBB6A== - -"@rollup/rollup-win32-ia32-msvc@4.27.4": - version "4.27.4" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.27.4.tgz#3457a3f44a84f51d8097c3606429e01f0d2d0ec2" - integrity sha512-KtwEJOaHAVJlxV92rNYiG9JQwQAdhBlrjNRp7P9L8Cb4Rer3in+0A+IPhJC9y68WAi9H0sX4AiG2NTsVlmqJeQ== - -"@rollup/rollup-win32-x64-msvc@4.27.4": - version "4.27.4" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.27.4.tgz#67d516613c9f2fe42e2d8b78e252d0003179d92c" - integrity sha512-3j4jx1TppORdTAoBJRd+/wJRGCPC0ETWkXOecJ6PPZLj6SptXkrXcNqdj0oclbKML6FkQltdz7bBA3rUSirZug== +"@rollup/rollup-android-arm-eabi@4.28.0": + version "4.28.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.28.0.tgz#462e7ecdd60968bc9eb95a20d185e74f8243ec1b" + integrity sha512-wLJuPLT6grGZsy34g4N1yRfYeouklTgPhH1gWXCYspenKYD0s3cR99ZevOGw5BexMNywkbV3UkjADisozBmpPQ== + +"@rollup/rollup-android-arm64@4.28.0": + version "4.28.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.28.0.tgz#78a2b8a8a55f71a295eb860a654ae90a2b168f40" + integrity sha512-eiNkznlo0dLmVG/6wf+Ifi/v78G4d4QxRhuUl+s8EWZpDewgk7PX3ZyECUXU0Zq/Ca+8nU8cQpNC4Xgn2gFNDA== + +"@rollup/rollup-darwin-arm64@4.28.0": + version "4.28.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.28.0.tgz#5b783af714f434f1e66e3cdfa3817e0b99216d84" + integrity sha512-lmKx9yHsppblnLQZOGxdO66gT77bvdBtr/0P+TPOseowE7D9AJoBw8ZDULRasXRWf1Z86/gcOdpBrV6VDUY36Q== + +"@rollup/rollup-darwin-x64@4.28.0": + version "4.28.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.28.0.tgz#f72484e842521a5261978034e18e20f778a2850d" + integrity sha512-8hxgfReVs7k9Js1uAIhS6zq3I+wKQETInnWQtgzt8JfGx51R1N6DRVy3F4o0lQwumbErRz52YqwjfvuwRxGv1w== + +"@rollup/rollup-freebsd-arm64@4.28.0": + version "4.28.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.28.0.tgz#3c919dff72b2fe344811a609c674a8347b033f62" + integrity sha512-lA1zZB3bFx5oxu9fYud4+g1mt+lYXCoch0M0V/xhqLoGatbzVse0wlSQ1UYOWKpuSu3gyN4qEc0Dxf/DII1bhQ== + +"@rollup/rollup-freebsd-x64@4.28.0": + version "4.28.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.28.0.tgz#b62a3a8365b363b3fdfa6da11a9188b6ab4dca7c" + integrity sha512-aI2plavbUDjCQB/sRbeUZWX9qp12GfYkYSJOrdYTL/C5D53bsE2/nBPuoiJKoWp5SN78v2Vr8ZPnB+/VbQ2pFA== + +"@rollup/rollup-linux-arm-gnueabihf@4.28.0": + version "4.28.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.28.0.tgz#0d02cc55bd229bd8ca5c54f65f916ba5e0591c94" + integrity sha512-WXveUPKtfqtaNvpf0iOb0M6xC64GzUX/OowbqfiCSXTdi/jLlOmH0Ba94/OkiY2yTGTwteo4/dsHRfh5bDCZ+w== + +"@rollup/rollup-linux-arm-musleabihf@4.28.0": + version "4.28.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.28.0.tgz#c51d379263201e88a60e92bd8e90878f0c044425" + integrity sha512-yLc3O2NtOQR67lI79zsSc7lk31xjwcaocvdD1twL64PK1yNaIqCeWI9L5B4MFPAVGEVjH5k1oWSGuYX1Wutxpg== + +"@rollup/rollup-linux-arm64-gnu@4.28.0": + version "4.28.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.28.0.tgz#93ce2addc337b5cfa52b84f8e730d2e36eb4339b" + integrity sha512-+P9G9hjEpHucHRXqesY+3X9hD2wh0iNnJXX/QhS/J5vTdG6VhNYMxJ2rJkQOxRUd17u5mbMLHM7yWGZdAASfcg== + +"@rollup/rollup-linux-arm64-musl@4.28.0": + version "4.28.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.28.0.tgz#730af6ddc091a5ba5baac28a3510691725dc808b" + integrity sha512-1xsm2rCKSTpKzi5/ypT5wfc+4bOGa/9yI/eaOLW0oMs7qpC542APWhl4A37AENGZ6St6GBMWhCCMM6tXgTIplw== + +"@rollup/rollup-linux-powerpc64le-gnu@4.28.0": + version "4.28.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.28.0.tgz#b5565aac20b4de60ca1e557f525e76478b5436af" + integrity sha512-zgWxMq8neVQeXL+ouSf6S7DoNeo6EPgi1eeqHXVKQxqPy1B2NvTbaOUWPn/7CfMKL7xvhV0/+fq/Z/J69g1WAQ== + +"@rollup/rollup-linux-riscv64-gnu@4.28.0": + version "4.28.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.28.0.tgz#d488290bf9338bad4ae9409c4aa8a1728835a20b" + integrity sha512-VEdVYacLniRxbRJLNtzwGt5vwS0ycYshofI7cWAfj7Vg5asqj+pt+Q6x4n+AONSZW/kVm+5nklde0qs2EUwU2g== + +"@rollup/rollup-linux-s390x-gnu@4.28.0": + version "4.28.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.28.0.tgz#eb2e3f3a06acf448115045c11a5a96868c95a556" + integrity sha512-LQlP5t2hcDJh8HV8RELD9/xlYtEzJkm/aWGsauvdO2ulfl3QYRjqrKW+mGAIWP5kdNCBheqqqYIGElSRCaXfpw== + +"@rollup/rollup-linux-x64-gnu@4.28.0": + version "4.28.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.28.0.tgz#065952ef2aea7e837dc7e02aa500feeaff4fc507" + integrity sha512-Nl4KIzteVEKE9BdAvYoTkW19pa7LR/RBrT6F1dJCV/3pbjwDcaOq+edkP0LXuJ9kflW/xOK414X78r+K84+msw== + +"@rollup/rollup-linux-x64-musl@4.28.0": + version "4.28.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.28.0.tgz#3435d484d05f5c4d1ffd54541b4facce2887103a" + integrity sha512-eKpJr4vBDOi4goT75MvW+0dXcNUqisK4jvibY9vDdlgLx+yekxSm55StsHbxUsRxSTt3JEQvlr3cGDkzcSP8bw== + +"@rollup/rollup-win32-arm64-msvc@4.28.0": + version "4.28.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.28.0.tgz#69682a2a10d9fedc334f87583cfca83c39c08077" + integrity sha512-Vi+WR62xWGsE/Oj+mD0FNAPY2MEox3cfyG0zLpotZdehPFXwz6lypkGs5y38Jd/NVSbOD02aVad6q6QYF7i8Bg== + +"@rollup/rollup-win32-ia32-msvc@4.28.0": + version "4.28.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.28.0.tgz#b64470f9ac79abb386829c56750b9a4711be3332" + integrity sha512-kN/Vpip8emMLn/eOza+4JwqDZBL6MPNpkdaEsgUtW1NYN3DZvZqSQrbKzJcTL6hd8YNmFTn7XGWMwccOcJBL0A== + +"@rollup/rollup-win32-x64-msvc@4.28.0": + version "4.28.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.28.0.tgz#cb313feef9ac6e3737067fdf34f42804ac65a6f2" + integrity sha512-Bvno2/aZT6usSa7lRDL2+hMjVAGjuqaymF1ApZm31JXzniR/hvr14jpU+/z4X6Gt5BPlzosscyJZGUvguXIqeQ== "@sinclair/typebox@^0.27.8": version "0.27.8" @@ -2069,61 +2069,61 @@ "@types/yargs-parser" "*" "@typescript-eslint/eslint-plugin@^8.0.1": - version "8.16.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.16.0.tgz#ac56825bcdf3b392fc76a94b1315d4a162f201a6" - integrity sha512-5YTHKV8MYlyMI6BaEG7crQ9BhSc8RxzshOReKwZwRWN0+XvvTOm+L/UYLCYxFpfwYuAAqhxiq4yae0CMFwbL7Q== + version "8.17.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.17.0.tgz#2ee073c421f4e81e02d10e731241664b6253b23c" + integrity sha512-HU1KAdW3Tt8zQkdvNoIijfWDMvdSweFYm4hWh+KwhPstv+sCmWb89hCIP8msFm9N1R/ooh9honpSuvqKWlYy3w== dependencies: "@eslint-community/regexpp" "^4.10.0" - "@typescript-eslint/scope-manager" "8.16.0" - "@typescript-eslint/type-utils" "8.16.0" - "@typescript-eslint/utils" "8.16.0" - "@typescript-eslint/visitor-keys" "8.16.0" + "@typescript-eslint/scope-manager" "8.17.0" + "@typescript-eslint/type-utils" "8.17.0" + "@typescript-eslint/utils" "8.17.0" + "@typescript-eslint/visitor-keys" "8.17.0" graphemer "^1.4.0" ignore "^5.3.1" natural-compare "^1.4.0" ts-api-utils "^1.3.0" "@typescript-eslint/parser@^8.0.1": - version "8.16.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.16.0.tgz#ee5b2d6241c1ab3e2e53f03fd5a32d8e266d8e06" - integrity sha512-D7DbgGFtsqIPIFMPJwCad9Gfi/hC0PWErRRHFnaCWoEDYi5tQUDiJCTmGUbBiLzjqAck4KcXt9Ayj0CNlIrF+w== - dependencies: - "@typescript-eslint/scope-manager" "8.16.0" - "@typescript-eslint/types" "8.16.0" - "@typescript-eslint/typescript-estree" "8.16.0" - "@typescript-eslint/visitor-keys" "8.16.0" + version "8.17.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.17.0.tgz#2ee972bb12fa69ac625b85813dc8d9a5a053ff52" + integrity sha512-Drp39TXuUlD49F7ilHHCG7TTg8IkA+hxCuULdmzWYICxGXvDXmDmWEjJYZQYgf6l/TFfYNE167m7isnc3xlIEg== + dependencies: + "@typescript-eslint/scope-manager" "8.17.0" + "@typescript-eslint/types" "8.17.0" + "@typescript-eslint/typescript-estree" "8.17.0" + "@typescript-eslint/visitor-keys" "8.17.0" debug "^4.3.4" -"@typescript-eslint/scope-manager@8.16.0": - version "8.16.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.16.0.tgz#ebc9a3b399a69a6052f3d88174456dd399ef5905" - integrity sha512-mwsZWubQvBki2t5565uxF0EYvG+FwdFb8bMtDuGQLdCCnGPrDEDvm1gtfynuKlnpzeBRqdFCkMf9jg1fnAK8sg== +"@typescript-eslint/scope-manager@8.17.0": + version "8.17.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.17.0.tgz#a3f49bf3d4d27ff8d6b2ea099ba465ef4dbcaa3a" + integrity sha512-/ewp4XjvnxaREtqsZjF4Mfn078RD/9GmiEAtTeLQ7yFdKnqwTOgRMSvFz4et9U5RiJQ15WTGXPLj89zGusvxBg== dependencies: - "@typescript-eslint/types" "8.16.0" - "@typescript-eslint/visitor-keys" "8.16.0" + "@typescript-eslint/types" "8.17.0" + "@typescript-eslint/visitor-keys" "8.17.0" -"@typescript-eslint/type-utils@8.16.0": - version "8.16.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.16.0.tgz#585388735f7ac390f07c885845c3d185d1b64740" - integrity sha512-IqZHGG+g1XCWX9NyqnI/0CX5LL8/18awQqmkZSl2ynn8F76j579dByc0jhfVSnSnhf7zv76mKBQv9HQFKvDCgg== +"@typescript-eslint/type-utils@8.17.0": + version "8.17.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.17.0.tgz#d326569f498cdd0edf58d5bb6030b4ad914e63d3" + integrity sha512-q38llWJYPd63rRnJ6wY/ZQqIzPrBCkPdpIsaCfkR3Q4t3p6sb422zougfad4TFW9+ElIFLVDzWGiGAfbb/v2qw== dependencies: - "@typescript-eslint/typescript-estree" "8.16.0" - "@typescript-eslint/utils" "8.16.0" + "@typescript-eslint/typescript-estree" "8.17.0" + "@typescript-eslint/utils" "8.17.0" debug "^4.3.4" ts-api-utils "^1.3.0" -"@typescript-eslint/types@8.16.0": - version "8.16.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.16.0.tgz#49c92ae1b57942458ab83d9ec7ccab3005e64737" - integrity sha512-NzrHj6thBAOSE4d9bsuRNMvk+BvaQvmY4dDglgkgGC0EW/tB3Kelnp3tAKH87GEwzoxgeQn9fNGRyFJM/xd+GQ== +"@typescript-eslint/types@8.17.0": + version "8.17.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.17.0.tgz#ef84c709ef8324e766878834970bea9a7e3b72cf" + integrity sha512-gY2TVzeve3z6crqh2Ic7Cr+CAv6pfb0Egee7J5UAVWCpVvDI/F71wNfolIim4FE6hT15EbpZFVUj9j5i38jYXA== -"@typescript-eslint/typescript-estree@8.16.0": - version "8.16.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.16.0.tgz#9d741e56e5b13469b5190e763432ce5551a9300c" - integrity sha512-E2+9IzzXMc1iaBy9zmo+UYvluE3TW7bCGWSF41hVWUE01o8nzr1rvOQYSxelxr6StUvRcTMe633eY8mXASMaNw== +"@typescript-eslint/typescript-estree@8.17.0": + version "8.17.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.17.0.tgz#40b5903bc929b1e8dd9c77db3cb52cfb199a2a34" + integrity sha512-JqkOopc1nRKZpX+opvKqnM3XUlM7LpFMD0lYxTqOTKQfCWAmxw45e3qlOCsEqEB2yuacujivudOFpCnqkBDNMw== dependencies: - "@typescript-eslint/types" "8.16.0" - "@typescript-eslint/visitor-keys" "8.16.0" + "@typescript-eslint/types" "8.17.0" + "@typescript-eslint/visitor-keys" "8.17.0" debug "^4.3.4" fast-glob "^3.3.2" is-glob "^4.0.3" @@ -2131,22 +2131,22 @@ semver "^7.6.0" ts-api-utils "^1.3.0" -"@typescript-eslint/utils@8.16.0": - version "8.16.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.16.0.tgz#c71264c437157feaa97842809836254a6fc833c3" - integrity sha512-C1zRy/mOL8Pj157GiX4kaw7iyRLKfJXBR3L82hk5kS/GyHcOFmy4YUq/zfZti72I9wnuQtA/+xzft4wCC8PJdA== +"@typescript-eslint/utils@8.17.0": + version "8.17.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.17.0.tgz#41c05105a2b6ab7592f513d2eeb2c2c0236d8908" + integrity sha512-bQC8BnEkxqG8HBGKwG9wXlZqg37RKSMY7v/X8VEWD8JG2JuTHuNK0VFvMPMUKQcbk6B+tf05k+4AShAEtCtJ/w== dependencies: "@eslint-community/eslint-utils" "^4.4.0" - "@typescript-eslint/scope-manager" "8.16.0" - "@typescript-eslint/types" "8.16.0" - "@typescript-eslint/typescript-estree" "8.16.0" + "@typescript-eslint/scope-manager" "8.17.0" + "@typescript-eslint/types" "8.17.0" + "@typescript-eslint/typescript-estree" "8.17.0" -"@typescript-eslint/visitor-keys@8.16.0": - version "8.16.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.16.0.tgz#d5086afc060b01ff7a4ecab8d49d13d5a7b07705" - integrity sha512-pq19gbaMOmFE3CbL0ZB8J8BFCo2ckfHBfaIsaOZgBIF4EoISJIdLX5xRhd0FGB0LlHReNRuzoJoMGpTjq8F2CQ== +"@typescript-eslint/visitor-keys@8.17.0": + version "8.17.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.17.0.tgz#4dbcd0e28b9bf951f4293805bf34f98df45e1aa8" + integrity sha512-1Hm7THLpO6ww5QU6H/Qp+AusUUl+z/CAm3cNZZ0jQvon9yicgO7Rwd+/WWRpMKLYV6p2UvdbR27c86rzCPpreg== dependencies: - "@typescript-eslint/types" "8.16.0" + "@typescript-eslint/types" "8.17.0" eslint-visitor-keys "^4.2.0" "@vitejs/plugin-react@^4.2.1": @@ -2318,9 +2318,9 @@ available-typed-arrays@^1.0.7: possible-typed-array-names "^1.0.0" axios@^1.2.0: - version "1.7.8" - resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.8.tgz#1997b1496b394c21953e68c14aaa51b7b5de3d6e" - integrity sha512-Uu0wb7KNqK2t5K+YQyVCLM76prD5sRFjKHbJYCP1J7JFGEQ6nN7HWn9+04LAeiJ3ji54lgS/gZCH1oxyrf1SPw== + version "1.7.9" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.9.tgz#d7d071380c132a24accda1b2cfc1535b79ec650a" + integrity sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw== dependencies: follow-redirects "^1.15.6" form-data "^4.0.0" @@ -2438,9 +2438,9 @@ caniuse-api@^3.0.0: lodash.uniq "^4.5.0" caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001646, caniuse-lite@^1.0.30001669: - version "1.0.30001684" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001684.tgz#0eca437bab7d5f03452ff0ef9de8299be6b08e16" - integrity sha512-G1LRwLIQjBQoyq0ZJGqGIJUXzJ8irpbjHLpVRXDvBEScFJ9b17sgK6vlx0GAJFE21okD7zXl08rRRUfq6HdoEQ== + version "1.0.30001686" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001686.tgz#0e04b8d90de8753188e93c9989d56cb19d902670" + integrity sha512-Y7deg0Aergpa24M3qLC5xjNklnKnhsmSyR/V89dLZ1n0ucJIFNs7PgR2Yfa/Zf6W79SbBicgtGxZr2juHkEUIA== chalk@^4.0.0: version "4.1.2" @@ -2628,9 +2628,9 @@ css-what@^6.1.0: integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== cssdb@^8.2.1: - version "8.2.1" - resolved "https://registry.yarnpkg.com/cssdb/-/cssdb-8.2.1.tgz#62a5d9a41e2c86f1d7c35981098fc5ce47c5766c" - integrity sha512-KwEPys7lNsC8OjASI8RrmwOYYDcm0JOW9zQhcV83ejYcQkirTEyeAGui8aO2F5PiS6SLpxuTzl6qlMElIdsgIg== + version "8.2.2" + resolved "https://registry.yarnpkg.com/cssdb/-/cssdb-8.2.2.tgz#0a5bcbc47a297e6b0296e6082f60363e17b337d4" + integrity sha512-Z3kpWyvN68aKyeMxOUGmffQeHjvrzDxbre2B2ikr/WqQ4ZMkhHu2nOD6uwSeq3TpuOYU7ckvmJRAUIt6orkYUg== cssesc@^3.0.0: version "3.0.0" @@ -2821,9 +2821,9 @@ domutils@^3.0.1: domhandler "^5.0.3" electron-to-chromium@^1.5.41: - version "1.5.67" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.67.tgz#66ebd2be4a77469ac2760ef5e9e460ba9a43a845" - integrity sha512-nz88NNBsD7kQSAGGJyp8hS6xSPtWwqNogA0mjtc2nUYeEf3nURK9qpV18TuBdDmEDgVWotS8Wkzf+V52dSQ/LQ== + version "1.5.68" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.68.tgz#4f46be4d465ef00e2100d5557b66f4af70e3ce6c" + integrity sha512-FgMdJlma0OzUYlbrtZ4AeXjKxKPk6KT8WOP8BjcqxWtlg8qyJQjRzPJzUtUn5GBg1oQ26hFs7HOOHJMYiJRnvQ== emojis-list@^3.0.0: version "3.0.0" @@ -3055,16 +3055,16 @@ eslint-visitor-keys@^4.2.0: integrity sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw== eslint@^9.1.1: - version "9.15.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.15.0.tgz#77c684a4e980e82135ebff8ee8f0a9106ce6b8a6" - integrity sha512-7CrWySmIibCgT1Os28lUU6upBshZ+GxybLOrmRzi08kS8MBuO8QA7pXEgYgY5W8vK3e74xv0lpjo9DbaGU9Rkw== + version "9.16.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.16.0.tgz#66832e66258922ac0a626f803a9273e37747f2a6" + integrity sha512-whp8mSQI4C8VXd+fLgSM0lh3UlmcFtVwUQjyKCFfsp+2ItAIYhlq/hqGahGqHE6cv9unM41VlqKk2VtKYR2TaA== dependencies: "@eslint-community/eslint-utils" "^4.2.0" "@eslint-community/regexpp" "^4.12.1" "@eslint/config-array" "^0.19.0" "@eslint/core" "^0.9.0" "@eslint/eslintrc" "^3.2.0" - "@eslint/js" "9.15.0" + "@eslint/js" "9.16.0" "@eslint/plugin-kit" "^0.2.3" "@humanfs/node" "^0.16.6" "@humanwhocodes/module-importer" "^1.0.1" @@ -3276,7 +3276,7 @@ gensync@^1.0.0-beta.2: resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== -get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.3, get-intrinsic@^1.2.4: +get-intrinsic@^1.2.1, get-intrinsic@^1.2.3, get-intrinsic@^1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== @@ -3328,12 +3328,12 @@ globalthis@^1.0.4: define-properties "^1.2.1" gopd "^1.0.1" -gopd@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" - integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== +gopd@^1.0.1, gopd@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.1.0.tgz#df8f0839c2d48caefc32a025a49294d39606c912" + integrity sha512-FQoVQnqcdk4hVM4JN1eromaun4iuS34oStkdlLENLdpULsuQcTyXj8w7ayhuUfPwEYZ1ZOooOTT6fdA9Vmx/RA== dependencies: - get-intrinsic "^1.1.3" + get-intrinsic "^1.2.4" graceful-fs@^4.2.9: version "4.2.11" @@ -3345,7 +3345,7 @@ graphemer@^1.4.0: resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== -has-bigints@^1.0.1, has-bigints@^1.0.2: +has-bigints@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ== @@ -3363,14 +3363,16 @@ has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.2: es-define-property "^1.0.0" has-proto@^1.0.1, has-proto@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd" - integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.1.0.tgz#deb10494cbbe8809bce168a3b961f42969f5ed43" + integrity sha512-QLdzI9IIO1Jg7f9GT1gXpPpXArAn6cS31R1eEZqz08Gc+uQ8/XiqHWt17Fiw+2p6oTTIq5GXEpQkAlA88YRl/Q== + dependencies: + call-bind "^1.0.7" -has-symbols@^1.0.2, has-symbols@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" - integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== +has-symbols@^1.0.3: + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" + integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== has-tostringtag@^1.0.0, has-tostringtag@^1.0.2: version "1.0.2" @@ -3401,9 +3403,9 @@ html-parse-stringify@^3.0.1: void-elements "3.1.0" i18next@^24.0.2: - version "24.0.2" - resolved "https://registry.yarnpkg.com/i18next/-/i18next-24.0.2.tgz#00ee14417675e5733902d60e0bbd424c1ecd2a5f" - integrity sha512-D88xyIGcWAKwBTAs4RSqASi8NXR/NhCVSTM4LDbdoU8qb/5dcEZjNCLDhtQBB7Epw/Cp1w2vH/3ujoTbqLSs5g== + version "24.0.5" + resolved "https://registry.yarnpkg.com/i18next/-/i18next-24.0.5.tgz#2678986eca46411cae0329542a84dd4cd7e5f2f0" + integrity sha512-1jSdEzgFPGLZRsQwydoMFCBBaV+PmrVEO5WhANllZPX4y2JSGTxUjJ+xVklHIsiS95uR8gYc/y0hYZWevucNjg== dependencies: "@babel/runtime" "^7.23.2" @@ -3471,20 +3473,20 @@ is-async-function@^2.0.0: dependencies: has-tostringtag "^1.0.0" -is-bigint@^1.0.1: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3" - integrity sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg== +is-bigint@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.1.0.tgz#dda7a3445df57a42583db4228682eba7c4170672" + integrity sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ== dependencies: - has-bigints "^1.0.1" + has-bigints "^1.0.2" -is-boolean-object@^1.1.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719" - integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA== +is-boolean-object@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.2.0.tgz#9743641e80a62c094b5941c5bb791d66a88e497a" + integrity sha512-kR5g0+dXf/+kXnqI+lu0URKYPKgICtHGGNCDSB10AaUFj3o/HkB3u7WfpRBJGFopxxY0oH3ux7ZsDjLtK7xqvw== dependencies: - call-bind "^1.0.2" - has-tostringtag "^1.0.0" + call-bind "^1.0.7" + has-tostringtag "^1.0.2" is-callable@^1.1.3, is-callable@^1.2.7: version "1.2.7" @@ -3548,12 +3550,13 @@ is-negative-zero@^2.0.3: resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.3.tgz#ced903a027aca6381b777a5743069d7376a49747" integrity sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw== -is-number-object@^1.0.4: - version "1.0.7" - resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.7.tgz#59d50ada4c45251784e9904f5246c742f07a42fc" - integrity sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ== +is-number-object@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.1.0.tgz#5a867e9ecc3d294dda740d9f127835857af7eb05" + integrity sha512-KVSZV0Dunv9DTPkhXwcZ3Q+tUc9TsaE1ZwX5J2WMvsSGS6Md8TFPun5uwh0yRdrNerI6vf/tbJxqSx4c1ZI1Lw== dependencies: - has-tostringtag "^1.0.0" + call-bind "^1.0.7" + has-tostringtag "^1.0.2" is-number@^7.0.0: version "7.0.0" @@ -3561,12 +3564,14 @@ is-number@^7.0.0: integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== is-regex@^1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" - integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== + version "1.2.0" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.2.0.tgz#41b9d266e7eb7451312c64efc37e8a7d453077cf" + integrity sha512-B6ohK4ZmoftlUe+uvenXSbPJFo6U37BH7oO1B3nQH8f/7h27N56s85MhUtbFJAziz5dcmuR3i8ovUl35zp8pFA== dependencies: - call-bind "^1.0.2" - has-tostringtag "^1.0.0" + call-bind "^1.0.7" + gopd "^1.1.0" + has-tostringtag "^1.0.2" + hasown "^2.0.2" is-set@^2.0.3: version "2.0.3" @@ -3580,19 +3585,22 @@ is-shared-array-buffer@^1.0.2, is-shared-array-buffer@^1.0.3: dependencies: call-bind "^1.0.7" -is-string@^1.0.5, is-string@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" - integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg== +is-string@^1.0.7, is-string@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.1.0.tgz#8cb83c5d57311bf8058bc6c8db294711641da45d" + integrity sha512-PlfzajuF9vSo5wErv3MJAKD/nqf9ngAs1NFQYm16nUYFO2IzxJ2hcm+IOCg+EEopdykNNUhVq5cz35cAUxU8+g== dependencies: - has-tostringtag "^1.0.0" + call-bind "^1.0.7" + has-tostringtag "^1.0.2" -is-symbol@^1.0.3, is-symbol@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c" - integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg== +is-symbol@^1.0.4, is-symbol@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.1.0.tgz#ae993830a56d4781886d39f9f0a46b3e89b7b60b" + integrity sha512-qS8KkNNXUZ/I+nX6QT8ZS1/Yx0A444yhzdTKxCzKkNjQ9sHErBxJnJAgh+f5YhusYECEcjo4XcyH87hn6+ks0A== dependencies: - has-symbols "^1.0.2" + call-bind "^1.0.7" + has-symbols "^1.0.3" + safe-regex-test "^1.0.3" is-typed-array@^1.1.13: version "1.1.13" @@ -3742,9 +3750,9 @@ levn@^0.4.1: type-check "~0.4.0" lilconfig@^3.1.1: - version "3.1.2" - resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-3.1.2.tgz#e4a7c3cb549e3a606c8dcc32e5ae1005e62c05cb" - integrity sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow== + version "3.1.3" + resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-3.1.3.tgz#a1bcfd6257f9585bf5ae14ceeebb7b559025e4c4" + integrity sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw== lines-and-columns@^1.1.6: version "1.2.4" @@ -4576,9 +4584,9 @@ prettier-linter-helpers@^1.0.0: fast-diff "^1.1.2" prettier@^3.0.1: - version "3.4.1" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.4.1.tgz#e211d451d6452db0a291672ca9154bc8c2579f7b" - integrity sha512-G+YdqtITVZmOJje6QkXQWzl3fSfMxFwm1tjTyo9exhkmWSqC4Yhd1+lug++IlR2mvRVAxEDDWYkQdeSztajqgg== + version "3.4.2" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.4.2.tgz#a5ce1fb522a588bf2b78ca44c6e6fe5aa5a2b13f" + integrity sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ== prop-types-extra@^1.1.0: version "1.1.1" @@ -4845,30 +4853,30 @@ rollup-plugin-gzip@^3.1.0: integrity sha512-9xemMyvCjkklgNpu6jCYqQAbvCLJzA2nilkiOGzFuXTUX3cXEFMwIhsIBRF7kTKD/SnZ1tNPcxFm4m4zJ3VfNQ== rollup@^4.23.0: - version "4.27.4" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.27.4.tgz#b23e4ef4fe4d0d87f5237dacf63f95a499503897" - integrity sha512-RLKxqHEMjh/RGLsDxAEsaLO3mWgyoU6x9w6n1ikAzet4B3gI2/3yP6PWY2p9QzRTh6MfEIXB3MwsOY0Iv3vNrw== + version "4.28.0" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.28.0.tgz#eb8d28ed43ef60a18f21d0734d230ee79dd0de77" + integrity sha512-G9GOrmgWHBma4YfCcX8PjH0qhXSdH8B4HDE2o4/jaxj93S4DPCIDoLcXz99eWMji4hB29UFCEd7B2gwGJDR9cQ== dependencies: "@types/estree" "1.0.6" optionalDependencies: - "@rollup/rollup-android-arm-eabi" "4.27.4" - "@rollup/rollup-android-arm64" "4.27.4" - "@rollup/rollup-darwin-arm64" "4.27.4" - "@rollup/rollup-darwin-x64" "4.27.4" - "@rollup/rollup-freebsd-arm64" "4.27.4" - "@rollup/rollup-freebsd-x64" "4.27.4" - "@rollup/rollup-linux-arm-gnueabihf" "4.27.4" - "@rollup/rollup-linux-arm-musleabihf" "4.27.4" - "@rollup/rollup-linux-arm64-gnu" "4.27.4" - "@rollup/rollup-linux-arm64-musl" "4.27.4" - "@rollup/rollup-linux-powerpc64le-gnu" "4.27.4" - "@rollup/rollup-linux-riscv64-gnu" "4.27.4" - "@rollup/rollup-linux-s390x-gnu" "4.27.4" - "@rollup/rollup-linux-x64-gnu" "4.27.4" - "@rollup/rollup-linux-x64-musl" "4.27.4" - "@rollup/rollup-win32-arm64-msvc" "4.27.4" - "@rollup/rollup-win32-ia32-msvc" "4.27.4" - "@rollup/rollup-win32-x64-msvc" "4.27.4" + "@rollup/rollup-android-arm-eabi" "4.28.0" + "@rollup/rollup-android-arm64" "4.28.0" + "@rollup/rollup-darwin-arm64" "4.28.0" + "@rollup/rollup-darwin-x64" "4.28.0" + "@rollup/rollup-freebsd-arm64" "4.28.0" + "@rollup/rollup-freebsd-x64" "4.28.0" + "@rollup/rollup-linux-arm-gnueabihf" "4.28.0" + "@rollup/rollup-linux-arm-musleabihf" "4.28.0" + "@rollup/rollup-linux-arm64-gnu" "4.28.0" + "@rollup/rollup-linux-arm64-musl" "4.28.0" + "@rollup/rollup-linux-powerpc64le-gnu" "4.28.0" + "@rollup/rollup-linux-riscv64-gnu" "4.28.0" + "@rollup/rollup-linux-s390x-gnu" "4.28.0" + "@rollup/rollup-linux-x64-gnu" "4.28.0" + "@rollup/rollup-linux-x64-musl" "4.28.0" + "@rollup/rollup-win32-arm64-msvc" "4.28.0" + "@rollup/rollup-win32-ia32-msvc" "4.28.0" + "@rollup/rollup-win32-x64-msvc" "4.28.0" fsevents "~2.3.2" run-parallel@^1.1.9: @@ -4903,9 +4911,9 @@ safe-regex-test@^1.0.3: is-regex "^1.1.4" sass@^1.75.0: - version "1.81.0" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.81.0.tgz#a9010c0599867909dfdbad057e4a6fbdd5eec941" - integrity sha512-Q4fOxRfhmv3sqCLoGfvrC9pRV8btc0UtqL9mN6Yrv6Qi9ScL55CVH1vlPP863ISLEEMNLLuu9P+enCeGHlnzhA== + version "1.82.0" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.82.0.tgz#30da277af3d0fa6042e9ceabd0d984ed6d07df70" + integrity sha512-j4GMCTa8elGyN9A7x7bEglx0VgSpNUG4W4wNedQ33wSMdnkqQCT8HTwOaVSV4e6yQovcu/3Oc4coJP/l0xhL2Q== dependencies: chokidar "^4.0.0" immutable "^5.0.2" @@ -5342,9 +5350,9 @@ vite-plugin-stimulus-hmr@^3.0.0: stimulus-vite-helpers "^3.0.0" vite@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/vite/-/vite-6.0.1.tgz#24c9caf24998f0598de37bed67e50ec5b9dfeaf0" - integrity sha512-Ldn6gorLGr4mCdFnmeAOLweJxZ34HjKnDm4HGo6P66IEqTxQb36VEdFJQENKxWjupNfoIjvRUnswjn1hpYEpjQ== + version "6.0.2" + resolved "https://registry.yarnpkg.com/vite/-/vite-6.0.2.tgz#7a22630c73c7b663335ddcdb2390971ffbc14993" + integrity sha512-XdQ+VsY2tJpBsKGs0wf3U/+azx8BBpYRHFAyKm5VeEZNOJZRB63q7Sc8Iup3k0TrN3KO6QgyzFf+opSbfY1y0g== dependencies: esbuild "^0.24.0" postcss "^8.4.49" @@ -5365,15 +5373,15 @@ warning@^4.0.0, warning@^4.0.3: loose-envify "^1.0.0" which-boxed-primitive@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" - integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== - dependencies: - is-bigint "^1.0.1" - is-boolean-object "^1.1.0" - is-number-object "^1.0.4" - is-string "^1.0.5" - is-symbol "^1.0.3" + version "1.1.0" + resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.1.0.tgz#2d850d6c4ac37b95441a67890e19f3fda8b6c6d9" + integrity sha512-Ei7Miu/AXe2JJ4iNF5j/UphAgRoma4trE6PtisM09bPygb3egMH3YLW/befsWb1A1AxvNSFidOFTB18XtnIIng== + dependencies: + is-bigint "^1.1.0" + is-boolean-object "^1.2.0" + is-number-object "^1.1.0" + is-string "^1.1.0" + is-symbol "^1.1.0" which-builtin-type@^1.1.4: version "1.2.0" From 54197307d6e4f1f22e2c251e01cd3d6571c67cdb Mon Sep 17 00:00:00 2001 From: Diego Steiner Date: Thu, 5 Dec 2024 14:41:11 +0000 Subject: [PATCH 12/14] feat: add accounting_settings --- app/models/accounting_settings.rb | 7 +++++++ app/models/invoice.rb | 17 ++++++++++------- app/models/invoice_part.rb | 6 +++--- app/models/invoice_parts/add.rb | 2 +- app/models/organisation.rb | 2 ++ app/models/tenant.rb | 5 +++++ ..._add_accounting_settings_to_organisations.rb | 5 +++++ db/schema.rb | 3 ++- 8 files changed, 35 insertions(+), 12 deletions(-) create mode 100644 app/models/accounting_settings.rb create mode 100644 db/migrate/20241205133043_add_accounting_settings_to_organisations.rb diff --git a/app/models/accounting_settings.rb b/app/models/accounting_settings.rb new file mode 100644 index 000000000..65f261d32 --- /dev/null +++ b/app/models/accounting_settings.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AccountingSettings < Settings + attribute :tenant_debitor_account_nr_base, :integer, default: -> { 0 } + attribute :debitor_account_nr, :string + attribute :currency_account_nr, :string +end diff --git a/app/models/invoice.rb b/app/models/invoice.rb index 2473ce2fa..b073fa80f 100644 --- a/app/models/invoice.rb +++ b/app/models/invoice.rb @@ -188,12 +188,15 @@ def vat_amounts end def journal_entries - [ - Accounting::JournalEntry.new( - account: booking.tenant.accounting_account_nr, date: issued_at, amount:, amount_type: :brutto, side: :soll, - reference: ref, source: self, currency:, booking:, - text: [self.class.model_name.human, ref].join(' ') - ) - ] + invoice_parts.map(&:journal_entries) + [debitor_journal_entry] + invoice_parts.map(&:journal_entries) + end + + def debitor_journal_entry + Accounting::JournalEntry.new( + account: booking.tenant.accounting_debitor_account_nr, + date: issued_at, amount:, amount_type: :brutto, side: :soll, + reference: ref, source: self, currency:, booking:, + text: [self.class.model_name.human, ref].join(' ') + ) end end diff --git a/app/models/invoice_part.rb b/app/models/invoice_part.rb index f6c2718a0..cbd995cee 100644 --- a/app/models/invoice_part.rb +++ b/app/models/invoice_part.rb @@ -70,9 +70,9 @@ def journal_entries nil end - def amount_netto - amount - (vat_category&.amount_tax(amount) || 0) - end + # def amount_netto + # amount - (vat_category&.amount_tax(amount) || 0) + # end def self.from_usage(usage, **attributes) return unless usage diff --git a/app/models/invoice_parts/add.rb b/app/models/invoice_parts/add.rb index 04ad7a7a5..e684dc93c 100644 --- a/app/models/invoice_parts/add.rb +++ b/app/models/invoice_parts/add.rb @@ -34,7 +34,7 @@ def calculated_amount def journal_entries # rubocop:disable Metrics/AbcSize [ Accounting::JournalEntry.new( - account: tarif&.accounting_account_nr, date: invoice.issued_at, amount: brutto.abs, amount_type: :brutto, + account: tarif&.accounting_account_nr, date: invoice.issued_at, amount: amount.abs, amount_type: :brutto, side: :haben, tax_code: vat_category&.accounting_vat_code, reference: invoice.ref, source: self, currency: organisation.currency, booking:, cost_center: tarif&.accounting_profit_center_nr, text: [invoice.class.model_name.human, invoice.ref, self.class.model_name.human, label].join(' ') diff --git a/app/models/organisation.rb b/app/models/organisation.rb index e0b5d1889..bc01d5a18 100644 --- a/app/models/organisation.rb +++ b/app/models/organisation.rb @@ -84,6 +84,7 @@ class Organisation < ApplicationRecord validate do errors.add(:settings, :invalid) unless settings.valid? errors.add(:smtp_settings, :invalid) unless smtp_settings.nil? || smtp_settings.valid? + errors.add(:accounting_settings, :invalid) unless accounting_settings.nil? || accounting_settings.valid? errors.add(:creditor_address, :invalid) if creditor_address&.lines&.count&.> 3 errors.add(:account_address, :invalid) if account_address&.lines&.count&.> 3 errors.add(:iban, :invalid) if iban.present? && !iban.valid? @@ -91,6 +92,7 @@ class Organisation < ApplicationRecord attribute :booking_flow_type, default: -> { BookingFlows::Default.to_s } attribute :settings, Settings::Type.new(OrganisationSettings), default: -> { OrganisationSettings.new } + attribute :accounting_settings, Settings::Type.new(AccountingSettings), default: -> { AccountingSettings.new } attribute :smtp_settings, Settings::Type.new(SmtpSettings) attribute :iban, IBAN::Type.new diff --git a/app/models/tenant.rb b/app/models/tenant.rb index 75b77c910..f7fd3ed47 100644 --- a/app/models/tenant.rb +++ b/app/models/tenant.rb @@ -146,4 +146,9 @@ def merge_with_new(tenant) assign_attributes(tenant&.changed_values&.except(:email, :organisation_id) || {}) self end + + def accounting_debitor_account_nr + @accounting_debitor_account_nr ||= organisation.accounting_settings&.debitor_account_nr.presence || + (id + (organisation.accounting_settings&.tenant_debitor_account_nr_base || 0)) + end end diff --git a/db/migrate/20241205133043_add_accounting_settings_to_organisations.rb b/db/migrate/20241205133043_add_accounting_settings_to_organisations.rb new file mode 100644 index 000000000..559fdb6ce --- /dev/null +++ b/db/migrate/20241205133043_add_accounting_settings_to_organisations.rb @@ -0,0 +1,5 @@ +class AddAccountingSettingsToOrganisations < ActiveRecord::Migration[8.0] + def change + add_column :organisations, :accounting_settings, :jsonb, default: {} + end +end diff --git a/db/schema.rb b/db/schema.rb index 9de500b42..27972344e 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2024_12_05_130304) do +ActiveRecord::Schema[8.0].define(version: 2024_12_05_133043) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" enable_extension "pgcrypto" @@ -479,6 +479,7 @@ t.string "account_address" t.text "cors_origins" t.jsonb "nickname_label_i18n", default: {} + t.jsonb "accounting_settings", default: {} t.index ["slug"], name: "index_organisations_on_slug", unique: true end From 800c2d9e7af216670927b4ba996502f1b6f98d8f Mon Sep 17 00:00:00 2001 From: Diego Steiner Date: Thu, 5 Dec 2024 21:23:09 +0000 Subject: [PATCH 13/14] feat: improve taf export --- .../manage/data_digests_controller.rb | 3 +- .../accounting_journal_entry.rb | 15 +- app/models/invoice.rb | 4 + app/services/taf_block.rb | 173 +++++++++++------- spec/services/taf_block_spec.rb | 13 +- 5 files changed, 127 insertions(+), 81 deletions(-) diff --git a/app/controllers/manage/data_digests_controller.rb b/app/controllers/manage/data_digests_controller.rb index 0c6092946..9557a69bb 100644 --- a/app/controllers/manage/data_digests_controller.rb +++ b/app/controllers/manage/data_digests_controller.rb @@ -10,12 +10,13 @@ def index respond_with :manage, @data_digests.order(created_at: :ASC) end - def show + def show # rubocop:disable Metrics/AbcSize respond_to do |format| format.html format.csv { send_data @data_digest.format(:csv), filename: "#{@data_digest.label}.csv" } format.pdf { send_data @data_digest.format(:pdf), filename: "#{@data_digest.label}.pdf" } format.taf { send_data @data_digest.format(:taf), filename: "#{@data_digest.label}.taf" } + format.text { render plain: @data_digest.format(:taf) } end rescue Prawn::Errors::CannotFit redirect_to manage_data_digests_path, alert: t('.pdf_error') diff --git a/app/models/data_digest_templates/accounting_journal_entry.rb b/app/models/data_digest_templates/accounting_journal_entry.rb index ba0d988af..216f52d9f 100644 --- a/app/models/data_digest_templates/accounting_journal_entry.rb +++ b/app/models/data_digest_templates/accounting_journal_entry.rb @@ -82,23 +82,22 @@ class AccountingJournalEntry < Tabular def records(period) invoice_filter = ::Invoice::Filter.new(issued_at_after: period&.begin, issued_at_before: period&.end) invoices = invoice_filter.apply(::Invoice.joins(:booking).where(bookings: { organisation: organisation }).kept) - invoices.map(&:journal_entries) + invoices.index_with(&:journal_entries) end def crunch(records) - records.flatten.compact.map do |record| + records.values.flatten.compact.map do |record| template_context_cache = {} columns.map { |column| column.body(record, template_context_cache) } end end formatter(:taf) do |_options = {}| - data.flat_map do |record| - journal_entry = ::Accounting::JournalEntryGroup.new(**record) - [ - TafBlock.build_from(journal_entry) - ] - end.join("\n") + records.keys.map do |source| + TafBlock::Collection.new do + derive(source) + end + end.join("\n\n") end protected diff --git a/app/models/invoice.rb b/app/models/invoice.rb index b073fa80f..bb724e81b 100644 --- a/app/models/invoice.rb +++ b/app/models/invoice.rb @@ -191,6 +191,10 @@ def journal_entries [debitor_journal_entry] + invoice_parts.map(&:journal_entries) end + def human_ref + ref + end + def debitor_journal_entry Accounting::JournalEntry.new( account: booking.tenant.accounting_debitor_account_nr, diff --git a/app/services/taf_block.rb b/app/services/taf_block.rb index 08926341d..4fb6dac98 100644 --- a/app/services/taf_block.rb +++ b/app/services/taf_block.rb @@ -5,33 +5,84 @@ class TafBlock SEPARATOR = "\n" attr_reader :type, :properties, :children - def initialize(type, *children, **properties, &) + def initialize(type, **properties, &) @type = type - @children = children.select { _1.is_a?(TafBlock) } - @properties = properties + @properties = properties.to_h + @children = Collection.new(&) + end - yield self if block_given? + def self.block(...) + new(...) end - def property(**new_properties) - @properties.merge!(new_properties) + Value = Data.define(:value) do + delegate :to_s, to: :value + + def self.derive(value) + new derive_value(value) + end + + def self.derive_value(value) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/MethodLength + case value + when ::FalseClass, ::TrueClass + value ? '1' : '0' + when ::BigDecimal, ::Float + format('%.2f', value) + when ::Numeric + value.to_s + when ::Date, ::DateTime, ::ActiveSupport::TimeWithZone + value.strftime('%d.%m.%Y') + when ::String + "\"#{value.gsub(/["']/, '""')}\"" + when ::Enumerable + "[#{value.to_a.each { derive_value(_1) }.join(',')}]" + when Value + value + else + derive_value value.to_s + end + end end - def child(*new_children) - @children += new_children.select { _1.is_a?(TafBlock) } + class Collection + delegate_missing_to :@blocks + + def initialize(&) + @blocks = [] + instance_eval(&) if block_given? + end + + def block(...) + @blocks += Array.wrap(TafBlock.new(...)) + end + + def derive(...) + @blocks += Array.wrap(TafBlock.derive(...)) + end + + def serialize(indent_level: 0, indent_with: ' ', separate_with: "\n") + @blocks.map do |block| + block.serialize(indent_level: indent_level + 1, indent_with:, separate_with:) if block.is_a?(TafBlock) + end.compact.join(separate_with + separate_with) + end + + def to_s + serialize(indent_level: -1) + end end def serialize(indent_level: 0, indent_with: ' ', separate_with: "\n") indent = [indent_with * indent_level].join separate_and_indent = [separate_with, indent, indent_with].join - serialized_children = serialize_children(indent_level:, indent_with:, separate_with:) + serialized_children = children.serialize(indent_level:, indent_with:, separate_with:) + serialized_properties = properties.compact.map { |key, value| "#{key}=#{Value.derive(value)}" } [ # tag_start indent, "{#{type}", # properties - separate_and_indent, self.class.serialize_properies(properties, join_with: separate_and_indent), + separate_and_indent, serialized_properties.join(separate_and_indent), # children - (children.present? && separate_with) || nil, serialized_children&.join(separate_with), + (serialized_children.present? && separate_with) || nil, serialized_children, # tag end separate_with, indent, '}' ].compact.join @@ -41,88 +92,61 @@ def to_s serialize end - def self.serialize_value(value) # rubocop:disable Metrics/MethodLength - case value - when ::FalseClass, ::TrueClass - value ? '1' : '0' - when ::BigDecimal, ::Float - format('%.2f', value) - when ::Numeric - value.to_s - when ::Date, ::DateTime, ::ActiveSupport::TimeWithZone - value.strftime('%d.%m.%Y') - else - "\"#{value.to_s.gsub('"', '""')}\"".presence - end - end - - def serialize_children(indent_level:, indent_with:, separate_with:) - children.map do |child| - child.serialize(indent_level: indent_level + 1, indent_with:, separate_with:) - end - end - - def self.serialize_properies(properties, join_with: ' ') - return '' unless properties.is_a?(Hash) - - properties.compact.flat_map { |key, value| "#{key}=#{serialize_value(value)}" }.join(join_with) - end - def self.factories @factories ||= {} end - def self.register_factory(klass, &build_block) - factories[klass] = build_block + def self.derive_from(klass, &derive_block) + factories[klass] = derive_block end - def self.build_from(value, **options) - build_block = factories[factories.keys.find { |klass| value.is_a?(klass) }] - instance_exec(value, options, &build_block) if build_block.present? + def self.derive(value, **override, &block) + derive_block = factories[factories.keys.find { |klass| value.is_a?(klass) }] + instance_exec(value, override, block, &derive_block) if derive_block.present? end - register_factory Accounting::JournalEntryGroup do |value, **options| - new(:Blg, *value.items.map { TafBlock.build_from(_1) }, **{ + derive_from Accounting::JournalEntryGroup do |value, **override| + new(:Blg, **{ # Date; The date of the booking. - Date: options.fetch(:Date, value.date), + Date: override.fetch(:Date, value.date), Orig: true }) end - register_factory Accounting::JournalEntry do |value, **options| + derive_from Accounting::JournalEntry do |journal_entry, **override| new(:Bk, **{ # The Id of a book keeping account. [Fibu-Konto] - AccId: options.fetch(:AccId, value.account), + AccId: journal_entry.account, # Integer; Booking type: 1=cost booking, 2=tax booking - BType: options.fetch(:BType, value.amount_type&.to_sym == :tax || 1), + BType: journal_entry.amount_type&.to_sym == :tax || 1, # String[13], This is the cost type account - CAcc: options.fetch(:CAcc, value.cost_center), + CAcc: journal_entry.cost_center, # Integer; This is the index of the booking that represents the cost booking which is attached to this booking - CIdx: options.fetch(:CIdx, value.index), + CIdx: journal_entry.index, # String[9]; A user definable code. - Code: options.fetch(:Code, nil)&.slice(0..8), + Code: nil, # Date; The date of the booking. - Date: options.fetch(:Date, value.date), + Date: journal_entry.date, - # IntegerAuxilliary flags. This value consists of the sum of one or more of + # IntegerAuxilliary flags. This journal_entry consists of the sum of one or more of # the following biases: # 1 - The booking is the first one into the specified OP. # 16 - This is a hidden booking. [Transitorische] # 32 - This booking is the exit booking, as oposed to the return booking. # Only valid if the hidden flag is set. - Flags: options.fetch(:Flags, nil), + Flags: nil, # String[5]; The Id of the tax. [MWSt-Kürzel] - TaxId: options.fetch(:TaxId, value.tax_code), + TaxId: journal_entry.tax_code, # String[61*]; This string specifies the first line of the booking text. - Text: options.fetch(:Text, value.text&.slice(0..59)&.lines&.first&.strip || '-'), # rubocop:disable Style/SafeNavigationChainLength + Text: journal_entry.text&.slice(0..59)&.lines&.first&.strip || '-', # rubocop:disable Style/SafeNavigationChainLength # String[*]; This string specifies the second line of the booking text. # (*)Both fields Text and Text2 are stored in the same memory location, @@ -131,35 +155,52 @@ def self.build_from(value, **options) # Be careful not to put too many characters onto one single line, because # most Reports are not designed to display a full string containing 60 # characters. - Text2: options.fetch(:Text2, value.text&.slice(0..59)&.lines&.[](1..-1)&.join("\n")).presence, # rubocop:disable Style/SafeNavigationChainLength + Text2: journal_entry.text&.slice(0..59)&.lines&.[](1..-1)&.join("\n").presence, # rubocop:disable Style/SafeNavigationChainLength # Integer; This is the index of the booking that represents the tax booking # which is attached to this booking. - TIdx: options.fetch(:TIdx, (value.amount_type&.to_sym == :tax && value.index) || nil), + TIdx: (journal_entry.amount_type&.to_sym == :tax && journal_entry.index) || nil, # Boolean; Booking type. # 0 a debit booking [Soll] # 1 a credit booking [Haben] - Type: options.fetch(:Type, { soll: 0, haben: 1 }[value.side]), + Type: { soll: 0, haben: 1 }[journal_entry.side], # Currency; The net amount for this booking. [Netto-Betrag] - ValNt: options.fetch(:ValNt, value.amount_type&.to_sym == :netto ? value.amount : nil), + ValNt: journal_entry.amount_type&.to_sym == :netto ? journal_entry.amount : nil, # Currency; The tax amount for this booking. [Brutto-Betrag] - ValBt: options.fetch(:ValBt, value.amount_type&.to_sym == :brutto ? value.amount : nil), + ValBt: journal_entry.amount_type&.to_sym == :brutto ? journal_entry.amount : nil, # Currency; The tax amount for this booking. [Steuer-Betrag] - ValTx: options.fetch(:ValTx, value.amount_type&.to_sym == :tax ? value.amount : nil), + ValTx: journal_entry.amount_type&.to_sym == :tax ? journal_entry.amount : nil, # Currency; The gross amount for this booking in the foreign currency specified # by currency of the account AccId. [FW-Betrag] # ValFW : not implemented # String[13]The OP id of this booking. - OpId: options.fetch(:OpId, nil), + OpId: journal_entry.reference, # The PK number of this booking. - PkKey: options.fetch(:PkKey, nil) - }) + PkKey: nil + }, **override) + end + + derive_from Invoice do |invoice, **override| + next unless invoice.is_a?(Invoices::Invoice) || invoice.is_a?(Invoices::Deposit) + + op_id = invoice.human_ref + pk_key = [Value.new(invoice.booking.tenant.accounting_debitor_account_nr), + Value.new(invoice.organisation.accounting_settings.currency_account_nr)] + journal_entries = invoice.journal_entries.to_a + + [ + new(:OPd, **{ PkKey: pk_key, OpId: op_id, ZabId: '15T' }, **override), + new(:Blg, **{ OpId: op_id, Date: invoice.issued_at, Orig: true }, **override) do + derive(journal_entries.shift, Flags: 1, OpId: op_id) + journal_entries.each { derive(_1, OpId: op_id) } + end + ] end end diff --git a/spec/services/taf_block_spec.rb b/spec/services/taf_block_spec.rb index b32e75a1f..f43409a57 100644 --- a/spec/services/taf_block_spec.rb +++ b/spec/services/taf_block_spec.rb @@ -4,9 +4,8 @@ describe TafBlock, type: :model do subject(:taf_block) do - described_class.new(:Blg, text: 'TAF is "great"') do |taf| - taf.property test: 1 - taf.child described_class.new(:Bk, test: 2) + described_class.new(:Blg, text: 'TAF is "great"', test: 1) do + block(:Bk, test: 2) end end @@ -37,11 +36,11 @@ end end - context '::build_from' do + context '::derive' do let(:booking) { create(:booking) } let(:currency) { booking.organisation.currency } describe 'Accounting::JournalEntry' do - subject(:taf_block) { described_class.build_from(journal_entry) } + subject(:taf_block) { described_class.derive(journal_entry) } let(:journal_entry) do Accounting::JournalEntry.new(account: 1050, amount: 2091.75, date: Date.new(2024, 10, 5), reference: '1234', amount_type: :netto, side: :soll, tax_code: 'MwSt38', booking:, currency:, @@ -60,13 +59,14 @@ Text2="Second Line, but its longer than sixty ""chars"", " Type=0 ValNt=2091.75 + OpId="1234" } TAF end end describe 'Accounting::JournalEntry' do - subject(:taf_block) { described_class.build_from(journal_entry) } + subject(:taf_block) { described_class.derive(journal_entry) } let(:journal_entry) do Accounting::JournalEntry.new(account: 1050, amount: 2091.75, date: Date.new(2024, 10, 5), reference: '1234', amount_type: :netto, side: :soll, tax_code: 'MwSt38', booking:, currency:, @@ -85,6 +85,7 @@ Text2="Second Line, but its longer than sixty ""chars"", " Type=0 ValNt=2091.75 + OpId="1234" } TAF end From ca0c60a2b20a14b9d3472bf18f0f3a5d5cb2209b Mon Sep 17 00:00:00 2001 From: Diego Steiner Date: Thu, 5 Dec 2024 23:22:33 +0000 Subject: [PATCH 14/14] feat: improve taf export --- .rubocop.yml | 1 + app/models/accounting.rb | 21 ---- .../accounting_journal_entry.rb | 1 + app/models/invoice.rb | 6 +- app/models/invoice_parts/add.rb | 4 +- app/models/tenant.rb | 3 +- app/services/taf_block.rb | 109 ++++++++++-------- spec/services/taf_block_spec.rb | 8 +- 8 files changed, 77 insertions(+), 76 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 356a3badf..69e84c01b 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -62,6 +62,7 @@ Naming/MethodParameterName: AllowedNames: - "to" - "at" + - "as" Rails/EnvironmentVariableAccess: AllowReads: true diff --git a/app/models/accounting.rb b/app/models/accounting.rb index 3578ca9ed..37770efca 100644 --- a/app/models/accounting.rb +++ b/app/models/accounting.rb @@ -46,25 +46,4 @@ def to_s ].compact.join(' ') end end - - JournalEntryGroup = Data.define(:id, :date, :items) do - extend ActiveModel::Translation - extend ActiveModel::Naming - - def initialize(**args) - args.symbolize_keys! - date = args.delete(:date)&.then { _1.try(:to_date) || Date.parse(_1).to_date } - items = Array.wrap(args.delete(:items)).map do |item| - case item - when Hash, JournalEntry - JournalEntry.new(**item.to_h) - end - end.compact - super(id: nil, **args, items:, date:) - end - - def to_h - super.merge(items: items.map(&:to_h)) - end - end end diff --git a/app/models/data_digest_templates/accounting_journal_entry.rb b/app/models/data_digest_templates/accounting_journal_entry.rb index 216f52d9f..38c687281 100644 --- a/app/models/data_digest_templates/accounting_journal_entry.rb +++ b/app/models/data_digest_templates/accounting_journal_entry.rb @@ -95,6 +95,7 @@ def crunch(records) formatter(:taf) do |_options = {}| records.keys.map do |source| TafBlock::Collection.new do + derive(source.booking.tenant) derive(source) end end.join("\n\n") diff --git a/app/models/invoice.rb b/app/models/invoice.rb index bb724e81b..01bea0730 100644 --- a/app/models/invoice.rb +++ b/app/models/invoice.rb @@ -192,15 +192,15 @@ def journal_entries end def human_ref - ref + format('HV%05d', id) end def debitor_journal_entry Accounting::JournalEntry.new( account: booking.tenant.accounting_debitor_account_nr, date: issued_at, amount:, amount_type: :brutto, side: :soll, - reference: ref, source: self, currency:, booking:, - text: [self.class.model_name.human, ref].join(' ') + reference: human_ref, source: self, currency:, booking:, + text: [self.class.model_name.human, human_ref].join(' ') ) end end diff --git a/app/models/invoice_parts/add.rb b/app/models/invoice_parts/add.rb index e684dc93c..19f9c04e1 100644 --- a/app/models/invoice_parts/add.rb +++ b/app/models/invoice_parts/add.rb @@ -35,9 +35,9 @@ def journal_entries # rubocop:disable Metrics/AbcSize [ Accounting::JournalEntry.new( account: tarif&.accounting_account_nr, date: invoice.issued_at, amount: amount.abs, amount_type: :brutto, - side: :haben, tax_code: vat_category&.accounting_vat_code, reference: invoice.ref, source: self, + side: :haben, tax_code: vat_category&.accounting_vat_code, reference: invoice.human_ref, source: self, currency: organisation.currency, booking:, cost_center: tarif&.accounting_profit_center_nr, - text: [invoice.class.model_name.human, invoice.ref, self.class.model_name.human, label].join(' ') + text: [invoice.class.model_name.human, invoice.human_ref, label].join(' ') ) ] end diff --git a/app/models/tenant.rb b/app/models/tenant.rb index f7fd3ed47..0d880c174 100644 --- a/app/models/tenant.rb +++ b/app/models/tenant.rb @@ -148,7 +148,6 @@ def merge_with_new(tenant) end def accounting_debitor_account_nr - @accounting_debitor_account_nr ||= organisation.accounting_settings&.debitor_account_nr.presence || - (id + (organisation.accounting_settings&.tenant_debitor_account_nr_base || 0)) + @accounting_debitor_account_nr ||= (organisation.accounting_settings&.tenant_debitor_account_nr_base || 0) + id end end diff --git a/app/services/taf_block.rb b/app/services/taf_block.rb index 4fb6dac98..e9c32ce0b 100644 --- a/app/services/taf_block.rb +++ b/app/services/taf_block.rb @@ -7,7 +7,7 @@ class TafBlock def initialize(type, **properties, &) @type = type - @properties = properties.to_h + @properties = properties.transform_values { Value.cast(_1) } @children = Collection.new(&) end @@ -15,32 +15,38 @@ def self.block(...) new(...) end - Value = Data.define(:value) do - delegate :to_s, to: :value - - def self.derive(value) - new derive_value(value) + Value = Data.define(:value, :as) do + CAST_BLOCKS = { # rubocop:disable Lint/ConstantDefinitionInBlock + boolean: ->(value) { value ? '1' : '0' }, + decimal: ->(value) { format('%.2f', value) }, + number: ->(value) { value.to_i.to_s }, + date: ->(value) { value.strftime('%d.%m.%Y') }, + string: ->(value) { "\"#{value.gsub(/["']/, '""')}\"" }, + symbol: ->(value) { value.to_s }, + vector: ->(value) { "[#{value.to_a.map(&:to_s).join(',')}]" }, + value: ->(value) { value } + }.freeze + + CAST_CLASSES = { # rubocop:disable Lint/ConstantDefinitionInBlock + boolean: [::FalseClass, ::TrueClass], + decimal: [::BigDecimal, ::Float], + number: [::Numeric], + date: [::Date, ::DateTime, ::ActiveSupport::TimeWithZone], + string: [::String] + }.freeze + + def self.cast(value, as: nil) + return value if value.is_a?(Value) + return nil if value.blank? + + as = CAST_CLASSES.find { |_key, klasses| klasses.any? { |klass| value.is_a?(klass) } }&.first if as.nil? + value = CAST_BLOCKS.fetch(as).call(value) + + new(value, as) end - def self.derive_value(value) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/MethodLength - case value - when ::FalseClass, ::TrueClass - value ? '1' : '0' - when ::BigDecimal, ::Float - format('%.2f', value) - when ::Numeric - value.to_s - when ::Date, ::DateTime, ::ActiveSupport::TimeWithZone - value.strftime('%d.%m.%Y') - when ::String - "\"#{value.gsub(/["']/, '""')}\"" - when ::Enumerable - "[#{value.to_a.each { derive_value(_1) }.join(',')}]" - when Value - value - else - derive_value value.to_s - end + def serialize + value.to_s end end @@ -75,14 +81,14 @@ def serialize(indent_level: 0, indent_with: ' ', separate_with: "\n") indent = [indent_with * indent_level].join separate_and_indent = [separate_with, indent, indent_with].join serialized_children = children.serialize(indent_level:, indent_with:, separate_with:) - serialized_properties = properties.compact.map { |key, value| "#{key}=#{Value.derive(value)}" } + serialized_properties = properties.compact.map { |key, value| "#{key}=#{value.serialize}" } [ # tag_start indent, "{#{type}", # properties - separate_and_indent, serialized_properties.join(separate_and_indent), + separate_and_indent, serialized_properties.join(separate_and_indent), separate_with, # children - (serialized_children.present? && separate_with) || nil, serialized_children, + (separate_with if children.present?), serialized_children, # tag end separate_with, indent, '}' ].compact.join @@ -100,18 +106,9 @@ def self.derive_from(klass, &derive_block) factories[klass] = derive_block end - def self.derive(value, **override, &block) + def self.derive(value, **override) derive_block = factories[factories.keys.find { |klass| value.is_a?(klass) }] - instance_exec(value, override, block, &derive_block) if derive_block.present? - end - - derive_from Accounting::JournalEntryGroup do |value, **override| - new(:Blg, **{ - # Date; The date of the booking. - Date: override.fetch(:Date, value.date), - - Orig: true - }) + instance_exec(value, **override, &derive_block) if derive_block.present? end derive_from Accounting::JournalEntry do |journal_entry, **override| @@ -187,20 +184,40 @@ def self.derive(value, **override, &block) }, **override) end - derive_from Invoice do |invoice, **override| + derive_from Invoice do |invoice, **_override| next unless invoice.is_a?(Invoices::Invoice) || invoice.is_a?(Invoices::Deposit) - op_id = invoice.human_ref - pk_key = [Value.new(invoice.booking.tenant.accounting_debitor_account_nr), - Value.new(invoice.organisation.accounting_settings.currency_account_nr)] - journal_entries = invoice.journal_entries.to_a + op_id = Value.cast(invoice.human_ref, as: :symbol) + pk_key = [invoice.booking.tenant.accounting_debitor_account_nr, + invoice.organisation.accounting_settings.currency_account_nr].then { "[#{_1.join(',')}]" } + + journal_entries = invoice.journal_entries.flatten.compact [ - new(:OPd, **{ PkKey: pk_key, OpId: op_id, ZabId: '15T' }, **override), - new(:Blg, **{ OpId: op_id, Date: invoice.issued_at, Orig: true }, **override) do + new(:OPd, **{ PkKey: pk_key, OpId: op_id, ZabId: '15T' }), + new(:Blg, **{ OpId: op_id, Date: invoice.issued_at, Orig: true }) do derive(journal_entries.shift, Flags: 1, OpId: op_id) journal_entries.each { derive(_1, OpId: op_id) } end ] end + + derive_from Tenant do |tenant, **_override| + [ + new(:Adr, **{ + AdrId: tenant.accounting_debitor_account_nr, + Line1: tenant.full_name, + Road: tenant.street_address, + CCode: tenant.country_code, + ACode: tenant.zipcode, + City: tenant.city + }), + new(:PKd, **{ + PkKey: Value.cast(tenant.accounting_debitor_account_nr, as: :symbol), + AdrId: Value.cast(tenant.accounting_debitor_account_nr, as: :symbol), + AccId: Value.cast(tenant.organisation.accounting_settings.currency_account_nr, as: :symbol) + }) + + ] + end end diff --git a/spec/services/taf_block_spec.rb b/spec/services/taf_block_spec.rb index f43409a57..e92de020c 100644 --- a/spec/services/taf_block_spec.rb +++ b/spec/services/taf_block_spec.rb @@ -12,10 +12,10 @@ describe '#initialize' do it 'works as DSL' do expect(taf_block.type).to eq(:Blg) - expect(taf_block.properties).to eq({ test: 1, text: 'TAF is "great"' }) + expect(taf_block.properties.transform_values(&:value)).to eq({ test: '1', text: '"TAF is ""great"""' }) expect(taf_block.children.count).to eq(1) expect(taf_block.children.first.type).to eq(:Bk) - expect(taf_block.children.first.properties).to eq({ test: 2 }) + expect(taf_block.children.first.properties.transform_values(&:value)).to eq({ test: '2' }) expect(taf_block.children.first.children.count).to eq(0) end end @@ -28,8 +28,10 @@ {Blg text="TAF is ""great""" test=1 + {Bk test=2 + } } TAF @@ -60,6 +62,7 @@ Type=0 ValNt=2091.75 OpId="1234" + } TAF end @@ -86,6 +89,7 @@ Type=0 ValNt=2091.75 OpId="1234" + } TAF end