Generate invoice model

There are three main components:
1. The invoice model
2. order serializers: serialize the order for the invoice
3. data presenters: the object that will be use to access the order's serialize data
This commit is contained in:
Mohamed ABDELLANI
2023-03-08 09:08:36 +01:00
committed by Konrad
parent dd224b953d
commit 0fbf88190e
58 changed files with 725 additions and 93 deletions

View File

@@ -33,7 +33,7 @@ module Spree
def model_class
const_name = controller_name.classify
return "Spree::#{const_name}".constantize if Spree.const_defined?(const_name)
return "Spree::#{const_name}".constantize if Object.const_defined?("Spree::#{const_name}")
nil
end

View File

@@ -6,6 +6,10 @@ module Spree
respond_to :json
authorize_resource class: false
def index
@order = Spree::Order.find_by_number(params[:order_id])
end
def create
invoice_service = BulkInvoiceService.new
invoice_service.start_pdf_job(params[:order_ids])

View File

@@ -99,6 +99,11 @@ module Spree
end
def print
# This is for testing on realtime
# I'll replace it later
data = Invoice::OrderSerializer.new(@order).serializable_hash
@invoice=Invoice.new(order: @order, data: data, date: Time.now.to_date)
@invoice_presenter= @invoice.presenter
render_with_wicked_pdf InvoiceRenderer.new.args(@order)
end

15
app/models/invoice.rb Normal file
View File

@@ -0,0 +1,15 @@
# frozen_string_literal: true
class Invoice < ApplicationRecord
belongs_to :order, class_name: 'Spree::Order'
serialize :data, Hash
before_validation :serialize_order
def presenter
@presenter ||= Invoice::DataPresenter.new(self)
end
def serialize_order
self.data ||= order.serialize_for_invoice
end
end

View File

@@ -0,0 +1,102 @@
class Invoice::DataPresenter
attr_reader :invoice
delegate :data, :date, to: :invoice
FINALIZED_NON_SUCCESSFUL_STATES = %w(canceled returned).freeze
extend Invoice::DataPresenterAttributes
attributes :included_tax_total, :additional_tax_total, :state, :total, :payment_total,
:currency
attributes :number, :note, :special_instructions, prefix: :order
attributes_with_presenter :order_cycle, :distributor, :customer, :ship_address,
:shipping_method, :bill_address
array_attribute :sorted_line_items, class_name: 'LineItem'
array_attribute :all_eligible_adjustments, class_name: 'Adjustment'
array_attribute :payments, class_name: 'Payment'
relevant_attributes :order_note, :distributor, :sorted_line_items
def initialize(invoice)
@invoice = invoice
end
def has_taxes_included
included_tax_total > 0
end
def total_tax
additional_tax_total + included_tax_total
end
def order_completed_at
return nil if data[:completed_at].blank?
Time.zone.parse(data[:completed_at])
end
def checkout_adjustments(exclude: [], reject_zero_amount: true)
adjustments = all_eligible_adjustments
if exclude.include? :line_item
adjustments.reject! { |a|
a.adjustable_type == 'Spree::LineItem'
}
end
if reject_zero_amount
adjustments.reject! { |a| a.amount == 0 }
end
adjustments
end
def invoice_date
date
end
def paid?
data[:payment_state] == 'paid' || data[:payment_state] == 'credit_owed'
end
def outstanding_balance?
!new_outstanding_balance.zero?
end
def new_outstanding_balance
if state.in?(FINALIZED_NON_SUCCESSFUL_STATES)
-payment_total
else
total - payment_total
end
end
def outstanding_balance_label
new_outstanding_balance.negative? ? I18n.t(:credit_owed) : I18n.t(:balance_due)
end
def last_payment
payments.max_by(&:created_at)
end
def last_payment_method
last_payment&.payment_method
end
def display_outstanding_balance
Spree::Money.new(new_outstanding_balance, currency: currency)
end
def display_checkout_tax_total
Spree::Money.new(total_tax, currency: currency)
end
def display_checkout_total_less_tax
Spree::Money.new(total - total_tax, currency: currency)
end
def display_total
Spree::Money.new(total, currency: currency)
end
end

View File

@@ -0,0 +1,25 @@
class Invoice::DataPresenter::Address < Invoice::DataPresenter::Base
attributes :firstname, :lastname, :address1, :address2, :city, :zipcode, :company, :phone
attributes_with_presenter :state
def full_name
"#{firstname} #{lastname}".strip
end
def address_part1
render_address([address1, address2])
end
def address_part2
render_address([city, zipcode, state&.name])
end
def full_address
render_address([address1, address2, city, zipcode, state&.name])
end
private
def render_address(address_parts)
address_parts.reject(&:blank?).join(', ')
end
end

View File

