diff --git a/app/models/invoice.rb b/app/models/invoice.rb index 33a128a3e0..b86d362b5b 100644 --- a/app/models/invoice.rb +++ b/app/models/invoice.rb @@ -4,6 +4,7 @@ class Invoice < ApplicationRecord belongs_to :order, class_name: 'Spree::Order' serialize :data, Hash before_validation :serialize_order + after_create :cancel_previous_invoices def presenter @presenter ||= Invoice::DataPresenter.new(self) @@ -14,4 +15,8 @@ class Invoice < ApplicationRecord self.data = Invoice::OrderSerializer.new(order).serializable_hash end + + def cancel_previous_invoices + order.invoices.where.not(id:).update_all(cancelled: true) + end end diff --git a/app/views/spree/admin/invoices/_invoices_table.html.haml b/app/views/spree/admin/invoices/_invoices_table.html.haml index 1718521112..7d312a7139 100644 --- a/app/views/spree/admin/invoices/_invoices_table.html.haml +++ b/app/views/spree/admin/invoices/_invoices_table.html.haml @@ -18,7 +18,7 @@ %td.align-center.label = invoice.presenter.total %td.align-center.label - = t(invoice.status) + = t(invoice.cancelled ? :cancelled : :active) %td.align-center.label =link_to(t(:download),print_admin_order_path(@order,invoice_id: invoice.id),target: "_blank") diff --git a/config/locales/en.yml b/config/locales/en.yml index 896cb91aa0..8ca1b63541 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -394,6 +394,11 @@ en: confirm_resend_order_confirmation: "Are you sure you want to resend the order confirmation email?" must_have_valid_business_number: "%{enterprise_name} must have a valid ABN before invoices can be sent." invoice: "Invoice" + invoices: "Invoices" + file: "File" + active: "Active" + download: "Download" + cancelled: "Cancelled" more: "More" say_no: "No" say_yes: "Yes" diff --git a/db/migrate/20230629015322_set_default_invoice_status.rb b/db/migrate/20230629015322_set_default_invoice_status.rb new file mode 100644 index 0000000000..4dd10d3eb4 --- /dev/null +++ b/db/migrate/20230629015322_set_default_invoice_status.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +class SetDefaultInvoiceStatus < ActiveRecord::Migration[7.0] + def change + add_column :invoices, :cancelled, :boolean, default: false, null: false + ActiveRecord::Base.connection.execute(<<-SQL.squish + UPDATE invoices + SET cancelled = true + WHERE status = 'inactive' + SQL + ) + remove_column :invoices, :status, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index 533ad446c6..90a51e7208 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -306,12 +306,12 @@ ActiveRecord::Schema[7.0].define(version: 2023_07_06_033212) do 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.boolean "cancelled", default: false, null: false t.index ["order_id"], name: "index_invoices_on_order_id" end diff --git a/spec/models/invoice_spec.rb b/spec/models/invoice_spec.rb index 6bd6257c1e..81dad4005e 100644 --- a/spec/models/invoice_spec.rb +++ b/spec/models/invoice_spec.rb @@ -3,18 +3,18 @@ require 'spec_helper' RSpec.describe Invoice, type: :model do + let(:distributor) { create(:distributor_enterprise) } + let(:order) { create(:order, :with_line_item, :completed, distributor:) } describe 'presenter' do it 'should return an instance of Invoice::DataPresenter' do - invoice = create(:invoice) + invoice = create(:invoice, order:) expect(invoice.presenter).to be_a(Invoice::DataPresenter) end end describe 'serialize_order' do - let!(:distributor) { create(:distributor_enterprise) } - let!(:order) { create(:order, :with_line_item, :completed, distributor: distributor) } it 'serializes the order' do - invoice = create(:invoice, order: order) + invoice = create(:invoice, order:) expect(invoice.data).to eq(Invoice::OrderSerializer.new(order).serializable_hash) end end diff --git a/spec/system/admin/orders/invoices_spec.rb b/spec/system/admin/orders/invoices_spec.rb new file mode 100644 index 0000000000..cdd3a8de6e --- /dev/null +++ b/spec/system/admin/orders/invoices_spec.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: true + +require 'system_helper' + +describe ' + As an administrator + I want to create an invoice for an order +' do + include WebHelper + include AuthenticationHelper + + let(:user) { create(:user) } + let(:product) { create(:simple_product) } + let(:distributor) { create(:distributor_enterprise, owner: user, charges_sales_tax: true) } + let(:order_cycle) do + create(:simple_order_cycle, name: 'One', distributors: [distributor], + variants: [product.variants.first]) + end + + let(:order) do + create(:order_with_totals_and_distribution, + distributor:, user:, + order_cycle:, state: 'complete', + payment_state: 'balance_due') + end + let(:customer) { order.customer } + + before do + Flipper.enable(:invoices) + order.finalize! + login_as_admin + visit spree.edit_admin_order_path(order) + end + + context 'when the order has no invoices' do + it 'creates an invoice for the order' do + click_link 'Invoices' + + expect { + click_link 'New Invoice' + expect(page).to have_no_link "New Invoice" + }.to change { order.invoices.count }.by(1) + + invoice = order.invoices.first + expect(invoice.cancelled).to eq false + expect(invoice.number).to eq 1 + end + end + + context 'when the order has an invoice' do + let!(:latest_invoice){ create(:invoice, order:, number: 1, cancelled: false) } + + context 'order not updated since latest invoice' do + it 'should not render new invoice button' do + click_link 'Invoices' + expect(page).to_not have_link 'New Invoice' + end + end + + # For reference check: + # https://docs.google.com/spreadsheets/d/1hOM6UL4mWeRCFLcDQ3fTkbhbUQ2WvIUCCA1IerDBtUA/edit#gid=0 + context 'order updated since latest invoice' do + context 'changes require regenerating' do + let(:new_note){ 'new note' } + before do + order.update!(note: new_note) + end + + it 'updates the lastest invoice for the order' do + click_link 'Invoices' + expect { + click_link 'New Invoice' + expect(page).to have_no_link "New Invoice" + }.to change { order.reload.invoices.count }.by(0) + .and change { latest_invoice.reload.presenter.note }.from("").to(new_note) + + expect(latest_invoice.reload.cancelled).to eq false + end + end + + context 'changes require generating a new invoice' do + before do + order.line_items.first.update!(quantity: 2) + end + + it 'creates a new invoice for the order' do + click_link 'Invoices' + expect { + click_link 'New Invoice' + expect(page).to have_no_link "New Invoice" + }.to change { order.reload.invoices.count }.by(1) + + expect(latest_invoice.reload.cancelled).to eq true + expect(latest_invoice.presenter.sorted_line_items.first.quantity).to eq 1 + + new_invoice = order.invoices.last + expect(new_invoice.cancelled).to eq false + expect(new_invoice.number).to eq 2 + expect(new_invoice.presenter.sorted_line_items.first.quantity).to eq 2 + end + end + end + end +end