Skip to content

Commit

Permalink
A view object based architecture for printing arbitrary objects
Browse files Browse the repository at this point in the history
Through clever naming of attributes and delegates, all integration tests
are still green. Orders will have the exact same API as before.

* Move invoice rendering methods to Spree::BookkeepingDocument
* Add view objects to connect Spree Objects with Spree::BookkeepingDocument

Your PDF template should be agnostic about the structure of your
printable object. The view objects achieve that.

* Improve NotImplementeError messages
* Add item and adjustment POROs, invoices controller
* Add order#documents tab, translations
* Fix feature spec: Documents tab instead of print buttons
* Rrename all the partials
* Rename "file" to "pdf"
* Add a working index action to the PdfsController
* Fix specs accordingly
* Remove selecting template feature

Before, you were able to select a template from the backend.

As it is considerable development effort to create a Prawn template,
I think it makes sense to remove this option from the user. Now, there's
a PDF model, and you can create a new PDF by creating it with the underlying
Object as `printable` and a string specifying the template to be used.

* First version of index.html.erb
* Make BookkeepingDocument model searchable
* Move invoice number logic to Spree::BookkeepingDocument
* Add a base view that complains when essential things are not defined on a view
* Mmove increasing invoice numbers to base invoice view
* Tests for deprecated methods on Spree::Order
* Use order number for order packaging slip
  • Loading branch information