@@ -0,0 +1,19 @@
class Invoice::DataPresenter::Adjustment < Invoice::DataPresenter::Base
attributes :adjustable_type, :label, :included_tax_total, :additional_tax_total, :amount,
:currency
def display_amount
Spree::Money.new(amount, currency: currency)
end
def display_taxes(display_zero: false)
if included_tax_total.positive?
amount = Spree::Money.new(included_tax_total, currency: currency)
I18n.t(:tax_amount_included, amount: amount)
elsif additional_tax_total.positive?
Spree::Money.new(additional_tax_total, currency: currency)
elsif display_zero
Spree::Money.new(0.00, currency: currency)
end
end
end

View File

@@ -0,0 +1,7 @@
class Invoice::DataPresenter::Base
attr :data
def initialize(data)
@data = data
end
extend Invoice::DataPresenterAttributes
end

View File

@@ -0,0 +1,2 @@
class Invoice::DataPresenter::BillAddress < Invoice::DataPresenter::Address
end

View File

@@ -0,0 +1,2 @@
class Invoice::DataPresenter::BusinessAddress < Invoice::DataPresenter::Address
end

View File

@@ -0,0 +1,3 @@
class Invoice::DataPresenter::Contact < Invoice::DataPresenter::Base
attributes :name, :email
end

View File

@@ -0,0 +1,3 @@
class Invoice::DataPresenter::Customer < Invoice::DataPresenter::Base
attributes :code, :email
end

View File

@@ -0,0 +1,9 @@
class Invoice::DataPresenter::Distributor < Invoice::DataPresenter::Base
attributes :name, :abn, :acn, :logo_url, :display_invoice_logo, :invoice_text, :email_address
attributes_with_presenter :contact, :address, :business_address
relevant_attributes :name
def display_invoice_logo?
display_invoice_logo == true
end
end

View File

@@ -0,0 +1,20 @@
class Invoice::DataPresenter::LineItem < Invoice::DataPresenter::Base
attributes :quantity, :price_with_adjustments, :added_tax, :included_tax, :currency
attributes_with_presenter :variant
relevant_attributes :quantity
delegate :name_to_display, :options_text, to: :variant
def display_amount_with_adjustments
Spree::Money.new(price_with_adjustments, currency: currency)
end
def display_line_items_taxes(display_zero = true)
if included_tax.positive?
Spree::Money.new( included_tax, currency: currency)
elsif added_tax.positive?
Spree::Money.new( added_tax, currency: currency)
elsif display_zero
Spree::Money.new(0.00, currency: currency)
end
end
end

View File

@@ -0,0 +1,3 @@
class Invoice::DataPresenter::OrderCycle < Invoice::DataPresenter::Base
attributes :name
end

View File

@@ -0,0 +1,17 @@
class Invoice::DataPresenter::Payment < Invoice::DataPresenter::Base
attributes :amount, :currency, :state
attributes_with_presenter :payment_method
def created_at
datetime = data&.[](:created_at)
datetime.present? ? Time.zone.parse(datetime) : nil
end
def display_amount
Spree::Money.new(amount, currency: currency)
end
def payment_method_name
payment_method&.name
end
end

View File

@@ -0,0 +1,3 @@
class Invoice::DataPresenter::PaymentMethod < Invoice::DataPresenter::Base
attributes :name, :description
end

View File

@@ -0,0 +1,4 @@
class Invoice::DataPresenter::Product < Invoice::DataPresenter::Base
attributes :name
attributes_with_presenter :supplier
end

View File

@@ -0,0 +1,2 @@
class Invoice::DataPresenter::ShipAddress < Invoice::DataPresenter::Address
end

View File

@@ -0,0 +1,3 @@
class Invoice::DataPresenter::ShippingMethod < Invoice::DataPresenter::Base
attributes :name, :require_ship_address
end

View File

@@ -0,0 +1,3 @@
class Invoice::DataPresenter::State < Invoice::DataPresenter::Base
attributes :name
end

View File

@@ -0,0 +1,3 @@
class Invoice::DataPresenter::Supplier < Invoice::DataPresenter::Base
attributes :name
end

View File

@@ -0,0 +1,10 @@
class Invoice::DataPresenter::Variant < Invoice::DataPresenter::Base
attributes :display_name, :options_text
attributes_with_presenter :product
def name_to_display
return product.name if display_name.blank?
display_name
end
end

View File

