Skip to content

Credit Note Allocations (plus minor tweaks) #125

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 40 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
85318d3
add updated_at to accounts
sensadrome Dec 17, 2017
00ab8a4
add time to accounts.xml
sensadrome Dec 17, 2017
acb8be4
Merge pull request #1 from datanautsuk/last_updated_time
sensadrome Dec 17, 2017
4a84a93
add Statuses to invoice request params
sensadrome Dec 17, 2017
fd6e9e7
Merge pull request #2 from datanautsuk/invoices_statuses
sensadrome Dec 17, 2017
663b11d
get date and time for invoice and credit notes
sensadrome Dec 17, 2017
2d9937b
Merge pull request #3 from datanautsuk/updated_at_as_time
sensadrome Dec 17, 2017
0a2c0ff
keep updated_at consistent through models
sensadrome Dec 18, 2017
024925f
Merge branch 'updated_at_as_time'
sensadrome Dec 18, 2017
466cc1b
add page option to credit note fetch
sensadrome Dec 18, 2017
39fdab4
Merge pull request #4 from datanautsuk/add_paging_to_credit_notes
sensadrome Dec 18, 2017
3cf2ed8
Add allocation model record
sensadrome Dec 19, 2017
0fe6d6f
for nested attributes don’t add to the attribute definitions
sensadrome Dec 19, 2017
e20bf15
Add date / time parsers to from_xml method on base
sensadrome Dec 19, 2017
4aa6dd2
Add allocations to credit_note and response
sensadrome Dec 19, 2017
beee9c4
add allocate_credit_note method to gateway and parse_response
sensadrome Dec 19, 2017
dd5e3e0
Add tests for allocations
sensadrome Dec 19, 2017
6c3da48
Merge pull request #5 from datanautsuk/credit_note_allocation
sensadrome Dec 19, 2017
b290bfa
use post to create credit notes
sensadrome Dec 19, 2017
20aa4c3
stub the post requests instead of the put requests
sensadrome Dec 19, 2017
1c1d325
dont output empty values to xml
sensadrome Dec 19, 2017
559f859
Optionally don’t flatten an array response
sensadrome Jan 2, 2018
f30db4e
Allow for different structure of Allocation
sensadrome Jan 2, 2018
5cf6434
add updated time to Item
sensadrome Jan 2, 2018
83b4139
Add amount paid to credit note structure
sensadrome Jan 2, 2018
15da8a5
dont use the same request sig when paging through invoices
sensadrome Jan 2, 2018
6a88f01
Option to not flatten a single response when it comes back from the API
sensadrome Jan 2, 2018
8c4b50d
add currency_code to payments
sensadrome Jan 2, 2018
10ea103
Fix Tracking on CreditNotes, Invoice/LineItems and ManualJournal
sensadrome Jan 3, 2018
1170bfa
set the node name to TrackingCategory
sensadrome Jan 5, 2018
00c0f9c
Merge branch 'master' into master
sensadrome Oct 23, 2018
0e7ecf5
Merge branch 'master' of https://github.com/xero-gateway/xero_gateway…
sensadrome Aug 27, 2019
f6dea68
Merge branch 'master' of github.com:datanautsuk/xero_gateway
sensadrome Aug 27, 2019
0aefa57
adds currency rate to credit note
sensadrome Aug 27, 2019
429b1d4
take account of when a line item amount is nil
sensadrome Aug 27, 2019
df94dbb
dont set source_xml
sensadrome Sep 4, 2019
0a416d3
Payments can be made against a credit note
sensadrome Mar 18, 2020
e2205b2
adds build payment method to gateway
sensadrome Mar 18, 2020
4f12bbe
Payment not Contact for build_payment
sensadrome Mar 18, 2020
8dee24f
Payment objects need a gateway accessor
sensadrome Mar 18, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions lib/xero_gateway.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
require File.join(File.dirname(__FILE__), 'xero_gateway', 'account')
require File.join(File.dirname(__FILE__), 'xero_gateway', 'accounts_list')
require File.join(File.dirname(__FILE__), 'xero_gateway', 'tracking_category')
require File.join(File.dirname(__FILE__), 'xero_gateway', 'tracking_option')
require File.join(File.dirname(__FILE__), 'xero_gateway', 'address')
require File.join(File.dirname(__FILE__), 'xero_gateway', 'phone')
require File.join(File.dirname(__FILE__), 'xero_gateway', 'contact_person')
Expand All @@ -35,6 +36,7 @@
require File.join(File.dirname(__FILE__), 'xero_gateway', 'invoice')
require File.join(File.dirname(__FILE__), 'xero_gateway', 'bank_transaction')
require File.join(File.dirname(__FILE__), 'xero_gateway', 'credit_note')
require File.join(File.dirname(__FILE__), 'xero_gateway', 'allocation')
require File.join(File.dirname(__FILE__), 'xero_gateway', 'journal_line')
require File.join(File.dirname(__FILE__), 'xero_gateway', 'manual_journal')
require File.join(File.dirname(__FILE__), 'xero_gateway', 'report')
Expand Down
4 changes: 3 additions & 1 deletion lib/xero_gateway/account.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
module XeroGateway
class Account
include Dates

