Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Initial checkin from Sourceforge-based repository.

Historical changes ignored; taking the opportunity to restructure app
for the much better Django-1.4 layout.
  • Loading branch information...
commit 7e4b9e9b5f3347fea1771d2cc62e9442b7c08c2f 1 parent 363c543
@chmarr authored
Showing with 7,701 additions and 0 deletions.
  1. +339 −0 COPYING
  2. +16 −0 COPYRIGHT
  3. +39 −0 README
  4. +20 −0 artshow-utils/apply_space_fees.py
  5. +52 −0 artshow-utils/apply_winnings_and_commission.py
  6. +72 −0 artshow-utils/createinvforbidder.py
  7. +101 −0 artshow-utils/email1.py
  8. +44 −0 artshow-utils/integrate_cheques.py
  9. +94 −0 artshow-utils/integrate_reg_db.py
  10. +33 −0 artshow-utils/invoicegen.py
  11. +22 −0 artshow-utils/loadbatchscan.py
  12. +68 −0 artshow-utils/overgrid.py
  13. +229 −0 artshow-utils/print_cheques.py
  14. +36 −0 artshow-utils/scanner_reader.py
  15. +30 −0 artshow-utils/sent_cheque_details.py
  16. +25 −0 artshow-utils/tweak.py
  17. 0  artshow/__init__.py
  18. +83 −0 artshow/addbidder.py
  19. +432 −0 artshow/admin.py
  20. +14 −0 artshow/artshow_settings.py
  21. +76 −0 artshow/bidsheets.py
  22. +111 −0 artshow/cashier.py
  23. +114 −0 artshow/cheques.py
  24. +157 −0 artshow/csvreports.py
  25. +40 −0 artshow/email1.py
  26. +1 −0  artshow/fixtures/artshowpaymenttypes.json
  27. +1 −0  artshow/fixtures/artshowspaces.json
  28. +155 −0 artshow/invoicegen.py
  29. +10 −0 artshow/media/styles.css
  30. +497 −0 artshow/migrations/0001_initial.py
  31. +247 −0 artshow/migrations/0002_invoicepayment.py
  32. +234 −0 artshow/migrations/0003_auto__del_field_invoice_total_paid.py
  33. +235 −0 artshow/migrations/0004_auto__add_field_artist_website.py
  34. +236 −0 artshow/migrations/0005_auto__add_field_piece_code.py
  35. +234 −0 artshow/migrations/0006_fixup_piece_codes.py
  36. +237 −0 artshow/migrations/0007_auto__add_field_piece_media.py
  37. +248 −0 artshow/migrations/0008_auto__add_chequepayment.py
  38. 0  artshow/migrations/__init__.py
  39. +94 −0 artshow/mod11codes.py
  40. +332 −0 artshow/models.py
  41. +135 −0 artshow/pdfreports.py
  42. +298 −0 artshow/processbatchscan.py
  43. +189 −0 artshow/reports.py
  44. +74 −0 artshow/static/cashierscript.js
  45. +2 −0  artshow/static/cashierstyle.css
  46. +4 −0 artshow/static/jquery.min.js
  47. +37 −0 artshow/templates/admin/email_selected_confirmation.html
  48. +7 −0 artshow/templates/artshow/artist-mailing-label.html
  49. +12 −0 artshow/templates/artshow/artist-panel-report.html
  50. +23 −0 artshow/templates/artshow/artist-payment-report.html
  51. +10 −0 artshow/templates/artshow/artist-piece-report.html
  52. +18 −0 artshow/templates/artshow/artist_main.html
  53. +7 −0 artshow/templates/artshow/bidderadd.html
  54. +16 −0 artshow/templates/artshow/bidderbulkadd.html
  55. +20 −0 artshow/templates/artshow/cashier.html
  56. +47 −0 artshow/templates/artshow/cashier_bidder.html
  57. +6 −0 artshow/templates/artshow/cashier_invoice.html
  58. +10 −0 artshow/templates/artshow/dataentry.html
  59. +7 −0 artshow/templates/artshow/generate-piece-stickers.html
  60. +7 −0 artshow/templates/artshow/index.html
  61. +7 −0 artshow/templates/artshow/kit-contents.html
  62. +12 −0 artshow/templates/artshow/panel-artist-report.html
  63. +18 −0 artshow/templates/artshow/reports-artists.html
  64. +19 −0 artshow/templates/artshow/reports-balances.html
  65. +38 −0 artshow/templates/artshow/reports-winning-bidders.html
  66. +35 −0 artshow/templates/artshow/reports.html
  67. +12 −0 artshow/templates/artshow/sales-percentiles.html
  68. +51 −0 artshow/templates/artshow/show-summary.html
  69. +19 −0 artshow/templates/artshow/voice-auction.html
  70. +23 −0 artshow/templates/base_generic.html
  71. +23 −0 artshow/tests.py
  72. +38 −0 artshow/urls.py
  73. +17 −0 artshow/views.py
  74. 0  artshowjockey/__init__.py
  75. +169 −0 artshowjockey/settings.py
  76. +154 −0 artshowjockey/settings.py.orig
  77. +11 −0 artshowjockey/urls.py
  78. +28 −0 artshowjockey/wsgi.py
  79. +10 −0 manage_artshowjockey.py
  80. +1 −0  num2word/__init__.py
  81. +62 −0 num2word/num2word.py
  82. +154 −0 num2word/num2word_DE.py
  83. +137 −0 num2word/num2word_EN.py
  84. +63 −0 num2word/num2word_EN_GB.py
  85. +55 −0 num2word/num2word_EN_GB_old.py
  86. +147 −0 num2word/num2word_ES.py
  87. +44 −0 num2word/num2word_EU.py
  88. +119 −0 num2word/num2word_FR.py
  89. +264 −0 num2word/num2word_base.py
  90. +34 −0 num2word/orderedmapping.py