@@ -0,0 +1,43 @@
module Invoice::DataPresenterAttributes
extend ActiveSupport::Concern
def attributes(*attributes,prefix: nil)
attributes.each do |attribute|
define_method([prefix,attribute].reject(&:blank?).join("_")) do
data&.[](attribute)
end
end
end
def attributes_with_presenter(*attributes)
attributes.each do |attribute|
define_method(attribute) do
instance_variable = instance_variable_get("@#{attribute}")
return instance_variable if instance_variable
instance_variable_set("@#{attribute}",
Invoice::DataPresenter.const_get(attribute.to_s.classify).new(data&.[](attribute)))
end
end
end
def array_attribute(attribute_name,class_name: nil)
define_method(attribute_name) do
instance_variable = instance_variable_get("@#{attribute_name}")
return instance_variable if instance_variable
instance_variable_set("@#{attribute_name}",
data&.[](attribute_name)&.map { |item|
Invoice::DataPresenter.const_get(class_name).new(item)
})
end
end
def relevant_attributes(*attributes)
define_method(:relevant_values) do
attributes.map do |attribute|
send(attribute)
end
end
end
end

View File

@@ -68,6 +68,7 @@ module Spree
},
class_name: 'Spree::Adjustment',
dependent: :destroy
has_many :invoices
belongs_to :order_cycle
belongs_to :distributor, class_name: 'Enterprise'
@@ -574,6 +575,29 @@ module Spree
end
end
def can_generate_new_invoice?
return true if invoices.empty?
!invoice_comparator.equal? current_state_invoice, invoices.last
end
def invoice_comparator
@invoice_comparator ||= InvoiceComparator.new
end
def current_state_invoice
Invoice.new(
order: self,
data: serialize_for_invoice,
date: Time.now.to_date,
number: invoices.count + 1
)
end
def serialize_for_invoice
Invoice::OrderSerializer.new(self).serializable_hash
end
private
def deliver_order_confirmation_email

View File

@@ -0,0 +1,4 @@
class Invoice::AddressSerializer < ActiveModel::Serializer
attributes :firstname, :lastname, :address1, :address2, :city, :zipcode, :phone, :company
has_one :state, serializer: Invoice::StateSerializer
end

View File

@@ -0,0 +1,3 @@
class Invoice::AdjustmentSerializer < ActiveModel::Serializer
attributes :adjustable_type, :label, :included_tax_total,:additional_tax_total, :amount, :currency
end

View File

@@ -0,0 +1,3 @@
class Invoice::CustomerSerializer < ActiveModel::Serializer
attributes :code, :email
end

View File

@@ -0,0 +1,9 @@
class Invoice::EnterpriseSerializer < ActiveModel::Serializer
attributes :name, :abn, :acn, :invoice_text, :email_address, :display_invoice_logo, :logo_url
has_one :contact, serializer: Invoice::UserSerializer
has_one :business_address, serializer: Invoice::AddressSerializer
has_one :address, serializer: Invoice::AddressSerializer
def logo_url
object.logo_url(:small)
end
end

View File

@@ -0,0 +1,8 @@
class Invoice::LineItemSerializer < ActiveModel::Serializer
attributes :quantity,
:price_with_adjustments,
:added_tax,
:included_tax
has_one :variant, serializer: Invoice::VariantSerializer
end

View File

@@ -0,0 +1,3 @@
class Invoice::OrderCycleSerializer < ActiveModel::Serializer
attributes :name
end

View File

@@ -0,0 +1,22 @@
class Invoice::OrderSerializer < ActiveModel::Serializer
attributes :number, :special_instructions, :note, :payment_state, :total, :payment_total, :state,
:currency, :additional_tax_total, :included_tax_total, :completed_at, :has_taxes_included
has_one :order_cycle, serializer: Invoice::OrderCycleSerializer
has_one :customer, serializer: Invoice::CustomerSerializer
has_one :distributor, serializer: Invoice::EnterpriseSerializer
has_one :bill_address, serializer: Invoice::AddressSerializer
has_one :shipping_method, serializer: Invoice::ShippingMethodSerializer
has_one :ship_address, serializer: Invoice::AddressSerializer
has_many :sorted_line_items, serializer: Invoice::LineItemSerializer
has_many :sorted_line_items, serializer: Invoice::LineItemSerializer
has_many :payments, serializer: Invoice::PaymentSerializer
has_many :all_eligible_adjustments, serializer: Invoice::AdjustmentSerializer
def all_eligible_adjustments
object.all_adjustments.eligible.where.not(originator_type: 'Spree::TaxRate')
end
def completed_at
object.completed_at.to_s
end
end

View File

@@ -0,0 +1,3 @@
class Invoice::PaymentMethodSerializer < ActiveModel::Serializer
attributes :name, :description
end

View File

@@ -0,0 +1,8 @@
class Invoice::PaymentSerializer < ActiveModel::Serializer
attributes :state, :created_at, :amount, :currency
has_one :payment_method, serializer: Invoice::PaymentMethodSerializer
def created_at
object.created_at.to_s
end
end

View File

@@ -0,0 +1,4 @@
class Invoice::ProductSerializer < ActiveModel::Serializer
attributes :name
has_one :supplier, serializer: Invoice::EnterpriseSerializer
end

