Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Created job for sending monthly invoices #2424

Merged
merged 10 commits into from
Aug 31, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ GIT

GIT
remote: https://github.com/internetee/e_invoice.git
revision: 312cac173935f434e449d1714f3497bfee9f8995
revision: 9f850465697a2448a31ebddb83c1be5a5a9be3d2
branch: master
specs:
e_invoice (0.1.0)
e_invoice (0.1.3)
builder (~> 3.2)
nokogiri
savon
Expand Down Expand Up @@ -603,4 +603,4 @@ DEPENDENCIES
wkhtmltopdf-binary (~> 0.12.5.1)

BUNDLED WITH
2.3.16
2.3.21
10 changes: 10 additions & 0 deletions app/jobs/delete_monthly_invoices_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
class DeleteMonthlyInvoicesJob < ApplicationJob
queue_as :default

def perform
@month = Time.zone.now - 1.month
invoices = Invoice.where(monthly_invoice: true, issue_date: @month.end_of_month.to_date,
in_directo: false, e_invoice_sent_at: nil)
invoices.delete_all
end
end
3 changes: 2 additions & 1 deletion app/jobs/send_e_invoice_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ def perform(invoice_id, payable: true)

def need_to_process_invoice?(invoice:, payable:)
logger.info "Checking if need to process e-invoice #{invoice}, payable: #{payable}"
unprocessable = invoice.do_not_send_e_invoice? && (invoice.monthly_invoice ? true : payable)
return false if invoice.blank?
return false if invoice.do_not_send_e_invoice? && payable
return false if unprocessable

true
end
Expand Down
147 changes: 147 additions & 0 deletions app/jobs/send_monthly_invoices_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
class SendMonthlyInvoicesJob < ApplicationJob # rubocop:disable Metrics/ClassLength
queue_as :default

def perform(dry: false)
@dry = dry
@month = Time.zone.now - 1.month
@directo_client = new_directo_client
@min_directo_num = Setting.directo_monthly_number_min.presence.try(:to_i)
@max_directo_num = Setting.directo_monthly_number_max.presence.try(:to_i)

send_monthly_invoices
end

def new_directo_client
DirectoApi::Client.new(ENV['directo_invoice_url'], Setting.directo_sales_agent,
Setting.directo_receipt_payment_term)
end

# rubocop:disable Metrics/MethodLength
def send_monthly_invoices
Registrar.with_cash_accounts.find_each do |registrar|
summary = registrar.monthly_summary(month: @month)
next if summary.nil?

invoice = registrar.monthly_invoice(month: @month) || create_invoice(summary, registrar)
next if invoice.nil? || @dry

send_email_to_registrar(invoice: invoice, registrar: registrar)
send_e_invoice(invoice.id)
next if invoice.in_directo

Rails.logger.info("[DIRECTO] Trying to send monthly invoice #{invoice.number}")
@directo_client = new_directo_client
directo_invoices = @directo_client.invoices.add_with_schema(invoice: summary,
schema: 'summary')
next unless directo_invoices.size.positive?

directo_invoices.last.number = invoice.number
sync_with_directo
end
end

# rubocop:enable Metrics/MethodLength

def send_email_to_registrar(invoice:, registrar:)
InvoiceMailer.invoice_email(invoice: invoice,
recipient: registrar.billing_email)
.deliver_now
end

def send_e_invoice(invoice_id)
SendEInvoiceJob.set(wait: 1.minute).perform_later(invoice_id, payable: false)
end

def create_invoice(summary, registrar)
invoice = registrar.init_monthly_invoice(normalize(summary))
invoice.number = assign_monthly_number
return unless invoice.save!

update_monthly_invoice_number(num: invoice.number)
invoice
end

def sync_with_directo
invoices_xml = @directo_client.invoices.as_xml

Rails.logger.info("[Directo] - attempting to send following XML:\n #{invoices_xml}")

res = @directo_client.invoices.deliver(ssl_verify: false)
process_directo_response(res.body, invoices_xml)
rescue SocketError, Errno::ECONNREFUSED, Timeout::Error, Errno::EINVAL, Errno::ECONNRESET,
EOFError, Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::ProtocolError
Rails.logger.info('[Directo] Failed to communicate via API')
end

def assign_monthly_number
last_directo_num = [Setting.directo_monthly_number_last.presence.try(:to_i),
@min_directo_num].compact.max || 0
raise 'Directo Counter is out of period!' if directo_counter_exceedable?(1, last_directo_num)

last_directo_num + 1
end

def directo_counter_exceedable?(invoices_count, last_directo_num)
return true if @max_directo_num && @max_directo_num < (last_directo_num + invoices_count)

false
end

