Invoice-Demo

if 0 {


Introduction

MiHa 2015-05-13: This is a demo for a tcl/tk-program with all the essential GUI-elements.
It acts as a simple invoice-program with menu, entry-form and output (planned: one printed page).

All the data is just strings of text, no validation, no calculation.

With ideas and code from the following pages:

}


Program 1

 # InvoiceDemo09.tcl - Mike Hase - 2015-05-13 / 2015-05-21
 # https://wiki.tcl-lang.org/29284
 # Simple "invoice"-themed GUI-demo, with menu, form for data-entry, output to screen and printer.
 # (output to printer is not done yet)

  package require Tk

  set Prog(Title)    "Invoice-Demo"
  set Prog(Version)  "v0.09"
  set Prog(Date)     "2015-05-21"
  set Prog(Author)   "Mike Hase"
  set Prog(HomePage) "https://wiki.tcl-lang.org/29284"
  set Prog(Contact)  "[email protected]"
  set Prog(About)    "A GUI-demo (disguised as an invoice-program:)\nPlus a printer-test."

 #---+----1----+----2----+----3----+----4----+----5----+----6----+----7----+

 global data maxFNr startF1 startF2

#set FieldNames1 {"Date                " Title Name Street City Phone }
 set FieldNames1 { Title Name Street City Phone }
#set FieldNames2 { art1Nr art1qty art1desc art1price price1 }
#set FieldNames2 { Article-Number Quantity Description Price Total }
 set FieldNames2 { Item Description Unit-Price Quantity Amount }
                  #---+----1----+----2----+

 set fNr   0
 set artNr 0
 set lenLb 16
 set lenEn 20

 
 #: Boilerplate-text / Company-Address:

 set data(Adr1) "We have no T-Shirts Inc."
 set data(Adr2) "Marketplace 9"
 set data(Adr3) "Bigcity"
 set data(Adr4) "Somewhere"
 set data(Fon)  "555-4321-0"
 set data(Fax)  "555-4321-9"
 set data(Mail) "[email protected]"
                #---+----1----+----2----+----3

 # Build data-entry form:
 ## Todo: put this form into its own dialog

 #: Frame for customer-data:
 frame .f1   ;# -background red

 set field "Date"
 incr fNr
 set data($fNr) $fNr        ;## Test
 set lb [label .f1.lb$field -text $field ]
 set en [entry .f1.en$field -width 10 -textvar data($fNr) ]
 grid $lb $en -padx 4 -pady 2
 grid $lb -sticky e
 grid $en -sticky w


 set   hdr [label .f1.lbHdr -text "Customer:"]
 grid $hdr -columnspan 2  -pady 8 

 foreach field $FieldNames1 {
        incr fNr
        set data($fNr) $fNr        ;## Test
        set lb [label .f1.lb$field -width $lenLb -justify right -text $field ]        
        set en [entry .f1.en$field -width $lenEn -justify left  -textvar data($fNr) ]
        grid $lb $en -padx 4 -pady 2
        grid $lb -sticky e
        grid $en -sticky w
        if { $fNr == 2 } { set startF1 $en }
 }

 #: Frame for article-data:
 # Same layout as above.  Todo: place all fields for an article in one line
 frame .f2   ;# -background green
 incr artNr                                ;# Todo: extend to more articles

 set   hdr [label .f2.hdr -text "Purchase:"]
 grid $hdr -columnspan 2  -pady 8 

 foreach field $FieldNames2 {
        incr fNr
        set data($fNr) $fNr        ;##
        set lb [label .f2.lb$field -width $lenLb -justify right -text $field ]
        set en [entry .f2.en$field -width $lenEn -justify right -textvar data($fNr) ]
        grid $lb $en -padx 4 -pady 2
        grid $lb -sticky e
        grid $en -sticky w
       #if { $startF2 == "" } { set startF2 $en }
        if { ![info exists startF2] } { set startF2 $en }
 }
 set maxFNr $fNr