View File

@@ -0,0 +1,3 @@
class Invoice::ShippingMethodSerializer < ActiveModel::Serializer
attributes :name, :require_ship_address
end

View File

@@ -0,0 +1,3 @@
class Invoice::StateSerializer < ActiveModel::Serializer
attributes :name
end

View File

@@ -0,0 +1,3 @@
class Invoice::UserSerializer < ActiveModel::Serializer
attributes :email
end

View File

@@ -0,0 +1,4 @@
class Invoice::VariantSerializer < ActiveModel::Serializer
attributes :display_name, :options_text
has_one :product, serializer: Invoice::ProductSerializer
end

View File

@@ -0,0 +1,38 @@
class OrderInvoiceComparator
def equal?(invoice1, invoice2)
# We'll use a recursive BFS algorithm to find if current state of invoice is outdated
# the root will be the order
# On each node, we'll a list of relevant attributes that will be used on the comparison
bfs(invoice1.presenter, invoice2.presenter)
end
def bfs(node1, node2)
simple_values1, presenters1 = group_relevant_values(node1)
simple_values2, presenters2 = group_relevant_values(node2)
return false if simple_values1 != simple_values2
return false if presenters1.size != presenters2.size
presenters1.zip(presenters2).each do |presenter1, presenter2|
return false unless bfs(presenter1, presenter2)
end
true
end
def group_relevant_values(node)
return [[], []] unless node.respond_to?(:relevant_values)
grouped = node.relevant_values.group_by(&grouper)
[grouped[:simple] || [], grouped[:presenters]&.flatten || []]
end
def grouper
proc do |value|
if value.is_a?(Array) || value.class.to_s.starts_with?("Invoice::DataPresenter")
:presenters
else
:simple
end
end
end
end

View File

@@ -0,0 +1,24 @@
%table.index{"data-hook" => "invoices"}
%thead{"data-hook" => "invoice_head"}
%tr
%th= "#{t('spree.date')}/#{t('spree.time')}"
%th= t(:invoice_number)
%th= t(:amount)
%th= t(:status)
%th= t(:file)
%tbody
- @order.invoices.each do |invoice|
- tr_class = cycle('odd', 'even')
- tr_id = spree_dom_id(invoice)
%tr{:class => tr_class, "data-hook" => "invoice_row", :id => tr_id}
%td.align-center.created_at
= pretty_time(invoice.date)
%td.align-center.label
= invoice.number
%td.align-center.label
= invoice.data['order']['total']
%td.align-center.label
= t(invoice.status)
%td.align-center.label
=link_to(t(:download),invoice.status)

View File

@@ -0,0 +1,14 @@
= render partial: 'spree/admin/shared/order_page_title'
= render partial: 'spree/admin/shared/order_tabs', locals: { current: 'Invoices' }
- content_for :page_title do
%i.icon-arrow-right
= t(:invoices)
- content_for :page_actions do
- if @order.can_generate_new_invoice?
%li= button_link_to t(:new_invoice), new_admin_order_adjustment_url(@order), :icon => 'icon-plus', disabled: true
= render partial: 'spree/admin/shared/order_links'
%li= button_link_to t(:back_to_orders_list), admin_orders_path, :icon => 'icon-arrow-left'
= render :partial => 'invoices_table'

View File

@@ -0,0 +1,6 @@
%h5.inline-header
= "#{raw(line_item.variant.product.name)}"
- unless line_item.variant.product.name.include? line_item.name_to_display
%span= "- #{raw(line_item.name_to_display)}"
- if line_item.options_text
= "(#{raw(line_item.options_text)})"

View File

@@ -0,0 +1,4 @@
%p.callout{style: "margin-top: 30px"}
%strong= t :additional_information
%p{style: "margin: 5px"}
= @invoice_presenter.order_note

View File

@@ -0,0 +1,20 @@
%p.callout
%span{:style => "float:right;"}
- if @invoice_presenter.outstanding_balance?
= @invoice_presenter.outstanding_balance_label
\:
%strong= @invoice_presenter.display_outstanding_balance
- else
- if @invoice_presenter.paid?
= t :email_payment_paid
- else
= t :email_payment_not_paid
%strong
= t :email_payment_summary
- if @invoice_presenter.payments.any?
= render partial: 'spree/admin/orders/_invoice/payments_list', locals: { payments: @invoice_presenter.payments }
- if @invoice_presenter.last_payment_method
%p.callout{style: "margin-top: 40px"}
%strong
= t :email_payment_description
%p{style: "margin: 5px"}= @invoice_presenter.last_payment_method.description

View File

