class LucaDeal::Invoice

Public Class Methods

new(date = nil) click to toggle source
# File lib/luca_deal/invoice.rb, line 18
def initialize(date = nil)
  @date = issue_date(date)
end

Public Instance Methods

compose_mail(dat, mode: nil, attachment: :html) click to toggle source
# File lib/luca_deal/invoice.rb, line 52
def compose_mail(dat, mode: nil, attachment: :html)
  @company = set_company
  invoice_vars(dat)

  mail = Mail.new
  mail.to = dat.dig('customer', 'to') if mode.nil?
  mail.subject = CONFIG.dig('invoice', 'mail_subject') || 'Your Invoice is available'
  if mode == :preview
    mail.cc = CONFIG.dig('mail', 'preview') || CONFIG.dig('mail', 'from')
    mail.subject = '[preview] ' + mail.subject
  end
  mail.text_part = Mail::Part.new(body: render_erb(search_template('invoice-mail.txt.erb')), charset: 'UTF-8')
  mail.attachments[attachment_name(dat, attachment)] = render_invoice(attachment)
  mail
end
deliver_mail(attachment_type = nil, mode: nil) click to toggle source
# File lib/luca_deal/invoice.rb, line 22
def deliver_mail(attachment_type = nil, mode: nil)
  invoices = self.class.asof(@date.year, @date.month)
  raise "No invoice for #{@date.year}/#{@date.month}" if invoices.count.zero?

  invoices.each do |dat, path|
    next if has_status?(dat, 'mail_delivered')

    deliver_one(dat, path, mode: mode, attachment_type: attachment_type)
  end
end
due_date(date) click to toggle source

TODO: support due_date variation

# File lib/luca_deal/invoice.rb, line 249
def due_date(date)
  next_month = date.next_month
  Date.new(next_month.year, next_month.month, -1)
end
export_json() click to toggle source
# File lib/luca_deal/invoice.rb, line 150
def export_json
  labels = export_labels
  [].tap do |res|
    self.class.asof(@date.year, @date.month) do |dat|
      item = {}
      item['date'] = dat['issue_date']
      item['debit'] = []
      item['credit'] = []
      dat['subtotal'].map do |sub|
        if readable(sub['items']) != 0
          item['debit'] << { 'label' => labels[:debit][:items], 'amount' => readable(sub['items']) }
          item['credit'] << { 'label' => labels[:credit][:items], 'amount' => readable(sub['items']) }
        end
        if readable(sub['tax']) != 0
          item['debit'] << { 'label' => labels[:debit][:tax], 'amount' => readable(sub['tax']) }
          item['credit'] << { 'label' => labels[:credit][:tax], 'amount' => readable(sub['tax']) }
        end
      end
      item['x-customer'] = dat['customer']['name'] if dat.dig('customer', 'name')
      item['x-editor'] = 'LucaDeal'
      res << item
    end
    puts JSON.dump(res)
  end
end
gen_invoice!(invoice) click to toggle source
# File lib/luca_deal/invoice.rb, line 238
def gen_invoice!(invoice)
  id = invoice.dig('contract_id')
  self.class.create(invoice, date: @date, codes: Array(id))
end
get_customer(id) click to toggle source
# File lib/luca_deal/invoice.rb, line 211
def get_customer(id)
  {}.tap do |res|
    Customer.find(id) do |dat|
      customer = parse_current(dat)
      res['id'] = customer['id']
      res['name'] = customer.dig('name')
      res['address'] = customer.dig('address')
      res['address2'] = customer.dig('address2')
      res['to'] = customer.dig('contacts').map { |h| take_current(h, 'mail') }.compact
    end
  end
end
get_products(products) click to toggle source
# File lib/luca_deal/invoice.rb, line 224
def get_products(products)
  return [] if products.nil?

  [].tap do |res|
    products.each do |product|
      Product.find(product['id'])['items'].each do |item|
        item['product_id'] = product['id']
        item['qty'] ||= 1
        res << item
      end
    end
  end
end
invoice_object(contract) click to toggle source
# File lib/luca_deal/invoice.rb, line 193
def invoice_object(contract)
  {}.tap do |invoice|
    invoice['contract_id'] = contract['id']
    invoice['customer'] = get_customer(contract.dig('customer_id'))
    invoice['due_date'] = due_date(@date)
    invoice['issue_date'] = @date
    invoice['sales_fee'] = contract['sales_fee'] if contract.dig('sales_fee')
    invoice['items'] = get_products(contract['products'])
                         .concat(contract['items']&.map { |i| i['qty'] ||= 1; i } || [])
                         .compact
    invoice['items'].reject! do |item|
      item.dig('type') == 'initial' && subsequent_month?(contract.dig('terms', 'effective'))
    end
    invoice['subtotal'] = subtotal(invoice['items'])
                            .map { |k, v| v.tap { |dat| dat['rate'] = k } }
  end