if 0 {        ;# these buttons can be used as an alternative to the m+menu
 #: Frame for buttons:  
 frame  .f8 -background blue
 button .f8.b1 -text New     -command { data0 }
 button .f8.b2 -text Test1   -command { data1 }
 button .f8.b3 -text Test2   -command { data2 }

 button .f8.b4 -text Show    -command { showData2 }
 button .f8.b5 -text Print   -command { printData }

 button .f8.b6 -text Exit    -command { exit }

 grid .f8.b1  .f8.b2  .f8.b3  .f8.b4  .f8.b5  .f8.b6
 pack .f1 .f2 .f8 -side top
}
 pack .f1 .f2  -side top

 #---+----1----+----2----+----3----+----4----+----5----+----6----+----7----+

 # Menu:

  proc m+ {head name {cmd ""}} {
  #: Menu-creator "m+" by R.Suchenwirth and DKF, 2006-08-24, see https://wiki.tcl-lang.org/16327
    # Uses the undocumented and unsupported feature ::tk::UnderlineAmpersand
    if {![winfo exists .m.m$head]} {
       foreach {l u} [::tk::UnderlineAmpersand $head] break
       .m add cascade -label $l -underline $u -menu [menu .m.m$head -tearoff 0]
    }
    if {[regexp ^-+$ $name]} {
       .m.m$head add separator
    } else {
       foreach {l u} [::tk::UnderlineAmpersand $name] break
       .m.m$head add command -label $l -underline $u -comm $cmd
    }
  }


 # GUI: define menu, bindings

  . configure -menu [menu .m]
  m+ &File &Open      { Dummy "Open" }
  m+ &File &Save      { Dummy "Save" }
  m+ &File -----
  m+ &File &Exit        exit

  m+ &Edit &Clear       data0 
  m+ &Edit Testdata&1   data1 
  m+ &Edit Testdata&2   data2 

  m+ &Output &List      showData2
  m+ &Output &Show      showData3
  m+ &Output &Print     printData 

  m+ &Help &About       About
  m+ &Help &Help        Help
  m+ &Help -----
  m+ &Help &Debug     { console show }


  bind all <Key-F1>     Help
  bind all <Key-F2>   { console show }
  bind all <Key-F3>     bell
  bind all <Key-F4>     exit

  bind all <Key-F5>     data0 
  bind all <Key-F6>     data1 
  bind all <Key-F7>     data2 
  bind all <Key-F8>     showData2

  bind all <Key-F9>     showData3
  bind all <Key-F10>    printData

 #---+----1----+----2----+----3----+----4----+----5----+----6----+----7----+

  proc Dummy {txt} {
  #: Alert: "Function X not implemented yet"
    bell
    tk_messageBox -icon warning  -title "Demo" \
                  -message "Function '$txt' not implemented yet."
  }

  proc About {} {
  #: Short info about the program
    global Prog
    set txt    "$Prog(Title) $Prog(Version) - "
    append txt "$Prog(Date)\nby $Prog(Author) - $Prog(Contact)\n\n$Prog(About)"
    tk_messageBox -icon info -message $txt -title "About $Prog(Title) $Prog(Version)"
  }

  proc _Help {} { Dummy Help }                ;# needs work

  proc Help {} {
    global Prog
   
    set w .help
    if { [winfo exists $w] } {
        wm deiconify $w
        raise $w
        focus $w
        return;
    }
    toplevel $w
    wm title $w "$Prog(Title) - Help"
    frame $w.f
    text $w.f.t -wrap word
    scrollbar $w.f.sb
    $w.f.t  configure -yscrollcommand "$w.f.sb set"
    $w.f.sb configure -command "$w.f.t yview"

    $w.f.t tag configure h1 -font {{} 18 bold}   -justify center -foreground red
    $w.f.t tag configure h2 -font {{} 14 italic} -justify center
    $w.f.t tag configure h3 -font {{} 10 bold } 

    $w.f.t insert end "$Prog(Title)\n" h1
    $w.f.t insert end "Version $Prog(Version)\n\n" h2

    $w.f.t insert end "$Prog(Title) is a GUI-demo that acts as a simple invoice-program.\nThere is a form for data-enty, and output of that data to screen and printer.\n\n"

    $w.f.t insert end "HOTKEYS:\n" h3
    $w.f.t insert end "* F1 - Show this help.\n* F2 - Activate Debug/Console.\n* F4 - Exit program.\n"
    $w.f.t insert end "* F5 - Edit: Clear entry-fields.\n* F6, F7 - Edit: Fill entry-fields with testdata.\n"
    $w.f.t insert end "* F9 - Output data to screen.\n\n"

    $w.f.t insert end "DEBUG:\n" h3
    $w.f.t insert end "  The console offers access to the program's internals and variables.\n\n"
    $w.f.t insert end "Commands:\n  ?: show console-help, e: Exit program.\n"
    $w.f.t insert end "  d0: Clear entry-fields. d1, d2: Fill-in testdata.\n"
    $w.f.t insert end "  s0, s1, s2, s3: Show data in different formats.\n\n"
    $w.f.t insert end "You can enter any tcl-command in the console, for example:\n"
    $w.f.t insert end "% info proc\n"
    $w.f.t insert end "% info var\n"
    $w.f.t insert end "% puts \$maxFNr\n"
    $w.f.t insert end "% parray data\n"
    $w.f.t insert end "% set data(3) \"Johnson\"\n"
    $w.f.t insert end "\n"

    $w.f.t insert end "CREDITS:\n" h3
    $w.f.t insert end "Written by $Prog(Author) ($Prog(Contact)), source at: $Prog(HomePage).\n"
   
    $w.f.t configure -state disabled
    pack $w.f    -side top   -expand 1 -fill both
    pack $w.f.t  -side left  -expand 1 -fill both
    pack $w.f.sb -side right -fill y
    frame  $w.f2
    pack   $w.f2 -side top -pady 8
    button $w.f2.b -text "Close" -command "wm withdraw $w" -width 9
    pack   $w.f2.b
    wm protocol $w WM_DELETE_WINDOW "wm withdraw $w"
    focus $w.f.t
  };# Help


 #---+----1----+----2----+----3----+----4----+----5----+----6----+----7----+

 # Routines to edit/fill in data:

 proc data0 {} {
 #: Clear data in entry-fields
   for {set f 1} {$f <= $::maxFNr} {incr f} {
      set ::data($f) ""
   }
  #set ::data(1) "2015-05-20"
   set cs [clock seconds]
   set td [clock format $cs  -format "%Y-%m-%d"]
   set ::data(1) "$td"
   focus .f1.enDate
 }

 proc data1 {} {
 #: Fill entry-fields with testdata
   set ::data(1) "2015-05-13"
   set ::data(2) "Mr."
   set ::data(3) "Smith"
   set ::data(4) "Mainstreet 123"
   set ::data(5) "Newtown"
   set ::data(6) "555-6789"

   set ::data(7) "0815"
   set ::data(8) "T-Shirt ultrablack, size XL"
   set ::data(9) " 9.99"
   set ::data(10) "2"
   set ::data(11) "19.98"
   focus $::startF1
 }

 proc data2 {} {
 #: Fill entry-fields with testdata
   set fNr   0
   set values {
      "2015-05-14"
      "Mrs." "Miller" "Highstreet 99" "Oldsville" "555-9876"
      "0816" "T-Shirt XL" "12.34" "1" "12.34"
   }
   foreach v $values {
        incr fNr
        set ::data($fNr) $v
   }
   focus $::startF2
 }

 #---+----1----+----2----+----3----+----4----+----5----+----6----+----7----+

 # Output-Routines:

  proc showData0 {} { 
    puts "INVOICE-data:"
    parray ::data
  }

  proc showData1 {} { 
  #: Show the data on the console
    global data maxFNr

    puts "INVOICE\n=="
    for {set f 1} {$f <= $maxFNr} {incr f} {
      puts "Field#$f : $data($f)"
    }
    puts "--\nTHANKS !"
  }

  proc showData2  {} { 
  #: Show the data in a simple messagebox
    global data maxFNr

    set txt    "INVOICE\n==\n"
    for {set f 1} {$f <= $maxFNr} {incr f} {
      append txt "Field#$f : $data($f)\n"
    }
    append txt "--\nTHANKS !"

    tk_messageBox -icon info -message $txt -title "ShowData2"
  }

  proc showData3 {} {                                ;# from: TkOverload Help
  #: Show the data formatted like an invoice-letter
    global data
    catch {destroy .dia} 
    toplevel .dia
    wm title .dia "Invoice"
    wm geom .dia "+[expr {[winfo x .] + [winfo width .] + 10}]+[winfo y .]"

    text .dia.t -relief raised -wrap word -width 70 -height 23 \
        -padx 10 -pady 10 -cursor {} -yscrollcommand {.dia.sb set}
    scrollbar .dia.sb -orient vertical -command {.dia.t yview}
    button .dia.dismiss -text Dismiss -command {destroy .dia}
    pack .dia.dismiss -side bottom -pady 10
    pack .dia.sb      -side right  -fill y
    pack .dia.t       -side top    -expand 1 -fill both

    set bold   "[font actual [.dia.t cget -font]] -weight bold"
    set italic "[font actual [.dia.t cget -font]] -slant  italic"
    .dia.t tag config    h1    -justify center -foregr blue -font "Times 20 bold"
    .dia.t tag configure h2    -justify left   -font "Times 12 bold"
    .dia.t tag configure bold  -font $bold
    .dia.t tag configure ital  -font $italic
    .dia.t tag configure n     -lmargin1 15 -lmargin2 15

    .dia.t insert end "Invoice\n" h1

    # Todo: place picture with logo here

    set m "--- --- --- --- --- ---\n\n"

    .dia.t insert end "From:\n" h2
    .dia.t insert end "$data(Adr1)\n$data(Adr2)\n$data(Adr3)\n$data(Adr4)\n" bold $m .\n\n 

    .dia.t insert end "To:\n" h2
    .dia.t insert end "$data(2) $data(3)\n$data(4)\n$data(5)\n" bold $m .\n\n 

    # Todo: tweak formatting

   #.dia.t insert end "Item         Description         Unit Price         Quantity         Amount\n" bold $m n 

    foreach field $::FieldNames2 {
        .dia.t insert end "$field    " bold
    }
    .dia.t insert end "\n" 

   #set m "$data(8) x $data(7) $data(9) -- $ $data(10) ea. = $ $data(11)\n"
    set m "$data(7)  $data(8)   $data(9) * $data(10) = $ $data(11)\n"
    .dia.t insert end $m n 

    set m "$ $data(11)\n\n"
    .dia.t insert end "Total:\n" bold $m n

    set m ""
    append m "This product is meant for educational purposes only. "
    append m "Keep away from fire or flames. "
    append m "For recreational use only. "
    append m "Some assembly required. "
    append m "Do not use while operating a motor vehicle or heavy equipment. "
    append m "Any resemblance to real persons, living or dead is purely coincidental. "
    append m "Avoid contact with skin. "
    append m "No other warranty expressed or implied. "
    append m "One size fits all. "
    append m "Do not disturb. " 
    append m "Action figures sold separately. "
    append m "Batteries not included. "
    append m "Price does not include taxes. "
    append m "Use only as directed. "
    append m "Keep away from sunlight. "
    append m "All rights reserved. "
    append m "Keep cool. \n"
    .dia.t insert end "Standard Disclaimer\n" bold $m n

    .dia.t config -state disabled
    focus .dia.t
  }

 # Todo:
  proc printData {} { Dummy printData }

 #---+----1----+----2----+----3----+----4----+----5----+----6----+----7----+

 #: Test+Debug:

  proc ?    {} { help }
  proc help {} {
    puts "\n$::Prog(Title) $::Prog(Version)"
    puts "--"
    puts "argv0 : $::argv0"
    puts "argv  : $::argv"
    puts "tcl/tk: $::tcl_patchLevel / $::tk_patchLevel \n"
    puts "d0,d1,d2, s0..s3:data0,data1,data2, showData0..3  ?:help  e:exit \n"
  }

  proc d0 {} { data0 }
  proc d1 {} { data1 }
  proc d2 {} { data2 }

  proc s0 {} { showData0 }
  proc s1 {} { showData1 }
  proc s2 {} { showData2 }
  proc s3 {} { showData3 }

  proc e  {} { exit }

 #---+----1----+----2----+----3----+----4----+----5----+----6----+----7----+

 #: Main:

  wm title . "$Prog(Title) $Prog(Version)"
  help

 # Todo: make and include an icon here
  if {[file exists  demo.ico]} {
    wm iconbitmap . demo.ico
  }

 focus -force .

 ### EOF ###