@@ -0,0 +1,14 @@
%table.payments-list
%thead
%tr
%th= t('.date_time')
%th= t('.payment_method')
%th.payment-state= t('.payment_state')
%th.amount= t('.amount')
%tbody
- payments.each do |payment|
%tr
%td= l(payment.created_at, format: "%b %d, %Y %H:%M")
%td.payment-method-name= payment.payment_method_name
%td.payment-state.payment-state-value= t(payment.state, scope: [:spree, :payment_states], default: payment.state.capitalize)
%td.amount= payment.display_amount.to_html

View File

@@ -6,33 +6,33 @@
%th{:align => "right", :width => "15%"}
%h4= t(:invoice_column_qty)
%th{:align => "right", :width => "15%"}
%h4= @order.total_tax > 0 ? t(:invoice_column_tax) : ""
%h4= @invoice_presenter.total_tax > 0 ? t(:invoice_column_tax) : ""
%th{:align => "right", :width => "15%"}
%h4= t(:invoice_column_price)
%tbody
- @order.sorted_line_items.each do |item|
- @invoice_presenter.sorted_line_items.each do |item|
%tr
%td
= render 'spree/shared/line_item_name', line_item: item
= render 'spree/admin/orders/_invoice/line_item_name', line_item: item
%br
%small
%em= raw(item.variant.product.supplier.name)
%td{:align => "right"}
= item.quantity
%td{:align => "right"}
= display_line_items_taxes(item)
= item.display_line_items_taxes
%td{:align => "right"}
= item.display_amount_with_adjustments
- checkout_adjustments_for(@order, exclude: [:line_item]).reverse_each do |adjustment|
- taxable = adjustment.adjustable_type == "Spree::Shipment" ? adjustment.adjustable : adjustment
- @invoice_presenter.checkout_adjustments(exclude: [:line_item]).reverse_each do |adjustment|
- taxable = adjustment#.adjustable_type == "Spree::Shipment" ? adjustment.adjustable : adjustment
%tr
%td
%strong= "#{raw(adjustment.label)}"
%td{:align => "right"}
1
%td{:align => "right"}
= display_taxes(taxable, display_zero: false)
= adjustment.display_taxes
%td{:align => "right"}
= adjustment.display_amount
%tfoot
@@ -40,16 +40,16 @@
%td{:align => "right", :colspan => "2"}
%strong= t(:invoice_tax_total)
%td{:align => "right", :colspan => "2"}
%strong= display_checkout_tax_total(@order)
%strong= @invoice_presenter.display_checkout_tax_total
%tr
%td{:align => "right", :colspan => "2"}
%strong= t(:total_excl_tax)
%td{:align => "right", :colspan => "2"}
%strong= display_checkout_total_less_tax(@order)
%strong= @invoice_presenter.display_checkout_total_less_tax
%tr
%td{:align => "right", :colspan => "2"}
%strong= t(:total_incl_tax)
%td{:align => "right", :colspan => "2"}
%strong= @order.display_total
%strong= @invoice_presenter.display_total
%p
&nbsp;

View File

@@ -6,10 +6,10 @@
%th{:align => "right", :width => "15%"}
%h5= t(:invoice_column_qty)
%th{:align => "right", :width => "15%"}
%h5= @order.has_taxes_included ? t(:invoice_column_unit_price_with_taxes) : t(:invoice_column_unit_price_without_taxes)
%h5= @invoice_presenter.has_taxes_included ? t(:invoice_column_unit_price_with_taxes) : t(:invoice_column_unit_price_without_taxes)
%th{:align => "right", :width => "15%"}
%h5= @order.has_taxes_included ? t(:invoice_column_price_with_taxes) : t(:invoice_column_price_without_taxes)
- if @order.total_tax > 0
%h5= @invoice_presenter.has_taxes_included ? t(:invoice_column_price_with_taxes) : t(:invoice_column_price_without_taxes)
- if @invoice_presenter.total_tax > 0
%th{:align => "right", :width => "15%"}
%h5= t(:invoice_column_tax_rate)
%tbody
@@ -44,9 +44,9 @@
%tfoot
%tr
%td{:align => "right", :colspan => "3"}
%strong= @order.has_taxes_included ? t(:total_incl_tax) : t(:total_excl_tax)
%strong= @invoice_presenter.has_taxes_included ? t(:total_incl_tax) : t(:total_excl_tax)
%td{:align => "right", :colspan => "2"}
%strong= @order.has_taxes_included ? @order.display_total : display_checkout_total_less_tax(@order)
%strong= @invoice_presenter.has_taxes_included ? @invoice_presenter.display_total : @invoice_presenter.display_checkout_total_less_tax
- display_checkout_taxes_hash(@order).each do |tax|
%tr
%td{:align => "right", :colspan => "3"}
@@ -55,8 +55,8 @@
= tax[:amount]
%tr
%td{:align => "right", :colspan => "3"}
= @order.has_taxes_included ? t(:total_excl_tax) : t(:total_incl_tax)
= @invoice_presenter.has_taxes_included ? t(:total_excl_tax) : t(:total_incl_tax)
%td{:align => "right", :colspan => "2"}
= @order.has_taxes_included ? display_checkout_total_less_tax(@order) : @order.display_total
= @invoice_presenter.has_taxes_included ? @invoice_presenter.display_checkout_total_less_tax : @invoice_presenter.display_total
%p
&nbsp;