View
339 COPYING
@@ -0,0 +1,339 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along
+ with this program; if not, write to the Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) year name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ <signature of Ty Coon>, 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
View
16 COPYRIGHT
@@ -0,0 +1,16 @@
+Artshow-Jockey - Art Show Management Software
+Copyright (C) 2009,2010 Chris Cogdon
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License along
+with this program; if not, write to the Free Software Foundation, Inc.,
+51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
View
39 README
@@ -0,0 +1,39 @@
+Artshow-Jockey - Art Show Management Software
+Copyright (c) 2009,2010 Chris Cogdon
+
+Installation
+------------
+
+This is a Django application. Install Django by following the standard Django instructions
+and create a new project. Copy the "artshow" directory from this installation into
+your project (or symlink from there to here) and make the following modifications
+to the top-level files:
+
+to urls.py, urlpatters, add:
+
+ (r'^artshow/', include ( 'yourprojectname.artshow.urls' ) ),
+
+you should also enable django's administration interface, if you haven't already,
+as Artshow Jockey relies heavily on it.
+
+to settings.py, TEMPLATE_DIRS, add:
+
+ "/full/path/to/yourprojectname/artshow/templates/",
+
+to settings.py, INSTALLED_APPS, add:
+
+ "yourprojectname.artshow',
+
+and add the following to settings.py as well, with appropriate mods:
+
+ARTSHOW_SHOW_NAME = "MY AWESOME ART SHOW"
+ARTSHOW_TAX_RATE = "0.0925"
+ARTSHOW_TAX_DESCRIPTION = "%9.25 CA TAX"
+
+Once all that's done, you should be able to go ./manage.py syncdb and get everything set up.
+
+Setting up the data
+-------------------
+
+First thing you'll want to do is add some space types so you can allocate spaces.
+
View
20 artshow-utils/apply_space_fees.py
@@ -0,0 +1,20 @@
+#! /usr/bin/env python
+# Artshow Jockey
+# Copyright (C) 2009, 2010 Chris Cogdon
+# See file COPYING for licence details
+
+from artshow.models import Artist, Payment, PaymentType
+import datetime
+
+for a in Artist.objects.all():
+ payment_type = PaymentType.objects.get(name="Panel Fee")
+ total = 0
+ for alloc in a.allocation_set.all():
+ total += alloc.space.price * alloc.allocated
+ if total <= 0:
+ print a, total, "not applying fees"
+ else:
+ print a, total
+ allocated_spaces_str = ", ".join ( "%s:%s" % (al.space.shortname,al.allocated) for al in a.allocation_set.all() )
+ payment = Payment ( artist=a, amount=-total, payment_type=payment_type, description="Panel Fee (%s)" % allocated_spaces_str, date=datetime.datetime.now() )
+ payment.save ()
View
52 artshow-utils/apply_winnings_and_commission.py
@@ -0,0 +1,52 @@
+#! /usr/bin/env python
+# Artshow Jockey
+# Copyright (C) 2009, 2010 Chris Cogdon
+# See file COPYING for licence details
+
+from artshow.models import Artist, Payment, PaymentType, Bid
+import datetime, decimal
+import optparse
+from settings import ARTSHOW_COMMISSION
+
+def apply_winnings_and_commissions ( ids=None ):
+ if ids:
+ artists = Artist.objects.filter(id__in=ids)
+ else:
+ artists = Artist.objects.all()
+ for a in artists:
+ pt_winning = PaymentType.objects.get(name="Winnings")
+ pt_commission = PaymentType.objects.get(name="Commission")
+ total_winnings = 0
+ total_pieces = 0
+ pieces_with_bids = 0
+ for piece in a.piece_set.all():
+ if piece.status != 0:
+ total_pieces += 1
+ try:
+ top_bid = piece.top_bid()
+ total_winnings += top_bid.amount
+ pieces_with_bids += 1
+ except Bid.DoesNotExist:
+ pass
+ commission = total_winnings * decimal.Decimal(ARTSHOW_COMMISSION)
+
+ if total_pieces > 0:
+ payment = Payment ( artist=a, amount=total_winnings, payment_type=pt_winning, description="Sales (%d piece%s, %d with bid%s)" % ( total_pieces, total_pieces!=1 and "s" or "", pieces_with_bids, pieces_with_bids != 1 and "s" or ""), date=datetime.datetime.now() )
+ payment.save ()
+
+ if commission > 0:
+ payment = Payment ( artist=a, amount=-commission, payment_type=pt_commission, description="10% Commission on sales", date=datetime.datetime.now() )
+ payment.save ()
+
+def get_options ():
+ parser = optparse.OptionParser ()
+ parser.add_option ( "--ids", type="str", default=None )
+ opts, args = parser.parse_args ()
+ if opts.ids:
+ opts.ids = opts.ids.split(",")
+ opts.ids = [int(x) for x in opts.ids]
+ return opts
+
+if __name__ == "__main__":
+ opts = get_options ()
+ apply_winnings_and_commissions ( opts.ids )
View
72 artshow-utils/createinvforbidder.py
@@ -0,0 +1,72 @@
+#! /usr/bin/env python26
+# Artshow Jockey
+# Copyright (C) 2009, 2010 Chris Cogdon
+# See file COPYING for licence details
+
+from artshow.models import Bidder, BidderId, Invoice, InvoiceItem
+import optparse, datetime
+import decimal
+import settings
+
+
+def get_options ():
+ parser = optparse.OptionParser ()
+ parser.add_option ( "--test", default=False, action="store_true" )
+ opts, args = parser.parse_args()
+ opts.bidderid = args[0]
+ return opts
+
+def create_invoice ( bidderid_id, test=False ):
+
+ now = datetime.datetime.now ()
+
+ bidderid = BidderId.objects.get ( id=bidderid_id )
+ bidder = bidderid.bidder
+ print "Bidder is", unicode(bidder)
+
+ pieces_to_add = []
+ total = 0
+
+ for b in bidder.top_bids():
+ print b.piece.id, unicode(b), b.buy_now_bid and "BuyNow" or "", b.piece.status, b.piece.voice_auction and "Voice auction" or ""
+ if b.piece.status == 2:
+ print "adding"
+ print
+ pieces_to_add.append ( b.piece )
+ else:
+ print "NOT ADDING ***"
+ print
+
+ input = raw_input ( "Proceed?" )
+ if input != "y":
+ raise SystemExit
+
+ if not test:
+ items_to_add = []
+ invoice = Invoice ( payer = bidder, paid_date=now )
+ total = 0
+ for p in pieces_to_add:
+ top_bid = p.top_bid ()
+ item = InvoiceItem ( piece=p, invoice=invoice, price=top_bid.amount )
+ items_to_add.append ( item )
+ total += top_bid.amount
+ tax_rate = decimal.Decimal(settings.ARTSHOW_TAX_RATE)
+ tax_paid = total * tax_rate
+ total_incl_tax = total + tax_paid
+ invoice.tax_paid = tax_paid
+ invoice.total_paid = total_incl_tax
+ invoice.save ()
+ for i in items_to_add:
+ i.invoice = invoice
+ i.save ()
+ for p in pieces_to_add:
+ p.status = 3
+ p.save ()
+
+ print "Invoice Created:", invoice.id
+
+
+if __name__ == "__main__":
+ opts = get_options ()
+ create_invoice ( opts.bidderid, test=opts.test )
+
View
101 artshow-utils/email1.py
@@ -0,0 +1,101 @@
+#! /usr/bin/env python26
+# Artshow Jockey
+# Copyright (C) 2009, 2010 Chris Cogdon
+# See file COPYING for licence details
+
+# TODO: Handle setting DJANGO environment variable
+
+import optparse, subprocess
+from django.template import Template, Context
+from artshow.models import Artist
+from artshow.email1 import make_email
+import sqlite3
+
+default_db = "data/artshowproj.db"
+default_wrap_cols = 79
+
+def send_email ( data, test=False ):
+
+ if test:
+ print data
+ print "-----------"
+ else:
+ p = subprocess.Popen ( ("/usr/lib/sendmail", "-t"), stdin=subprocess.PIPE )
+ p.stdin.write ( data )
+ p.stdin.close ()
+ print "sent email"
+
+
+def get_template ( db, template_name ):
+ cur = db.cursor ()
+ cur.execute ( "select template from artshow_emailtemplate where name=? limit 1", ( template_name, ) )
+ row = cur.fetchone ()
+ if row == None:
+ raise KeyError ( "email template %s not found" % template_name )
+ return row[0]
+
+def get_artist_ids ( db, ids, filters ):
+
+ cur = db.cursor ()
+ if filters:
+ cur.execute ( "create temporary table filters ( id int, state bool )" )
+ for f_str in filters:
+ checkoff,state = f_str.split("=")
+ state = {"yes":True,"no":False}[state.lower()]
+ cur.execute ( "insert into filters values ( (select id from artshow_checkoff where name=? or shortname=?), ? )", ( checkoff, checkoff, state ) )
+ cur.execute ( """
+create temporary table match_list as
+select artshow_artist.id as artist_id, count(positive_checkoffs.checkoff_id) as positive_matches, count(negative_checkoffs.checkoff_id) as negative_matches from artshow_artist
+left join ( select artshow_artist_checkoffs.artist_id as artist_id, artshow_artist_checkoffs.checkoff_id as checkoff_id from artshow_artist_checkoffs join filters on artshow_artist_checkoffs.checkoff_id=filters.id where filters.state=?) as negative_checkoffs on artshow_artist.id=negative_checkoffs.artist_id
+left join ( select artshow_artist_checkoffs.artist_id as artist_id, artshow_artist_checkoffs.checkoff_id as checkoff_id from artshow_artist_checkoffs join filters on artshow_artist_checkoffs.checkoff_id=filters.id where filters.state=?) as positive_checkoffs on artshow_artist.id=positive_checkoffs.artist_id
+group by artshow_artist.id
+having positive_matches=(select count(*) from filters where state=?) and negative_matches=0
+""", ( False, True, True ) )
+ else:
+ cur.execute ( "create temporary table match_list as select artshow_artist.id as artist_id from artshow_artist" )
+
+ if ids == "active":
+ cur.execute ( "select artshow_artist.id from artshow_artist join match_list on artshow_artist.id=match_list.artist_id where ( select sum(requested) from artshow_allocation where artist_id = artshow_artist.id) > 0" )
+ elif ids == "showing":
+ cur.execute ( "select artshow_artist.id from artshow_artist join match_list on artshow_artist.id=match_list.artist_id where ( select sum(allocated) from artshow_allocation where artist_id = artshow_artist.id) > 0" )
+ elif ids == "all":
+ cur.execute ( "select artshow_artist.id from artshow_artist join match_list on artshow_artist.id=match_list.artist_id" )
+ else:
+ cur.execute ( "create temporary table temp_ids ( id int )" )
+ for id in ids:
+ cur.execute ( "insert into temp_ids values (?)", ( id, ) )
+ cur.execute ( "select artshow_artist.id from artshow_artist where artistid in temp_ids" )
+
+ while True:
+ row = cur.fetchone()
+ if row==None: break
+ yield row[0]
+
+
+def send_emails ( template_name, db_name, ids=None, cols=default_wrap_cols, test=False, filters=[] ):
+ db = sqlite3.connect ( db_name )
+ template_str = get_template ( db, template_name )
+ for artist_id in get_artist_ids ( db, ids, filters ):
+ artist_obj = Artist.objects.get(id=artist_id)
+ email = make_email ( artist_obj, template_str, cols=cols )
+ send_email ( email, test=test )
+
+
+def get_opts ():
+ parser = optparse.OptionParser ()
+ parser.add_option ( "--test", default=False, action="store_true" )
+ parser.add_option ( "--db", default=default_db )
+ parser.add_option ( "--ids", default="active", type="str" )
+ parser.add_option ( "--cols", type="int", default=default_wrap_cols )
+ parser.add_option ( "--filter", action="append", default=[] )
+ opts, args = parser.parse_args ()
+ if opts.ids not in ( "active", "showing", "all" ):
+ s1 = opts.ids.split(',')
+ opts.ids = [ int(x) for x in s1 ]
+ opts.template = args[0]
+ return opts
+
+
+if __name__ == "__main__":
+ opts = get_opts ()
+ send_emails ( opts.template, opts.db, ids=opts.ids, cols=opts.cols, test=opts.test, filters=opts.filter )
View
44 artshow-utils/integrate_cheques.py
@@ -0,0 +1,44 @@
+#! /usr/bin/env python
+# Artshow Jockey
+# Copyright (C) 2009, 2010 Chris Cogdon
+# See file COPYING for licence details
+
+from artshow.models import Artist, Payment, PaymentType
+import optparse, decimal, csv, datetime
+
+def integrate_cheques ( csv_file ):
+ c = csv.DictReader(open(csv_file))
+
+ pt = PaymentType.objects.get(name='Payment Sent')
+
+ for row in c:
+ try:
+ artistid = row['artist']
+ chq = row['chq']
+ amount = decimal.Decimal(row['amount'])
+ except:
+ print >>sys.stderr, "bad row:", row
+ continue
+ try:
+ artist = Artist.objects.get(artistid=artistid)
+ except Artist.DoesNotExist:
+ print >>sys.stderr, "artist %d not found"
+ continue
+
+ payment = Payment(artist=artist, amount=-amount, payment_type=pt, description="Disbursement Cheque #"+chq, date=datetime.datetime.now() )
+ payment.save ()
+
+
+def get_options ():
+ parser = optparse.OptionParser ()
+ opts, args = parser.parse_args()
+ opts.infile = args[0]
+ return opts
+
+
+def main ():
+ opts = get_options ()
+ integrate_cheques ( opts.infile )
+
+if __name__ == "__main__":
+ main ()
View
94 artshow-utils/integrate_reg_db.py
@@ -0,0 +1,94 @@
+#! /usr/bin/env python
+# Artshow Jockey
+# Copyright (C) 2009, 2010 Chris Cogdon
+# See file COPYING for licence details
+
+from artshow.models import Bidder
+import optparse, csv
+
+class NameDoesntMatch ( Exception ): pass
+
+
+def integrate_reg_entry ( entry ):
+
+ try:
+ bidder = Bidder.objects.get ( regid = entry['badge_id'] )
+ except Bidder.DoesNotExist:
+ try:
+ bidder = Bidder.objects.get ( regid = entry['badge_id'].replace('-','') )
+ except Bidder.DoesNotExist:
+ return
+
+ print "Artshow DB:", bidder.name, bidder.bidder_ids()
+ print "Reg DB:", entry['real_name'], entry['badge_id']
+ if bidder.name != entry['real_name']:
+ print "names dont match"
+ while True:
+ i = raw_input ( "1) use Artshow DB name 2) use Reg DB name 3) reject" )
+ if i == "1":
+ real_name = bidder.name
+ break
+ elif i == "2":
+ real_name = entry['real_name']
+ break
+ elif i == "3":
+ real_name = None
+ break
+ else:
+ real_name = bidder.name
+
+ if real_name != None:
+ bidder.name = real_name
+ bidder.address1 = entry.get('address1','')
+ bidder.address2 = entry.get('address2','')
+ bidder.city = entry.get('city','')
+ bidder.state = entry.get('state','')
+ bidder.postcode = entry.get('postcode','')
+ bidder.country = entry.get('country','')
+ bidder.email = entry.get('email','')
+ bidder.phone = entry.get('phone','')
+ bidder.save ()
+ print "saved"
+ print
+ else:
+ print "rejected"
+ print
+ raise NameDoesntMatch
+
+
+def integrate_reg_db ( regfile, rejects_file=None ):
+
+ f = open(regfile)
+ c = csv.DictReader(f)
+
+ if rejects_file:
+ rf = open(rejects_file,"w")
+ rejects = csv.DictWriter(rf,['badge_id','real_name','fan_name','address1','address2','city','state','postcode','country','email','phone'])
+ else:
+ rejects = None
+
+ for entry in c:
+ try:
+ integrate_reg_entry ( entry )
+ except (Bidder.DoesNotExist,NameDoesntMatch):
+ if rejects:
+ rejects.writerow ( entry )
+
+ if rejects:
+ rf.close ()
+
+
+def get_options ():
+ parser = optparse.OptionParser ()
+ parser.add_option ( "--rejects-file", type="str", default=None )
+ opts, args = parser.parse_args ()
+ opts.regfile = args[0]
+ return opts
+
+def main ():
+ opts = get_options ()
+ integrate_reg_db ( opts.regfile, rejects_file=opts.rejects_file )
+
+if __name__ == "__main__":
+ main ()
+
View
33 artshow-utils/invoicegen.py
@@ -0,0 +1,33 @@
+#! /usr/bin/env python
+# Artshow Jockey
+# Copyright (C) 2009, 2010 Chris Cogdon
+# See file COPYING for licence details
+
+import sys
+sys.path.append ( "/home/chris/dev/artshowproj" )
+sys.path.append ( "/home/chris/dev" )
+
+from artshow import invoicegen
+from artshow.models import Invoice
+import optparse
+
+def print_invoices ( invoices, copy_name ):
+ for invoice_id in invoices:
+ invoice = Invoice.objects.get(id=invoice_id)
+ invoicegen.print_invoice ( invoice, copy_name )
+
+def get_options ():
+ parser = optparse.OptionParser ()
+ opts, args = parser.parse_args ()
+ opts.invoices = [ int(x) for x in args ]
+ return opts
+
+def main ():
+ opts = get_options ()
+ for c in [ "CUSTOMER COPY", "MERCHANT COPY", "PICK LIST" ]:
+ print_invoices ( opts.invoices, c )
+
+if __name__ == "__main__":
+ main ()
+
+
View
22 artshow-utils/loadbatchscan.py
@@ -0,0 +1,22 @@
+#! /usr/bin/env python26
+# Artshow Jockey
+# Copyright (C) 2009, 2010 Chris Cogdon
+# See file COPYING for licence details
+
+from artshow.models import BatchScan
+import optparse, datetime
+
+def load_batchscan ( filename ):
+ data = open(filename).read()
+ batchscan = BatchScan ( data=data, date_scanned=datetime.datetime.now() )
+ batchscan.save ()
+
+def get_options ():
+ parser = optparse.OptionParser ()
+ opts, args = parser.parse_args ()
+ opts.file = args[0]
+ return opts
+
+if __name__ == "__main__":
+ opts = get_options ()
+ load_batchscan ( opts.file )
View
68 artshow-utils/overgrid.py
@@ -0,0 +1,68 @@
+#! /usr/bin/env python
+
+from reportlab.pdfgen.canvas import Canvas
+from reportlab.lib.units import inch
+from reportlab.lib.styles import getSampleStyleSheet, TA_CENTER, ParagraphStyle
+from reportlab.platypus import Paragraph, Frame
+
+from pdfrw import PdfReader
+from pdfrw.buildxobj import pagexobj
+from pdfrw.toreportlab import makerl
+
+import optparse, sys
+
+def grid_overlay ( infile ):
+
+ c = Canvas ( sys.stdout )
+
+ pdf = PdfReader ( infile )
+ xobj = pagexobj ( pdf.pages[0] )
+ rlobj = makerl ( c, xobj )
+
+ sheet_offsets = [
+ (0,5.5),
+ (4.25,5.5),
+ (0,0),
+ (4.25,0),
+ ]
+
+ sheets_per_page = len(sheet_offsets)
+ sheet_num = 0
+
+ c.doForm ( rlobj )
+
+ xmax = 90
+ ymax = 115
+
+ dinch = inch * 0.1
+
+ thickline = 0.5
+ thinline = 0.1
+
+ for x in range(0,xmax):
+ if x % 10 == 0:
+ c.setLineWidth ( thickline )
+ else:
+ c.setLineWidth ( thinline )
+ c.line ( x*dinch, 0, x*dinch, ymax*dinch )
+
+ for y in range(0,ymax):
+ if y % 10 == 0:
+ c.setLineWidth ( thickline )
+ else:
+ c.setLineWidth ( thinline )
+ c.line ( 0, y*dinch, xmax*dinch, y*dinch )
+
+ c.showPage ()
+ c.save ()
+
+
+def get_options ():
+ parser = optparse.OptionParser ()
+ opts, args = parser.parse_args ()
+ opts.file = args[0]
+ return opts
+
+if __name__ == "__main__":
+ opts = get_options ()
+ grid_overlay ( opts.file )
View
229 artshow-utils/print_cheques.py
@@ -0,0 +1,229 @@
+#! /usr/bin/env python
+# Artshow Jockey
+# Copyright (C) 2009, 2010 Chris Cogdon
+# See file COPYING for licence details
+
+import sys, os, re, num2word, time, optparse
+from decimal import Decimal
+from artshow.models import Artist
+
+
+class DETAILS:
+ artist_id = None
+ name = None
+ fan_name = None
+ address1 = None
+ address2 = None
+ city = None
+ state = None
+ zip = None
+ country = None
+ check_name = None
+
+ def __init__ ( self ):
+ self.items = []
+
+ def normalise ( self ):
+ self.address2 = self.address2 or ""
+ for attr in [ 'name','address1','address2','city','state','zip','country','check_name']:
+ setattr ( self, attr, ( getattr ( self, attr ) or "").upper() )
+ self.cheque_amount = reduce ( lambda x, y: x + y.amount, self.items, Decimal(0) )
+ if not self.check_name:
+ self.check_name = self.name
+
+
+class ITEM:
+ name = None
+ amount = None
+
+ def __init__ ( self, name=None, amount=None ):
+ self.name=name
+ self.amount=amount
+
+
+class PRINT_GRID:
+ x_size = 87
+ y_size = 66
+ def __init__ ( self ):
+ self.data = [ " "*self.x_size for i in range(self.y_size) ]
+ def print_at ( self, x, y, msg ):
+ if x + len(msg) - 1 > self.x_size:
+ raise Exception ( "line too long" )
+ msg_len = len(msg)
+ s = self.data[y-1]
+ s = s[:x-1] + msg + s[x-1+msg_len:]
+ self.data[y-1] = s
+ self.last_line_printed = y
+ def print_on_next_line ( self, msg ):
+ self.print_at ( 1, self.last_line_printed+1, msg )
+ def save ( self, filename ):
+ f = open ( filename, "w" )
+ for line in self.data:
+ f.write ( line + "\n" )
+ f.close ()
+
+
+def dotpad ( s, max ):
+
+ l = len(s)
+
+ if l >= max:
+ return s
+
+ s += " " * (( max-l ) % 4)
+ s += " .." * ((max-l) / 4 )
+
+ return s
+
+
+
+def print_items ( details, grid, offset ):
+
+ date_str = time.strftime ( "%d %b %Y", time.localtime(details.cheque_date) ).upper()
+
+ s = "(%d) %s" % ( details.artist_id, details.name )
+ if details.fan_name:
+ s += " (%s)" % details.fan_name
+
+ grid.print_at ( 1, offset, s )
+ grid.print_at ( 77, offset, date_str )
+
+ grid.last_line_printed = offset+1
+
+ item_no = 0
+ for item in details.items:
+ item_no += 1
+ name = dotpad ( item.name, 71 )
+ grid.print_on_next_line ( " %2d. %-71s $%8.2f" % ( item_no, name, item.amount ) )
+
+ grid.print_on_next_line ( "-"*87 )
+ grid.print_on_next_line ( " %-71s $%8.2f" % ( "Total, and cheque amount", details.cheque_amount ) )
+
+ grid.print_on_next_line ( "" )
+ grid.print_on_next_line ( "Thank you for exhibiting at FurCon 2012! Please direct any questions to" )
+ grid.print_on_next_line ( "artshow@furtherconfusion.org, or by mail at the address above." )
+
+
+def create_and_save_cheque ( details, filename ):
+
+ details.normalise ()
+
+ if details.cheque_amount <= 0:
+ print >> sys.stderr, "Artist: %s (%d): Amount is %s: not generating" % ( details.fan_name, details.artist_id, details.cheque_amount )
+ return
+
+ if not details.address1:
+ print >> sys.stderr, "Artist: %s (%d): Address1 is blank: generating anyway" % ( details.fan_name, details.artist_id )
+
+ date_str = time.strftime ( "%d %b %Y", time.localtime(details.cheque_date) ).upper()
+
+ cheque_dollars = int(details.cheque_amount)
+ cheque_cents = int ((details.cheque_amount - cheque_dollars) * 100 + Decimal("0.5") )
+
+ cheque_words = "** " + num2word.n2w.to_cardinal ( cheque_dollars ) + " dollars and %d cents" % cheque_cents + " **"
+ cheque_words = cheque_words.upper ()
+
+ grid = PRINT_GRID ()
+
+ grid.print_at ( 77, 3, date_str )
+
+ grid.print_at ( 8, 7, details.check_name )
+ grid.print_at ( 77, 7, "$%-8.2f" % details.cheque_amount )
+
+ grid.print_at ( 1, 9, cheque_words )
+
+ grid.print_at ( 9, 14, details.name )
+ grid.print_at ( 9, 15, details.address1 )
+
+ if details.address2:
+ grid.print_at ( 9, 16, details.address2 )
+ one_if_no_address2 = 0
+ else:
+ one_if_no_address2 = 1
+
+ grid.print_at ( 9, 17-one_if_no_address2, "%s %s %s" % ( details.city, details.state, details.zip ) )
+
+ if details.country and details.country != "USA":
+ grid.print_at ( 9, 18-one_if_no_address2, details.country )
+
+ print_items ( details, grid, 23 )
+ print_items ( details, grid, 46 )
+
+ grid.save ( filename )
+
+
+def print_test_cheque ():
+
+ d = DETAILS ()
+ d.name = "Hello There"
+ d.artist_id = 13
+ d.fan_name = "Bottlebrush fox"
+ d.address1 = "123 Main St."
+ d.address2 = "Apt 123"
+ d.city = "Brownsville"
+ d.state = 'XY'
+ d.zip = "12345-4432"
+ d.country = "Australia"
+
+ d.cheque_date = time.time()
+
+ d.items.append ( ITEM ( "Payment received", 40 ) )
+ d.items.append ( ITEM ( "Payment received", 12 ) )
+ d.items.append ( ITEM ( "Panel Charge", -30 ) )
+ d.items.append ( ITEM ( "Total of winning bids", Decimal("672.0") ) )
+ d.items.append ( ITEM ( "10% Commission on winning bids", Decimal("-67.2") ) )
+ d.items.append ( ITEM ( "Postage for returned artwork", Decimal("6.66") ) )
+
+ create_and_save_cheque ( d, "testcheque" )
+
+
+def print_cheques ( ids ):
+
+ if ids:
+ artists = Artist.objects.filter ( artistid__in=ids )
+ else:
+ artists = Artist.objects.all ()
+
+ for a in artists:
+ print_cheque ( a )
+
+
+def print_cheque ( artist ):
+
+ details = DETAILS ()
+ details.artist_id = artist.artistid
+ details.name = artist.name
+ details.fan_name = artist.artistname()
+ details.address1 = artist.address1
+ details.address2 = artist.address2
+ details.city = artist.city
+ details.state = artist.state
+ details.zip = artist.postcode
+ details.country = artist.country
+ details.check_name = artist.chequename()
+
+ details.cheque_date = time.time()
+
+ for p in artist.payment_set.all():
+ details.items.append ( ITEM ( p.description, p.amount ) )
+
+ create_and_save_cheque ( details, "cheque-%d" % artist.artistid )
+
+
+def get_options ():
+ parser = optparse.OptionParser ()
+ parser.add_option ( "--ids", type="str", default=None )
+ parser.add_option ( "--test", default=False, action="store_true" )
+ opts, args = parser.parse_args ()
+ if opts.ids:
+ opts.ids = opts.ids.split(",")
+ opts.ids = [int(x) for x in opts.ids]
+ return opts
+
+
+if __name__ == "__main__":
+ opts = get_options ()
+ if opts.test:
+ print_test_cheque ()
+ else:
+ print_cheques ( opts.ids )
View
36 artshow-utils/scanner_reader.py
@@ -0,0 +1,36 @@
+#! /usr/bin/env python26
+# Artshow Jockey
+# Copyright (C) 2009, 2010 Chris Cogdon
+# See file COPYING for licence details
+
+from artshow.models import BatchScan
+import optparse, datetime
+import select
+
+def load_batchscan ( data ):
+ batchscan = BatchScan ( data=data, date_scanned=datetime.datetime.now() )
+ batchscan.save ()
+
+f = open ( "/dev/ttyUSB0" )
+
+while True:
+ data = []
+ print "waiting for new data"
+ l = f.readline ()
+ print "\a"
+ while True:
+ if not l:
+ print "oops. no data to read. wtf?"
+ l = l.strip()
+ if l:
+ data.append ( l )
+ print l
+ rlist, wlist, xlist = select.select ( [f], [], [f], 5.0 )
+ if not rlist and not xlist:
+ break
+ l = f.readline ()
+ print "timed out"
+ print "\a"
+ data_str = "\n".join ( data ) + "\n"
+ load_batchscan ( data_str )
+
View
30 artshow-utils/sent_cheque_details.py
@@ -0,0 +1,30 @@
+#! /usr/bin/env python
+# Artshow Jockey
+# Copyright (C) 2009, 2010 Chris Cogdon
+# See file COPYING for licence details
+
+from artshow.models import Payment, PaymentType
+import optparse, csv, sys
+
+def sent_cheque_details ():
+ csv_out = csv.writer ( sys.stdout )
+ csv_out.writerow ( ( 'date', 'recipient', 'description', 'amount' ) )
+ pt = PaymentType.objects.get ( name="Payment Sent" )
+ for p in Payment.objects.filter ( payment_type=pt ).order_by ( 'date','artist' ):
+ details = str(p.description).replace ( "Disbursement Cheque ","" )
+ csv_out.writerow ( ( p.date, p.artist.chequename(), details, "$%.02f" % -p.amount ) )
+
+
+def get_options ():
+ parser = optparse.OptionParser ()
+ opts, args = parser.parse_args ()
+ return opts
+
+def main ():
+ opts = get_options ()
+ sent_cheque_details ()
+
+if __name__ == "__main__":
+ main ()
+
+
View
25 artshow-utils/tweak.py
@@ -0,0 +1,25 @@
+#! /usr/bin/env python26
+# Artshow Jockey
+# Copyright (C) 2009, 2010 Chris Cogdon
+# See file COPYING for licence details
+
+from artshow.models import Piece
+import sys
+
+pid = int(sys.argv[1])
+piece = Piece.objects.get(id=pid)
+
+if piece.status == 1 and piece.voice_auction:
+ print "setting"
+ piece.status = 2
+ piece.save ()
+else:
+ print "not setting"
+ print "status = ", piece.status
+ print "voice_auction = ", piece.voice_auction
+
+
+
+
+
+
View
0  artshow/__init__.py
No changes.
View
83 artshow/addbidder.py
@@ -0,0 +1,83 @@
+# Artshow Jockey
+# Copyright (C) 2009, 2010 Chris Cogdon
+# See file COPYING for licence details
+
+from django.shortcuts import render_to_response
+from django.http import HttpResponse, HttpResponseRedirect
+from artshow.models import Bidder, BidderId
+from django import forms
+from django.core.context_processors import csrf
+from django.forms.util import ErrorList
+from django.core.exceptions import ValidationError
+import mod11codes
+
+bidders_per_page = 10
+
+def mod11check ( value ):
+ try:
+ mod11codes.check ( value )
+ except mod11codes.CheckDigitError:
+ raise ValidationError ( "Not a valid code" )
+
+
+class BidderAddForm ( forms.Form ):
+ bidderid = forms.CharField ( required=False, max_length=8, validators=[mod11check] )
+ name = forms.CharField ( required=False, max_length=100 )
+ regid = forms.CharField ( required=False, max_length=10 )
+ def clean ( self ):
+ super(BidderAddForm,self).clean()
+ cleaned_data = self.cleaned_data
+ if ( cleaned_data.get('bidderid') or cleaned_data.get('name') or cleaned_data.get('regid') ):
+ msg = u"This field is required"
+ if cleaned_data.get('bidderid') == "":
+ self._errors['bidderid'] = ErrorList([msg])
+ del cleaned_data['bidderid']
+ if cleaned_data.get('name') == "":
+ self._errors['name'] = ErrorList([msg])
+ del cleaned_data['name']
+ if cleaned_data.get('regid') == "":
+ self._errors['regid'] = ErrorList([msg])
+ del cleaned_data['regid']
+ return cleaned_data
+
+
+def bulk_add ( request ):
+ if request.method == "POST":
+ forms = [ BidderAddForm ( request.POST, prefix=str(i) ) for i in range(bidders_per_page) ]
+ all_valid=True
+ for form in forms:
+ if not form.is_valid():
+ all_valid=False
+ if all_valid:
+ for form in forms:
+ bidderid = form.cleaned_data.get('bidderid')
+ if not bidderid: continue
+ name = form.cleaned_data['name']
+ regid = form.cleaned_data['regid']
+ bidder = Bidder ( name=name, regid=regid )
+ bidder.save ()
+ bidderid = BidderId ( id=bidderid, bidder=bidder )
+ bidderid.save ()
+ return HttpResponseRedirect('.')
+ else:
+ forms = [ BidderAddForm ( prefix=str(i) ) for i in range(bidders_per_page) ]
+ c = {'forms':forms}
+ c.update(csrf(request))
+ return render_to_response ( 'artshow/bidderbulkadd.html', c )
+
+
+def single_add ( request ):
+ if request.method == 'POST':
+ form = BidderBulkAddForm ( request.POST )
+ if form.is_valid ():
+ bidderid = form.cleaned_data['bidderid']
+ name = form.cleaned_data['name']
+ regid = form.cleaned_data['regid']
+ bidder = Bidder ( name=name, bidderid=bidderid, regid=regid )
+ bidder.save ()
+ return HttpResponseRedirect('.')
+ else:
+ form = BidderBulkAddForm ()
+ c = {'form': form}
+ c.update(csrf(request))
+ return render_to_response ( 'artshow/bidderadd.html', c )
View
432 artshow/admin.py
@@ -0,0 +1,432 @@
+# Artshow Jockey
+# Copyright (C) 2009, 2010 Chris Cogdon
+# See file COPYING for licence details
+
+from models import *
+from django.contrib import admin, messages
+from django.core import urlresolvers
+from django.contrib.admin import helpers
+from django import template
+from django.shortcuts import render_to_response
+from django.utils.html import escape
+from django import forms
+from django.db import models
+import email1, processbatchscan
+from django.core.mail import send_mail
+import artshow_settings
+import smtplib, datetime, decimal
+from django.http import HttpResponse
+
+class FormfieldOverrider ( object ):
+ def formfield_for_dbfield ( self, db_field, **kwargs ):
+ try:
+ override = self.formfield_overrides2[db_field.name]
+ except KeyError:
+ pass
+ else:
+ kwargs['widget'] = override
+ return super(FormfieldOverrider,self).formfield_for_dbfield ( db_field, **kwargs )
+
+class TestWidget ( forms.TextInput ):
+ def __init__ ( self ):
+ super(TestWidget,self).__init__ ( attrs={'size':30} )
+
+class PanelCountWidget ( forms.TextInput ):
+ def __init__ ( self ):
+ super(PanelCountWidget,self).__init__ ( attrs={'size':6} )
+
+class PieceIdWidget ( forms.TextInput ):
+ def __init__ ( self ):
+ super(PieceIdWidget,self).__init__ ( attrs={'size':3} )
+
+class BidTextWidget ( forms.TextInput ):
+ def __init__ ( self ):
+ super(BidTextWidget,self).__init__ ( attrs={'size':6} )
+
+class MediaTextWidget ( forms.TextInput ):
+ def __init__ ( self ):
+ super(MediaTextWidget,self).__init__ ( attrs={'size':8} )
+
+class LocationTextWidget ( forms.TextInput ):
+ def __init__ ( self ):
+ super(LocationTextWidget,self).__init__ ( attrs={'size':4} )
+
+class ArtistAccessInline ( admin.TabularInline ):
+ model = ArtistAccess
+ extra = 0
+
+class AllocationInline ( FormfieldOverrider, admin.TabularInline ):
+ model = Allocation
+ extra = 1
+ formfield_overrides2 = {
+ 'requested': PanelCountWidget,
+ 'allocated': PanelCountWidget,
+ }
+
+class PieceInline ( FormfieldOverrider, admin.TabularInline ):
+ fields = ( "pieceid", "name", "media", "adult", "not_for_sale", "min_bid", "buy_now", "location", "voice_auction", "status" )
+# formfield_overrides = {
+# 'location': { 'widget': LocationTextWidget },
+# }
+ model = Piece
+ extra = 5
+ formfield_overrides2 = {
+ 'pieceid': PieceIdWidget,
+ 'media': MediaTextWidget,
+ 'location': LocationTextWidget,
+ 'min_bid': BidTextWidget,
+ 'buy_now': BidTextWidget,
+ 'invoice_price': BidTextWidget,
+ }
+ ordering = ( 'pieceid', )
+
+#class ProductInline ( admin.TabularInline ):
+# fields = ( "productid", "name", "adult", "price", "location" )
+# model = Product
+# extra = 1
+# ordering = ( 'productid', )
+
+class PaymentInline ( admin.TabularInline ):
+ model = Payment
+ extra = 1
+
+class ArtistAdmin ( admin.ModelAdmin ):
+ list_display = ( 'name', 'publicname', 'artistid', 'clickable_email', 'regid', 'requested_spaces', 'allocated_spaces', 'mailing_label' )
+ list_filter = ( 'mailin', 'country', 'checkoffs' )
+ search_fields = ( 'name', 'publicname', 'email', 'notes', 'artistid', 'regid' )
+ fieldsets = [
+ (None, { 'fields' : [ 'name', 'publicname', ( 'artistid', 'regid' ), ( 'reservationdate', 'mailin', 'agent' ), 'notes', 'checkoffs' ] } ),
+ ('Contact Information', { 'fields' : [ 'address1', 'address2', 'city', 'state', 'postcode', 'country', 'phone', 'email', 'website', 'name_for_cheque' ], 'classes' : ['collapse'] } ),
+ ]
+ inlines = [ArtistAccessInline,AllocationInline,PieceInline,PaymentInline]
+ def requested_spaces ( self, artist ):
+ return ", ".join ( "%s:%s" % (al.space.shortname,al.requested) for al in artist.allocation_set.all() )
+ def allocated_spaces ( self, artist ):
+ return ", ".join ( "%s:%s" % (al.space.shortname,al.allocated) for al in artist.allocation_set.all() )
+
+ def send_email ( self, request, queryset ):
+ opts = self.model._meta
+ app_label = opts.app_label
+ emails = []
+ template_id = None
+ if request.POST.get('post'):
+ template_id = request.POST.get('template')
+ if not template_id:
+ messages.error ( request, "Please select a template" )
+ else:
+ template_id = int(template_id)
+ selected_template = EmailTemplate.objects.get(pk=template_id)
+ if request.POST.get('send_email'):
+ for a in queryset:
+ body = email1.make_email ( a, selected_template.template )
+ try:
+ send_mail ( selected_template.subject, body, artshow_settings.ARTSHOW_EMAIL_SENDER, [ a.email ], fail_silently=False )
+ self.message_user ( request, "Mail to %s succeeded" % a.email )
+ except smtplib.SMTPException, x:
+ # TODO - use a error indicator
+ self.message_user ( request, "Mail to %s failed: %s" % ( a.email, x ) )
+ return None
+ else:
+ for a in queryset:
+ emails.append ( { 'to': a.email, 'body':email1.make_email ( a, selected_template.template ) } )
+ templates = EmailTemplate.objects.all()
+ context = {
+ "title": "Send E-mail to Artists",
+ "queryset": queryset,
+ "opts": opts,
+ "root_path": self.admin_site.root_path,
+ "app_label": app_label,
+ "action_checkbox_name": helpers.ACTION_CHECKBOX_NAME,
+ "templates": templates,
+ "emails": emails,
+ "template_id": template_id,
+ }
+ return render_to_response ( "admin/email_selected_confirmation.html", context, context_instance=template.RequestContext(request))
+ send_email.short_description = "Send E-mail"
+
+ def print_bidsheets ( self, request, queryset ):
+ import bidsheets
+ response = HttpResponse ( mimetype="application/pdf" )
+ bidsheets.generate_bidsheets_from_queryset ( template_pdf=artshow_settings.ARTSHOW_BLANK_BID_SHEET, output=response, artists=queryset )
+ self.message_user ( request, "Bid sheets printed." )
+ return response
+ print_bidsheets.short_description = "Print Bid Sheets"
+
+ def apply_space_fees ( self, request, artists ):
+ payment_type = PaymentType.objects.get(pk=artshow_settings.SPACE_FEE_PK)
+ for a in artists:
+ total = 0
+ for alloc in a.allocation_set.all():
+ total += alloc.space.price * alloc.allocated
+ if total > 0:
+ allocated_spaces_str = ", ".join ( "%s:%s" % (al.space.shortname,al.allocated) for al in a.allocation_set.all() )
+ payment = Payment ( artist=a, amount=-total, payment_type=payment_type, description=allocated_spaces_str, date=datetime.datetime.now() )
+ payment.save ()
+
+ def apply_winnings_and_commission ( self, request, artists ):
+ pt_winning = PaymentType.objects.get(pk=artshow_settings.SALES_PK)
+ pt_commission = PaymentType.objects.get(pk=artshow_settings.COMMISSION_PK)
+ for a in artists:
+ total_winnings = 0
+ total_pieces = 0
+ pieces_with_bids = 0
+ for piece in a.piece_set.all():
+ if piece.status != Piece.StatusNotInShow:
+ total_pieces += 1
+ try:
+ top_bid = piece.top_bid()
+ total_winnings += top_bid.amount
+ pieces_with_bids += 1
+ except Bid.DoesNotExist:
+ pass
+ commission = total_winnings * decimal.Decimal(artshow_settings.ARTSHOW_COMMISSION)
+ if total_pieces > 0:
+ payment = Payment ( artist=a, amount=total_winnings, payment_type=pt_winning, description="%d piece%s, %d with bid%s" % ( total_pieces, total_pieces!=1 and "s" or "", pieces_with_bids, pieces_with_bids != 1 and "s" or ""), date=datetime.datetime.now() )
+ payment.save ()
+ if commission > 0:
+ payment = Payment ( artist=a, amount=-commission, payment_type=pt_commission,
+ description="%s%% of sales" % (decimal.Decimal(artshow_settings.ARTSHOW_COMMISSION) * 100),
+ date=datetime.datetime.now() )
+ payment.save ()
+
+ def create_cheques ( self, request, artists ):
+ pt_paymentsent = PaymentType.objects.get ( pk=artshow_settings.PAYMENT_SENT_PK )
+ for a in artists:
+ balance = a.payment_set.aggregate ( balance=Sum ( 'amount' ) )['balance']
+ if balance > 0:
+ chq = ChequePayment ( artist=a, payment_type=pt_paymentsent, amount=-balance, date=datetime.datetime.now() )
+ chq.clean ()
+ chq.save ()
+
+ def allocate_spaces ( self, request, artists ):
+ artists = artists.order_by ( 'reservationdate', 'artistid' )
+ spaces_remaining = {}
+ for space in Space.objects.all ():
+ spaces_remaining[space.id] = space.remaining ()
+ for artist in artists:
+ for alloc in artist.allocation_set.all ():
+ needed = alloc.requested - alloc.allocated
+ to_allocate = min ( needed, spaces_remaining[alloc.space.id] )
+ if to_allocate > 0:
+ alloc.allocated += to_allocate
+ spaces_remaining[alloc.space.id] -= to_allocate
+ alloc.save ()
+
+ actions = ('send_email','print_bidsheets','apply_space_fees','apply_winnings_and_commission','create_cheques','allocate_spaces')
+ filter_horizontal = ('checkoffs',)
+
+admin.site.register(Artist,ArtistAdmin)
+
+class SpaceAdmin ( admin.ModelAdmin ):
+ list_display = ( 'name', 'shortname', 'price', 'available', 'allocated', 'remaining', 'waiting' )
+
+admin.site.register(Space,SpaceAdmin)
+
+class BidInline ( admin.TabularInline ):
+ model = Bid
+ raw_id_fields = ( 'bidder', )
+ extra = 1
+
+class PieceAdmin ( admin.ModelAdmin ):
+ def clear_scanned_flag ( self, request, pieces ):
+ pieces.update ( bidsheet_scanned=False )
+ self.message_user ( request, "Bidsheet_scanned flags have been cleared." )
+
+ def set_scanned_flag ( self, request, pieces ):
+ pieces.exclude(status=Piece.StatusNotInShow).update ( bidsheet_scanned=True )
+ self.message_user ( request, "Bidsheet_scanned flags have been set if the piece is or was in show." )
+
+ def clear_won_status ( self, request, pieces ):
+ pieces.filter(status=Piece.StatusWon).update ( status=Piece.StatusInShow )
+ self.message_user ( request, "Pieces marked as 'Won' have been returned to 'In Show'." )
+
+ def apply_won_status ( self, request, pieces ):
+ for p in pieces.filter(status=Piece.StatusInShow,voice_auction=False):
+ try:
+ top_bid = p.top_bid()
+ except ObjectDoesNotExist:
+ pass
+ else:
+ p.status = Piece.StatusWon
+ p.save ()
+ self.message_user ( request, "Pieces marked as 'In Show', not in Voice Auction and has a bid have been marked as 'Won'." )
+
+ def apply_won_status_incl_voice_auction ( self, request, pieces ):
+ for p in pieces.filter(status=Piece.StatusInShow):
+ try:
+ top_bid = p.top_bid()
+ except ObjectDoesNotExist:
+ pass
+ else:
+ p.status = Piece.StatusWon
+ p.save ()
+ self.message_user ( request, "Pieces marked as 'In Show' and has a bid have been marked as 'Won'." )
+
+ def apply_returned_status ( self, request, pieces ):
+ pieces.filter (status=Piece.StatusInShow).update ( status=Piece.StatusReturned )
+ self.message_user ( request, "Pieces marked as 'In Show' have been marked 'Returned'." )
+
+ def clickable_artist ( self, obj ):
+ return u'<a href="%s">%s</a>' % ( urlresolvers.reverse('admin:artshow_artist_change',args=(obj.artist.pk,)), escape(obj.artist.artistname()) )
+ clickable_artist.allow_tags = True
+ clickable_artist.short_description = "Artist"
+ def clickable_invoice ( self, obj ):
+ return u'<a href="%s">%s</a>' % ( urlresolvers.reverse('admin:artshow_invoice_change',args=(obj.invoice.id,)), obj.invoice )
+ clickable_invoice.allow_tags = True
+ clickable_invoice.short_description = "Invoice"
+ def top_bid ( self, obj ):
+ return obj.bid_set.exclude ( invalid=True ).order_by ( '-amount' )[0:1].get().amount
+ def min_bid_x ( self, obj ):
+ if obj.not_for_sale or obj.min_bid == None:
+ return "NFS"
+ else:
+ return obj.min_bid
+ min_bid_x.short_description = "Min Bid"
+ def buy_now_x ( self, obj ):
+ if obj.buy_now == None:
+ return "N/A"
+ else:
+ return obj.buy_now
+ buy_now_x.short_description = "Buy Now"
+ list_filter = ( 'adult', 'not_for_sale', 'voice_auction', 'status', 'bidsheet_scanned' )
+ search_fields = ( '=code', '=artist__artistid', 'name', '=location', 'artist__name', 'artist__publicname' )
+ list_display = ( 'code', 'clickable_artist', 'name', 'adult', 'min_bid_x', 'buy_now_x', 'location', 'voice_auction', 'status', 'top_bid' )
+ inlines = [BidInline]
+ # raw_id_fields = ( 'invoice', )
+ # TODO put 'invoiceitem' back into the list. Waiting on bug #16433
+ fields = ( 'artist', 'pieceid', 'name', 'media', 'location', 'not_for_sale', 'adult', 'min_bid', 'buy_now', 'voice_auction', 'bidsheet_scanned', 'status', 'top_bid' )
+ raw_id_fields = ( 'artist', )
+ readonly_fields = ( 'top_bid', 'invoiceitem', )
+ actions = ( 'clear_scanned_flag', 'set_scanned_flag', 'clear_won_status', 'apply_won_status', 'apply_won_status_to_voice_auction', 'apply_returned_status' )
+
+admin.site.register(Piece,PieceAdmin)
+
+#admin.site.register(Product)
+
+class BidderIdInline ( admin.TabularInline ):
+ model = BidderId
+
+class BidInline ( admin.TabularInline ):
+ model = Bid
+ fields = ( 'piece', 'amount', 'buy_now_bid', 'invalid' )
+ raw_id_fields = ( 'piece', )
+
+class BidderAdmin ( admin.ModelAdmin ):
+ def bidder_ids ( self, obj ):
+ return u", ".join ( [ bidderid.id for bidderid in obj.bidderid_set.all() ] )
+ # TODO, add mailing_label back in once we figure out how to do it for bidders and artists uniformly
+ list_display = ( 'name', 'bidder_ids', 'regid', 'clickable_email' )
+ search_fields = ( 'name', 'bidderid__id' )
+ fieldsets = [
+ (None, { 'fields' : [ 'name', 'regid', 'notes' ] } ),
+ ('Contact Information', { 'fields' : [ 'address1', 'address2', 'city', 'state', 'postcode', 'country', 'phone', 'email' ], 'classes' : ['collapse'] } ),
+ ]
+ inlines = [BidderIdInline,BidInline]
+
+admin.site.register(Bidder,BidderAdmin)
+
+class EmailTemplateAdmin ( admin.ModelAdmin ):
+ save_as = True
+
+admin.site.register(EmailTemplate,EmailTemplateAdmin)
+
+class PaymentAdmin ( admin.ModelAdmin ):
+ list_display = ( 'artist', 'amount', 'payment_type', 'description', 'date' )
+ list_filter = ( 'payment_type', )
+ date_hierarchy = 'date'
+ raw_id_fields = ( 'artist', )
+
+admin.site.register(Payment,PaymentAdmin)
+
+admin.site.register(PaymentType)
+
+class InvoiceItemInline ( admin.TabularInline ):
+ model = InvoiceItem
+ # fields = ( 'piece', 'top_bid', 'price', )
+ fields = ( 'piece', 'price', )
+ raw_id_fields = ( 'piece', )
+ # read_only_fields = ( 'top_bid', )
+ # formfield_overrides2 = {
+ # 'top_bid': TestWidget,
+ # }
+
+class InvoicePaymentInline ( admin.TabularInline ):
+ model = InvoicePayment
+
+class InvoiceAdmin ( admin.ModelAdmin ):
+ def bidder_name ( self, obj ):
+ return obj.payer.name
+ def num_pieces ( self, obj ):
+ return obj.invoiceitem_set.count()
+ raw_id_fields = ( 'payer', )
+ list_display = ( 'id', 'bidder_name', 'num_pieces', 'total_paid' )
+ search_fields = ( 'id', 'payer__name' )
+ inlines = [InvoiceItemInline,InvoicePaymentInline]
+
+
+admin.site.register(Invoice,InvoiceAdmin)
+
+class BidAdmin ( admin.ModelAdmin ):
+ raw_id_fields = ( "bidder", "piece" )
+
+admin.site.register(Bid,BidAdmin)
+
+class CheckoffAdmin ( admin.ModelAdmin ):
+ list_display = ( 'name', 'shortname' )
+
+admin.site.register(Checkoff,CheckoffAdmin)
+
+class BatchScanAdmin ( admin.ModelAdmin ):
+ list_display = ( 'id', 'batchtype', 'date_scanned', 'processed' )
+ list_filter = ( 'batchtype', 'processed', )
+ fields = ( 'id', 'batchtype', 'data', 'date_scanned', 'processed', 'processing_log' )
+ readonly_fields = ( 'id', )
+ actions = ('process_batch',)
+
+ def process_batch ( self, request, queryset ):
+ opts = self.model._meta
+ for bs in queryset:
+ processbatchscan.process_batchscan ( bs.id )
+ self.message_user ( request, "Processed batch %d" % bs.id )
+ process_batch.short_description = "Process Batch"
+
+
+admin.site.register(BatchScan,BatchScanAdmin)
+
+# class BidderIdAdmin ( admin.ModelAdmin ):
+# list_display = ( 'id', 'bidder' )
+# search_fields = ( 'id', )
+# raw_id_fields = ( 'bidder', )
+
+#admin.site.register(BidderId,BidderIdAdmin)
+
+#admin.site.register(Event)
+
+#class TaskAdmin ( admin.ModelAdmin ):
+# def due_at_date ( self, task ):
+# return task.due_at.auto_occur
+# def due_occurred ( self, task ):
+# return task.due_at.occurred
+# list_display = ( 'summary', 'due_at', 'due_at_date', 'due_occurred', 'done', 'actor' )
+# list_filter = ( 'done', 'actor' )
+
+#admin.site.register(Task,TaskAdmin)
+
+class ChequePaymentAdmin ( admin.ModelAdmin ):
+ def cheque_amount ( self, obj ):
+ return -obj.amount
+ def print_cheques ( self, request, cheqs ):
+ import cheques
+ response = HttpResponse ( mimetype="text/plain" )
+ for c in cheqs:
+ cheques.cheque_to_text ( c, response )
+ return response
+
+ list_display = ( 'artist', 'date', 'payee', 'number', 'cheque_amount' )
+ list_editable = ( 'number', )
+ search_fields = ( 'artist__artistid', 'artist__name', 'payee', 'number' )
+ fields = ( 'artist', 'date', 'payee', 'number', 'amount' )
+ raw_id_fields = ( 'artist', )
+ actions = ('print_cheques',)
+
+admin.site.register ( ChequePayment, ChequePaymentAdmin )
View
14 artshow/artshow_settings.py
@@ -0,0 +1,14 @@
+SPACE_FEE_PK = 3
+PAYMENT_SENT_PK = 5
+COMMISSION_PK = 6
+SALES_PK = 7
+
+from django.conf import settings
+
+_globals_dict = globals()
+_settings_dict = settings.__dict__
+for k in _settings_dict.keys():
+ if k.startswith("ARTSHOW_"):
+ _globals_dict[k] = _settings_dict[k]
+del _globals_dict
+del _settings_dict
View
76 artshow/bidsheets.py
@@ -0,0 +1,76 @@
+#! /usr/bin/env python
+
+from reportlab.pdfgen.canvas import Canvas
+from reportlab.lib.units import inch
+from reportlab.lib.styles import getSampleStyleSheet, TA_CENTER, ParagraphStyle
+from reportlab.platypus import Paragraph, Frame
+
+from pdfrw import PdfReader
+from pdfrw.buildxobj import pagexobj
+from pdfrw.toreportlab import makerl
+
+from cgi import escape
+
+import optparse
+
+default_style = ParagraphStyle ( "default_style", fontName="Helvetica", alignment=TA_CENTER, allowWidows=0, allowOrphans=0 )
+
+
+def draw_msg_into_frame ( frame, canvas, msg, font_size, min_font_size ):
+ for size in range ( font_size, min_font_size-1, -1 ):
+ current_style = ParagraphStyle ( "temp_style", parent=default_style, fontSize=size, leading=size )
+ story = [ Paragraph ( escape(msg), current_style ) ]
+ frame.addFromList ( story, canvas )
+ if len(story) == 0: break
+ else:
+ raise Exception ( "Could not flow text into box." )
+
+
+def text_into_box ( canvas, msg, x0, y0, x1, y1, fontName="Helvetica", fontSize=18, minFontSize=6, units=inch ):
+ frame = Frame ( x0*units, y0*units, (x1-x0)*units, (y1-y0)*units, leftPadding=2, rightPadding=2, topPadding=0, bottomPadding=4, showBoundary=0 )
+ draw_msg_into_frame ( frame, canvas, msg, fontSize, minFontSize )
+
+
+
+def generate_bidsheets_from_queryset ( template_pdf, output, artists ):
+
+ c = Canvas(output)
+
+ pdf = PdfReader ( template_pdf )
+ xobj = pagexobj ( pdf.pages[0] )
+ rlobj = makerl ( c, xobj )
+
+ sheet_offsets = [
+ (0,5.5),
+ (4.25,5.5),
+ (0,0),
+ (4.25,0),
+ ]
+
+ sheets_per_page = len(sheet_offsets)
+ sheet_num = 0
+
+ for artist in artists.order_by ( 'artistid' ):
+ for piece in artist.piece_set.all().order_by ( 'pieceid' ):
+ c.saveState ()
+ c.translate ( sheet_offsets[sheet_num][0]*inch, sheet_offsets[sheet_num][1]*inch )
+ c.doForm ( rlobj )
+
+ text_into_box ( c, str(piece.artist.artistid), 2.6, 4.9, 3.2, 5.2 )
+ text_into_box ( c, str(piece.pieceid), 3.3, 4.9, 3.9, 5.2 )
+ text_into_box ( c, piece.artist.artistname(), 0.65, 4.1, 2.95, 4.5 )
+ text_into_box ( c, piece.name, 0.65, 3.7, 2.95, 4.1 )
+ text_into_box ( c, piece.media, 0.65, 3.32, 2.95, 3.7 )
+ text_into_box ( c, piece.not_for_sale and "NFS" or str(piece.min_bid), 3.1, 4.1, 3.9, 4.35 )
+ text_into_box ( c, piece.buy_now and str(piece.buy_now) or "X", 3.1, 3.7, 3.9, 3.95 )
+ # text_into_box ( c, piece.not_for_sale and "NFS" or str(piece.min_bid), 3.1, 3.35, 3.9, 3.6 )
+
+ c.restoreState ()
+ sheet_num += 1
+ if sheet_num == sheets_per_page:
+ c.showPage ()
+ sheet_num = 0
+
+ if sheet_num != 0:
+ c.showPage ()
+ c.save ()
View
111 artshow/cashier.py
@@ -0,0 +1,111 @@
+# Artshow Jockey
+# Copyright (C) 2009, 2010, 2011 Chris Cogdon
+# See file COPYING for licence details
+
+from django.shortcuts import render_to_response, get_object_or_404, redirect
+from django.http import HttpResponse
+from artshow.models import Bidder, Piece, InvoicePayment, InvoiceItem, Bid, Invoice
+from django import forms
+from django.core.context_processors import csrf
+from django.views.decorators.csrf import csrf_protect
+from django.db.models import Q
+from django.forms.formsets import formset_factory, BaseFormSet
+from django.forms.models import modelformset_factory
+from django.template import RequestContext
+import logging, datetime
+logger = logging.getLogger ( __name__ )
+import subprocess
+
+class BidderSearchForm ( forms.Form ):
+ text = forms.CharField ()
+
+def cashier ( request ):
+ if request.method == "POST":
+ form = BidderSearchForm ( request.POST )
+ if form.is_valid ():
+ text = form.cleaned_data['text']
+ # TODO - the following will return multiple entries for name base if bidder has two IDs
+ bidders = Bidder.objects.filter ( Q(name__icontains=text) | Q(bidderid__id=text) )
+ else:
+ bidders = []
+ else:
+ form = BidderSearchForm ()
+ bidders = []
+
+ c = { "form":form, "bidders":bidders }
+ c.update(csrf(request))
+ return render_to_response ( 'artshow/cashier.html', c )
+
+class TaxPaidForm ( forms.Form ):
+ tax_paid = forms.DecimalField ()
+
+PaymentFormSet = modelformset_factory ( InvoicePayment, fields=("amount","payment_method"), extra=2 )
+
+class SelectPieceForm ( forms.Form ):
+ select = forms.BooleanField ()
+
+#TODO probably need a @transaction.commit_on_success here
+
+def cashier_bidder ( request, bidder_id ):
+
+ bidder = get_object_or_404 ( Bidder, pk=bidder_id )
+
+ all_bids = bidder.top_bids( unsold_only=True )
+ available_bids = []
+ pending_bids = []
+ bid_dict = {}
+ for bid in all_bids:
+ if bid.piece.status == Piece.StatusWon:
+ available_bids.append ( bid )
+ bid_dict[bid.pk] = bid
+ else:
+ pending_bids.append ( bid )
+
+ if request.method == "POST":
+ for bid in available_bids:
+ form = SelectPieceForm ( request.POST, prefix="bid-%d"%bid.pk )
+ bid.form = form
+ tax_form = TaxPaidForm ( request.POST, prefix="tax" )
+ payment_formset = PaymentFormSet ( request.POST, prefix="payment", queryset=InvoicePayment.objects.none() )
+ if all( bid.form.is_valid() for bid in available_bids ) and tax_form.is_valid() and payment_formset.is_valid():
+ logger.debug ( "Holy crap, everything passed" )
+ tax_paid = tax_form.cleaned_data['tax_paid']
+ total_paid = tax_paid + sum ( bid.amount for bid in available_bids )
+ invoice = Invoice ( payer=bidder, tax_paid=tax_form.cleaned_data['tax_paid'], total_paid=total_paid,
+ paid_date=datetime.datetime.now() )
+ invoice.save ()
+ payments = payment_formset.save(commit=False)
+ for payment in payments:
+ payment.invoice = invoice
+ payment.save ()
+ for bid in available_bids:
+ if bid.form.cleaned_data['select']:
+ invoice_item = InvoiceItem ( piece=bid.piece, price=bid.amount, invoice=invoice )
+ invoice_item.save ()
+ bid.piece.status = Piece.StatusSold
+ bid.piece.save ()
+ p = subprocess.Popen ( ["/home/chris/dev/artshowproj/artshow-utils/igen", str(invoice.id)], stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE )
+ output, error = p.communicate ()
+ logger.debug ( "printer returned: %s", output )
+ if error:
+ logger.error ( "printer error returned: %s", error )
+ return redirect ( cashier_invoice, invoice_id=invoice.id )
+ else:
+ for bid in available_bids:
+ form = SelectPieceForm ( prefix="bid-%d"%bid.pk, initial={"select":False} )
+ bid.form = form
+ tax_form = TaxPaidForm ( prefix="tax" )
+ payment_formset = PaymentFormSet ( prefix="payment", queryset=InvoicePayment.objects.none() )
+ c = dict ( bidder=bidder, available_bids=available_bids, pending_bids=pending_bids, tax_form=tax_form, payment_formset=payment_formset )
+ # c.update ( csrf(request) )
+ return render_to_response ( 'artshow/cashier_bidder.html', c, context_instance=RequestContext(request) )
+
+# TODO... show a mock-up of the invoice here.
+
+def cashier_invoice ( request, invoice_id ):
+ invoice = get_object_or_404 ( Invoice, pk=invoice_id )
+ c = dict ( invoice=invoice )
+ # c.update ( csrf(request) )
+ return render_to_response ( 'artshow/cashier_invoice.html', c, context_instance=RequestContext(request) )
+
+
View
114 artshow/cheques.py
@@ -0,0 +1,114 @@
+#! /usr/bin/env python
+# Artshow Jockey
+# Copyright (C) 2009-2012 Chris Cogdon
+# See file COPYING for licence details
+
+import sys, os, re, num2word, time, optparse
+from decimal import Decimal
+from artshow.models import Artist
+from email1 import wrap
+import artshow_settings
+
+class PRINT_GRID:
+ x_size = 87
+ y_size = 66
+ def __init__ ( self ):
+ self.data = [ " "*self.x_size for i in range(self.y_size) ]
+ def print_at ( self, x, y, msg ):
+ if x + len(msg) - 1 > self.x_size:
+ raise Exception ( "line too long" )
+ msg_len = len(msg)
+ s = self.data[y-1]
+ s = s[:x-1] + msg + s[x-1+msg_len:]
+ self.data[y-1] = s
+ self.last_line_printed = y
+ def print_on_next_line ( self, msg ):
+ self.print_at ( 1, self.last_line_printed+1, msg )
+ def save ( self, f ):
+ for line in self.data:
+ print >>f, line
+
+def dotpad ( s, max ):
+ l = len(s)
+ if l >= max:
+ return s
+ s += " " * (( max-l ) % 4)
+ s += " .." * ((max-l) / 4 )
+ return s
+
+
+def cheque_to_text ( cheque, f ):
+
+ date_str = cheque.date.strftime ( "%d %b %Y" ).upper()
+
+ cheque_amount = -cheque.amount
+
+ cheque_dollars = int(cheque_amount)
+ cheque_cents = int ((cheque_amount - cheque_dollars) * 100 + Decimal("0.5") )
+
+ cheque_words = "** " + num2word.n2w.to_cardinal ( cheque_dollars ) + " dollars and %d cents" % cheque_cents + " **"
+ cheque_words = cheque_words.upper ()
+
+ grid = PRINT_GRID ()
+
+ grid.print_at ( 77, 3, date_str )
+
+ grid.print_at ( 8, 7, cheque.payee.upper() )
+ grid.print_at ( 77, 7, "$%-8.2f" % cheque_amount )
+
+ grid.print_at ( 1, 9, cheque_words )
+
+ mailing_label_lines = [ cheque.payee, cheque.artist.address1 ]
+ if cheque.artist.address2:
+ mailing_label_lines.append ( cheque.artist.address2 )
+ mailing_label_lines.append ( "%s %s %s" % ( cheque.artist.city, cheque.artist.state, cheque.artist.postcode ) )
+ if cheque.artist.country and cheque.artist.country != "USA":
+ mailing_label_lines.append ( cheque.artist.country )
+ mailing_label_lines = [ x.upper() for x in mailing_label_lines ]
+
+ for i in range(len(mailing_label_lines)):
+ grid.print_at ( 9, 14+i, mailing_label_lines[i] )
+
+ print_items ( cheque, grid, 23, True )
+ print_items ( cheque, grid, 46, False )
+
+ grid.save ( f )
+
+
+def print_items ( cheque, grid, offset, payee_side ):
+
+ date_str = cheque.date.strftime ( "%d %b %Y" ).upper()
+
+ s = "(%d) %s" % ( cheque.artist.artistid, cheque.artist.name )
+ if cheque.artist.publicname:
+ s += " (%s)" % cheque.artist.publicname
+
+ grid.print_at ( 1, offset, s )
+ grid.print_at ( 77, offset, date_str )
+
+ grid.last_line_printed = offset+1
+
+ item_no = 0
+ for item in cheque.artist.payment_set.all().order_by ( 'date', 'id' ):
+ item_no += 1
+ if item.id == cheque.id:
+ name = dotpad ( "This cheque", 71 )
+ else:
+ name = dotpad ( "%s: %s" % ( item.payment_type.name, item.description), 71 )[:71]
+ grid.print_on_next_line ( " %2d. %-71s $%8.2f" % ( item_no, name, item.amount ) )
+
+ grid.print_on_next_line ( " %71s $%8.2f" % ( "Balance:", cheque.artist.balance() ) )
+
+ if payee_side:
+ grid.print_on_next_line ( "" )
+ message = wrap ( artshow_settings.ARTSHOW_CHEQUE_THANK_YOU, cols=78, always_wrap=True )
+ message_lines = message.split ( "\n" )
+ for l in message_lines:
+ grid.print_on_next_line ( l )
+
+ else:
+ grid.print_on_next_line ( "" )
+ grid.print_on_next_line ( "Signature: _____________________________________________ Date: __________" )
+ grid.print_on_next_line ( "I have received this cheque and agree to return any amount paid in error." )
+
+
View
157 artshow/csvreports.py
@@ -0,0 +1,157 @@
+# Artshow Jockey
+# Copyright (C) 2009-2012 Chris Cogdon
+# See file COPYING for licence details
+
+from artshow.models import *
+from django.http import HttpResponse
+import csv
+
+
+def artists ( request ):
+
+ artists = Artist.objects.all ().order_by('artistid')
+ spaces = Space.objects.all ()
+ checkoffs = Checkoff.objects.all()
+
+ field_names = [ 'artistid', 'name', 'address1', 'address2', 'city', 'state', 'postcode',
+ 'country', 'phone', 'email', 'regid', 'artistname', 'website', 'mailin', 'agent', 'reservationdate',
+ 'nameforcheque' ]
+
+ for space in spaces:
+ field_names += [ 'req-'+space.shortname, 'alloc-'+space.shortname ]
+
+ for checkoff in checkoffs:
+ field_names += [ 'chk-'+checkoff.shortname ]
+
+ field_names_d = {}
+ for n in field_names:
+ field_names_d[n] = n
+
+ response = HttpResponse ( mimetype="text/csv" )
+ response['Content-Disposition'] = "attachment; filename=artists.csv"
+ c = csv.DictWriter ( response, field_names )
+ c.writerow ( field_names_d )
+
+ for a in artists:
+ d = dict ( artistid=a.artistid, name=a.name, address1=a.address1, address2=a.address2, city=a.city, state=a.state,
+ postcode=a.postcode, country=a.country, phone=a.phone, email=a.email, regid=a.regid, artistname=a.artistname(),
+ website=a.website, mailin= a.mailin and "Yes" or "No", agent=a.agent,
+ reservationdate=str(a.reservationdate) )
+ for alloc in a.allocation_set.all():
+ d['req-'+alloc.space.shortname] = str(alloc.requested)
+ d['alloc-'+alloc.space.shortname] = str(alloc.allocated)
+ for checkoff in a.checkoffs.all():
+ d['chk-'+checkoff.shortname] = checkoff.shortname
+ c.writerow ( d )
+
+ return response
+
+
+def pieces ( request ):
+
+ pieces = Piece.objects.all ().order_by ( 'artist__artistid', 'pieceid' )
+
+ field_names = [ 'artistid', 'pieceid', 'code', 'artistname', 'title', 'media', 'min_bid', 'buy_now', 'adult', 'not_for_sale', 'status', 'top_bid', 'bought_now', 'voice_auction', 'bidder_name', 'bidder_ids' ]
+
+ field_names_d = {}
+ for n in field_names:
+ field_names_d[n] = n
+
+ response = HttpResponse ( mimetype="text/csv" )
+ response['Content-Disposition'] = "attachment; filename=pieces.csv"
+ c = csv.DictWriter ( response, field_names )
+ c.writerow ( field_names_d )
+
+ for p in pieces:
+ try:
+ top_bid = p.top_bid()
+ except Bid.DoesNotExist:
+ top_bid = None
+ d = dict ( artistid=p.artist.artistid, pieceid=p.pieceid, code=p.code, artistname=p.artist.artistname(),
+ title=p.name, media=p.media, min_bid=p.min_bid, buy_now=p.buy_now, adult=p.adult and "Yes" or "No", not_for_sale=p.not_for_sale and "Yes" or "No",
+ status = p.get_status_display(), top_bid=top_bid and top_bid.amount or "",
+ bought_now = top_bid and ( top_bid.buy_now_bid and "Yes" or "No" ) or "",
+ voice_auction = p.voice_auction and "Yes" or "No",
+ bidder_name=top_bid and top_bid.bidder.name or "", bidder_ids = top_bid and (", ".join(top_bid.bidder.bidder_ids()) or "" ),
+ )
+ c.writerow ( d )
+
+ return response
+
+def bidders ( request ):
+