TYPE = {
'CURRENT' => '',
Expand Down Expand Up @@ -48,7 +49,7 @@ class Account
'ZERORATED' => 'Zero-rated supplies/sales from overseas (NZ Only)'
} unless defined?(TAX_TYPE)

attr_accessor :account_id, :code, :name, :type, :status, :account_class, :tax_type, :description, :system_account, :enable_payments_to_account, :currency_code
attr_accessor :account_id, :code, :name, :type, :status, :account_class, :tax_type, :description, :system_account, :enable_payments_to_account, :currency_code, :updated_at

def initialize(params = {})
params.each do |k,v|
Expand Down Expand Up @@ -94,6 +95,7 @@ def self.from_xml(account_element)
when "SystemAccount" then account.system_account = element.text
when "EnablePaymentsToAccount" then account.enable_payments_to_account = (element.text == 'true')
when "CurrencyCode" then account.currency_code = element.text
when "UpdatedDateUTC" then account.updated_at = parse_date_time(element.text)
end
end
account
Expand Down
19 changes: 19 additions & 0 deletions lib/xero_gateway/allocation.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
module XeroGateway
class Allocation < BaseRecord
attributes(
'AppliedAmount' => :float,
'Date' => :date,
'CreditNoteID' => :string,
'CreditNoteNumber' => :string,
'Invoice' => {
'InvoiceID' => :string,
'InvoiceNumber' => :string
}
)

alias invoice_id invoice_invoice_id
alias invoice_number invoice_invoice_number
alias invoice_id= invoice_invoice_id=
alias invoice_number= invoice_invoice_number=
end
end
52 changes: 29 additions & 23 deletions lib/xero_gateway/base_record.rb
Original file line number Diff line number Diff line change
@@ -1,30 +1,26 @@
module XeroGateway
class BaseRecord

class UnsupportedAttributeType < StandardError; end

class_attribute :element_name
class_attribute :attribute_definitions
class_attribute :attribute_definitions_readonly

# The source XML record that initialized this instance.
attr_reader :source_xml

class << self
def attributes(hash)
hash.each do |k, v|
attribute k, v
end
end

def attribute(name, value)
def attribute(name, value, nested = false)
self.attribute_definitions ||= {}
self.attribute_definitions[name] = value
self.attribute_definitions[name] = value unless nested

case value
when Hash
value.each do |k, v|
attribute("#{name}#{k}", v)
attribute("#{name}#{k}", v, true)
end
else
attr_accessor name.underscore
Expand Down Expand Up @@ -64,7 +60,6 @@ def to_xml(builder = Builder::XmlMarkup.new)
end