View File

@@ -6,32 +6,32 @@
%td{ :align => "left", colspan: 3 }
%h6
= "#{t('.issued_on')}: "
= l Time.zone.now.to_date
= l @invoice_presenter.invoice_date
%tr{ valign: "top" }
%td{ :align => "left" }
%h4
= "#{t('.tax_invoice')}: "
= "#{@order.number}"
= "#{@invoice_presenter.order_number}"
%td{width: "10%" }
&nbsp;
%td{ :align => "right" }
%h4= @order.order_cycle&.name
%h4= @invoice_presenter.order_cycle.name
%tr{ valign: "top" }
%td{ align: "left", colspan: 3 }
- if @order.distributor.business_address.blank?
%strong= "#{t('.from')}: #{@order.distributor.name}"
- if @invoice_presenter.distributor.business_address.blank?
%strong= "#{t('.from')}: #{@invoice_presenter.distributor.name}"
- else
%strong= "#{t('.from')}: #{@order.distributor.business_address.company}"
- if @order.distributor.abn.present?
%strong= "#{t('.from')}: #{@invoice_presenter.distributor.business_address.company}"
- if @invoice_presenter.distributor.abn.present?
%br
= "#{t(:abn)} #{@order.distributor.abn}"
= "#{t(:abn)} #{@invoice_presenter.distributor.abn}"
%br
- if @order.distributor.business_address.blank?
= @order.distributor.address.full_address
- if @invoice_presenter.distributor.business_address.blank?
= @invoice_presenter.distributor.address.full_address
- else
= @order.distributor.business_address.full_address
= @invoice_presenter.distributor.business_address.full_address
%br
= @order.distributor.contact.email
= @invoice_presenter.distributor.contact.email
%tr{ valign: "top" }
%td{ colspan: 3 }
&nbsp;
@@ -39,44 +39,44 @@
%td{ align: "left" }
%strong= "#{t('.to')}:"
%br
- if @order.bill_address
= @order.bill_address.full_name
- if @order&.customer&.code.present?
- if @invoice_presenter.bill_address
= @invoice_presenter.bill_address.full_name
- if @invoice_presenter.customer.code.present?
%br
= "#{t('.code')}: #{@order.customer.code}"
= "#{t('.code')}: #{@invoice_presenter.customer.code}"
%br
- if @order.bill_address
= @order.bill_address.full_address
- if @invoice_presenter.bill_address
= @invoice_presenter.bill_address.full_address
%br
- if @order&.customer&.email.present?
= "#{@order.customer.email},"
- if @order.bill_address
= "#{@order.bill_address.phone}"
- if @invoice_presenter.customer.email.present?
= "#{@invoice_presenter.customer.email},"
- if @invoice_presenter.bill_address
= "#{@invoice_presenter.bill_address.phone}"
%td
&nbsp;
%td{ align: "left", style: "border-left: .1em solid black; padding-left: 1em" }
%strong= "#{t('.shipping')}: #{@order.shipping_method&.name}"
- if @order.shipping_method&.require_ship_address
%strong= "#{t('.shipping')}: #{@invoice_presenter.shipping_method.name}"
- if @invoice_presenter.shipping_method.require_ship_address
%br
= @order.ship_address.full_name
= @invoice_presenter.ship_address.full_name
%br
= @order.ship_address.full_address
= @invoice_presenter.ship_address.full_address
%br
= @order.ship_address.phone
- if @order.special_instructions.present?
= @invoice_presenter.ship_address.phone
- if @invoice_presenter.order_special_instructions.present?
%br
%br
%strong= t :customer_instructions
= @order.special_instructions
= @invoice_presenter.order_special_instructions
= render 'spree/admin/orders/invoice_table'
- if @order.distributor.invoice_text.present?
- if @invoice_presenter.distributor.invoice_text.present?
%p
= @order.distributor.invoice_text
= @invoice_presenter.distributor.invoice_text
= render 'spree/shared/payment'
= render 'spree/admin/orders/_invoice/payment'
- if @order.note.present?
= render partial: 'spree/shared/order_note'
- if @invoice_presenter.order_note.present?
= render partial: 'spree/admin/orders/_invoice/order_note'

View File

