Skip to content
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

Mysql support and other fixes #11

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
35 changes: 26 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,32 +10,49 @@ GnuCash invoice printer for human beings.

## Usage

GnuCash invoice printer currently supports SQLite and the usage is dead simple.
GnuCash invoice printer currently supports SQLite and MySQL (gnucash-3.x) and
the usage is dead simple.

$ gnucash-invoice --help


List all invoices that can be printed:

$ gnucash-invoice --dbpath /path/to/db.sqlite3
$ gnucash-invoice --db-path /path/to/db.sqlite3

or:

$ gnucash-invoice -a mysql -h db.somewhere.net -d dbname -u user -p sekreet
Copy link
Owner

Choose a reason for hiding this comment

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

Probably it's better to change to simply accept --db argument that will be a URL like:

sqlite:///path/to/db.sqlite3
mysql://user:pass@db.somewherenet/dbname

Copy link
Owner

Choose a reason for hiding this comment

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

Not saying I insist. Just a thought,


#00004 Customer A (2012-11-24)
#00005 Customer B (2012-11-24)
-----------------------------------------------------------------------------
Invoice ID Customer Reference Opened at Posted at Due at
-----------------------------------------------------------------------------
0001 Customer A Order #01 2016-12-31 2016-12-31 2017-01-30
0002 Customer B Service #02 2017-03-30 2017-03-30 2017-04-29
...

Print out invoice by it's number:

$ gnucash-invoice --dbpath /path/to/db.sqlite3 00004 > invoice-00004.html

Print out invoice by it's number (ID) -- output is HTML:

$ gnucash-invoice --db-path /path/to/db.sqlite3 00004 > invoice-00004.html


Here's how generated invoice will look like:

---

