Files
dolibarr_shine_reconciliation/lib/reconciliation/dolibarr_fetcher.rb
Kevin Bataille 4decb3cb3c Initial implementation of the reconciliation script
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>
2026-02-26 00:23:07 +01:00

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