def from_xml(base_element)
@source_xml = base_element
from_xml_attributes(base_element)
self
end
Expand All @@ -86,19 +81,30 @@ def from_xml_attributes(element, attribute = nil, attr_definition = self.class.a
return
end

value = case attr_definition
when :boolean then element.text == "true"
when :float then element.text.to_f
when :integer then element.text.to_i
when :currency then BigDecimal(element.text)
when :date then Dates::Helpers.parse_date(element.text)
when :datetime then Dates::Helpers.parse_date_time(element.text)
when :datetime_utc then Dates::Helpers.parse_date_time_utc(element.text)
when Array then array_from_xml(element, attr_definition)
when Class
attr_definition.from_xml(element) if attr_definition.respond_to?(:from_xml)
else element.text
end if element.text.present? || element.children.present?
if element.text.present? || element.children.present?
value = case attr_definition
when :boolean
element.text == "true"
when :float
element.text.to_f
when :integer
element.text.to_i
when :currency
BigDecimal(element.text)
when :date
Dates::Helpers.parse_date(element.text)
when :datetime
Dates::Helpers.parse_date_time(element.text)
when :datetime_utc
Dates::Helpers.parse_date_time_utc(element.text)
when Array
array_from_xml(element, attr_definition)
when Class
attr_definition.from_xml(element) if attr_definition.respond_to?(:from_xml)
else
element.text
end
end

send("#{attribute.underscore}=", value)
end
Expand Down Expand Up @@ -130,8 +136,8 @@ def to_xml_attributes(builder = Builder::XmlMarkup.new, path = nil, attr_definit
end
end unless value.nil?
else
value = send("#{path}#{attr}".underscore)
builder.__send__(attr, value) unless value.nil?
attr_value = send("#{path}#{attr}".underscore)
builder.__send__(attr, attr_value) unless attr_value.nil?
end
end
end
Expand Down
10 changes: 9 additions & 1 deletion lib/xero_gateway/credit_note.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,11 @@ class CreditNote
attr_accessor :line_items_downloaded

# All accessible fields
attr_accessor :credit_note_id, :credit_note_number, :type, :status, :date, :reference, :line_amount_types, :currency_code, :payments, :fully_paid_on, :amount_credited
attr_accessor :credit_note_id, :credit_note_number, :type, :status, :date,
:reference, :line_amount_types, :currency_code, :payments,
:fully_paid_on, :amount_credited, :updated_at, :allocations,
:amount_paid, :currency_rate

attr_writer :line_items, :contact

def initialize(params = {})
Expand All @@ -55,6 +59,7 @@ def initialize(params = {})
end

@line_items ||= []
@allocations ||= []
end

# Validate the Address record according to what will be valid by the gateway.
Expand Down Expand Up @@ -191,8 +196,10 @@ def self.from_xml(credit_note_element, gateway = nil, options = {})
when "CreditNoteNumber" then credit_note.credit_note_number = element.text
when "Type" then credit_note.type = element.text
when "CurrencyCode" then credit_note.currency_code = element.text
when "CurrencyRate" then credit_note.currency_rate = BigDecimal(element.text)
when "Contact" then credit_note.contact = Contact.from_xml(element)
when "Date" then credit_note.date = parse_date(element.text)
when "UpdatedDateUTC" then credit_note.updated_at = parse_date_time(element.text)
when "Status" then credit_note.status = element.text
when "Reference" then credit_note.reference = element.text
when "LineAmountTypes" then credit_note.line_amount_types = element.text
Expand All @@ -204,6 +211,7 @@ def self.from_xml(credit_note_element, gateway = nil, options = {})
when "AmountDue" then credit_note.amount_due = BigDecimal(element.text)
when "AmountPaid" then credit_note.amount_paid = BigDecimal(element.text)
when "AmountCredited" then credit_note.amount_credited = BigDecimal(element.text)
when "Allocations" then element.children.each { |allocation| credit_note.allocations << Allocation.from_xml(allocation) }
end
end
credit_note
Expand Down
43 changes: 34 additions & 9 deletions lib/xero_gateway/gateway.rb
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ def get_invoices(options = {})
request_params[:InvoiceID] = options[:invoice_id] if options[:invoice_id]
request_params[:InvoiceNumber] = options[:invoice_number] if options[:invoice_number]
request_params[:order] = options[:order] if options[:order]
request_params[:Statuses] = [*options[:status]].join(',') if options[:status]
request_params[:ModifiedAfter] = options[:modified_since] if options[:modified_since]
request_params[:IDs] = Array(options[:invoice_ids]).join(",") if options[:invoice_ids]
request_params[:InvoiceNumbers] = Array(options[:invoice_numbers]).join(",") if options[:invoice_numbers]
Expand All @@ -178,7 +179,9 @@ def get_invoices(options = {})

response_xml = http_get(@client, "#{@xero_url}/Invoices", request_params)

parse_response(response_xml, {:request_params => request_params}, {:request_signature => 'GET/Invoices'})
parse_response(response_xml,
{ request_params: request_params },
request_signature: "GET/Invoices#{'p' if options[:page]}")
end

# Retrieves a single invoice
Expand Down Expand Up @@ -291,19 +294,21 @@ def create_invoices(invoices)
#
# Note : modified_since is in UTC format (i.e. Brisbane is UTC+10)
def get_credit_notes(options = {})


parse_options = options.delete(:parse_options) || {}
request_params = {}

request_params[:CreditNoteID] = options[:credit_note_id] if options[:credit_note_id]
request_params[:CreditNoteNumber] = options[:credit_note_number] if options[:credit_note_number]
request_params[:order] = options[:order] if options[:order]
request_params[:ModifiedAfter] = options[:modified_since] if options[:modified_since]

request_params[:Statuses] = [*options[:status]].join(',') if options[:status]
request_params[:where] = options[:where] if options[:where]
request_params[:page] = options[:page] if options[:page]

response_xml = http_get(@client, "#{@xero_url}/CreditNotes", request_params)

parse_response(response_xml, {:request_params => request_params}, {:request_signature => 'GET/CreditNotes'})
parse_response(response_xml, {:request_params => request_params}, {:request_signature => 'GET/CreditNotes', :parse_options => parse_options})
end

# Retrieves a single credit_note
Expand Down Expand Up @@ -356,8 +361,8 @@ def build_credit_note(credit_note = {})
# create_credit_note(credit_note)
def create_credit_note(credit_note)
request_xml = credit_note.to_xml
response_xml = http_put(@client, "#{@xero_url}/CreditNotes", request_xml)
response = parse_response(response_xml, {:request_xml => request_xml}, {:request_signature => 'PUT/credit_note'})
response_xml = http_post(@client, "#{@xero_url}/CreditNotes", request_xml)
response = parse_response(response_xml, {:request_xml => request_xml}, {:request_signature => 'POST/credit_note'})

# Xero returns credit_notes inside an <CreditNotes> tag, even though there's only ever
# one for this request
Expand All @@ -370,6 +375,14 @@ def create_credit_note(credit_note)
response
end

def allocate_credit_note(credit_note_id, invoice_id, allocated_amount)
allocation = XeroGateway::Allocation.new(invoice_id: invoice_id,
applied_amount: allocated_amount)
request_xml = allocation.to_xml
response_xml = http_put(@client, "#{@xero_url}/CreditNotes/#{credit_note_id}/Allocations", request_xml)
parse_response(response_xml, {:request_xml => request_xml}, {:request_signature => 'PUT/credit_note'})
end

#
# Creates an array of credit_notes with a single API request.
#
Expand All @@ -385,9 +398,9 @@ def create_credit_notes(credit_notes)
end
}

response_xml = http_put(@client, "#{@xero_url}/CreditNotes", request_xml, {})
response_xml = http_post(@client, "#{@xero_url}/CreditNotes", request_xml, {})

response = parse_response(response_xml, {:request_xml => request_xml}, {:request_signature => 'PUT/credit_notes'})
response = parse_response(response_xml, {:request_xml => request_xml}, {:request_signature => 'POST/credit_notes'})
response.credit_notes.each_with_index do | response_credit_note, index |
credit_notes[index].credit_note_id = response_credit_note.credit_note_id if response_credit_note && response_credit_note.credit_note_id
end
Expand Down Expand Up @@ -573,6 +586,14 @@ def get_items
parse_response(response_xml, {}, {:request_signature => 'GET/items'})
end

def build_payment(payment = {})
case payment
when Payment then payment.gateway = self
when Hash then payment = Payment.new(payment.merge({:gateway => self}))
end
payment
end

#
# Create Payment record in Xero
#
Expand Down Expand Up @@ -760,6 +781,7 @@ def save_manual_journal(manual_journal)

def parse_response(raw_response, request = {}, options = {})

parse_options = options.delete(:parse_options) || {}
response = XeroGateway::Response.new

doc = REXML::Document.new(raw_response, :ignore_whitespace_nodes => :all)
Expand Down Expand Up @@ -793,6 +815,9 @@ def parse_response(raw_response, request = {}, options = {})
response.response_item << ManualJournal.from_xml(child, self, {:journal_lines_downloaded => options[:request_signature] != "GET/ManualJournals"})
end
when "CreditNotes" then element.children.each {|child| response.response_item << CreditNote.from_xml(child, self, {:line_items_downloaded => options[:request_signature] != "GET/CreditNotes"}) }
when "Allocations" then element.children.each do |child|
response.response_item << Allocation.from_xml(child)
end
when "Accounts" then element.children.each {|child| response.response_item << Account.from_xml(child) }
when "TaxRates" then element.children.each {|child| response.response_item << TaxRate.from_xml(child) }
when "Items" then element.children.each {|child| response.response_item << Item.from_xml(child) }
Expand All @@ -815,7 +840,7 @@ def parse_response(raw_response, request = {}, options = {})
end if response_element

# If a single result is returned don't put it in an array
if response.response_item.is_a?(Array) && response.response_item.size == 1
if response.response_item.is_a?(Array) && response.response_item.size == 1 && !parse_options[:dont_flatten]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good solution for now, but we should really build up a list of the entities that are "flattenable" and those that just happen to be returning a single value :)