@@ -6,89 +6,89 @@
%td{ :align => "left" }
%h4
= t :tax_invoice
- if @order.distributor.display_invoice_logo? && @order.distributor.logo.variable?
- if @invoice_presenter.distributor.display_invoice_logo? && @invoice_presenter.distributor.logo_url
%td{ :align => "right", rowspan: 2 }
= wicked_pdf_image_tag @order.distributor.logo_url(:small)
= wicked_pdf_image_tag @invoice_presenter.distributor.logo_url
%tr{ valign: "top" }
%td{ :align => "left" }
- if @order.distributor.business_address.blank?
%strong= @order.distributor.name
- if @invoice_presenter.distributor.business_address.blank?
%strong= @invoice_presenter.distributor.name
%br
= @order.distributor.address.address_part1
= @invoice_presenter.distributor.address.address_part1
%br
= @order.distributor.address.address_part2
= @invoice_presenter.distributor.address.address_part2
%br
= @order.distributor.email_address
- if @order.distributor.phone.present?
= @invoice_presenter.distributor.email_address
- if @invoice_presenter.distributor.phone.present?
%br
= @order.distributor.phone
= @invoice_presenter.distributor.phone
- else
%strong= @order.distributor.business_address.company
%strong= @invoice_presenter.distributor.business_address.company
%br
= @order.distributor.business_address.address_part1
= @invoice_presenter.distributor.business_address.address_part1
%br
= @order.distributor.business_address.address_part2
= @invoice_presenter.distributor.business_address.address_part2
%br
= @order.distributor.email_address
- if @order.distributor.business_address.phone.present?
= @invoice_presenter.distributor.email_address
- if @invoice_presenter.distributor.business_address.phone.present?
%br
= @order.distributor.business_address.phone
- if @order.distributor.abn.present?
= @invoice_presenter.distributor.business_address.phone
- if @invoice_presenter.distributor.abn.present?
%br
= "#{t :abn} #{@order.distributor.abn}"
- if @order.distributor.acn.present?
= "#{t :abn} #{@invoice_presenter.distributor.abn}"
- if @invoice_presenter.distributor.acn.present?
%br
= "#{t :acn} #{@order.distributor.acn}"
= "#{t :acn} #{@invoice_presenter.distributor.acn}"
%tr{ valign: "top" }
%td{ :align => "left", colspan: 2 } &nbsp;
%tr{ valign: "top" }
%td{ :align => "left" }
%br
= t :invoice_issued_on
= l Time.zone.now.to_date
= l @invoice_presenter.invoice_date
%br
= t :date_of_transaction
= l @order.completed_at.to_date
= l @invoice_presenter.order_completed_at.to_date
%br
= t :order_number
= @order.number
= @invoice_presenter.order_number
%td{ :align => "right" }
= t :invoice_billing_address
%br
- if @order.bill_address
%strong= @order.bill_address.full_name
- if @order&.customer&.code.present?
- if @invoice_presenter.bill_address
%strong= @invoice_presenter.bill_address.full_name
- if @invoice_presenter&.customer&.code.present?
%br
= "Code: #{@order.customer.code}"
= "Code: #{@invoice_presenter.customer.code}"
%br
- if @order.bill_address
= @order.bill_address.address_part1
- if @invoice_presenter.bill_address
= @invoice_presenter.bill_address.address_part1
%br
- if @order.bill_address
= @order.bill_address.address_part2
- if @order.bill_address.phone.present?
- if @invoice_presenter.bill_address
= @invoice_presenter.bill_address.address_part2
- if @invoice_presenter.bill_address.phone.present?
%br
= @order.bill_address.phone
- if @order&.customer&.email.present?
= @invoice_presenter.bill_address.phone
- if @invoice_presenter&.customer&.email.present?
%br
= @order.customer.email
= @invoice_presenter.customer.email
= render 'spree/admin/orders/invoice_table2'
- if @order.special_instructions.present?
- if @invoice_presenter.order_special_instructions.present?
%p.callout
%strong
= t :customer_instructions
%p
%em= @order.special_instructions
%em= @invoice_presenter.order_special_instructions
%p
&nbsp;
- if @order.distributor.invoice_text.present?
- if @invoice_presenter.distributor.invoice_text.present?
%p
= @order.distributor.invoice_text
= @invoice_presenter.distributor.invoice_text
= render 'spree/shared/payment'
= render 'spree/admin/orders/_invoice/payment'
- if @order.note.present?
= render partial: 'spree/shared/order_note'
- if @invoice_presenter.order_note.present?
= render partial: 'spree/admin/orders/_invoice/order_note'

View File

@@ -61,6 +61,10 @@
%li{ class: adjustments_classes }
= link_to_with_icon 'icon-cogs', t(:adjustments), spree.admin_order_adjustments_url(@order)
- invoices_classes = "active" if current == 'Invoices'
%li{ class: invoices_classes }
= link_to_with_icon 'icon-cogs', t(:invoices), spree.admin_order_invoices_url(@order)
- if @order.completed?
- authorizations_classes = "active" if current == "Return Authorizations"
%li{ class: authorizations_classes }