![Example](https://pbs.twimg.com/media/A_PnovSCYAAsQBy.png:large)
![Example output](https://pbs.twimg.com/media/A_PnovSCYAAsQBy.png:large)

---


Tested on Funtoo Linux as of 2018-07-26 with GnuCash-3.1 and Ruby-2.4.3 (RubyGems-2.6.14)


## TODO

* TESTS!!!
* Fix models backend to allow specify db host:port
* Fix models backend to allow specify db host:port [OK]
* ??? Add backend for old-school XML format
* Use currency from an account book instead of hardcoded
* ??? Export into FreshBooks
Expand Down
2 changes: 2 additions & 0 deletions gnucash-invoice.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ Gem::Specification.new do |gem|
gem.summary = "gnucash-invoice-#{GnuCash::Invoice::VERSION}"
gem.description = "GnuCash invoice printer for human beings."

gem.add_dependency "date", "~> 1.0.0"
gem.add_dependency "mysql2", "~> 0.5.2"
gem.add_dependency "sequel", "~> 3.41"
gem.add_dependency "slim", "~> 1.3"
gem.add_dependency "sass", "~> 3.2"
Expand Down
6 changes: 6 additions & 0 deletions lib/config.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# constants
DATE_FMT = "%Y-%m-%d".freeze # ISO
Copy link
Owner

Choose a reason for hiding this comment

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

Can you move those constants under GnuCash module? And place them inside of lib/gnucash.rb?

Copy link
Owner

Choose a reason for hiding this comment

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

I'm fine if you will move it to lib/gnucash/constants.rb though or lib/gnucash/core.rb for example.

INVOICE_LIST_FMT = "%-16s %-32s %-32s %-12s %-12s %-10s".freeze
INVOICE_LIST_HDR = ('-' * 119).freeze
TIMESTAMP_MSQL_FMT = "%Y%m%d %H%M%S %z".freeze # GnuCash-3.x
TIMESTAMP_SQLT_FMT = "%Y-%m-%d %H:%M:%S".freeze
25 changes: 23 additions & 2 deletions lib/gnucash.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,35 @@

module GnuCash
class NoDatabaseConnection < StandardError; end
class NoDatabaseFound < StandardError; end

class << self
def connection
@connection || fail(NoDatabaseConnection)
@connection || raise(NoDatabaseConnection)
end

def connect!(db)
@connection = Sequel.connect "sqlite://#{db}"
# must check path *before* connecting
# File.open(db)
raise(NoDatabaseFound, "File not found or unreadable") unless File.readable?(db)
@connection = Sequel.connect(
adapter: 'sqlite',
database: db,
test: true
)
end

def connect_mysql!(hostport, user, password, db)
hp = hostport.split(':')
@connection = Sequel.connect(
adapter: 'mysql2',
user: user,
host: hp[0],
port: hp[1], # always ignored on GNU/Linux when host == 'localhost'
database: db,
password: password,
test: true
)
end

def root
Expand Down
20 changes: 10 additions & 10 deletions lib/gnucash/invoice.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# internal
require "config"
require "gnucash"
require "gnucash/invoice/version"
require "gnucash/invoice/entry"
Expand All @@ -13,16 +14,15 @@ module GnuCash
class Invoice
class InvoiceNotFound < StandardError; end

DATE_FMT = "%Y/%m/%d"

include Timestamps

attr_reader :raw, :id, :opened_at, :posted_at, :notes
attr_reader :raw, :id, :bid, :opened_at, :posted_at, :notes

def initialize(data)
@raw = data

@id = data[:id]
@bid = data[:billing_id]
@opened_at = from_timestamp data[:date_opened]
@posted_at = from_timestamp data[:date_posted]
@notes = data[:notes]
Expand Down Expand Up @@ -76,23 +76,23 @@ def due_date?
end

def to_s
open_date = "(#{opened_at.strftime DATE_FMT})"
post_date = "[#{posted_at.strftime DATE_FMT}]" if posted?
due_date = "X #{self.due_date.strftime DATE_FMT}" if due_date?
open_date = opened_at.strftime(DATE_FMT)
post_date = posted_at.strftime(DATE_FMT) if posted?
due_date = self.due_date.strftime(DATE_FMT) if due_date?

format "%-16s %-32s %s %s %s",
id, customer.name, open_date, post_date, due_date
format INVOICE_LIST_FMT,
id, customer.name, bid, open_date, post_date, due_date
end

class << self
def all
dataset.map { |data| new(data) }
dataset.order(:id).map { |data| new(data) }
end

def find(id)
data = dataset.where(:id => id).first

fail InvoiceNotFound, "ID: #{id}" unless data
abort(InvoiceNotFound, "ID: #{id}") unless data

new data
end
Expand Down
115 changes: 96 additions & 19 deletions lib/gnucash/invoice/runner.rb
Original file line number Diff line number Diff line change
@@ -1,19 +1,39 @@
# stdlib
require "optparse"
require "config"

module GnuCash
class Invoice
module Runner
class << self

USAGE = "Usage:\n\n gnucash-invoice [OPTIONS] [invoice-ID]\n\nOptions:\n\n"
EXAMPLE = "\nExamples:

$ gnucash-invoice -d path-to-sqlitedb inv-1234
$ gnucash-invoice -a mysql -h db.somewhere.net -d dbname -u user -p sekreet
"

def run(argv)

options = parse argv

unless options[:db]
puts "ERROR: No database selected"
exit 1
case options[:adapter]
when 'sqlite'
GnuCash.connect! options[:db]
when 'mysql'
GnuCash.connect_mysql!(
options[:host], options[:user],
options[:password], options[:db]
)
else
abort("ERROR: #{options[:adapter]}: unsupported DB adapter")
end

GnuCash.connect! options[:db]
# Useless?
# unless options[:db]
# abort("ERROR: No database selected for #{options[:adapter]}")
# end

if options[:invoice_id]
# Print out invoice
Expand All @@ -22,38 +42,95 @@ def run(argv)
end

# Show known invoices
puts INVOICE_LIST_HDR
puts INVOICE_LIST_FMT % [
'Invoice ID', 'Customer', 'Reference', 'Opened at', 'Posted at',
'Due at'
]
puts INVOICE_LIST_HDR
Copy link
Owner

Choose a reason for hiding this comment

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

I don't think these two should be constants.

I think we should add teminal-table gem instead and use it here:

puts Terminal::Table.new({
  :headings => ['Invoice ID', 'Customer', 'Reference', 'Opened at', 'Posted at', 'Due at']
  :rows => Invoice.all.map { |invoice| [invoice.id, ...] }
})

Invoice.all.each { |invoice| puts invoice }

exit 0
rescue Sequel::DatabaseConnectionError
puts "ERROR: Can't connect to database: #{options[:db].inspect}"
exit 2
rescue => e
puts "ERROR: #{e}"
exit 3

rescue OptionParser::ParseError, OptionParser::MissingArgument => e
abort("ERROR: #{e.message}\n#{USAGE} (see #{$0} -H)")

rescue GnuCash::NoDatabaseFound,
GnuCash::NoDatabaseConnection,
Sequel::DatabaseConnectionError => e
abort("ERROR: #{options[:db].inspect}: Can't connect to database: #{e}")
Copy link
Owner

Choose a reason for hiding this comment

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

It's better to use wording:

abort("ERROR: Can't connect to database #{options[:db].inspect}\n#{e}")


# other exceptions beyond this point are *bugs*
# rescue => e
# abort("ERROR: #{options[:db].inspect}: #{e}")

Copy link
Owner

Choose a reason for hiding this comment

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

Just remove this then :D

end

private

def parse(argv)
options = { :template => "templates" }
options = {
:adapter => "sqlite",
:host => "localhost",
:template => "templates",
:user => "root"
}

OptionParser.new do |opts|
opts.on("--dbpath [DATABASE]",
"SQLite database path") do |path|
puts "WARNING: --dbpath is deprecated use --db-path instead"
options[:db] = path

opts.banner = USAGE


# validation by regex looks broken :-(
# <https://bugs.ruby-lang.org/issues/14728>
# <https://bugs.ruby-lang.org/issues/10021>
opts.on(
'-a', '--adapter', '=ADAPTER', %w[sqlite mysql],
'DB adapter, one in [mysql,sqlite]. Default "sqlite"'
) do |adapter|
options[:adapter] = adapter
end

opts.on("-d", "--db-path [DATABASE]",
"SQLite database path") do |path|
opts.on(
'-d', '--db-path', '=DATABASE',
'DB path (SQLite) OR name (MySQL)',
) do |path|
options[:db] = path
end

opts.on("-t", "--template [TEMPLATE]",
"Template directory path") do |path|
opts.on(
'-t', '--template', '=TEMPLATE',
'Template directory path'
) do |path|
options[:template] = path
end

opts.on(
'-h', '--host', '=HOST[:PORT]',
'MySQL host. Default: "localhost" (PORT ignored with this)'
) do |host|
# TO-DO: canonicalize URI?
options[:host] = host
end

opts.on(
'-u', '--user', '=USER', 'MySQL DB user. Default: "root"'
) do |user|
options[:user] = user
end

opts.on(
'-p', '--password', '=PASSWORD', 'MySQL DB password'
) do |password|
options[:password] = password
end

opts.on('-H', '--help', 'Prints this help') do
puts opts
puts EXAMPLE
exit
end

opts.parse! argv
end

Expand Down
4 changes: 2 additions & 2 deletions lib/gnucash/options.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ class << self
def [](key)
data = dataset.where(:name => "options/#{key}").first

return "[Unkown option #{key}" unless data
return "[Unkown option '#{key}']" unless data

case data[:slot_type]
when 4 then data[:string_val]
else "[Unknown data type for #{key}]"
else "[Unknown data type for '#{key}']"
end
end

Expand Down
8 changes: 5 additions & 3 deletions lib/gnucash/timestamps.rb
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
require "config"

module GnuCash
module Timestamps
FORMAT = "%Y%m%d%H%M%S"

def from_timestamp(value)
case value
when ::DateTime then value
when ::DateTime, ::Time then value # GnuCash-3.x
when ::Numeric, /\A\d+\z/ then parse value.to_s
when ::String, /\A\d+\z/ then parse value
else fail "#{value}: don't know how to treat this timestamp"
end
end

private

def parse(str)
::DateTime.strptime(str, FORMAT).to_time
::DateTime.strptime(str, TIMESTAMP_SQLT_FMT).to_time
end
end
end
Loading