Refactor and improve invoices

- `runner 'Invoice.cancel_overdue_invoices'` in `schedule.rb` is
changed to `rake 'invoices:cancel_overdue'`.
- `invoices.payment_term` database column is removed and its value is
hardcoded in UI.
- `invoices.paid_at` is removed as unused
- `invoices.due_date` column's type is now `date`.
- `Invoice#invoice_items` renamed to `Invoice#items` and `Invoice`
interface to get a list of items is unified.
- Default date format in UI.
- Default translations are used.
- Tests improved.
- Specs converted to tests and removed along with factories.
- Database structure improved.
This commit is contained in:
Artur Beljajev 2018-10-17 12:21:04 +03:00
parent d86ec026e3
commit a97728c0f3
65 changed files with 758 additions and 341 deletions

View file

@ -41,9 +41,7 @@ class BankTransaction < ActiveRecord::Base
return unless registrar
return unless invoice_num
return unless invoice
return if invoice.binded?
return if invoice.cancelled?
return unless invoice.payable?
return if invoice.total != sum
create_activity(registrar, invoice)
@ -62,7 +60,7 @@ class BankTransaction < ActiveRecord::Base
return
end
if invoice.binded?
if invoice.paid?
errors.add(:base, I18n.t('invoice_is_already_binded'))
return
end

View file

@ -0,0 +1,28 @@
module Concerns
module Invoice
module Cancellable
extend ActiveSupport::Concern
included do
scope :non_cancelled, -> { where(cancelled_at: nil) }
end
def cancellable?
unpaid? && not_cancelled?
end
def cancel
raise 'Invoice cannot be cancelled' unless cancellable?
update!(cancelled_at: Time.zone.now)
end
def cancelled?
cancelled_at
end
def not_cancelled?
!cancelled?
end
end
end
end

View file

@ -0,0 +1,28 @@
module Concerns
module Invoice
module Payable
extend ActiveSupport::Concern
included do
scope :unpaid, -> { where('id NOT IN (SELECT invoice_id FROM account_activities WHERE' \
' invoice_id IS NOT NULL)') }
end
def payable?
unpaid? && not_cancelled?
end
def paid?
account_activity
end
def receipt_date
account_activity.created_at.to_date
end
def unpaid?
!paid?
end
end
end
end

View file

