code-mancers/invoicing

View on GitHub
lib/invoicing/ledger_item/render_ubl.rb

Summary

Maintainability
D
1 day
Test Coverage
require "active_support/concern"
require 'builder'

module Invoicing
  module LedgerItem
    # Included into ActiveRecord model object when +acts_as_ledger_item+ is invoked.
    module RenderUBL
      extend ActiveSupport::Concern

      # Renders this invoice or credit note into a complete XML document conforming to the
      # {OASIS Universal Business Language}[http://ubl.xml.org/] (UBL) open standard for interchange
      # of business documents ({specification}[http://www.oasis-open.org/committees/ubl/]). This
      # format, albeit a bit verbose, is increasingly being adopted as an international standard. It
      # can represent some very complicated multi-currency, multi-party business relationships, but
      # is still quite usable for simple cases.
      #
      # It is recommended that you present machine-readable UBL data in your application in the
      # same way as you present human-readable invoices in HTML. For example, in a Rails controller,
      # you could use:
      #
      #   class AccountsController < ApplicationController
      #     def show
      #       @ledger_item = LedgerItem.find(params[:id])
      #       # ... check whether current user has access to this document ...
      #       respond_to do |format|
      #         format.html # show.html.erb
      #         format.xml  { render :xml => @ledger_item.render_ubl }
      #       end
      #     end
      #   end
      def render_ubl(options={})
        UBLOutputBuilder.new(self, options).build
      end


      class UBLOutputBuilder #:nodoc:
        # XML Namespaces required by UBL
        UBL_NAMESPACES = {
          "xmlns:cac" => "urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2",
          "xmlns:cbc" => "urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2"
        }

        UBL_DOC_NAMESPACES = {
          :Invoice              => "urn:oasis:names:specification:ubl:schema:xsd:Invoice-2",
          :SelfBilledInvoice    => "urn:oasis:names:specification:ubl:schema:xsd:SelfBilledInvoice-2",
          :CreditNote           => "urn:oasis:names:specification:ubl:schema:xsd:CreditNote-2",
          :SelfBilledCreditNote => "urn:oasis:names:specification:ubl:schema:xsd:SelfBilledCreditNote-2"
        }

        attr_reader :ledger_item, :options, :cached_values, :doc_type, :factor

        def initialize(ledger_item, options)
          @ledger_item = ledger_item
          @options = options
          @cached_values = {}
          subtype = ledger_item.send(:ledger_item_class_info).subtype
          @doc_type =
            if [:invoice, :credit_note].include? subtype
              if total_amount >= BigDecimal('0')
                @factor = BigDecimal('1')
                sender_details.symbolize_keys[:is_self] ? :Invoice : :SelfBilledInvoice
              else
                @factor = BigDecimal('-1')
                sender_details.symbolize_keys[:is_self] ? :CreditNote : :SelfBilledCreditNote
              end
            else
              raise RuntimeError, "render_ubl not implemented for ledger item subtype #{subtype.inspect}"
            end
        end

        # For convenience while building the XML structure, method_missing redirects method calls
        # to the ledger item (taking account of method renaming via acts_as_ledger_item options);
        # calls to foo_of(line_item) are redirected to line_item.foo (taking account of method
        # renaming via acts_as_line_item options).
        def method_missing(method_id, *args, &block)
          method_id = method_id.to_sym
          if method_id.to_s =~ /^(.*)_of$/
            method_id = $1.to_sym
            line_item = args[0]
            line_item.send(:line_item_class_info).get(line_item, method_id)
          else
            cached_values[method_id] ||= ledger_item.send(:ledger_item_class_info).get(ledger_item, method_id)
          end
        end

        # Returns a UBL XML rendering of the ledger item previously passed to the constructor.
        def build
          ubl = Builder::XmlMarkup.new :indent => 4
          ubl.instruct! :xml

          ubl.ubl doc_type, UBL_NAMESPACES.clone.update({'xmlns:ubl' => UBL_DOC_NAMESPACES[doc_type]}) do |invoice|
            invoice.cbc :ID, identifier
            invoice.cbc :UUID, uuid if uuid

            issue_date_formatted, issue_time_formatted = date_and_time(issue_date || Time.now)
            invoice.cbc :IssueDate, issue_date_formatted
            invoice.cbc :IssueTime, issue_time_formatted

            # Different document types have the child elements InvoiceTypeCode, Note and
            # TaxPointDate in a different order. WTF?!
            if doc_type == :Invoice
              invoice.cbc :InvoiceTypeCode, method_missing(:type)
              invoice.cbc :Note, description
              invoice.cbc :TaxPointDate, issue_date_formatted
            else
              invoice.cbc :TaxPointDate, issue_date_formatted
              invoice.cbc :InvoiceTypeCode, method_missing(:type) if doc_type == :SelfBilledInvoice
              invoice.cbc :Note, description
            end

            invoice.cac :InvoicePeriod do |invoice_period|
              build_period(invoice_period, period_start, period_end)
            end if period_start && period_end

            if [:Invoice, :CreditNote].include?(doc_type)

              invoice.cac :AccountingSupplierParty do |supplier|
                build_party supplier, sender_details
              end
              invoice.cac :AccountingCustomerParty do |customer|
                customer.cbc :SupplierAssignedAccountID, recipient_id
                build_party customer, recipient_details
              end

            elsif [:SelfBilledInvoice, :SelfBilledCreditNote].include?(doc_type)

              invoice.cac :AccountingCustomerParty do |customer|
                build_party customer, recipient_details
              end
              invoice.cac :AccountingSupplierParty do |supplier|
                supplier.cbc :CustomerAssignedAccountID, sender_id
                build_party supplier, sender_details
              end

            end

            invoice.cac :PaymentTerms do |payment_terms|
              payment_terms.cac :SettlementPeriod do |settlement_period|
                build_period(settlement_period, issue_date || Time.now, due_date)
              end
            end if due_date && [:Invoice, :SelfBilledInvoice].include?(doc_type)

            invoice.cac :TaxTotal do |tax_total|
              tax_total.cbc :TaxAmount, (factor*tax_amount).to_s, :currencyID => currency
            end if tax_amount

            invoice.cac :LegalMonetaryTotal do |monetary_total|
              monetary_total.cbc :TaxExclusiveAmount, (factor*(total_amount - tax_amount)).to_s,
                :currencyID => currency if tax_amount
              monetary_total.cbc :PayableAmount, (factor*total_amount).to_s, :currencyID => currency
            end

            line_items.sorted(:tax_point).each do |line_item|
              line_tag = if [:CreditNote, :SelfBilledCreditNote].include? doc_type
                :CreditNoteLine
              else
                :InvoiceLine
              end

              invoice.cac line_tag do |invoice_line|
                build_line_item(invoice_line, line_item)
              end
            end
          end
          ubl.target!
        end


        # Given a <tt>Builder::XmlMarkup</tt> instance and two datetime objects, builds a UBL
        # representation of the period between the two dates and times, something like the
        # following:
        #
        #   <cbc:StartDate>2008-05-06</cbc:StartTime>
        #   <cbc:StartTime>12:34:56+02:00</cbc:StartTime>
        #   <cbc:EndDate>2008-07-02</cbc:EndDate>
        #   <cbc:EndTime>01:02:03+02:00</cbc:EndTime>
        def build_period(xml, start_datetime, end_datetime)
          start_date, start_time = date_and_time(start_datetime)
          end_date, end_time = date_and_time(end_datetime)
          xml.cbc :StartDate, start_date
          xml.cbc :StartTime, start_time
          xml.cbc :EndDate, end_date
          xml.cbc :EndTime, end_time
        end


        # Given a <tt>Builder::XmlMarkup</tt> instance and a supplier/customer details hash (as
        # returned by <tt>LedgerItem#sender_details</tt> and <tt>LedgerItem#recipient_details</tt>,
        # builds a UBL representation of that party, something like the following:
        #
        #   <cac:Party>
        #       <cac:PartyName>
        #           <cbc:Name>The Big Bank</cbc:Name>
        #       </cac:PartyName>
        #       <cac:PostalAddress>
        #           <cbc:StreetName>Paved With Gold Street</cbc:StreetName>
        #           <cbc:CityName>London</cbc:CityName>
        #           <cbc:PostalZone>E14 5HQ</cbc:PostalZone>
        #           <cac:Country><cbc:IdentificationCode>GB</cbc:IdentificationCode></cac:Country>
        #       </cac:PostalAddress>
        #   </cac:Party>
        def build_party(xml, details)
          details = details.symbolize_keys
          xml.cac :Party do |party|
            party.cac :PartyName do |party_name|
              party_name.cbc :Name, details[:name]
            end if details[:name]

            party.cac :PostalAddress do |postal_address|
              street1, street2 = details[:address].strip.split("\n", 2)
              postal_address.cbc :StreetName,           street1               if street1
              postal_address.cbc :AdditionalStreetName, street2               if street2
              postal_address.cbc :CityName,             details[:city]        if details[:city]
              postal_address.cbc :PostalZone,           details[:postal_code] if details[:postal_code]
              postal_address.cbc :CountrySubentity,     details[:state]       if details[:state]
              postal_address.cac :Country do |country|
                country.cbc :IdentificationCode, details[:country_code] if details[:country_code]
                country.cbc :Name,               details[:country]      if details[:country]
              end if details[:country_code] || details[:country]
            end

            party.cac :PartyTaxScheme do |party_tax_scheme|
              party_tax_scheme.cbc :CompanyID, details[:tax_number]
              party_tax_scheme.cac :TaxScheme do |tax_scheme|
                tax_scheme.cbc :ID, "VAT" # TODO: make country-dependent (e.g. GST in Australia)
              end
            end if details[:tax_number]

            party.cac :Contact do |contact|
              contact.cbc :Name, details[:contact_name]
            end if details[:contact_name]
          end
        end


        # Given a <tt>Builder::XmlMarkup</tt> instance and a +LineItem+ instance, builds a UBL
        # representation of that line item, something like the following:
        #
        #   <cbc:ID>123</cbc:ID>
        #   <cbc:UUID>0cc659f0-cfac-012b-481d-0017f22d32c0</cbc:UUID>
        #   <cbc:InvoicedQuantity>1</cbc:InvoicedQuantity>
        #   <cbc:LineExtensionAmount currencyID="GBP">123.45</cbc:LineExtensionAmount>
        #   <cbc:TaxPointDate>2009-01-01</cbc:TaxPointDate>
        #   <cac:TaxTotal><cbc:TaxAmount currencyID="GBP">12.34</cbc:TaxAmount></cac:TaxTotal>
        #   <cac:Item><cbc:Description>Foo bar baz</cbc:Description></cac:Item>
        def build_line_item(invoice_line, line_item)
          invoice_line.cbc :ID, id_of(line_item)
          invoice_line.cbc :UUID, uuid_of(line_item) if uuid_of(line_item)
          quantity_tag = [:Invoice, :SelfBilledInvoice].include?(doc_type) ? :InvoicedQuantity : :CreditedQuantity
          invoice_line.cbc quantity_tag, quantity_of(line_item) if quantity_of(line_item)
          invoice_line.cbc :LineExtensionAmount, (factor*net_amount_of(line_item)).to_s, :currencyID => currency
          if tax_point_of(line_item)
            tax_point_date, tax_point_time = date_and_time(tax_point_of(line_item))
            invoice_line.cbc :TaxPointDate, tax_point_date
          end

          invoice_line.cac :TaxTotal do |tax_total|
            tax_total.cbc :TaxAmount, (factor*tax_amount_of(line_item)).to_s, :currencyID => currency
          end if tax_amount_of(line_item)

          invoice_line.cac :Item do |item|
            item.cbc :Description, description_of(line_item)
            #cac:BuyersItemIdentification
            #cac:SellersItemIdentification
            #cac:ClassifiedTaxCategory
            #cac:ItemInstance
          end

          #cac:Price
        end

        private

        # Returns an array of two strings, <tt>[date, time]</tt> in the format specified by UBL,
        # for a given datetime value.
        def date_and_time(value)
          value.in_time_zone(Time.zone || 'Etc/UTC').xmlschema.split('T')
        end
      end
    end
  end
end