def process_directo_response(body, req)
Rails.logger.info "[Directo] - Responded with body: #{body}"
Nokogiri::XML(body).css('Result').each do |res|
inv = Invoice.find_by(number: res.attributes['docid'].value.to_i)
mark_invoice_as_sent_to_directo(res: res, req: req, invoice: inv)
end
end

def mark_invoice_as_sent_to_directo(res:, req:, invoice: nil)
directo_record = Directo.new(response: res.as_json.to_h,
request: req, invoice_number: res.attributes['docid'].value.to_i)
directo_record.item = invoice
invoice.update(in_directo: true)

directo_record.save!
end

def update_monthly_invoice_number(num:)
return unless num.to_i > Setting.directo_monthly_number_last.to_i

Setting.directo_monthly_number_last = num.to_i
end

private

# rubocop:disable Metrics/MethodLength
def normalize(summary, lines: [])
sum = summary.dup
line_map = Hash.new 0
sum['invoice_lines'].each { |l| line_map[l] += 1 }

line_map.each_key do |count|
count['quantity'] = line_map[count] unless count['unit'].nil?
regex = /Domeenide ettemaks|Domains prepayment/
count['quantity'] = -1 if count['description'].match?(regex)
lines << count
end

sum['invoice_lines'] = summarize_lines(lines)
sum
end
# rubocop:enable Metrics/MethodLength

def summarize_lines(invoice_lines, lines: [])
line_map = Hash.new 0
invoice_lines.each do |l|
hash = l.with_indifferent_access.except(:start_date, :end_date)
line_map[hash] += 1
end

line_map.each_key do |count|
count['price'] = (line_map[count] * count['price'].to_f).round(3) unless count['price'].nil?
lines << count
end

lines
end
end
1 change: 1 addition & 0 deletions app/mailers/invoice_mailer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ def invoice_email(invoice:, recipient:, paid: false)

subject = default_i18n_subject(invoice_number: invoice.number)
subject << I18n.t('invoice.already_paid') if paid
subject << I18n.t('invoice.monthly_invoice') if invoice.monthly_invoice
attachments["invoice-#{invoice.number}.pdf"] = invoice.as_pdf
mail(to: recipient, subject: subject)
end
Expand Down
29 changes: 20 additions & 9 deletions app/models/concerns/registrar/book_keeping.rb
Original file line number Diff line number Diff line change
@@ -1,22 +1,27 @@
module Registrar::BookKeeping
module Registrar::BookKeeping # rubocop:disable Metrics/ModuleLength
extend ActiveSupport::Concern

DOMAIN_TO_PRODUCT = { 'ee': '01EE', 'com.ee': '02COM', 'pri.ee': '03PRI',
'fie.ee': '04FIE', 'med.ee': '05MED' }.freeze

included do
scope :with_cash_accounts, (lambda do
joins(:accounts).where('accounts.account_type = ? AND test_registrar != ?',
Account::CASH,
true)
end)
end

def monthly_summary(month:)
activities = monthly_activites(month)
return unless activities.any?

invoice = {
'number': 1,
'customer': compose_directo_customer,
'number': 1, 'customer': compose_directo_customer,
'language': language == 'en' ? 'ENG' : '', 'currency': activities.first.currency,
'date': month.end_of_month.strftime('%Y-%m-%d')
}.as_json

invoice['invoice_lines'] = prepare_invoice_lines(month: month, activities: activities)

invoice
end

Expand Down Expand Up @@ -55,20 +60,25 @@ def monthly_activites(month)
.where(activity_type: [AccountActivity::CREATE, AccountActivity::RENEW])
end

def monthly_invoice(month:)
invoices.where(monthly_invoice: true, issue_date: month.end_of_month.to_date,
cancelled_at: nil).first
end

def new_monthly_invoice_line(activity:, duration: nil)
price = load_price(activity)
line = {
'product_id': DOMAIN_TO_PRODUCT[price.zone_name.to_sym],
'quantity': 1,
'unit': language == 'en' ? 'pc' : 'tk',
}
}.with_indifferent_access

finalize_invoice_line(line, price: price, duration: duration, activity: activity)
end

def finalize_invoice_line(line, price:, activity:, duration:)
yearly = price.duration.in_years.to_i >= 1
line['price'] = yearly ? (price.price.amount / price.duration.in_years.to_i) : price.price.amount
line['price'] = yearly ? (price.price.amount / price.duration.in_years.to_i).to_f : price.price.amount.to_f
line['description'] = description_in_language(price: price, yearly: yearly)