response.response_item = response.response_item.first
end

Expand Down
12 changes: 9 additions & 3 deletions lib/xero_gateway/invoice.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ class Invoice
# All accessible fields
attr_accessor :invoice_id, :invoice_number, :invoice_type, :invoice_status, :date, :due_date, :reference, :branding_theme_id,
:line_amount_types, :currency_code, :currency_rate, :payments, :fully_paid_on, :amount_due, :amount_paid, :amount_credited,
:sent_to_contact, :url, :updated_date_utc
:sent_to_contact, :url, :updated_at, :credit_note_allocations
attr_writer :contact, :line_items

def initialize(params = {})
Expand All @@ -58,6 +58,7 @@ def initialize(params = {})
end

@line_items ||= []
@credit_note_allocations ||= []
end

# Validate the Address record according to what will be valid by the gateway.
Expand Down Expand Up @@ -213,8 +214,8 @@ def self.from_xml(invoice_element, gateway = nil, options = {})
when "Contact" then invoice.contact = Contact.from_xml(element)
when "Date" then invoice.date = parse_date(element.text)
when "DueDate" then invoice.due_date = parse_date(element.text)
when "UpdatedDateUTC" then invoice.updated_at = parse_date_time(element.text)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changing this probably makes the PR a breaking change unfortunately. Do you think it would be worth maintaining the updated_date_utc API (i.e aliasing it?)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well the reason for the change was simply that the attr_accessor is for updated_at and not updated_at_utc.