Example icon: InvoiceDemo_icon

It looks like the upload-procedure of the wiki doesn't like .ico - files.
So, just copy any icon you like to the location of the program, and rename it to demo.ico


Output

Dump of the contents of the entry-fields, filled with testdata:

INVOICE
==
Field#1 : 2015-05-13
Field#2 : Mr.
Field#3 : Smith
Field#4 : Mainstreet 123
Field#5 : Newtown
Field#6 : 555-6789
Field#7 : 0815
Field#8 : T-Shirt ultrablack, size XL
Field#9 :  9.99
Field#10 : 2
Field#11 : 19.98

Remarks

MiHa: This is a nice small demo of a simple program using only basic, essential GUI-features:

  • Menu
  • Buttons (to see them, change that "if 0"-section to "if 1")
  • Hotkeys (F1..F9, etc.)
  • Labels
  • Entry-fields
  • Messagebox
  • Title at top of window
  • Icon at top of window (currently, this needs an external file)
  • frames, pack and grid to manage the layout

There is lots of more advanced GUI-stuff not covered so far, e.g.:

  • Checkboxes
  • Radiobuttons
  • Fancy textformating, fonts (i.e. for a nice help-window)
  • Listbox / Multicolumn listbox / Tables
  • Scrollbars
  • dialog-windows separate from the main-window (i.e. for the data-entry form)

But if any of that gets included, and the program gets more complex because of that,
it might be better to put that into a separate program Invoice-Demo2.

The data is just simple strings of text, all of the same length, no validation, no calculation, no special logic, etc.
This definitely needs work for any real-world application.
BTW, I haven't found yet any good, nice, short routine for data-entry (such as m+ for menus).

Well, I tried to do a bit here:

  • Date as a single entry-field
  • Customer- and purchase-data separated with a label above each one

The size of the grid-columns and the alignment of the labels in these separated sections don't match any more,
To keep the layout looking good requires tweaking.
That means the code there quickly gets complicated and hard-to-understand.
This is not good for demo that should be easy-to-understand.

Also, there is no file-I/O for data or configuration.

I haven't looked into the printing stuff yet, so if anybody thinks he is up to the task, you are welcome to fill in that part.


See also

  • GUI Primer - essentially my notes researching GUI-stuff on tcl/tk

All in all, it looks like printing from Tcl/tk in a "native, portable way" is kind of a hard problem, and still mostly unsolved.