end
issue_date(date) click to toggle source
# File lib/luca_deal/invoice.rb, line 243
def issue_date(date)
  base =  date.nil? ? Date.today : Date.parse(date)
  Date.new(base.year, base.month, -1)
end
monthly_invoice(target = 'monthly') click to toggle source
# File lib/luca_deal/invoice.rb, line 183
def monthly_invoice(target = 'monthly')
  Contract.new(@date.to_s).active do |contract|
    next if contract.dig('terms', 'billing_cycle') != target
    # TODO: provide another I/F for force re-issue if needed
    next if duplicated_contract? contract['id']

    gen_invoice!(invoice_object(contract))
  end
end
preview_mail(attachment_type = nil) click to toggle source
# File lib/luca_deal/invoice.rb, line 33
def preview_mail(attachment_type = nil)
  invoices = self.class.asof(@date.year, @date.month)
  raise "No invoice for #{@date.year}/#{@date.month}" if invoices.count.zero?

  invoices.each do |dat, path|
    deliver_one(dat, path, mode: :preview, attachment_type: attachment_type)
  end
end
preview_stdout() click to toggle source

Render HTML to console

# File lib/luca_deal/invoice.rb, line 44
def preview_stdout
  self.class.asof(@date.year, @date.month) do |dat, _|
    @company = set_company
    invoice_vars(dat)
    puts render_invoice
  end
end
render_invoice(file_type = :html) click to toggle source
# File lib/luca_deal/invoice.rb, line 68
def render_invoice(file_type = :html)
  case file_type
  when :html
    render_erb(search_template('invoice.html.erb'))
  when :pdf
    erb2pdf(search_template('invoice.html.erb'))
  else
    raise 'This filetype is not supported.'
  end
end
single_invoice(contract_id) click to toggle source
# File lib/luca_deal/invoice.rb, line 176
def single_invoice(contract_id)
  contract = Contract.find(contract_id)
  raise "Invoice already exists for #{contract_id}. exit" if duplicated_contract? contract['id']

  gen_invoice!(invoice_object(contract))
end
stats(count = 1) click to toggle source

Output seriarized invoice data to stdout. Returns previous N months on multiple count

Example YAML output

---
- records:
  - customer: Example Co.
    subtotal: 100000
    tax: 10000
    due: 2020-10-31
    issue_date: '2020-09-30'
  count: 1
  total: 100000
  tax: 10000
# File lib/luca_deal/invoice.rb, line 94
def stats(count = 1)
  [].tap do |collection|
    scan_date = @date.next_month
    count.times do
      scan_date = scan_date.prev_month
      {}.tap do |stat|
        stat['records'] = self.class.asof(scan_date.year, scan_date.month).map do |invoice|
          amount = invoice['subtotal'].inject(0) { |sum, sub| sum + sub['items'] }
          tax = invoice['subtotal'].inject(0) { |sum, sub| sum + sub['tax'] }
          {
            'customer' => invoice.dig('customer', 'name'),
            'subtotal' => amount,
            'tax' => tax,
            'due' => invoice.dig('due_date'),
            'mail' => invoice.dig('status')&.select { |a| a.keys.include?('mail_delivered') }&.first
          }
        end
        stat['issue_date'] = scan_date.to_s
        stat['count'] = stat['records'].count
        stat['total'] = stat['records'].inject(0) { |sum, rec| sum + rec.dig('subtotal') }
        stat['tax'] = stat['records'].inject(0) { |sum, rec| sum + rec.dig('tax') }
        collection << readable(stat)
      end
    end
  end
end
stats_email() click to toggle source

send payment list to preview address or from address.

