www/fundraising/invoice.cgi (435 lines of code) (raw):

#!/usr/bin/env ruby # encoding: utf-8 $LOAD_PATH.unshift '/srv/whimsy/lib' require 'wunderbar' require "date" require "yaml" require 'whimsy/asf' user = ASF::Person.new($USER) unless user.asf_chair_or_member? print "Status: 401 Unauthorized\r\n" print "WWW-Authenticate: Basic realm=\"ASF Members and Officers\"\r\n\r\n" exit end HISTORY = '/var/tools/invoice' if %r{/(?<invoice>\d+)(\.\w+)?$} =~ ENV['PATH_INFO'] invoice_path = File.join(HISTORY, invoice) if File.exist? invoice_path form = YAML.load_file(invoice_path) ENV['QUERY_STRING'] = form.map {|k,v| "#{k}=#{CGI.escape(v.first)}"}.join("&") if form end end _html do _head_ do _title "ASF Invoice #{invoice}" _style %{ .c1 {padding-top:2pt;margin-right:9pt;text-align:right;padding-bottom:2pt} .c5 {vertical-align:top;width:51.8pt} .c7 {vertical-align:top;width:134.1pt;border-style:solid;background-color:#f2f2f2;border-color:#888;border-width:1pt;padding:0pt 3.6pt 0pt 3.6pt} table {border-collapse:collapse} tr {height: 15pt} .c10 {vertical-align:top;width:76.5pt;border-style:solid;border-color:#888;border-width:1pt;padding:0pt 3.6pt 0pt 3.6pt} .c11 {vertical-align:top;width:72pt;border-style:solid;border-color:#888;border-width:1pt;padding:0pt 3.6pt 0pt 3.6pt} .c13 {color:#404040} .c15 {vertical-align:top;width:396pt;border-style:solid;border-color:#888;border-width:1pt;padding:0pt 3.6pt 0pt 3.6pt} .c16 {vertical-align:top;width:67.5pt;padding:0pt 3.6pt 0pt 3.6pt} .c17 {padding-top:2pt;padding-bottom:4pt} .c18 {vertical-align:top;width:267.8pt;padding:0pt 3.6pt 0pt 3.6pt} .c20 {vertical-align:top;width:267.8pt} .c21 {text-align:right} .c23 {width:287.2pt;padding:0pt 3.6pt 0pt 3.6pt} .c26 {width:220.5pt} .c29 {padding-top:2pt;text-align:center;padding-bottom:2pt} .c30 {margin: 1em 5em} th {font-size: 10pt; font-weight: normal; } p {min-height:9pt;font-size:9pt;margin:0;font-family:Verdana} form, img {margin-left: auto; margin-right: auto; display: block; padding-bottom: 10pt} body {max-width:540pt; padding:0 36pt} input, textarea {width: 300pt} input[name=total] {background-color: #8e8; color: #000; border: outset 1px; padding: 2px; border-radius: 0.3em} input[type=button] {width: 73pt} .index {border-spacing: 20px 5px; border-collapse: separate} .index th {border-bottom: solid black} .index input[type=submit] {width: 10em; display: block; margin-left: auto; margin-right: auto} .buttons a {text-decoration: none} .buttons button {width: 100pt} } _script src: '/jquery.min.js' _script src: '/jquery-ui.min.js' end _body? do _img alt: "Logo", src: "https://id.apache.org/img/asf_logo_wide.png" _svg width: '100%', height: 30 do _path d: 'M0,0h230v14h-230z', fill: '#636' _path d: 'M245,0h230v14h-230z', fill: '#996' _path d: 'M490,0h230v14h-230z', fill: '#669' end invoice ||= @invoice_number invoice ||= Dir.chdir(HISTORY) {Dir['*'].max || 1000}.succ @invoice_number = nil if ENV['REQUEST_URI'].end_with? '?' base = ENV['REQUEST_URI'].dup base.chomp! ENV['QUERY_STRING'] base.chomp! '?' base.chomp! ENV['PATH_INFO'] if ENV['PATH_INFO'].to_s.end_with? '/' _table_.index do _thead_ do _tr do _td colspan: 4 do _form action: "#{base}/#{invoice}" do _input type: 'submit', value: 'New Invoice' end end end _tr do _th 'Invoice' _th 'Date' _th 'Customer' _th 'Amount' end end _tbody do Dir.chdir(HISTORY) do Dir['*'].sort.reverse.each do |invoice| form = YAML.load_file(File.join(HISTORY, invoice)) if form _tr_ do _td {_a invoice, href: invoice} _td Date.parse(form['date'].first) _td form['customer'].first _td.c21 form['total'].first.sub('$ ', '') end end end end end end elsif not @invoice_number and not _.pdf? start = Date.today finish = Date.new(start.year+1, start.month, start.day)-1 _form_ method: 'post', action: "#{base}/#{invoice}" do _table style: 'margin-left: auto; margin-right: auto' do _tr.presets! style: 'display: none' do _td 'Presets' _td do _input type: 'button', value: 'Bronze', 'data-amount' => 5_000 _input type: 'button', value: 'Silver', 'data-amount' => 20_000 _input type: 'button', value: 'Gold', 'data-amount' => 40_000 _input type: 'button', value: 'Platinum', 'data-amount' => 100_000 end end _tr do _td 'E-Mail' _td do _input type: 'email', name: 'email', value: @email || 'fundraising@apache.org' end end _tr do _td 'Invoice Number' _td { _input name: 'invoice_number', value: invoice} end _tr do _td 'Customer Name' _td do _input name: 'customer', value: @customer, required: true, autofocus: true end end _tr do _td 'Purchase Order #' _td { _input name: 'po_number', value: @po_number } end _tr do _td 'Bill to' _td { _textarea @bill_to, name: 'bill_to', rows: 6, required: true } end _tr do _td 'Item Description' _td do _textarea @item, name: 'item', rows: 6, required: true, placeholder: "quantity - description @ $ price" end end _tr do _td 'Amount' _td do _input name: 'total', value: @total or '$ 0' end end end _input name: 'date', type: 'hidden', value: start.strftime("%B %d, %Y") _input type: 'submit', value: 'Save', style: 'margin-top: 1em; width: 10em; display: block; margin-left: auto; margin-right: auto' end _div.instructions! do _h4 'Instructions:' _p.c30 do _'The' _em 'Amount' _ 'field contains the sum of the dollar amounts entered in the' _em 'Item Description' _ 'field.' end _p.c30 do _ 'To have the dollar amount placed in the third column of the' _ 'invoice form, place it at the end of the line preceded by an' _em '@' _ 'sign.' end _p.c30 do _ 'To enter a quantity, start the line with an integer followed by a' _em '-' _ '(dash) character.' end end _script %{ $('#presets').show(); $('#presets input[type=button]').click(function() { var amount = '$ ' + $(this).attr('data-amount'); amount = amount.toString().replace(/(\\d)(?=(\\d\\d\\d)+$)/g, "$1,"); var item = "2013 " + $(this).val() + " Sponsorship @ " + amount + "\\n\\n"; item += "Start Date: #{start.strftime("%B %d, %Y")}\\n\"; item += "End Date: #{finish.strftime("%B %d, %Y")}\\n"; $('textarea[name=item]').val(item).keyup(); if ($('input[name=customer]').val() == '') { $('input[name=customer]').focus(); } }); $('textarea[name=item]').keyup(function() { var total = 0; // Process each line in turn var lines = $(this).val().match(/[^\\r\\n]+/g); for (var i=0; lines && i<lines.length; i++) { var line = lines[i]; // Look for a $price at the end var price = line.match(/\\$\\s?([,\\d]+(\\.\\d\\d)?)$/); if (price && price.length > 0) { // Bingo, it's a price one var amt = parseFloat(price[1].replace(/,/,'')); // Did they give a quantity at the start? var qty = line.match(/^(\\d+)\\s*[\\@\\-]/); if (qty && qty.length > 0) { // This is a "quantity - text @ $price" var quantity = parseInt( qty[1] ); total += (quantity * amt); } else { // This is a "text $price" total += amt; } } } // Turn it into a $ figure with commas // TODO Support other currencies total = total.toFixed(2); total = total.replace(/(\\d)(?=(\\d\\d\\d)+[$\\.])/g, "$1,"); if ($('input[name=total]').val() != '$ ' + total) { $('input[name=total]').stop().css('backgroundColor', '#FF0'). val('$ ' + total).animate({'backgroundColor': '#8e8'}, 1000); $("input[type=submit]").attr('disabled', (total=='0')); } }).keyup(); $("input[name=invoice_number],input[name=total]"). focus(function(){ $(this).blur(); }); } else _table_ do unless _.pdf? _thead_ do _tr do _th.buttons colspan: 2 do _a href: "#{base}/#{invoice}?" do _button 'Edit' end _a href: "#{base}/#{invoice}.pdf" do _button 'Generate PDF' end end end end end _tbody do _tr do _td style: "width: 270pt; color: #006" do _p "Dept. 9660" _p "Los Angeles, CA 90084-9660, USA" _p "E-mail: #{@email ||'fundraising@apache.org'}" _p "US IRS Tax/EIN: 47-0825376" end _td style: "width: 270pt; text-align: right" do _p "Invoice", style: "font-size: 28pt; color: #636" _p @date end end end end _p _table_ do _tbody do _tr do _td.c26 do _table do _tbody do _tr do _td.c16 do _p "Invoice No.", class: "c17 c13" end _td.c23 do _p @invoice_number end end _tr do _td.c16 do _p "Customer:", class: "c17 c13" end _td.c23 do _p @customer end end if @po_number and not @po_number.empty? _tr do _td.c16 do _p "Reference:", class: "c17 c13" end _td.c23 do _p "PO##{@po_number}" end end end end end end _td.c5 _td.c20 do _table do _tbody do _tr do _td.c18 do _p 'Bill To:', class: "c17 c13" end end _tr do _td.c18 do @bill_to.lines.each do |line| _p line.chomp end end end end end end end end end _p style: 'height: 30pt' _table_ do _thead do _tr do _th 'Quantity', class: "c11" _th 'Item', class: "c15" _th 'Total', class: "c11" end end _tbody do @item.lines.each do |line| line.gsub!(/^(\d+)\s-\s*/,'') quantity = $1 if line.match(/[-@]?\s?\$\s?([,\d\.]+)$/) amt = $1.gsub(',', '') quantity ||= '1' price = quantity.to_i * amt.to_f # Format the float as a 2dp number price = "%0.2f" % price # Now make it look pretty with commas price = price.gsub(/(\d)(?=(\d\d\d)+(?!\d))/, '\1,') else quantity = price = '' end _tr do _td.c11 do _p.c29 quantity end _td.c15 do _p.c17 line.chomp end _td.c10 do _p.c1 price end end end (10-@item.lines.count).times do _tr do _td.c11 _td.c15 _td.c10 end end end end _p _table_ style: "margin-left: auto" do _tbody do _tr do _td.c7 do _p "Subtotal:", class: "c17 c21 c13" end _td.c10 do _p.c1 @total end end _tr do _td.c7 do _p 'Tax:', class: "c17 c21 c13" end _td.c10 do _p.c1 "-" end end _tr do _td.c7 do _p 'Shipping:', class: "c17 c21 c13" end _td.c10 do _p.c1 '-' end end _tr do _td.c7 do _p "Miscellaneous:", class: "c17 c21 c13" end _td.c10 do _p.c1 "-" end end _tr do _td.c7 do _p "Balance Due:", class: "c17 c21 c13" end _td.c10 do _p.c1 @total end end end end _div style: "margin-top: 30pt; color: #006" do _p "Please make checks payable to “The Apache Software Foundation”." _p _p "Wire and ACH payments information:" _p "Beneficiary: “Apache Software Foundation”" _p "Routing #: 121 000 248 (for domestic wire or ACH)" _p "SWIFT: WFBIUS6S (for international wire)" _p "Account #: 3189163755" _p "Wells Fargo Bank" end if @invoice_number =~ /\A\d+\z/ File.open(File.join(HISTORY, @invoice_number), 'w') do |file| file.write params.to_yaml end else _p "Invalid invoice number #{@invoice_number}, could not create invoice" end end end end