Created job for sending monthly invoices

This commit is contained in:
Sergei Tsõganov 2022-08-21 19:11:39 +03:00
parent a5f803b57a
commit d589aa1681
18 changed files with 1103 additions and 43 deletions

View file

@ -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

View file

@ -17,7 +17,7 @@ class SendEInvoiceJob < ApplicationJob
def need_to_process_invoice?(invoice:, payable:)
logger.info "Checking if need to process e-invoice #{invoice}, payable: #{payable}"
return false if invoice.blank?
return false if invoice.do_not_send_e_invoice? && payable
return false if invoice.do_not_send_e_invoice? && (invoice.monthly_invoice ? true : payable)
true
end

View file

@ -0,0 +1,155 @@
class SendMonthlyInvoicesJob < ApplicationJob
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
def send_monthly_invoices
Registrar.where.not(test_registrar: true).find_each do |registrar|
next unless registrar.cash_account
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
InvoiceMailer.invoice_email(invoice: invoice,
recipient: registrar.billing_email)
.deliver_now
SendEInvoiceJob.set(wait: 1.minute).perform_now(invoice.id, payable: false)
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
def create_invoice(summary, registrar)
vat_rate = ::Invoice::VatRateCalculator.new(registrar: registrar).calculate
invoice = Invoice.new(
number: assign_monthly_number,
issue_date: summary['date'].to_date,
due_date: summary['date'].to_date,
currency: 'EUR',
description: I18n.t('invoice.monthly_invoice_description'),
seller_name: Setting.registry_juridical_name,
seller_reg_no: Setting.registry_reg_no,
seller_iban: Setting.registry_iban,
seller_bank: Setting.registry_bank,
seller_swift: Setting.registry_swift,
seller_vat_no: Setting.registry_vat_no,
seller_country_code: Setting.registry_country_code,
seller_state: Setting.registry_state,
seller_street: Setting.registry_street,
seller_city: Setting.registry_city,
seller_zip: Setting.registry_zip,
seller_phone: Setting.registry_phone,
seller_url: Setting.registry_url,
seller_email: Setting.registry_email,
seller_contact_name: Setting.registry_invoice_contact,
buyer: registrar,
buyer_name: registrar.name,
buyer_reg_no: registrar.reg_no,
buyer_country_code: registrar.address_country_code,
buyer_state: registrar.address_state,
buyer_street: registrar.address_street,
buyer_city: registrar.address_city,
buyer_zip: registrar.address_zip,
buyer_phone: registrar.phone,
buyer_url: registrar.website,
buyer_email: registrar.email,
reference_no: registrar.reference_no,
vat_rate: vat_rate,
monthly_invoice: true,
metadata: { items: summary['invoice_lines'] },
total: 0
)
return unless invoice.save!
update_directo_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_numbers
invoices_count = @directo_client.invoices.count
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?(invoices_count,
last_directo_num)
@directo_client.invoices.each do |inv|
last_directo_num += 1
inv.number = last_directo_num
end
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(res: res, req: req, invoice: inv)
end
end
def mark_invoice_as_sent(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_directo_number(num:)
return unless num.to_i > Setting.directo_monthly_number_last.to_i
Setting.directo_monthly_number_last = num.to_i
end
end

View file

@ -4,6 +4,7 @@ class InvoiceMailer < ApplicationMailer
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

View file

@ -55,6 +55,11 @@ module Registrar::BookKeeping
.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 = {
@ -68,7 +73,7 @@ module Registrar::BookKeeping
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)

View file

@ -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
@ -118,7 +121,7 @@ class Invoice < ApplicationRecord
end
def subtotal
items.map(&:item_sum_without_vat).reduce(:+)
items.map(&:item_sum_without_vat).reduce(:+) || 0
end
def vat_amount
@ -131,7 +134,11 @@ class Invoice < ApplicationRecord
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

View file

@ -46,10 +46,17 @@ class Invoice
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
if invoice.monthly_invoice
i.subtotal = 0
i.vat_rate = 0
i.vat_amount = 0
i.total = 0
else
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
end
e_invoice_invoice_items << e_invoice_invoice_item
end
@ -66,9 +73,15 @@ class Invoice
i.beneficiary_name = invoice.seller_name
i.beneficiary_account_number = invoice.seller_iban
i.payer_name = invoice.buyer_name
i.subtotal = invoice.subtotal
i.vat_amount = invoice.vat_amount
i.total = invoice.total
if invoice.monthly_invoice
i.subtotal = 0
i.vat_amount = 0
i.total = 0
else
i.subtotal = invoice.subtotal
i.vat_amount = invoice.vat_amount
i.total = invoice.total
end
i.currency = invoice.currency
i.delivery_channel = %i[internet_bank portal]
i.payable = payable

View file

@ -14,7 +14,8 @@ class Invoice
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

View file

@ -4,11 +4,12 @@
= @invoice
.col-sm-8
%h1.text-right.text-center-xs
- if @invoice.unpaid?
= link_to(t(:payment_received), new_admin_bank_statement_path(invoice_id: @invoice.id), class: 'btn btn-default')
- unless @invoice.monthly_invoice
- if @invoice.unpaid?
= link_to(t(:payment_received), new_admin_bank_statement_path(invoice_id: @invoice.id), class: 'btn btn-default')
- if @invoice.paid? and !@invoice.cancelled?
= link_to(t(:cancel_payment), cancel_paid_admin_invoices_path(invoice_id: @invoice.id), method: 'post', data: { confirm: t(:are_you_sure) }, class: 'btn btn-warning')
- if @invoice.paid? && !@invoice.cancelled?
= link_to(t(:cancel_payment), cancel_paid_admin_invoices_path(invoice_id: @invoice.id), method: 'post', data: { confirm: t(:are_you_sure) }, class: 'btn btn-warning')
= link_to(t('.download_btn'), download_admin_invoice_path(@invoice), class: 'btn btn-default')
= link_to(t('.deliver_btn'), new_admin_invoice_delivery_path(@invoice), class: 'btn btn-default')

View file

@ -0,0 +1,277 @@
%html{lang: I18n.locale.to_s}
%head
%meta{charset: "utf-8"}
:css
.container {
margin: auto;
font-size: 12px;
}
.col-md-12 {
}
.col-md-6 {
width: 49%;
display: inline-block;
}
.col-xs-4 {
width: 33%;
}
.col-xs-2 {
width: 16%;
}
.col-md-3 {
width: 24%;
display: inline-block;
}
.left {
float: left;
}
.left {
padding-right: 5px;
}
.right {
float: right;
}
.text-right {
text-align: right;
}
dt {
float: left;
width: 100px;
clear: left;
text-align: right;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-weight: bold;
line-height: 1.42857;
}
dd {
margin-left: 120px;
line-height: 1.42857;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 12px;
}
th {
text-align: left;
border: 0px;
border-top: 1px solid #DDD;
padding: 6px;
}
thead th {
border-bottom: 2px solid #DDD;
border-top: 0px;
}
td {
border-top: 1px solid #DDD;
}
td {
padding: 6px;
}
.no-border {
border: 0px;
}
hr {
height: 1px;
border: 0;
color: #DDD;
background-color: #DDD;
}
.clear {
clear: both;
}
.pull-down {
margin-top: 50px;
}
#header {
position: relative;
min-height: 100px;
}
img {
width: 106px;
height: 102px;
}
#header-content {
position: absolute;
bottom: 0;
}
#footer {
position: absolute;
bottom: 0px;
width: 99%;
}
h1 {
margin-bottom: 5px;
}
%body
.container
#header.row
.col-sm-6.left
#header-content
%h1
= @invoice
.col-sm-6.right
%img{src: "#{Rails.root}/public/eis-logo-black-et.png"}
.clear
%hr
.row
.col-md-6.left
%h4
Details
%hr
%dl.dl-horizontal
%dt= t(:issue_date)
%dd= l @invoice.issue_date
- if @invoice.cancelled?
%dt= Invoice.human_attribute_name :cancelled_at
%dd= l @invoice.cancelled_at
%dt= t(:due_date)
- if @invoice.cancelled?
%dd= t(:cancelled)
- else
%dd= l @invoice.due_date
%dt= t(:issuer)
%dd= @invoice.seller_contact_name
- if @invoice.description.present?
%dt= t(:description)
%dd=@invoice.description
%dt= Invoice.human_attribute_name :reference_no
%dd= @invoice.reference_no
.col-md-6.right
%h4= t(:client)
%hr
%dl.dl-horizontal
%dt= t(:name)
%dd= @invoice.buyer_name
%dt= t(:reg_no)
%dd= @invoice.buyer_reg_no
- if @invoice.buyer_address.present?
%dt= Invoice.human_attribute_name :address
%dd= @invoice.buyer_address
- if @invoice.buyer_country.present?
%dt= t(:country)
%dd= @invoice.buyer_country
- if @invoice.buyer_phone.present?
%dt= t(:phone)
%dd= @invoice.buyer_phone
- if @invoice.buyer_url.present?
%dt= t(:url)
%dd= @invoice.buyer_url
- if @invoice.buyer_email.present?
%dt= t(:email)
%dd= @invoice.buyer_email
.clear
.row.pull-down
.col-md-12
.table-responsive
%table.table.table-hover.table-condensed
%thead
%tr
%th{class: 'col-xs-1'}= t(:code)
%th{class: 'col-xs-1'}= InvoiceItem.human_attribute_name :quantity
%th{class: 'col-xs-1'}= t(:unit)
%th{class: 'col-xs-5'}= t(:description)
%th{class: 'col-xs-2'}= t(:price)
%th{class: 'col-xs-2'}= t(:total)
%tbody
- @invoice.each do |invoice_item|
%tr
%td= invoice_item.product_id
%td= invoice_item.quantity
%td= invoice_item.unit
%td= invoice_item.description
- if invoice_item.price && invoice_item.quantity
%td= currency(invoice_item.price)
%td= "#{currency((invoice_item.price * invoice_item.quantity).round(3))} #{@invoice.currency}"
- else
%td= ''
%td= ''
%tfoot
%tr
%th{colspan: 4}
%th= Invoice.human_attribute_name :subtotal
%td= number_to_currency(0)
%tr
%th.no-border{colspan: 4}
%th= "VAT #{number_to_percentage(@invoice.vat_rate, precision: 1)}"
%td= number_to_currency(0)
%tr
%th.no-border{colspan: 4}
%th= t(:total)
%td= number_to_currency(0)
#footer
%hr
.row
.col-md-3.left
= @invoice.seller_name
%br
= @invoice.seller_address
%br
= @invoice.seller_country
%br
= "#{t('reg_no')} #{@invoice.seller_reg_no}"
%br
= "#{Registrar.human_attribute_name :vat_no} #{@invoice.seller_vat_no}"
.col-md-3.left
= @invoice.seller_phone
%br
= @invoice.seller_email
%br
= @invoice.seller_url
.col-md-3.text-right.left
= t(:bank)
%br
= t(:iban)
%br
= t(:swift)
.col-md-3.left
= @invoice.seller_bank
%br
= @invoice.seller_iban
%br
= @invoice.seller_swift

View file

@ -234,7 +234,7 @@
%td= invoice_item.unit
%td= invoice_item.quantity
%td= currency(invoice_item.price)
%td= "#{currency(invoice_item.item_sum_without_vat)} #{@invoice.currency}"
%td= "#{currency(invoice_item.item_sum_without_vat)} #{@invoice.currency}"
%tfoot
%tr
%th{colspan: 3}