# File lib/luca_deal/invoice.rb, line 123
def stats_email
  {}.tap do |res|
    stats(3).each.with_index(1) do |stat, i|
      stat['records'].each do |record|
        res[record['customer']] ||= {}
        res[record['customer']]['customer_name'] ||= record['customer']
        res[record['customer']]["amount#{i}"] ||= record['subtotal']
        res[record['customer']]["tax#{i}"] ||= record['tax']
      end
      if i == 1
        @issue_date = stat['issue_date']
        @total_amount = stat['total']
        @total_tax = stat['tax']
        @total_count = stat['count']
      end
    end
    @invoices = res.values
  end
  @company = CONFIG.dig('company', 'name')

  mail = Mail.new
  mail.to = CONFIG.dig('mail', 'preview') || CONFIG.dig('mail', 'from')
  mail.subject = 'Check monthly payment list'
  mail.html_part = Mail::Part.new(body: render_erb(search_template('monthly-payment-list.html.erb')), content_type: 'text/html; charset=UTF-8')
  LucaSupport::Mail.new(mail, PJDIR).deliver
end

Private Instance Methods

attachment_name(dat, type) click to toggle source
# File lib/luca_deal/invoice.rb, line 329
def attachment_name(dat, type)
  id = %r{/}.match(dat['id']) ? dat['id'].gsub('/', '') : dat['id'][0, 7]
  "invoice-#{id}.#{type}"
end
deliver_one(invoice, path, mode: nil, attachment_type: nil) click to toggle source
# File lib/luca_deal/invoice.rb, line 267
def deliver_one(invoice, path, mode: nil, attachment_type: nil)
  attachment_type ||= CONFIG.dig('invoice', 'attachment') || :html
  mail = compose_mail(invoice, mode: mode, attachment: attachment_type.to_sym)
  LucaSupport::Mail.new(mail, PJDIR).deliver
  self.class.add_status!(path, 'mail_delivered') if mode.nil?
end
duplicated_contract?(id) click to toggle source
# File lib/luca_deal/invoice.rb, line 334
def duplicated_contract?(id)
  self.class.asof(@date.year, @date.month, @date.day) do |_f, path|
    return true if path.include?(id)
  end
  false
end
export_labels() click to toggle source

TODO: load labels from CONFIG before country defaults

# File lib/luca_deal/invoice.rb, line 280
def export_labels
  case CONFIG['country']
  when 'jp'
    {
      debit: { items: '売掛金', tax: '売掛金' },
      credit: { items: '売上高', tax: '売上高' }
    }
  else
    {
      debit: { items: 'Accounts receivable - trade', tax: 'Accounts receivable - trade' },
      credit: { items: 'Amount of Sales', tax: 'Amount of Sales' }
    }
  end
end
invoice_vars(invoice_dat) click to toggle source

set variables for ERB template

# File lib/luca_deal/invoice.rb, line 258
def invoice_vars(invoice_dat)
  @customer = invoice_dat['customer']
  @items = readable(invoice_dat['items'])
  @subtotal = readable(invoice_dat['subtotal'])
  @issue_date = invoice_dat['issue_date']
  @due_date = invoice_dat['due_date']
  @amount = readable(invoice_dat['subtotal'].inject(0) { |sum, i| sum + i['items'] + i['tax'] })
end
lib_path() click to toggle source
# File lib/luca_deal/invoice.rb, line 274
def lib_path
  __dir__
end
load_tax_rate(name) click to toggle source

load Tax Rate from config.

# File lib/luca_deal/invoice.rb, line 323
def load_tax_rate(name)
  return 0 if CONFIG.dig('tax_rate', name).nil?

  BigDecimal(take_current(CONFIG['tax_rate'], name).to_s)
end
set_company() click to toggle source

load user company profile from config.

# File lib/luca_deal/invoice.rb, line 297
def set_company
  {}.tap do |h|
    h['name'] = CONFIG.dig('company', 'name')
    h['address'] = CONFIG.dig('company', 'address')
    h['address2'] = CONFIG.dig('company', 'address2')
  end
end
subsequent_month?(effective_date) click to toggle source
# File lib/luca_deal/invoice.rb, line 341
def subsequent_month?(effective_date)
  effective_date = Date.parse(effective_date) unless effective_date.respond_to? :year
  effective_date.year != @date.year || effective_date.month != @date.month
end
subtotal(items) click to toggle source

calc items & tax amount by tax category

# File lib/luca_deal/invoice.rb, line 307
def subtotal(items)
  {}.tap do |subtotal|
    items.each do |i|
      rate = i.dig('tax') || 'default'
      qty = i['qty'] || BigDecimal('1')
      subtotal[rate] = { 'items' => 0, 'tax' => 0 } if subtotal.dig(rate).nil?
      subtotal[rate]['items'] += i['price'] * qty
    end
    subtotal.each do |rate, amount|
      amount['tax'] = (amount['items'] * load_tax_rate(rate))
    end
  end
end