when "FullyPaidOnDate" then invoice.fully_paid_on = parse_date(element.text)
when "UpdatedDateUTC" then invoice.updated_date_utc = parse_date(element.text)
when "Status" then invoice.invoice_status = element.text
when "Reference" then invoice.reference = element.text
when "BrandingThemeID" then invoice.branding_theme_id = element.text
Expand All @@ -223,13 +224,18 @@ def self.from_xml(invoice_element, gateway = nil, options = {})
when "SubTotal" then invoice.sub_total = BigDecimal(element.text)
when "TotalTax" then invoice.total_tax = BigDecimal(element.text)
when "Total" then invoice.total = BigDecimal(element.text)
when "Payments" then element.children.each { | payment | invoice.payments << Payment.from_xml(payment) }
when "Payments" then element.children.each do |payment|
invoice.payments << Payment.from_xml(payment).tap { |p| p.currency_code = invoice.currency_code }
end
when "AmountDue" then invoice.amount_due = BigDecimal(element.text)
when "AmountPaid" then invoice.amount_paid = BigDecimal(element.text)
when "AmountCredited" then invoice.amount_credited = BigDecimal(element.text)
when "SentToContact" then invoice.sent_to_contact = (element.text.strip.downcase == "true")
when "Url" then invoice.url = element.text
when "ValidationErrors" then invoice.errors = element.children.map { |error| Error.parse(error) }
when "CreditNotes" then element.children.each do |credit_note|
invoice.credit_note_allocations << Allocation.from_xml(credit_note)
end
end
end
invoice
Expand Down
3 changes: 2 additions & 1 deletion lib/xero_gateway/item.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ class Item < BaseRecord
"PurchaseDetails" => {
"UnitPrice" => :float,
"AccountCode" => :string
}
},
'UpdatedDateUTC' => :datetime
})
end
end
Loading