mamhoff authored and futhr committed Jul 19, 2015
1 parent a46c611 commit 1522eca
Show file tree
Hide file tree
Showing 61 changed files with 1,530 additions and 359 deletions.
37 changes: 21 additions & 16 deletions README.md
Expand Up @@ -3,7 +3,18 @@
[![Build Status](https://travis-ci.org/spree-contrib/spree_print_invoice.svg?branch=master)](https://travis-ci.org/spree-contrib/spree_print_invoice)
[![Code Climate](https://codeclimate.com/github/spree-contrib/spree_print_invoice/badges/gpa.svg)](https://codeclimate.com/github/spree-contrib/spree_print_invoice)

This extension provides a "Print Invoice" button (per default) on the Admin Orders view screen which generates a PDF of the order details. It's fully extendable so you can add own _print slips_ from your own Rails app. It also comes with a packaging slip.
This extension provides two things:

* A model `Spree::Invoice`, which is generated when an order is completed or a reimbursement is created. It holds a date and a contiguous invoice number to comply with European tax regulation.
* A model `Spree::BookkeepingDocument`, which generates PDFs from any Spree Object with the help of View objects that translate between different object structures and PDF templates.

The Gem contains example implementations for Invoices for `Spree::Order` as well as `Spree::Reimbursement`. The basic structure looks like this:

`Spree::BookkeepingDocument` takes as constructor arguments a `printable` (polymorphic AR object) and a `template` (string). It then passes on all actual data generation to a ViewObject. You can find these objects in `app/spree/printables/#{printable}/#{template}_view.rb`. The object will be instantiated upon PDF generation (look at the `Spree::BookkeepingDocument#render_pdf` method to see how it's done).

`Spree::Order` and `Spree::Reimbursement` are patched so that they generate both an invoice number and date and a PDF.

In the `Spree::Admin::OrdersController#show` view, you'll find an additional button `Documents`, where all printable documents will be listed.

## Installation

Expand All @@ -17,7 +28,7 @@ Run
bundle && exec rails g spree_print_invoice:install
```

Enjoy! Now allow to generate invoices with sequential numbers.
Enjoy! Now you can generate invoices and packaging slips with sequential numbers from arbitrary Spree objects.

---

Expand All @@ -33,31 +44,21 @@ Enjoy! Now allow to generate invoices with sequential numbers.

3. Override any of the partial templates. They are address, footer, totals, header, bye, and the line_items. In bye the text `:thanks` is printed. The `:extra_note` hook has been deprecated as Spree no longer supports hooks.

4. Set `:suppress_anonymous_address` option to get blank addresses for anonymous email addresses (as created by my spree_last_address extension for empty/unknown user info).

5. Many european countries requires numeric and sequential invoices numbers. To use invoices sequential number fill the specific field in "General Settings" or by setting:
4. Many european countries requires numeric and sequential invoices numbers. To use invoices sequential number fill the specific field in "General Settings" or by setting:

```ruby
Spree::PrintInvoice::Config.set(next_number: [1|'your current next invoice number'])
```

The next invoice number will be the one that you specified. You will able to increase it in any moment, for example, to re-sync invoices number if you are making invoices also in other programs for the same business name.

6. Enable packaging slips, by setting:

```ruby
Spree::PrintInvoice::Config.set(print_buttons: 'invoice,packaging_slip') # comma separated list
```

Use above feature for your own template if you want. For each button_name, define `button_name_print` text in your locale.

7. Set page/document options with:
5. Set page/document options with:

```ruby
Spree::PrintInvoice::Config.set(prawn_options: { page_layout: :landscape, page_size: 'A4', margin: [50, 100, 150, 200] })
```

8. Enable PDF storage feature
6. Enable PDF storage feature

PDF files can be stored to disk. This is very handy, if you want to send these files as email attachment.

Expand All @@ -76,7 +77,11 @@ In order to customize the build in invoice and packaging slip templates you need
$ bundle exec rails g spree_print_invoice:templates
```

You can then customize them at `app/views/spree/admin/orders/invoice.pdf.prawn` and `app/views/spree/admin/orders/packaging_slip.pdf.prawn`.
You can then customize them at the following locations:

* `app/views/spree/printables/order/invoice.pdf.prawn`
* `app/views/spree/printables/reimbursement/invoice.pdf.prawn`
* `app/views/spree/printables/order/packaging_slip.pdf.prawn`.

---

Expand Down
36 changes: 36 additions & 0 deletions app/controllers/spree/admin/bookkeeping_documents_controller.rb
@@ -0,0 +1,36 @@
module Spree
module Admin
class BookkeepingDocumentsController < ResourceController
before_action :load_order, if: :order_focused?

helper_method :order_focused?

def show
respond_with(@bookkeeping_document) do |format|
format.pdf do
send_data @bookkeeping_document.pdf, type: 'application/pdf', disposition: 'inline'
end
end
end

def index
# Massaging the params for the index view like Spree::Admin::Orders#index
params[:q] ||= {}
@search = Spree::BookkeepingDocument.ransack(params[:q])
@bookkeeping_documents = @search.result
@bookkeeping_documents = @bookkeeping_documents.where(printable: @order) if order_focused?
@bookkeeping_documents = @bookkeeping_documents.page(params[:page] || 1).per(10)
end

private

def order_focused?
params[:order_id].present?
end

def load_order
@order = Spree::Order.find_by(number: params[:order_id])
end
end
end
end
28 changes: 0 additions & 28 deletions app/controllers/spree/admin/orders_controller_decorator.rb

This file was deleted.

149 changes: 149 additions & 0 deletions app/models/spree/bookkeeping_document.rb
@@ -0,0 +1,149 @@
module Spree
class BookkeepingDocument < ActiveRecord::Base
PERSISTED_ATTRS = [
:firstname,
:lastname,
:email,
:total,
:number
]

# Spree::BookkeepingDocument cares about creating PDFs. Whenever it needs to know
# anything about the document to send to the view, it asks a view object.
#
# +printable+ should be an Object, such as Spree::Order or Spree::Shipment.
# template should be a string, such as "invoice" or "packaging_slip"
#
belongs_to :printable, polymorphic: true
validates :printable, :template, presence: true
validates *PERSISTED_ATTRS, presence: true, if: -> { self.persisted? }
scope :invoices, -> { where(template: 'invoice') }

before_create :copy_view_attributes
after_save :after_save_actions


# An instance of Spree::Printable::#{YourModel}::#{YourTemplate}Presenter
#
def view
@_view ||= view_class.new(printable)
end

def date
created_at.to_date
end

def template_name
"spree/printables/#{single_lower_case_name(printable.class.name)}/#{template}"
end

# If the document is called from the view with some method it doesn't know,
# just call the view object. It should know.
def method_missing(method_name, *args, &block)
if view.respond_to? method_name
view.send(method_name, *args, &block)
else
super
end
end

def document_type
"#{printable_type.demodulize.tableize.singularize}_#{template}"
end

# Returns the given template as pdf binary suitable for Rails send_data
#
# If the file is already present it returns this
# else it generates a new file, stores and returns this.
#
# You can disable the pdf file generation with setting
#
# Spree::PrintInvoice::Config.store_pdf to false
#
def pdf
if Spree::PrintInvoice::Config.store_pdf
send_or_create_pdf
else
render_pdf
end
end

# = The PDF file_name
#
def file_name
@_file_name ||= "#{template}-#{printable.number}.pdf"
end

# = PDF file path
#
def file_path
@_file_path ||= Rails.root.join(storage_path, "#{file_name}")
end

# = PDF storage folder path for given template name
#
# Configure the storage path with +Spree::PrintInvoice::Config.storage_path+
#
# Each template type gets it own pluralized folder inside
# of +Spree::PrintInvoice::Config.storage_path+
#
# == Example:
#
# storage_path('invoice') => "tmp/pdf_prints/invoices"
#
# Creates the folder if it's not present yet.
#
def storage_path
storage_path = Rails.root.join(Spree::PrintInvoice::Config.storage_path, template.pluralize)
FileUtils.mkdir_p(storage_path)
storage_path
end

# Renders the prawn template for give template name in context of ActionView.
#
# Prawn templates need to be placed in the correct folder. For example, for a PDF from
# a Spree::Order with the invoice template, it would be
# the +app/views/spree/printables/order/invoices+ folder.
#
# Assigns +@doc+ instance variable
#
def render_pdf
ActionView::Base.new(
ActionController::Base.view_paths,
{doc: self}
).render(template: "#{template_name}.pdf.prawn")
end

private

def copy_view_attributes
PERSISTED_ATTRS.each do |attr|
self.send("#{attr}=", view.send(attr))
end
end

# For a Spree::Order printable and an "invoice" template,
# you would get "spree/documents/order/invoice_view"
# --> Spree::Printables::Order::InvoiceView
#
def view_class
@_view_class ||= "#{template_name}_view".classify.constantize
end

def single_lower_case_name(class_string)
@_single_lower_class_name ||= class_string.demodulize.tableize.singularize
end

# Sends stored pdf for given template from disk.
#
# Renders and stores it if it's not yet present.
#
def send_or_create_pdf
unless File.exist?(file_path)
File.open(file_path, 'wb') { |f| f.puts render_pdf }
end

IO.binread(file_path)
end
end
end

0 comments on commit 1522eca

Please sign in to comment.