View File

@@ -3117,8 +3117,10 @@ See the %{link} to find out more about %{sitename}'s features and to start using
no_orders_found: "No Orders Found"
order_information: "Order Information"
new_payment: "New Payment"
new_invoice: "New Invoice"
date_completed: "Date Completed"
amount: "Amount"
invoice_number: "Invoice Number"
state_names:
ready: Ready
pending: Pending

View File

@@ -100,6 +100,7 @@ Spree::Core::Engine.routes.draw do
end
resources :adjustments
resources :invoices
resources :payments do
member do

View File

@@ -0,0 +1,15 @@
# frozen_string_literal: true
class CreateInvoices < ActiveRecord::Migration[6.1]
def change
create_table :invoices do |t|
t.references :order, foreign_key: true, foreign_key: { to_table: :spree_orders }
t.string :status
t.integer :number
t.jsonb :data
t.date :date, default: -> { "CURRENT_TIMESTAMP" }, nil: false
t.timestamps
end
end
end

View File

@@ -302,6 +302,17 @@ ActiveRecord::Schema[7.0].define(version: 2023_05_22_120633) do
t.index ["enterprise_id", "variant_id"], name: "index_inventory_items_on_enterprise_id_and_variant_id", unique: true
end
create_table "invoices", force: :cascade do |t|
t.bigint "order_id"
t.string "status"
t.integer "number"
t.jsonb "data"
t.date "date", default: -> { "CURRENT_TIMESTAMP" }
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["order_id"], name: "index_invoices_on_order_id"
end
create_table "order_cycle_schedules", id: :serial, force: :cascade do |t|
t.integer "order_cycle_id", null: false
t.integer "schedule_id", null: false
@@ -1259,6 +1270,7 @@ ActiveRecord::Schema[7.0].define(version: 2023_05_22_120633) do
add_foreign_key "exchanges", "enterprises", column: "receiver_id", name: "exchanges_receiver_id_fk"
add_foreign_key "exchanges", "enterprises", column: "sender_id", name: "exchanges_sender_id_fk"
add_foreign_key "exchanges", "order_cycles", name: "exchanges_order_cycle_id_fk"
add_foreign_key "invoices", "spree_orders", column: "order_id"
add_foreign_key "order_cycle_schedules", "order_cycles", name: "oc_schedules_order_cycle_id_fk"
add_foreign_key "order_cycle_schedules", "schedules", name: "oc_schedules_schedule_id_fk"
add_foreign_key "order_cycles", "enterprises", column: "coordinator_id", name: "order_cycles_coordinator_id_fk"

View File

@@ -0,0 +1,6 @@
# frozen_string_literal: true
FactoryBot.define do
factory :invoice, class: Invoice do
end
end

View File

@@ -0,0 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Invoice, type: :model do
pending "add some examples to (or delete) #{__FILE__}"
end

View File

@@ -0,0 +1,54 @@
# frozen_string_literal: true
require 'spec_helper'
describe OrderInvoiceComparator do
describe '#equal?' do
let!(:order) { create(:completed_order_with_fees) }
let(:current_state_invoice){ order.current_state_invoice }
let!(:invoice){ create(:invoice, order: order) }
context "changes on the order object" do
it "returns true if the order didn't change" do
expect(OrderInvoiceComparator.new.equal?(current_state_invoice, invoice)).to be true
end
it "returns true if a relevant attribute changes" do
order.update!(note: 'THIS IS AN UPDATE')
expect(OrderInvoiceComparator.new.equal?(current_state_invoice, invoice)).to be false
end
it "returns true if a non-relevant attribute changes" do
order.update!(last_ip_address: "192.168.172.165")
expect(OrderInvoiceComparator.new.equal?(current_state_invoice, invoice)).to be true
end
end
context "change on associate objects (belong_to)" do
let(:distributor){ order.distributor }
it "returns false if the distributor change relavant attribute" do
distributor.update!(name: 'THIS IS A NEW NAME')
expect(OrderInvoiceComparator.new.equal?(current_state_invoice, invoice)).to be false
end
it "returns true if the distributor change non-relavant attribute" do
distributor.update!(description: 'THIS IS A NEW DESCRIPTION')
expect(OrderInvoiceComparator.new.equal?(current_state_invoice, invoice)).to be true
end
end
context "changes on associate objects (has_many)" do
let(:line_item){ order.line_items.first }
it "return true if relavant attribute change" do
line_item.update!(quantity: line_item.quantity + 1)
expect(OrderInvoiceComparator.new.equal?(current_state_invoice, invoice)).to be false
end
end
end
end