From 0fbf88190ee0692ec89b8cfb16d79ec913d23a74 Mon Sep 17 00:00:00 2001 From: Mohamed ABDELLANI Date: Wed, 8 Mar 2023 09:08:36 +0100 Subject: [PATCH] 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 --- .../spree/admin/base_controller.rb | 2 +- .../spree/admin/invoices_controller.rb | 4 + .../spree/admin/orders_controller.rb | 5 + app/models/invoice.rb | 15 +++ app/models/invoice/data_presenter.rb | 102 ++++++++++++++++++ app/models/invoice/data_presenter/address.rb | 25 +++++ .../invoice/data_presenter/adjustment.rb | 19 ++++ app/models/invoice/data_presenter/base.rb | 7 ++ .../invoice/data_presenter/bill_address.rb | 2 + .../data_presenter/business_address.rb | 2 + app/models/invoice/data_presenter/contact.rb | 3 + app/models/invoice/data_presenter/customer.rb | 3 + .../invoice/data_presenter/distributor.rb | 9 ++ .../invoice/data_presenter/line_item.rb | 20 ++++ .../invoice/data_presenter/order_cycle.rb | 3 + app/models/invoice/data_presenter/payment.rb | 17 +++ .../invoice/data_presenter/payment_method.rb | 3 + app/models/invoice/data_presenter/product.rb | 4 + .../invoice/data_presenter/ship_address.rb | 2 + .../invoice/data_presenter/shipping_method.rb | 3 + app/models/invoice/data_presenter/state.rb | 3 + app/models/invoice/data_presenter/supplier.rb | 3 + app/models/invoice/data_presenter/variant.rb | 10 ++ .../invoice/data_presenter_attributes.rb | 43 ++++++++ app/models/spree/order.rb | 24 +++++ app/serializers/invoice/address_serializer.rb | 4 + .../invoice/adjustment_serializer.rb | 3 + .../invoice/customer_serializer.rb | 3 + .../invoice/enterprise_serializer.rb | 9 ++ .../invoice/line_item_serializer.rb | 8 ++ .../invoice/order_cycle_serializer.rb | 3 + app/serializers/invoice/order_serializer.rb | 22 ++++ .../invoice/payment_method_serializer.rb | 3 + app/serializers/invoice/payment_serializer.rb | 8 ++ app/serializers/invoice/product_serializer.rb | 4 + .../invoice/shipping_method_serializer.rb | 3 + app/serializers/invoice/state_serializer.rb | 3 + app/serializers/invoice/user_serializer.rb | 3 + app/serializers/invoice/variant_serializer.rb | 4 + app/services/order_invoice_comparator.rb | 38 +++++++ .../admin/invoices/_invoices_table.html.haml | 24 +++++ .../spree/admin/invoices/index.html.haml | 14 +++ .../orders/_invoice/_line_item_name.html.haml | 6 ++ .../orders/_invoice/_order_note.html.haml | 4 + .../admin/orders/_invoice/_payment.html.haml | 20 ++++ .../orders/_invoice/_payments_list.html.haml | 14 +++ .../admin/orders/_invoice_table.html.haml | 20 ++-- .../admin/orders/_invoice_table2.html.haml | 14 +-- .../spree/admin/orders/invoice.html.haml | 68 ++++++------ .../spree/admin/orders/invoice2.html.haml | 82 +++++++------- .../spree/admin/shared/_order_tabs.html.haml | 4 + config/locales/en.yml | 2 + config/routes/spree.rb | 1 + db/migrate/20230308075421_create_invoices.rb | 15 +++ db/schema.rb | 12 +++ spec/factories/invoice_factory.rb | 6 ++ spec/models/invoice_spec.rb | 7 ++ .../services/order_invoice_comparator_spec.rb | 54 ++++++++++ 58 files changed, 725 insertions(+), 93 deletions(-) create mode 100644 app/models/invoice.rb create mode 100644 app/models/invoice/data_presenter.rb create mode 100644 app/models/invoice/data_presenter/address.rb create mode 100644 app/models/invoice/data_presenter/adjustment.rb create mode 100644 app/models/invoice/data_presenter/base.rb create mode 100644 app/models/invoice/data_presenter/bill_address.rb create mode 100644 app/models/invoice/data_presenter/business_address.rb create mode 100644 app/models/invoice/data_presenter/contact.rb create mode 100644 app/models/invoice/data_presenter/customer.rb create mode 100644 app/models/invoice/data_presenter/distributor.rb create mode 100644 app/models/invoice/data_presenter/line_item.rb create mode 100644 app/models/invoice/data_presenter/order_cycle.rb create mode 100644 app/models/invoice/data_presenter/payment.rb create mode 100644 app/models/invoice/data_presenter/payment_method.rb create mode 100644 app/models/invoice/data_presenter/product.rb create mode 100644 app/models/invoice/data_presenter/ship_address.rb create mode 100644 app/models/invoice/data_presenter/shipping_method.rb create mode 100644 app/models/invoice/data_presenter/state.rb create mode 100644 app/models/invoice/data_presenter/supplier.rb create mode 100644 app/models/invoice/data_presenter/variant.rb create mode 100644 app/models/invoice/data_presenter_attributes.rb create mode 100644 app/serializers/invoice/address_serializer.rb create mode 100644 app/serializers/invoice/adjustment_serializer.rb create mode 100644 app/serializers/invoice/customer_serializer.rb create mode 100644 app/serializers/invoice/enterprise_serializer.rb create mode 100644 app/serializers/invoice/line_item_serializer.rb create mode 100644 app/serializers/invoice/order_cycle_serializer.rb create mode 100644 app/serializers/invoice/order_serializer.rb create mode 100644 app/serializers/invoice/payment_method_serializer.rb create mode 100644 app/serializers/invoice/payment_serializer.rb create mode 100644 app/serializers/invoice/product_serializer.rb create mode 100644 app/serializers/invoice/shipping_method_serializer.rb create mode 100644 app/serializers/invoice/state_serializer.rb create mode 100644 app/serializers/invoice/user_serializer.rb create mode 100644 app/serializers/invoice/variant_serializer.rb create mode 100644 app/services/order_invoice_comparator.rb create mode 100644 app/views/spree/admin/invoices/_invoices_table.html.haml create mode 100644 app/views/spree/admin/invoices/index.html.haml create mode 100644 app/views/spree/admin/orders/_invoice/_line_item_name.html.haml create mode 100644 app/views/spree/admin/orders/_invoice/_order_note.html.haml create mode 100644 app/views/spree/admin/orders/_invoice/_payment.html.haml create mode 100644 app/views/spree/admin/orders/_invoice/_payments_list.html.haml create mode 100644 db/migrate/20230308075421_create_invoices.rb create mode 100644 spec/factories/invoice_factory.rb create mode 100644 spec/models/invoice_spec.rb create mode 100644 spec/services/order_invoice_comparator_spec.rb diff --git a/app/controllers/spree/admin/base_controller.rb b/app/controllers/spree/admin/base_controller.rb index a51ac28730..e542b797be 100644 --- a/app/controllers/spree/admin/base_controller.rb +++ b/app/controllers/spree/admin/base_controller.rb @@ -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 diff --git a/app/controllers/spree/admin/invoices_controller.rb b/app/controllers/spree/admin/invoices_controller.rb index 27d0009e53..1f949e1033 100644 --- a/app/controllers/spree/admin/invoices_controller.rb +++ b/app/controllers/spree/admin/invoices_controller.rb @@ -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]) diff --git a/app/controllers/spree/admin/orders_controller.rb b/app/controllers/spree/admin/orders_controller.rb index 0122319adf..759f18633f 100644 --- a/app/controllers/spree/admin/orders_controller.rb +++ b/app/controllers/spree/admin/orders_controller.rb @@ -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 diff --git a/app/models/invoice.rb b/app/models/invoice.rb new file mode 100644 index 0000000000..8f90a05eae --- /dev/null +++ b/app/models/invoice.rb @@ -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 diff --git a/app/models/invoice/data_presenter.rb b/app/models/invoice/data_presenter.rb new file mode 100644 index 0000000000..8772def6b6 --- /dev/null +++ b/app/models/invoice/data_presenter.rb @@ -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 diff --git a/app/models/invoice/data_presenter/address.rb b/app/models/invoice/data_presenter/address.rb new file mode 100644 index 0000000000..4aca60a7bb --- /dev/null +++ b/app/models/invoice/data_presenter/address.rb @@ -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 diff --git a/app/models/invoice/data_presenter/adjustment.rb b/app/models/invoice/data_presenter/adjustment.rb new file mode 100644 index 0000000000..9b514235a9 --- /dev/null +++ b/app/models/invoice/data_presenter/adjustment.rb @@ -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 diff --git a/app/models/invoice/data_presenter/base.rb b/app/models/invoice/data_presenter/base.rb new file mode 100644 index 0000000000..9aa2d186e6 --- /dev/null +++ b/app/models/invoice/data_presenter/base.rb @@ -0,0 +1,7 @@ +class Invoice::DataPresenter::Base + attr :data + def initialize(data) + @data = data + end + extend Invoice::DataPresenterAttributes +end diff --git a/app/models/invoice/data_presenter/bill_address.rb b/app/models/invoice/data_presenter/bill_address.rb new file mode 100644 index 0000000000..7eb242da79 --- /dev/null +++ b/app/models/invoice/data_presenter/bill_address.rb @@ -0,0 +1,2 @@ +class Invoice::DataPresenter::BillAddress < Invoice::DataPresenter::Address +end \ No newline at end of file diff --git a/app/models/invoice/data_presenter/business_address.rb b/app/models/invoice/data_presenter/business_address.rb new file mode 100644 index 0000000000..7561bfe00c --- /dev/null +++ b/app/models/invoice/data_presenter/business_address.rb @@ -0,0 +1,2 @@ +class Invoice::DataPresenter::BusinessAddress < Invoice::DataPresenter::Address +end \ No newline at end of file diff --git a/app/models/invoice/data_presenter/contact.rb b/app/models/invoice/data_presenter/contact.rb new file mode 100644 index 0000000000..c239367412 --- /dev/null +++ b/app/models/invoice/data_presenter/contact.rb @@ -0,0 +1,3 @@ +class Invoice::DataPresenter::Contact < Invoice::DataPresenter::Base + attributes :name, :email +end diff --git a/app/models/invoice/data_presenter/customer.rb b/app/models/invoice/data_presenter/customer.rb new file mode 100644 index 0000000000..fbfb50502a --- /dev/null +++ b/app/models/invoice/data_presenter/customer.rb @@ -0,0 +1,3 @@ +class Invoice::DataPresenter::Customer < Invoice::DataPresenter::Base + attributes :code, :email +end diff --git a/app/models/invoice/data_presenter/distributor.rb b/app/models/invoice/data_presenter/distributor.rb new file mode 100644 index 0000000000..2945305d09 --- /dev/null +++ b/app/models/invoice/data_presenter/distributor.rb @@ -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 diff --git a/app/models/invoice/data_presenter/line_item.rb b/app/models/invoice/data_presenter/line_item.rb new file mode 100644 index 0000000000..1f4502560e --- /dev/null +++ b/app/models/invoice/data_presenter/line_item.rb @@ -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 diff --git a/app/models/invoice/data_presenter/order_cycle.rb b/app/models/invoice/data_presenter/order_cycle.rb new file mode 100644 index 0000000000..ac60525547 --- /dev/null +++ b/app/models/invoice/data_presenter/order_cycle.rb @@ -0,0 +1,3 @@ +class Invoice::DataPresenter::OrderCycle < Invoice::DataPresenter::Base + attributes :name +end diff --git a/app/models/invoice/data_presenter/payment.rb b/app/models/invoice/data_presenter/payment.rb new file mode 100644 index 0000000000..95b0490a10 --- /dev/null +++ b/app/models/invoice/data_presenter/payment.rb @@ -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 diff --git a/app/models/invoice/data_presenter/payment_method.rb b/app/models/invoice/data_presenter/payment_method.rb new file mode 100644 index 0000000000..0f0027c305 --- /dev/null +++ b/app/models/invoice/data_presenter/payment_method.rb @@ -0,0 +1,3 @@ +class Invoice::DataPresenter::PaymentMethod < Invoice::DataPresenter::Base + attributes :name, :description +end diff --git a/app/models/invoice/data_presenter/product.rb b/app/models/invoice/data_presenter/product.rb new file mode 100644 index 0000000000..008809c885 --- /dev/null +++ b/app/models/invoice/data_presenter/product.rb @@ -0,0 +1,4 @@ +class Invoice::DataPresenter::Product < Invoice::DataPresenter::Base + attributes :name + attributes_with_presenter :supplier +end \ No newline at end of file diff --git a/app/models/invoice/data_presenter/ship_address.rb b/app/models/invoice/data_presenter/ship_address.rb new file mode 100644 index 0000000000..a37fe70222 --- /dev/null +++ b/app/models/invoice/data_presenter/ship_address.rb @@ -0,0 +1,2 @@ +class Invoice::DataPresenter::ShipAddress < Invoice::DataPresenter::Address +end \ No newline at end of file diff --git a/app/models/invoice/data_presenter/shipping_method.rb b/app/models/invoice/data_presenter/shipping_method.rb new file mode 100644 index 0000000000..8a61f9dc52 --- /dev/null +++ b/app/models/invoice/data_presenter/shipping_method.rb @@ -0,0 +1,3 @@ +class Invoice::DataPresenter::ShippingMethod < Invoice::DataPresenter::Base + attributes :name, :require_ship_address +end diff --git a/app/models/invoice/data_presenter/state.rb b/app/models/invoice/data_presenter/state.rb new file mode 100644 index 0000000000..cdd7edaaa6 --- /dev/null +++ b/app/models/invoice/data_presenter/state.rb @@ -0,0 +1,3 @@ +class Invoice::DataPresenter::State < Invoice::DataPresenter::Base + attributes :name +end diff --git a/app/models/invoice/data_presenter/supplier.rb b/app/models/invoice/data_presenter/supplier.rb new file mode 100644 index 0000000000..15d38d5816 --- /dev/null +++ b/app/models/invoice/data_presenter/supplier.rb @@ -0,0 +1,3 @@ +class Invoice::DataPresenter::Supplier < Invoice::DataPresenter::Base + attributes :name +end diff --git a/app/models/invoice/data_presenter/variant.rb b/app/models/invoice/data_presenter/variant.rb new file mode 100644 index 0000000000..57aad76325 --- /dev/null +++ b/app/models/invoice/data_presenter/variant.rb @@ -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 \ No newline at end of file diff --git a/app/models/invoice/data_presenter_attributes.rb b/app/models/invoice/data_presenter_attributes.rb new file mode 100644 index 0000000000..8d98ef97a1 --- /dev/null +++ b/app/models/invoice/data_presenter_attributes.rb @@ -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 diff --git a/app/models/spree/order.rb b/app/models/spree/order.rb index d180a4b833..37b3c953ee 100644 --- a/app/models/spree/order.rb +++ b/app/models/spree/order.rb @@ -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 diff --git a/app/serializers/invoice/address_serializer.rb b/app/serializers/invoice/address_serializer.rb new file mode 100644 index 0000000000..40653f1b61 --- /dev/null +++ b/app/serializers/invoice/address_serializer.rb @@ -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 diff --git a/app/serializers/invoice/adjustment_serializer.rb b/app/serializers/invoice/adjustment_serializer.rb new file mode 100644 index 0000000000..ba250d474f --- /dev/null +++ b/app/serializers/invoice/adjustment_serializer.rb @@ -0,0 +1,3 @@ +class Invoice::AdjustmentSerializer < ActiveModel::Serializer + attributes :adjustable_type, :label, :included_tax_total,:additional_tax_total, :amount, :currency +end \ No newline at end of file diff --git a/app/serializers/invoice/customer_serializer.rb b/app/serializers/invoice/customer_serializer.rb new file mode 100644 index 0000000000..1f4062278c --- /dev/null +++ b/app/serializers/invoice/customer_serializer.rb @@ -0,0 +1,3 @@ +class Invoice::CustomerSerializer < ActiveModel::Serializer + attributes :code, :email +end diff --git a/app/serializers/invoice/enterprise_serializer.rb b/app/serializers/invoice/enterprise_serializer.rb new file mode 100644 index 0000000000..fc64c8777a --- /dev/null +++ b/app/serializers/invoice/enterprise_serializer.rb @@ -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 diff --git a/app/serializers/invoice/line_item_serializer.rb b/app/serializers/invoice/line_item_serializer.rb new file mode 100644 index 0000000000..87a8d38ad7 --- /dev/null +++ b/app/serializers/invoice/line_item_serializer.rb @@ -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 diff --git a/app/serializers/invoice/order_cycle_serializer.rb b/app/serializers/invoice/order_cycle_serializer.rb new file mode 100644 index 0000000000..71a5e9b953 --- /dev/null +++ b/app/serializers/invoice/order_cycle_serializer.rb @@ -0,0 +1,3 @@ +class Invoice::OrderCycleSerializer < ActiveModel::Serializer + attributes :name +end diff --git a/app/serializers/invoice/order_serializer.rb b/app/serializers/invoice/order_serializer.rb new file mode 100644 index 0000000000..fe313ee7bd --- /dev/null +++ b/app/serializers/invoice/order_serializer.rb @@ -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 diff --git a/app/serializers/invoice/payment_method_serializer.rb b/app/serializers/invoice/payment_method_serializer.rb new file mode 100644 index 0000000000..f106b47379 --- /dev/null +++ b/app/serializers/invoice/payment_method_serializer.rb @@ -0,0 +1,3 @@ +class Invoice::PaymentMethodSerializer < ActiveModel::Serializer + attributes :name, :description +end diff --git a/app/serializers/invoice/payment_serializer.rb b/app/serializers/invoice/payment_serializer.rb new file mode 100644 index 0000000000..e8a27e0bd0 --- /dev/null +++ b/app/serializers/invoice/payment_serializer.rb @@ -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 diff --git a/app/serializers/invoice/product_serializer.rb b/app/serializers/invoice/product_serializer.rb new file mode 100644 index 0000000000..143467ea96 --- /dev/null +++ b/app/serializers/invoice/product_serializer.rb @@ -0,0 +1,4 @@ +class Invoice::ProductSerializer < ActiveModel::Serializer + attributes :name + has_one :supplier, serializer: Invoice::EnterpriseSerializer +end \ No newline at end of file diff --git a/app/serializers/invoice/shipping_method_serializer.rb b/app/serializers/invoice/shipping_method_serializer.rb new file mode 100644 index 0000000000..76cd6684e0 --- /dev/null +++ b/app/serializers/invoice/shipping_method_serializer.rb @@ -0,0 +1,3 @@ +class Invoice::ShippingMethodSerializer < ActiveModel::Serializer + attributes :name, :require_ship_address +end diff --git a/app/serializers/invoice/state_serializer.rb b/app/serializers/invoice/state_serializer.rb new file mode 100644 index 0000000000..2fc552a7d2 --- /dev/null +++ b/app/serializers/invoice/state_serializer.rb @@ -0,0 +1,3 @@ +class Invoice::StateSerializer < ActiveModel::Serializer + attributes :name +end diff --git a/app/serializers/invoice/user_serializer.rb b/app/serializers/invoice/user_serializer.rb new file mode 100644 index 0000000000..a852e6979b --- /dev/null +++ b/app/serializers/invoice/user_serializer.rb @@ -0,0 +1,3 @@ +class Invoice::UserSerializer < ActiveModel::Serializer + attributes :email +end diff --git a/app/serializers/invoice/variant_serializer.rb b/app/serializers/invoice/variant_serializer.rb new file mode 100644 index 0000000000..94b316f4cd --- /dev/null +++ b/app/serializers/invoice/variant_serializer.rb @@ -0,0 +1,4 @@ +class Invoice::VariantSerializer < ActiveModel::Serializer + attributes :display_name, :options_text + has_one :product, serializer: Invoice::ProductSerializer +end \ No newline at end of file diff --git a/app/services/order_invoice_comparator.rb b/app/services/order_invoice_comparator.rb new file mode 100644 index 0000000000..04e1b68627 --- /dev/null +++ b/app/services/order_invoice_comparator.rb @@ -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 diff --git a/app/views/spree/admin/invoices/_invoices_table.html.haml b/app/views/spree/admin/invoices/_invoices_table.html.haml new file mode 100644 index 0000000000..7f356965db --- /dev/null +++ b/app/views/spree/admin/invoices/_invoices_table.html.haml @@ -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) + diff --git a/app/views/spree/admin/invoices/index.html.haml b/app/views/spree/admin/invoices/index.html.haml new file mode 100644 index 0000000000..3df54147a5 --- /dev/null +++ b/app/views/spree/admin/invoices/index.html.haml @@ -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' diff --git a/app/views/spree/admin/orders/_invoice/_line_item_name.html.haml b/app/views/spree/admin/orders/_invoice/_line_item_name.html.haml new file mode 100644 index 0000000000..30dd0c3667 --- /dev/null +++ b/app/views/spree/admin/orders/_invoice/_line_item_name.html.haml @@ -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)})" diff --git a/app/views/spree/admin/orders/_invoice/_order_note.html.haml b/app/views/spree/admin/orders/_invoice/_order_note.html.haml new file mode 100644 index 0000000000..f6a478faf3 --- /dev/null +++ b/app/views/spree/admin/orders/_invoice/_order_note.html.haml @@ -0,0 +1,4 @@ +%p.callout{style: "margin-top: 30px"} + %strong= t :additional_information + %p{style: "margin: 5px"} + = @invoice_presenter.order_note \ No newline at end of file diff --git a/app/views/spree/admin/orders/_invoice/_payment.html.haml b/app/views/spree/admin/orders/_invoice/_payment.html.haml new file mode 100644 index 0000000000..f1d19e9d65 --- /dev/null +++ b/app/views/spree/admin/orders/_invoice/_payment.html.haml @@ -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 diff --git a/app/views/spree/admin/orders/_invoice/_payments_list.html.haml b/app/views/spree/admin/orders/_invoice/_payments_list.html.haml new file mode 100644 index 0000000000..0729b14d8f --- /dev/null +++ b/app/views/spree/admin/orders/_invoice/_payments_list.html.haml @@ -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 diff --git a/app/views/spree/admin/orders/_invoice_table.html.haml b/app/views/spree/admin/orders/_invoice_table.html.haml index 7ed12adca1..71b9cc7c30 100644 --- a/app/views/spree/admin/orders/_invoice_table.html.haml +++ b/app/views/spree/admin/orders/_invoice_table.html.haml @@ -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   diff --git a/app/views/spree/admin/orders/_invoice_table2.html.haml b/app/views/spree/admin/orders/_invoice_table2.html.haml index ff24582867..8cb6cdd412 100644 --- a/app/views/spree/admin/orders/_invoice_table2.html.haml +++ b/app/views/spree/admin/orders/_invoice_table2.html.haml @@ -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   diff --git a/app/views/spree/admin/orders/invoice.html.haml b/app/views/spree/admin/orders/invoice.html.haml index d1dbc32264..b66306606f 100644 --- a/app/views/spree/admin/orders/invoice.html.haml +++ b/app/views/spree/admin/orders/invoice.html.haml @@ -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%" }   %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 }   @@ -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   %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' diff --git a/app/views/spree/admin/orders/invoice2.html.haml b/app/views/spree/admin/orders/invoice2.html.haml index c55a1f6637..aa29f39e9d 100644 --- a/app/views/spree/admin/orders/invoice2.html.haml +++ b/app/views/spree/admin/orders/invoice2.html.haml @@ -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 }   %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   -- 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' diff --git a/app/views/spree/admin/shared/_order_tabs.html.haml b/app/views/spree/admin/shared/_order_tabs.html.haml index 415c3ca861..067da26620 100644 --- a/app/views/spree/admin/shared/_order_tabs.html.haml +++ b/app/views/spree/admin/shared/_order_tabs.html.haml @@ -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 } diff --git a/config/locales/en.yml b/config/locales/en.yml index 66f658e6c5..7138fa4759 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -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 diff --git a/config/routes/spree.rb b/config/routes/spree.rb index a6e4bab5ed..f2b26246e7 100644 --- a/config/routes/spree.rb +++ b/config/routes/spree.rb @@ -100,6 +100,7 @@ Spree::Core::Engine.routes.draw do end resources :adjustments + resources :invoices resources :payments do member do diff --git a/db/migrate/20230308075421_create_invoices.rb b/db/migrate/20230308075421_create_invoices.rb new file mode 100644 index 0000000000..4211969e50 --- /dev/null +++ b/db/migrate/20230308075421_create_invoices.rb @@ -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 diff --git a/db/schema.rb b/db/schema.rb index 71537c92ce..2677aa2ba8 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -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" diff --git a/spec/factories/invoice_factory.rb b/spec/factories/invoice_factory.rb new file mode 100644 index 0000000000..4d4c8bc6f7 --- /dev/null +++ b/spec/factories/invoice_factory.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :invoice, class: Invoice do + end +end \ No newline at end of file diff --git a/spec/models/invoice_spec.rb b/spec/models/invoice_spec.rb new file mode 100644 index 0000000000..c432c97b87 --- /dev/null +++ b/spec/models/invoice_spec.rb @@ -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 diff --git a/spec/services/order_invoice_comparator_spec.rb b/spec/services/order_invoice_comparator_spec.rb new file mode 100644 index 0000000000..3a2371ef23 --- /dev/null +++ b/spec/services/order_invoice_comparator_spec.rb @@ -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 \ No newline at end of file