add_product_timeframe(line: line, activity: activity, duration: duration) if duration.present? && (duration > 1)
Expand All @@ -79,15 +89,16 @@ def finalize_invoice_line(line, price:, activity:, duration:)
def add_product_timeframe(line:, activity:, duration:)
create_time = activity.created_at
line['start_date'] = (create_time + (duration - 1).year).end_of_month.strftime('%Y-%m-%d')
line['end_date'] = (create_time + (duration - 1).year + 1).end_of_month.strftime('%Y-%m-%d')
line['end_date'] = (create_time + duration.year).end_of_month.strftime('%Y-%m-%d')
end

def description_in_language(price:, yearly:)
timeframe_string = yearly ? 'yearly' : 'monthly'
locale_string = "registrar.invoice_#{timeframe_string}_product_description"
length = yearly ? price.duration.in_years.to_i : price.duration.in_months.to_i

I18n.with_locale(language == 'en' ? 'en' : 'et') do
I18n.t(locale_string, tld: ".#{price.zone_name}", length: price.duration.in_years.to_i)
I18n.t(locale_string, tld: ".#{price.zone_name}", length: length)
end
end

Expand Down
13 changes: 10 additions & 3 deletions app/models/invoice.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,14 @@ class Invoice < ApplicationRecord
# rubocop:enable Layout/LineLength
# rubocop:enable Style/MultilineBlockLayout
validates :due_date, :currency, :seller_name,
:seller_iban, :buyer_name, :items, presence: true
:seller_iban, :buyer_name, presence: true
validates :items, presence: true, unless: -> { monthly_invoice }

before_create :set_invoice_number
before_create :calculate_total, unless: :total?
before_create :apply_default_buyer_vat_no, unless: :buyer_vat_no?
skip_callback :create, :before, :set_invoice_number, if: -> { monthly_invoice }
skip_callback :create, :before, :calculate_total, if: -> { monthly_invoice }

attribute :vat_rate, ::Type::VatRate.new

Expand Down Expand Up @@ -118,7 +121,7 @@ def order
end

def subtotal
items.map(&:item_sum_without_vat).reduce(:+)
items.map(&:item_sum_without_vat).reduce(:+) || 0
end

def vat_amount
Expand All @@ -131,7 +134,11 @@ def total
end

def each(&block)
items.each(&block)
if monthly_invoice
metadata['items'].map { |el| OpenStruct.new(el) }.each(&block)
else
items.each(&block)
end
end

def as_pdf
Expand Down
38 changes: 28 additions & 10 deletions app/models/invoice/e_invoice_generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,22 +41,16 @@ def generate

e_invoice_invoice_items = []
invoice.each do |invoice_item|
e_invoice_invoice_item = EInvoice::InvoiceItem.new.tap do |i|
i.description = invoice_item.description
i.price = invoice_item.price
i.quantity = invoice_item.quantity
i.unit = invoice_item.unit
i.subtotal = invoice_item.subtotal
i.vat_rate = invoice_item.vat_rate
i.vat_amount = invoice_item.vat_amount
i.total = invoice_item.total
end
e_invoice_invoice_item = generate_invoice_item(invoice, invoice_item)
e_invoice_invoice_items << e_invoice_invoice_item
end

e_invoice_name_item = e_invoice_invoice_items.shift if invoice.monthly_invoice

e_invoice_invoice = EInvoice::Invoice.new.tap do |i|
i.seller = seller
i.buyer = buyer
i.name = e_invoice_name_item&.description
i.items = e_invoice_invoice_items
i.number = invoice.number
i.date = invoice.issue_date
Expand All @@ -72,9 +66,33 @@ def generate
i.currency = invoice.currency
i.delivery_channel = %i[internet_bank portal]
i.payable = payable
i.monthly_invoice = invoice.monthly_invoice
end

EInvoice::EInvoice.new(date: Time.zone.today, invoice: e_invoice_invoice)
end

private

def generate_invoice_item(invoice, item)
EInvoice::InvoiceItem.new.tap do |i|
i.description = item.description
i.unit = item.unit
i.price = item.price
i.quantity = item.quantity
if invoice.monthly_invoice && item.price && item.quantity
i.product_id = item.product_id
i.vat_rate = invoice.vat_rate
i.subtotal = (item.price * item.quantity).round(3)
i.vat_amount = i.subtotal * (i.vat_rate / 100)
i.total = i.subtotal + i.vat_amount
else
i.subtotal = item.subtotal
i.vat_rate = item.vat_rate
i.vat_amount = item.vat_amount
i.total = item.total
end
end
end
end
end
3 changes: 2 additions & 1 deletion app/models/invoice/pdf_generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ def as_pdf
private

def invoice_html
ApplicationController.render(template: 'invoice/pdf', assigns: { invoice: invoice })
template = invoice.monthly_invoice ? 'invoice/monthly_pdf' : 'invoice/pdf'
ApplicationController.render(template: template, assigns: { invoice: invoice })
end
end
end
Loading