@ -3,7 +3,7 @@ class Directo < ActiveRecord::Base
belongs_to :item, polymorphic: true
def self.send_receipts
new_trans = Invoice.where(in_directo: false).where(cancelled_at: nil)
new_trans = Invoice.where(in_directo: false).non_cancelled
total = new_trans.count
counter = 0
Rails.logger.info("[DIRECTO] Will try to send #{total} invoices")
@ -26,7 +26,7 @@ class Directo < ActiveRecord::Base
xml.invoice(
"SalesAgent" => Setting.directo_sales_agent,
"Number" => num,
"InvoiceDate" => invoice.created_at.strftime("%Y-%m-%d"),
"InvoiceDate" => invoice.issue_date.strftime("%Y-%m-%d"),
"PaymentTerm" => Setting.directo_receipt_payment_term,
"Currency" => invoice.currency,
"CustomerCode"=> invoice.buyer.accounting_customer_code

View file

@ -1,16 +1,16 @@
class Invoice < ActiveRecord::Base
include Versions
include Concerns::Invoice::Cancellable
include Concerns::Invoice::Payable
belongs_to :seller, class_name: 'Registrar'
belongs_to :buyer, class_name: 'Registrar'
has_one :account_activity
has_many :invoice_items
has_many :items, class_name: 'InvoiceItem', dependent: :destroy
has_many :directo_records, as: :item, class_name: 'Directo'
accepts_nested_attributes_for :invoice_items
accepts_nested_attributes_for :items
scope :unbinded, lambda {
where('id NOT IN (SELECT invoice_id FROM account_activities where invoice_id IS NOT NULL)')
}
scope :all_columns, ->{select("invoices.*")}
scope :sort_due_date_column, ->{all_columns.select("CASE WHEN invoices.cancelled_at is not null THEN
(invoices.cancelled_at + interval '100 year') ELSE
@ -24,11 +24,14 @@ class Invoice < ActiveRecord::Base
scope :sort_by_sort_receipt_date_asc, ->{sort_receipt_date_column.order("sort_receipt_date ASC")}
scope :sort_by_sort_receipt_date_desc, ->{sort_receipt_date_column.order("sort_receipt_date DESC")}
scope :overdue, -> { unpaid.non_cancelled.where('due_date < ?', Time.zone.today) }
attr_accessor :billing_email
validates :billing_email, email_format: { message: :invalid }, allow_blank: true
validates :issue_date, presence: true
validates :due_date, :currency, :seller_name,
:seller_iban, :buyer_name, :invoice_items, presence: true
:seller_iban, :buyer_name, :items, presence: true
validates :vat_rate, numericality: { greater_than_or_equal_to: 0, less_than: 100 },
allow_nil: true
@ -56,35 +59,6 @@ class Invoice < ActiveRecord::Base
false
end
class << self
def cancel_overdue_invoices
STDOUT << "#{Time.zone.now.utc} - Cancelling overdue invoices\n" unless Rails.env.test?
cr_at = Time.zone.now - Setting.days_to_keep_overdue_invoices_active.days
invoices = Invoice.unbinded.where(
'due_date < ? AND cancelled_at IS NULL', cr_at
)
unless Rails.env.test?
invoices.each do |m|
STDOUT << "#{Time.zone.now.utc} Invoice.cancel_overdue_invoices: ##{m.id}\n"
end
end
count = invoices.update_all(cancelled_at: Time.zone.now)
STDOUT << "#{Time.zone.now.utc} - Successfully cancelled #{count} overdue invoices\n" unless Rails.env.test?
end
end
def binded?
account_activity.present?
end
def receipt_date
account_activity.try(:created_at)
end
def to_s
I18n.t('invoice_no', no: number)
end
@ -119,25 +93,6 @@ class Invoice < ActiveRecord::Base
"invoice-#{number}.pdf"
end
def cancel
if binded?
errors.add(:base, I18n.t('cannot_cancel_paid_invoice'))
return false
end
if cancelled?
errors.add(:base, I18n.t('cannot_cancel_cancelled_invoice'))
return false
end
self.cancelled_at = Time.zone.now
save
end
def cancelled?
cancelled_at.present?
end
def forward(html)
return false unless valid?
return false unless billing_email.present?
@ -146,12 +101,8 @@ class Invoice < ActiveRecord::Base
true
end
def items
invoice_items
end
def subtotal
invoice_items.map(&:item_sum_without_vat).reduce(:+)
items.map(&:item_sum_without_vat).reduce(:+)
end
def vat_amount
@ -164,6 +115,10 @@ class Invoice < ActiveRecord::Base
read_attribute(:total)
end
def each
items.each { |item| yield item }
end
private
def apply_default_vat_rate

View file

@ -3,6 +3,6 @@ class InvoiceItem < ActiveRecord::Base
belongs_to :invoice
def item_sum_without_vat
(amount * price).round(2)
(price * quantity).round(2)
end
end

View file

@ -51,8 +51,8 @@ class Registrar < ActiveRecord::Base
def issue_prepayment_invoice(amount, description = nil)
invoices.create(
due_date: (Time.zone.now.to_date + Setting.days_to_keep_invoices_active.days).end_of_day,
payment_term: 'prepayment',
issue_date: Time.zone.today,
due_date: (Time.zone.now + Setting.days_to_keep_invoices_active.days).to_date,
description: description,
currency: 'EUR',
seller_name: Setting.registry_juridical_name,
@ -82,11 +82,11 @@ class Registrar < ActiveRecord::Base
buyer_url: website,
buyer_email: email,
reference_no: reference_no,
invoice_items_attributes: [
items_attributes: [
{
description: 'prepayment',
unit: 'piece',
amount: 1,
quantity: 1,
price: amount
}
]