Standalone Ruby script reconciling GoCardless payments, Dolibarr invoices (via API), and Shine bank statements. Three-pass engine: GC↔Dolibarr matching, open invoice audit, payout↔bank verification. Includes dry-run and --fix mode to auto-mark Dolibarr invoices as paid. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
116 lines
3.8 KiB
Ruby
116 lines
3.8 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require "date"
|
|
|
|
module Reconciliation
|
|
class DolibarrFetcher
|
|
# Dolibarr invoice statuses
|
|
STATUS_DRAFT = 0
|
|
STATUS_VALIDATED = 1
|
|
STATUS_PAID = 2
|
|
STATUS_CANCELLED = 3
|
|
|
|
Invoice = Struct.new(
|
|
:id, # Dolibarr internal ID
|
|
:ref, # Invoice reference, e.g. "FA2407-0003"
|
|
:status, # 0=draft, 1=validated/open, 2=paid, 3=cancelled
|
|
:total_ttc, # Total amount TTC (euros, float)
|
|
:amount_cents, # total_ttc * 100, rounded (integer)
|
|
:paye, # 0=unpaid, 1=paid
|
|
:sumpayed, # Amount already paid (euros)
|
|
:remain_to_pay, # Remaining balance (euros)
|
|
:date, # Invoice date (Date)
|
|
:due_date, # Due date (Date)
|
|
:customer_id, # socid
|
|
:customer_name, # nom of third party if present
|
|
keyword_init: true
|
|
)
|
|
|
|
def initialize(client, from:, to:)
|
|
@client = client
|
|
@from = from
|
|
@to = to
|
|
end
|
|
|
|
# Fetches all validated invoices and resolves customer names.
|
|
# Note: querying status=1 returns all non-draft invoices in Dolibarr (open, paid, cancelled).
|
|
# Cancelled invoices are filtered out here.
|
|
def fetch_invoices
|
|
$stderr.puts "[DolibarrFetcher] Fetching thirdparty names..."
|
|
name_by_id = fetch_thirdparty_names
|
|
|
|
$stderr.puts "[DolibarrFetcher] Fetching invoices..."
|
|
raw_invoices = fetch_all(status: STATUS_VALIDATED)
|
|
|
|
invoices = raw_invoices
|
|
.map { |raw| parse_invoice(raw, name_by_id) }
|
|
.reject { |inv| inv.status == STATUS_CANCELLED }
|
|
|
|
open_count = invoices.count { |inv| inv.status == STATUS_VALIDATED }
|
|
paid_count = invoices.count { |inv| inv.status == STATUS_PAID }
|
|
$stderr.puts "[DolibarrFetcher] Total: #{invoices.size} invoices (#{open_count} open, #{paid_count} paid)"
|
|
invoices
|
|
rescue Dolibarr::Client::Error => e
|
|
$stderr.puts "[DolibarrFetcher] Error: #{e.message}"
|
|
raise
|
|
end
|
|
|
|
private
|
|
|
|
def fetch_all(status:, sqlfilters: nil)
|
|
invoices = []
|
|
page = 0
|
|
loop do
|
|
params = { status: status, limit: 100, page: page }
|
|
params[:sqlfilters] = sqlfilters if sqlfilters
|
|
batch = @client.get("/invoices", params)
|
|
break if batch.nil? || !batch.is_a?(Array) || batch.empty?
|
|
|
|
invoices.concat(batch)
|
|
break if batch.size < 100
|
|
|
|
page += 1
|
|
end
|
|
invoices
|
|
end
|
|
|
|
def parse_invoice(raw, name_by_id = {})
|
|
customer_id = raw["socid"].to_i
|
|
Invoice.new(
|
|
id: raw["id"].to_i,
|
|
ref: raw["ref"].to_s.strip,
|
|
status: raw["statut"].to_i,
|
|
total_ttc: raw["total_ttc"].to_f,
|
|
amount_cents: (raw["total_ttc"].to_f * 100).round,
|
|
paye: raw["paye"].to_i,
|
|
sumpayed: raw["sumpayed"].to_f,
|
|
remain_to_pay: raw["remaintopay"].to_f,
|
|
date: parse_unix_date(raw["datef"] || raw["date"]),
|
|
due_date: parse_unix_date(raw["date_lim_reglement"]),
|
|
customer_id: customer_id,
|
|
customer_name: name_by_id[customer_id] || raw.dig("thirdparty", "name").to_s.strip
|
|
)
|
|
end
|
|
|
|
def fetch_thirdparty_names
|
|
result = {}
|
|
page = 0
|
|
loop do
|
|
batch = @client.get("/thirdparties", limit: 100, page: page)
|
|
break if batch.nil? || !batch.is_a?(Array) || batch.empty?
|
|
batch.each { |tp| result[tp["id"].to_i] = tp["name"].to_s.strip }
|
|
break if batch.size < 100
|
|
page += 1
|
|
end
|
|
result
|
|
end
|
|
|
|
def parse_unix_date(value)
|
|
return nil if value.nil? || value.to_s.strip.empty? || value.to_i.zero?
|
|
Time.at(value.to_i).utc.to_date
|
|
rescue
|
|
nil
|
|
end
|
|
end
|
|
end
|