2017-01-30 13:06:38 +00:00
|
|
|
# coding: utf-8
|
|
|
|
require 'yaml'
|
2017-01-31 14:38:08 +00:00
|
|
|
require 'mysql2'
|
2017-02-01 10:18:38 +00:00
|
|
|
require 'optparse'
|
2017-02-06 15:57:06 +00:00
|
|
|
require 'digest'
|
2017-01-30 13:06:38 +00:00
|
|
|
|
2017-01-31 14:38:08 +00:00
|
|
|
load 'config.rb'
|
|
|
|
|
2017-02-01 10:18:38 +00:00
|
|
|
Options = {}
|
|
|
|
OptionParser.new do |opts|
|
|
|
|
opts.banner = "Usage: invoice.rb [options]"
|
|
|
|
opts.on("-d", "--draft", "Generate invoices with 'DRAFT' watermark") do |d|
|
|
|
|
Options[:draft] = d
|
|
|
|
end
|
|
|
|
opts.on("-v", "--verbose", "Run verbosely") do |v|
|
|
|
|
Options[:verbose] = v
|
|
|
|
end
|
|
|
|
opts.on("-s", "--silent", "Run silently (overrules -v)") do |s|
|
|
|
|
Options[:silent] = true
|
|
|
|
Options[:verbose] = false
|
|
|
|
end
|
2017-02-06 15:57:06 +00:00
|
|
|
opts.on("-m", "--mobilepay", "Generate QR codes for MobilePay") do |m|
|
|
|
|
require 'rqrcode'
|
2021-04-25 18:37:27 +00:00
|
|
|
Options[:mobilepay] = m
|
2017-02-06 15:57:06 +00:00
|
|
|
end
|
2017-02-01 10:18:38 +00:00
|
|
|
end.parse!
|
|
|
|
|
2021-04-25 18:37:27 +00:00
|
|
|
if Options[:draft]
|
|
|
|
Options[:mobilepay] = false
|
|
|
|
end
|
2017-02-01 12:43:05 +00:00
|
|
|
|
2017-02-01 11:43:11 +00:00
|
|
|
Transaction = Struct.new(:time, :person, :product, :amount)
|
2017-02-01 12:43:05 +00:00
|
|
|
|
2017-02-01 11:43:11 +00:00
|
|
|
Product = Struct.new(:id, :description, :price) do
|
2017-02-01 11:37:43 +00:00
|
|
|
def to_s
|
2017-02-01 11:43:11 +00:00
|
|
|
"#{id} => #{description}, #{price}"
|
2017-02-01 11:37:43 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2017-02-01 12:43:05 +00:00
|
|
|
class Person
|
2017-02-08 16:22:52 +00:00
|
|
|
def initialize(id, name, address)
|
2017-02-01 12:43:05 +00:00
|
|
|
@id = id
|
|
|
|
@name = name
|
2017-02-08 16:22:52 +00:00
|
|
|
@address = address
|
2017-02-01 12:43:05 +00:00
|
|
|
@purchases = {}
|
|
|
|
end
|
|
|
|
|
|
|
|
def to_s
|
|
|
|
id
|
|
|
|
end
|
|
|
|
|
|
|
|
attr_reader :id
|
2017-02-08 16:22:52 +00:00
|
|
|
attr_accessor :name, :purchases, :address
|
2017-02-01 12:43:05 +00:00
|
|
|
end
|
|
|
|
|
2017-01-30 13:06:38 +00:00
|
|
|
def read_file(file)
|
|
|
|
rows = Array.new
|
|
|
|
CSV.foreach(file, col_sep: ';', converters: :float) do |row|
|
2017-02-01 11:43:11 +00:00
|
|
|
rows << Transaction.new(row[0],row[1],row[2],row[3])
|
2017-01-30 13:06:38 +00:00
|
|
|
end
|
|
|
|
rows
|
|
|
|
end
|
|
|
|
|
2017-02-01 12:43:05 +00:00
|
|
|
def load_transactions_from_db(db)
|
2017-02-01 11:37:43 +00:00
|
|
|
transactions = Array.new
|
2017-02-01 12:43:05 +00:00
|
|
|
db.query("SELECT * FROM Transactions").each do |row|
|
2017-02-06 15:57:06 +00:00
|
|
|
if Date.today.monday?
|
|
|
|
previous_monday = Date.today.to_time
|
|
|
|
else
|
|
|
|
previous_monday = date_of_prev('monday').to_time
|
|
|
|
end
|
|
|
|
if previous_monday < row["time"]
|
2017-02-01 12:43:05 +00:00
|
|
|
transactions << Transaction.new(row["time"], row["buyer"],
|
|
|
|
row["product"], row["amount"])
|
2017-01-31 14:38:08 +00:00
|
|
|
end
|
|
|
|
end
|
2017-02-01 11:37:43 +00:00
|
|
|
return transactions
|
2017-01-31 14:38:08 +00:00
|
|
|
end
|
|
|
|
|
2017-02-01 12:43:05 +00:00
|
|
|
def load_persons_from_db(db)
|
|
|
|
persons = {}
|
|
|
|
db.query("SELECT * FROM Persons").each do |row|
|
2017-02-08 16:22:52 +00:00
|
|
|
address = []
|
|
|
|
if (row['address'])
|
|
|
|
address << row['address']
|
|
|
|
end
|
|
|
|
person = Person.new(row['id'],row['name'],address)
|
2017-02-06 15:57:06 +00:00
|
|
|
persons[row['id']] = person
|
|
|
|
if Options[:verbose]
|
|
|
|
puts "Found person: #{person}"
|
|
|
|
end
|
2017-02-01 12:43:05 +00:00
|
|
|
end
|
2017-02-06 15:57:06 +00:00
|
|
|
|
2017-02-01 12:43:05 +00:00
|
|
|
return persons
|
|
|
|
end
|
|
|
|
|
|
|
|
def load_products_from_db(db)
|
|
|
|
products = {}
|
|
|
|
db.query("SELECT * FROM Products").each do |p|
|
|
|
|
product = Product.new(p['id'], p['description'], p['price'])
|
|
|
|
products[p['id']] = product
|
|
|
|
if Options[:verbose]
|
|
|
|
puts "Found product: #{product}"
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
return products
|
|
|
|
end
|
|
|
|
|
2017-02-01 11:37:43 +00:00
|
|
|
# Partition transactions into the persons who they belong to
|
2017-02-01 12:43:05 +00:00
|
|
|
def partition_transactions(transactions,persons)
|
|
|
|
transactions.each do |t|
|
2017-02-01 11:37:43 +00:00
|
|
|
# Initialize data structures if not already done
|
2017-02-01 12:43:05 +00:00
|
|
|
if !(persons[t.person].purchases[t.product])
|
|
|
|
persons[t.person].purchases[t.product] = 0
|
2017-02-01 11:37:43 +00:00
|
|
|
end
|
|
|
|
|
2017-02-01 12:43:05 +00:00
|
|
|
persons[t.person].purchases[t.product] += t.amount
|
2017-01-30 13:06:38 +00:00
|
|
|
end
|
2017-02-01 11:37:43 +00:00
|
|
|
|
|
|
|
return persons
|
2017-01-30 13:06:38 +00:00
|
|
|
end
|
|
|
|
|
2017-02-06 15:57:06 +00:00
|
|
|
def generate_receipt(persons,products,db,draft)
|
2017-01-30 14:23:50 +00:00
|
|
|
counter = 0
|
2017-02-01 11:37:43 +00:00
|
|
|
|
|
|
|
# Iterate through persons who have purchased something in the given timeframe
|
2017-02-01 12:43:05 +00:00
|
|
|
persons.each do |id, person|
|
2017-02-06 15:57:06 +00:00
|
|
|
# Skip persons with no transactions
|
|
|
|
if person.purchases.empty?
|
|
|
|
if Options[:verbose]
|
|
|
|
puts "Skipping #{id} with no transactions"
|
|
|
|
end
|
|
|
|
next
|
|
|
|
end
|
2017-02-01 11:37:43 +00:00
|
|
|
|
2017-02-06 15:57:06 +00:00
|
|
|
counter += 1
|
|
|
|
total_payment = 0
|
|
|
|
|
2017-01-30 13:06:38 +00:00
|
|
|
yaml = YAML_BASE.clone
|
2017-02-08 16:22:52 +00:00
|
|
|
yaml["to"] = [person.name].concat person.address
|
2017-02-01 11:37:43 +00:00
|
|
|
yaml["invoice-nr"] = Time.now.strftime('%Y%W') + "-" + counter.to_s
|
2017-02-06 15:57:06 +00:00
|
|
|
|
|
|
|
|
2017-02-01 11:37:43 +00:00
|
|
|
|
|
|
|
# Iterate through products purchased and write data to yaml hash
|
2017-02-01 12:43:05 +00:00
|
|
|
person.purchases.each do |id, amount|
|
2017-01-30 13:06:38 +00:00
|
|
|
yaml["service"] = Array.new unless yaml["service"]
|
2017-02-06 15:57:06 +00:00
|
|
|
piece_price = products[id].price.to_i
|
|
|
|
price = piece_price.to_i*amount
|
2017-02-01 12:43:05 +00:00
|
|
|
hash = {description: products[id].description,
|
2017-02-06 15:57:06 +00:00
|
|
|
pieceprice: piece_price,
|
|
|
|
price: price, amount: amount}
|
|
|
|
total_payment += price
|
2017-02-01 11:37:43 +00:00
|
|
|
|
|
|
|
# Convert Symbols to Strings to ensure compatibility with pandoc
|
2017-01-30 13:06:38 +00:00
|
|
|
yaml["service"] << Hash[hash.map{ |k, v| [k.to_s, v] }]
|
|
|
|
end
|
2017-02-06 15:57:06 +00:00
|
|
|
total_payment = total_payment * 0.90
|
2017-01-31 15:13:26 +00:00
|
|
|
|
2017-02-01 11:37:43 +00:00
|
|
|
# Write draft watermark on invoice if marked as draft
|
2017-02-01 10:18:38 +00:00
|
|
|
if draft
|
2017-01-31 15:13:26 +00:00
|
|
|
yaml["draft"] = "true"
|
|
|
|
yaml["drafttext"] = "Udkast"
|
|
|
|
end
|
2017-02-01 11:37:43 +00:00
|
|
|
|
2017-02-06 15:57:06 +00:00
|
|
|
# Generate QR code for MobilePay if option set
|
|
|
|
if Options[:mobilepay]
|
|
|
|
mobilepay_url = "https://mobilepay.dk/da-dk/pages/betal.aspx?phone=#{MOBILEPAY_PHONE_NUMBER}&amount=#{total_payment.round(2)}&comment=Tilbagebetaling%20-%20Faktura%20nr.%20#{yaml["invoice-nr"]}&lock=1"
|
|
|
|
if Options[:verbose]
|
|
|
|
puts "Generating QR code for #{id} with URL #{mobilepay_url}"
|
|
|
|
end
|
|
|
|
yaml["qrcode"] = true
|
|
|
|
|
|
|
|
|
|
|
|
qrcode = RQRCode::QRCode.new(mobilepay_url)
|
|
|
|
image = qrcode.as_png(
|
|
|
|
resize_gte_to: false,
|
|
|
|
resize_exactly_to: false,
|
|
|
|
fill: 'white',
|
|
|
|
color: 'black',
|
|
|
|
size: 480,
|
|
|
|
border_modules: 4,
|
|
|
|
module_px_size: 6,
|
|
|
|
file: nil # path to write
|
|
|
|
)
|
|
|
|
File.open("./pandoc/qr.png","w+") do |file|
|
|
|
|
file << image
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2017-02-01 11:37:43 +00:00
|
|
|
# Convert Symbols to Strings to ensure compatibility with pandoc
|
2017-01-30 14:23:50 +00:00
|
|
|
output = Hash[yaml.map{ |k, v| [k.to_s, v] }].to_yaml
|
|
|
|
output += "---\n"
|
2017-02-06 15:57:06 +00:00
|
|
|
|
2017-02-01 11:37:43 +00:00
|
|
|
# Write output to file
|
2017-02-06 15:57:06 +00:00
|
|
|
File.open("./pandoc/details.yml","w+") do |file|
|
2017-02-01 10:18:38 +00:00
|
|
|
file << output
|
|
|
|
end
|
2017-02-01 11:37:43 +00:00
|
|
|
|
|
|
|
# Generate PDF using pandoc
|
2017-01-30 14:23:50 +00:00
|
|
|
`cd pandoc/; make -B`
|
2017-02-01 10:18:38 +00:00
|
|
|
`cp pandoc/output.pdf #{OUTPUT_PATH}/#{yaml["invoice-nr"]}.pdf`
|
2017-02-01 11:37:43 +00:00
|
|
|
|
2017-02-06 15:57:06 +00:00
|
|
|
# Upload PDF to MySQL database
|
|
|
|
File.open("pandoc/output.pdf") do |file|
|
|
|
|
data = file.read
|
|
|
|
hash = Digest::MD5.new.hexdigest data
|
|
|
|
statement = db.prepare("INSERT INTO Invoices (id, person, data, hash) VALUES (?, ?, ?, ?)
|
|
|
|
ON DUPLICATE KEY UPDATE person = ?, data = ?, hash = ?")
|
|
|
|
result = statement.execute(yaml["invoice-nr"],id,data,hash,id,data,hash)
|
|
|
|
end
|
|
|
|
|
2017-02-01 10:18:38 +00:00
|
|
|
puts "#{person}: #{yaml["invoice-nr"]}.pdf" unless Options[:silent]
|
2017-01-30 13:06:38 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2017-01-31 14:38:08 +00:00
|
|
|
def date_of_next(day)
|
|
|
|
date = Date.parse(day)
|
|
|
|
delta = date > Date.today ? 0 : 7
|
|
|
|
date + delta
|
|
|
|
end
|
|
|
|
|
|
|
|
def date_of_prev(day)
|
|
|
|
date = Date.parse(day)
|
|
|
|
delta = date < Date.today ? 0 : 7
|
|
|
|
date - delta
|
|
|
|
end
|
|
|
|
|
2017-02-01 12:43:05 +00:00
|
|
|
#################
|
2017-02-01 11:37:43 +00:00
|
|
|
|
2017-02-01 12:43:05 +00:00
|
|
|
db = Mysql2::Client.new(:host => DB_HOST, :username => DB_USER, :database => DB_DB)
|
2017-02-01 11:37:43 +00:00
|
|
|
|
2017-02-01 12:43:05 +00:00
|
|
|
transactions = load_transactions_from_db(db)
|
|
|
|
persons = load_persons_from_db(db)
|
|
|
|
products = load_products_from_db(db)
|
2017-02-01 11:37:43 +00:00
|
|
|
|
2017-02-06 15:57:06 +00:00
|
|
|
generate_receipt(partition_transactions(transactions,persons), products, db, Options[:draft])
|
|
|
|
|