summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore6
-rw-r--r--contrib/non-profit-audit-reports/GPLv3674
-rw-r--r--contrib/non-profit-audit-reports/LICENSE14
-rw-r--r--contrib/non-profit-audit-reports/README100
-rwxr-xr-xcontrib/non-profit-audit-reports/csv2ods.py107
-rwxr-xr-xcontrib/non-profit-audit-reports/demo.sh43
-rwxr-xr-xcontrib/non-profit-audit-reports/general-ledger-report.plx127
-rw-r--r--contrib/non-profit-audit-reports/ooolib2/__init__.py1987
-rwxr-xr-xcontrib/non-profit-audit-reports/readcsv.py31
-rw-r--r--contrib/non-profit-audit-reports/tests/Financial/Invoices/Invoice20110510.pdfbin0 -> 13890 bytes
-rw-r--r--contrib/non-profit-audit-reports/tests/Financial/Invoices/Invoice20110510.txt5
-rw-r--r--contrib/non-profit-audit-reports/tests/Projects/Blah/Expenses/hosting/AprilHostingReceipt.pdf106
-rw-r--r--contrib/non-profit-audit-reports/tests/Projects/Blah/Expenses/hosting/april-invoice.pdfbin0 -> 3153 bytes
-rw-r--r--contrib/non-profit-audit-reports/tests/Projects/Foo/Expenses/hosting/AprilHostingReceipt.pdfbin0 -> 14813 bytes
-rw-r--r--contrib/non-profit-audit-reports/tests/Projects/Foo/Expenses/hosting/AprilHostingReceipt.txt6
-rw-r--r--contrib/non-profit-audit-reports/tests/Projects/Foo/Invoices/Invoice20100101.pdfbin0 -> 14926 bytes
-rw-r--r--contrib/non-profit-audit-reports/tests/non-profit-test-data.ledger26
-rw-r--r--contrib/non-profit-audit-reports/tests/non-profit-test-data_chart-of-accounts.txt4
-rw-r--r--contrib/non-profit-audit-reports/tests/non-profit-test-data_general-ledger.odsbin0 -> 11412 bytes
19 files changed, 3236 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
index 50fb20c0..02f6433a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -98,3 +98,9 @@ system.hh
system.hh.[gp]ch*
Ledger*.dmg
Ledger*.sh
+# Files that generated by running ./demo.sh in contrib/non-profit-audit-reports
+contrib/non-profit-audit-reports/tests/chart-of-accounts.txt
+contrib/non-profit-audit-reports/tests/general-ledger.csv
+contrib/non-profit-audit-reports/tests/general-ledger.ods
+contrib/non-profit-audit-reports/tests/general-ledger.txt
+contrib/non-profit-audit-reports/general-ledger.zip
diff --git a/contrib/non-profit-audit-reports/GPLv3 b/contrib/non-profit-audit-reports/GPLv3
new file mode 100644
index 00000000..94a9ed02
--- /dev/null
+++ b/contrib/non-profit-audit-reports/GPLv3
@@ -0,0 +1,674 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. 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
+them 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 prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. 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.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey 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;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If 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 convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Use with the GNU Affero General Public License.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU 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 that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ 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.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+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.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ 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
+state 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 3 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, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ <program> Copyright (C) <year> <name of author>
+ This program 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, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<http://www.gnu.org/licenses/>.
+
+ The GNU 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. But first, please read
+<http://www.gnu.org/philosophy/why-not-lgpl.html>.
diff --git a/contrib/non-profit-audit-reports/LICENSE b/contrib/non-profit-audit-reports/LICENSE
new file mode 100644
index 00000000..c8c63924
--- /dev/null
+++ b/contrib/non-profit-audit-reports/LICENSE
@@ -0,0 +1,14 @@
+Contents under contrib/non-profit-audit-reports/ are licensed GPLv3-or-later.
+
+The GPLv3-or-later licensing of the contents of this directory does not,
+to our knowledge and belief, impact the licensing of any other part of
+Ledger. Parts of the files herein are likely derivative works of the rest
+of Ledger, but these works are under this subdirectory are not, to our
+knowledge, used, imported, included, copied, etc. into other parts of the
+codebase.
+
+In short, this is a small application written to use Ledger like a
+library, particularly via its Python API interface. It derives from
+Ledger, but Ledger does not, to our knowledge, derive from it.
+
+We are not lawyers and this is not legal advice.
diff --git a/contrib/non-profit-audit-reports/README b/contrib/non-profit-audit-reports/README
new file mode 100644
index 00000000..b4897f21
--- /dev/null
+++ b/contrib/non-profit-audit-reports/README
@@ -0,0 +1,100 @@
+README
+
+This document provides backround on the enclosed example
+
+Demo
+----
+To run the demo do
+./demo.sh
+
+Which should generate the following files in tests/
+ chart-of-accounts.txt
+ general-ledger.txt
+ general-ledger.csv
+ general-ledger.ods
+
+And a final, "portable" zip file with the spreadsheet in
+ general-ledger.zip
+
+It *should* be possible to copy general-ledger.zip to another system,
+unzip it, open general-ledger.ods in Libre Office and have the relative
+links resolve correctly.
+
+NOTE: Export to PDF should also work.
+
+
+Known Dependencies
+------------------
+ledger (3.0)
+python (2.x)
+zip
+libdate-manip-perl
+libmath-gmp-perl
+
+
+Temporary Hacks
+---------------
+Due to an urgent project deadline the ooolib2 directory
+represents some fixes to:
+ http://ooolib.sourceforge.net/
+
+The proper version of this library can be installed on Debian systems with
+# apt-get install python-ooolib
+
+Compare the deltas to the current version with
+# diff -u /usr/share/pyshared/ooolib/__init__.py ooolib2/__init__.py
+
+Note also that the csv2ods.py treats columns 4 and 5 (numbering from 1) of the csv
+magically. If column 4 contains a non-empty string which is not 'Receipt'
+then it is interpreted as a relative path of an artifact to link to.
+Similary for column 5 and 'Invoice'.
+
+
+Sample PDF files
+----------------
+The sample PDF files were created as follows:
+
+paps --font="Courier 12" --paper letter --top-margin=18 tests/Projects/Foo/Expenses/hosting/AprilHostingReceipt.txt | ps2pdf - tests/Projects/Foo/Expenses/hosting/AprilHostingReceipt.pdf
+
+paps --font="Courier 12" --paper letter --top-margin=18 tests/Financial/Invoices/Invoice20110510.txt | ps2pdf - tests/Financial/Invoices/Invoice20110510.pdf
+
+
+Resources
+---------
+ooolib
+ http://ooolib.sourceforge.net/
+
+LIBPF
+ probably does not replace ooolib
+ http://wp.libpf.com/?p=82
+
+Libre Office Calc Guide (contains function reference)
+ https://www.libreoffice.org/get-help/documentation/
+
+Libre Office API
+ http://api.libreoffice.org/examples/examples.html
+ http://api.libreoffice.org/examples/DevelopersGuide/examples.html
+
+OpenOffice Developers Guide
+ http://wiki.openoffice.org/wiki/Documentation/DevGuide/OpenOffice.org_Developers_Guide
+
+Spreadsheet Documents
+ http://wiki.openoffice.org/wiki/Documentation/DevGuide/Spreadsheets/Spreadsheet_Documents
+
+How to correctly create ODF documents using zip
+(Do NOT do this, use ooolib instead)
+ http://www.jejik.com/articles/2010/03/how_to_correctly_create_odf_documents_using_zip/
+
+Line Breaks
+ fo:break-before="page"
+ http://books.evc-cit.info/oobook/ch03.html#page-content-section
+
+ODF Validator
+ http://opendocumentfellowship.com/validator
+
+Editing Hyperlinks
+ http://help.libreoffice.org/Common/Editing_Hyperlinks
+
+Perl OODoc
+NOTE: a replacement for POD, not ooolib
+ http://search.cpan.org/dist/OpenOffice-OODoc/
diff --git a/contrib/non-profit-audit-reports/csv2ods.py b/contrib/non-profit-audit-reports/csv2ods.py
new file mode 100755
index 00000000..f6150158
--- /dev/null
+++ b/contrib/non-profit-audit-reports/csv2ods.py
@@ -0,0 +1,107 @@
+#!/usr/bin/python
+# csv2ods.py
+# Convert example csv file to ods
+#
+# Copyright (c) 2012 Tom Marble
+# Copyright (c) 2012 Bradley M. Kuhn
+#
+# This program gives you software freedom; you can copy, modify, convey,
+# and/or redistribute it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 3 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 in a file called 'GPLv3'. If not, write to the:
+# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor
+# Boston, MA 02110-1301, USA.
+
+import sys, os, os.path, optparse
+import csv
+import ooolib2
+
+def err(msg):
+ print 'error: %s' % msg
+ sys.exit(1)
+
+def csv2ods(csvname, odsname, verbose = False):
+ if verbose:
+ print 'converting from %s to %s' % (csvname, odsname)
+ doc = ooolib2.Calc()
+ # add a pagebreak style
+ style = 'pagebreak'
+ style_pagebreak = doc.styles.get_next_style('row')
+ style_data = tuple([style, ('style:row-height', doc.styles.property_row_height)])
+ doc.styles.style_config[style_data] = style_pagebreak
+ # add a currency style
+ style = 'currency'
+ style_currency = doc.styles.get_next_style('cell')
+ style_data = tuple([style])
+ doc.styles.style_config[style_data] = style_currency
+
+ row = 1
+ csvdir = os.path.dirname(csvname)
+ if len(csvdir) == 0:
+ csvdir = '.'
+ csvfile = open(csvname, 'rb')
+ reader = csv.reader(csvfile, delimiter=',', quotechar='"')
+ for fields in reader:
+ if len(fields) > 0:
+ for col in range(len(fields)):
+ val = fields[col]
+ if len(val) > 0 and val[0] == '$':
+ doc.set_cell_value(col + 1, row, 'currency', val[1:])
+ else:
+ if ( (col == 5) and (val != 'Receipt') and len(val) > 0) or ( (col == 6) and (val != 'Invoice') and len(val) > 0):
+ linkrel = '../' + val # ../ means remove the name of the *.ods
+ linkname = os.path.basename(val) # name is just the last component
+ doc.set_cell_value(col + 1, row, 'link', (linkrel, linkname))
+ linkpath = csvdir + '/' + val
+ if verbose:
+ if os.path.exists(linkpath):
+ print 'relative link %s EXISTS at %s' % (val, linkpath)
+ else:
+ print 'relative link %s DOES NOT EXIST at %s' % (val, linkpath)
+ else:
+ doc.set_cell_value(col + 1, row, 'string', val)
+ else:
+ # enter an empty string for blank lines
+ doc.set_cell_value(1, row, 'string', '')
+ # put a pagebreak here
+ doc.sheets[doc.sheet_index].set_sheet_config(('row', row), style_pagebreak)
+ row += 1
+ # save the file
+ doc.save(odsname)
+
+def main():
+ program = os.path.basename(sys.argv[0])
+ version = '0.1'
+ parser = optparse.OptionParser(usage='%prog [--help] [--verbose]',
+ version='%prog ' + version)
+ parser.add_option('-v', '--verbose', action='store_true',
+ dest='verbose',
+ help='provide extra information while processing')
+ parser.add_option('-c', '--csv', action='store',
+ help='csv file to process')
+ parser.add_option('-o', '--ods', action='store',
+ help='ods output filename')
+ (options, args) = parser.parse_args()
+ if len(args) != 0:
+ parser.error("not expecting extra args")
+ if not os.path.exists(options.csv):
+ err('csv does not exist: %s' % options.csv)
+ if not options.ods:
+ (root, ext) = os.path.splitext(options.csv)
+ options.ods = root + '.ods'
+ if options.verbose:
+ print '%s: verbose mode on' % program
+ print 'csv:', options.csv
+ print 'ods:', options.ods
+ csv2ods(options.csv, options.ods, options.verbose)
+
+if __name__ == '__main__':
+ main()
diff --git a/contrib/non-profit-audit-reports/demo.sh b/contrib/non-profit-audit-reports/demo.sh
new file mode 100755
index 00000000..6a9dcadf
--- /dev/null
+++ b/contrib/non-profit-audit-reports/demo.sh
@@ -0,0 +1,43 @@
+#!/bin/sh
+# demo.sh
+# Demonstrate a non-profit GL export and conversion to ODS
+
+program=$(basename $0)
+dir=$(dirname $0)
+cd $dir
+dir=$(pwd -P)
+export PYTHONPATH=$dir/ooolib2
+
+getcsv=$dir/general-ledger-report.plx
+csv2ods=$dir/csv2ods.py
+
+echo "Demonstrating ledger to ODS export in $dir/tests"
+cd $dir/tests
+sampledata=non-profit-test-data.ledger
+echo " based on the sample data in $sampledata"
+
+$getcsv 2011/03/01 2012/03/01 -f $sampledata
+if [ -e general-ledger.csv ]; then
+ echo "data was exported to: general-ledger.csv"
+else
+ echo "error creating csv file"
+ exit 1
+fi
+
+$csv2ods --verbose --csv general-ledger.csv
+if [ -e general-ledger.ods ]; then
+ echo "csv was converted to: general-ledger.ods"
+else
+ echo "error creating ods file"
+ exit 1
+fi
+
+# create a portable zip file with the spreadsheet
+# and the linked artifacts
+
+echo creating portable zipfile...
+zip -r ../general-ledger.zip general-ledger.ods Financial Projects -x '*.txt'
+
+echo " "
+echo "created general-ledger.zip"
+
diff --git a/contrib/non-profit-audit-reports/general-ledger-report.plx b/contrib/non-profit-audit-reports/general-ledger-report.plx
new file mode 100755
index 00000000..5286d625
--- /dev/null
+++ b/contrib/non-profit-audit-reports/general-ledger-report.plx
@@ -0,0 +1,127 @@
+#!/usr/bin/perl
+# general-ledger-report.plx -*- Perl -*-
+#
+# Script to generate a General Ledger report that accountants like
+# using Ledger.
+#
+# Copyright (C) 2011, 2012 Bradley M. Kuhn
+# Copyright (C) 2012 Tom Marble
+#
+# This program gives you software freedom; you can copy, modify, convey,
+# and/or redistribute it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 3 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 in a file called 'GPLv3'. If not, write to the:
+# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor
+# Boston, MA 02110-1301, USA.
+
+use strict;
+use warnings;
+
+use Math::BigFloat;
+use Date::Manip;
+
+my $LEDGER_CMD = "/usr/local/bin/ledger";
+
+my $ACCT_WIDTH = 75;
+
+sub ParseNumber($) {
+ $_[0] =~ s/,//g;
+ return Math::BigFloat->new($_[0]);
+}
+
+Math::BigFloat->precision(-2);
+my $ZERO = Math::BigFloat->new("0.00");
+
+if (@ARGV < 3) {
+ print STDERR "usage: $0 <BEGIN_DATE> <END_DATE> <OTHER_LEDGER_OPTS>\n";
+ exit 1;
+}
+
+my($beginDate, $endDate, @otherLedgerOpts) = @ARGV;
+
+my(@chartOfAccountsOpts) = ('-F', "%150A\n", '-w', '-s',
+ '-b', $beginDate, '-e', $endDate, @otherLedgerOpts, 'reg');
+
+open(CHART_DATA, "-|", $LEDGER_CMD, @chartOfAccountsOpts)
+ or die "Unable to run $LEDGER_CMD @chartOfAccountsOpts: $!";
+
+open(CHART_OUTPUT, ">", "chart-of-accounts.txt") or die "unable to write chart-of-accounts.txt: $!";
+
+my @accounts;
+while (my $line = <CHART_DATA>) {
+ chomp $line;
+ $line =~ s/^\s*//; $line =~ s/\s*$//;
+ push(@accounts, $line);
+
+}
+close(CHART_DATA); die "error reading ledger output for chart of accounts: $!" unless $? == 0;
+
+open(CHART_OUTPUT, ">", "chart-of-accounts.txt") or die "unable to write chart-of-accounts.txt: $!";
+
+my @sortedAccounts;
+foreach my $acct (
+ # Proper sorting for a chart of accounts
+ sort {
+ if ($a =~ /^Assets/ and $b !~ /^Assets/) {
+ return -1;
+ } elsif ($a =~ /^Liabilities/ and $b !~ /^Liabilitie/) {
+ return -1;
+ } else {
+ return $a cmp $b;
+ }
+ } @accounts) {
+ print CHART_OUTPUT "$acct\n";
+ push(@sortedAccounts, $acct);
+}
+close(CHART_OUTPUT); die "error writing to chart-of-accounts.txt: $!" unless $? == 0;
+
+my $formattedEndDate = new Date::Manip::Date;
+die "badly formatted end date, $endDate" if $formattedEndDate->parse($endDate);
+my $oneDayLess = new Date::Manip::Delta;
+die "bad one day less" if $oneDayLess->parse("- 1 day");
+$formattedEndDate = $formattedEndDate->calc($oneDayLess);
+$formattedEndDate = $formattedEndDate->printf("%Y/%m/%d");
+
+open(GL_TEXT_OUT, ">", "general-ledger.txt") or die "unable to write general-ledger.txt: $!";
+open(GL_CSV_OUT, ">", "general-ledger.csv") or die "unable to write general-ledger.csv: $!";
+
+foreach my $acct (@sortedAccounts) {
+ print GL_TEXT_OUT "\n\nACCOUNT: $acct\nFROM: $beginDate TO $formattedEndDate\n\n";
+ my @acctLedgerOpts = ('-F',
+ "%(date) %-.10C %-.80P %-.80N %18t %18T\n", '-w', '--sort', 'd',
+ '-b', $beginDate, '-e', $endDate, @otherLedgerOpts, 'reg', $acct);
+ open(GL_TEXT_DATA, "-|", $LEDGER_CMD, @acctLedgerOpts)
+ or die "Unable to run $LEDGER_CMD @acctLedgerOpts: $!";
+
+ foreach my $line (<GL_TEXT_DATA>) {
+ print GL_TEXT_OUT $line;
+ }
+ close(GL_TEXT_DATA); die "error reading ledger output for chart of accounts: $!" unless $? == 0;
+
+ print GL_CSV_OUT "\n\"ACCOUNT:\",\"$acct\"\n\"PERIOD START:\",\"$beginDate\"\n\"PERIOD END:\",\"$formattedEndDate\"\n";
+ print GL_CSV_OUT '"DATE","CHECK NUM","NAME","TRANSACTION AMT","RUNNING TOTAL","Receipt","Invoice"', "\n";
+ @acctLedgerOpts = ('-F', '"%(date)","%C","%P","%t","%T","%(tag(\'Receipt\'))","%(tag(\'Invoice\'))"\n', '-w', '--sort', 'd', '-b', $beginDate, '-e', $endDate, @otherLedgerOpts, 'reg', $acct);
+ open(GL_CSV_DATA, "-|", $LEDGER_CMD, @acctLedgerOpts)
+ or die "Unable to run $LEDGER_CMD @acctLedgerOpts: $!";
+
+ foreach my $line (<GL_CSV_DATA>) {
+ print GL_CSV_OUT $line;
+ }
+ close(GL_CSV_DATA); die "error reading ledger output for chart of accounts: $!" unless $? == 0;
+}
+close(GL_TEXT_OUT); die "error writing to general-ledger.txt: $!" unless $? == 0;
+close(GL_CSV_OUT); die "error writing to general-ledger.csv: $!" unless $? == 0;
+###############################################################################
+#
+# Local variables:
+# compile-command: "perl -c general-ledger-report.plx"
+# End:
+
diff --git a/contrib/non-profit-audit-reports/ooolib2/__init__.py b/contrib/non-profit-audit-reports/ooolib2/__init__.py
new file mode 100644
index 00000000..6106fc5c
--- /dev/null
+++ b/contrib/non-profit-audit-reports/ooolib2/__init__.py
@@ -0,0 +1,1987 @@
+"ooolib-python - Copyright (C) 2006-2009 Joseph Colton"
+
+# ooolib-python - Python module for creating Open Document Format documents.
+# Copyright (C) 2006-2009 Joseph Colton
+
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+
+# This library 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
+# Lesser General Public License for more details.
+
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+# You can contact me by email at josephcolton@gmail.com
+
+# Import Standard Modules
+import zipfile # Needed for reading/writing documents
+import time
+import sys
+import glob
+import os
+import re
+import xml.parsers.expat # Needed for parsing documents
+
+def version_number():
+ "Get the ooolib-python version number"
+ return "0.0.17"
+
+def version():
+ "Get the ooolib-python version"
+ return "ooolib-python-%s" % version_number()
+
+def clean_string(data):
+ "Returns an XML friendly copy of the data string"
+
+ data = unicode(data) # This line thanks to Chris Ender
+
+ data = data.replace('&', '&amp;')
+ data = data.replace("'", '&apos;')
+ data = data.replace('"', '&quot;')
+ data = data.replace('<', '&lt;')
+ data = data.replace('>', '&gt;')
+ data = data.replace('\t', '<text:tab-stop/>')
+ data = data.replace('\n', '<text:line-break/>')
+ return data
+
+class XML:
+ "XML Class - Used to convert nested lists into XML"
+ def __init__(self):
+ "Initialize ooolib XML instance"
+ pass
+
+ def _xmldata(self, data):
+ datatype = data.pop(0)
+ datavalue = data.pop(0)
+ outstring = '%s' % datavalue
+ return outstring
+
+ def _xmltag(self, data):
+ outstring = ''
+ # First two
+ datatype = data.pop(0)
+ dataname = data.pop(0)
+ outstring = '<%s' % dataname
+ # Element Section
+ element = 1
+ while(data):
+ # elements
+ newdata = data.pop(0)
+ if (newdata[0] == 'element' and element):
+ newstring = self._xmlelement(newdata)
+ outstring = '%s %s' % (outstring, newstring)
+ continue
+ if (newdata[0] != 'element' and element):
+ element = 0
+ outstring = '%s>' % outstring
+ if (newdata[0] == 'tag' or newdata[0] == 'tagline'):
+ outstring = '%s\n' % outstring
+ if (newdata[0] == 'tag'):
+ newstring = self._xmltag(newdata)
+ outstring = '%s%s' % (outstring, newstring)
+ continue
+ if (newdata[0] == 'tagline'):
+ newstring = self._xmltagline(newdata)
+ outstring = '%s%s' % (outstring, newstring)
+ continue
+ if (newdata[0] == 'data'):
+ newstring = self._xmldata(newdata)
+ outstring = '%s%s' % (outstring, newstring)
+ continue
+ if (element):
+ element = 0
+ outstring = '%s>\n' % outstring
+ outstring = '%s</%s>\n' % (outstring, dataname)
+ return outstring
+
+ def _xmltagline(self, data):
+ outstring = ''
+ # First two
+ datatype = data.pop(0)
+ dataname = data.pop(0)
+ outstring = '<%s' % dataname
+ # Element Section
+ while(data):
+ # elements
+ newdata = data.pop(0)
+ if (newdata[0] != 'element'): break
+ newstring = self._xmlelement(newdata)
+ outstring = '%s %s' % (outstring, newstring)
+ outstring = '%s/>\n' % outstring
+ # Non-Element Section should not exist
+ return outstring
+
+ def _xmlelement(self, data):
+ datatype = data.pop(0)
+ dataname = data.pop(0)
+ datavalue = data.pop(0)
+ outstring = '%s="%s"' % (dataname, datavalue)
+ return outstring
+
+ def convert(self, data):
+ """Convert nested lists into XML
+
+ The convert method takes a nested lists and converts them
+ into XML to be used in Open Document Format documents.
+ There are three types of lists that are recognized at this
+ time. They are as follows:
+
+ 'tag' - Tag opens a set of data that is eventually closed
+ with a similar tag.
+ List: ['tag', 'xml']
+ XML: <xml></xml>
+
+ 'tagline' - Taglines are similar to tags, except they open
+ and close themselves.
+ List: ['tagline', 'xml']
+ XML: <xml/>
+
+ 'element' - Elements are pieces of information stored in an
+ opening tag or tagline.
+ List: ['element', 'color', 'blue']
+ XML: color="blue"
+
+ 'data' - Data is plain text directly inserted into the XML
+ document.
+ List: ['data', 'hello']
+ XML: hello
+
+ Bring them all together for something like this.
+
+ Lists:
+ ['tag', 'xml', ['element', 'a', 'b'], ['tagline', 'xml2'],
+ ['data', 'asdf']]
+
+ XML:
+ <xml a="b"><xml2/>asdf</xml>
+ """
+ outlines = []
+ outlines.append('<?xml version="1.0" encoding="UTF-8"?>')
+ if (type(data) == type([]) and len(data) > 0):
+ if data[0] == 'tag': outlines.append(self._xmltag(data))
+ return outlines
+
+class Meta:
+ "Meta Data Class"
+
+ def __init__(self, doctype, debug=False):
+ self.doctype = doctype
+
+ # Set the debug mode
+ self.debug = debug
+
+ # The generator should always default to the version number
+ self.meta_generator = version()
+ self.meta_title = ''
+ self.meta_subject = ''
+ self.meta_description = ''
+ self.meta_keywords = []
+ self.meta_creator = 'ooolib-python'
+ self.meta_editor = ''
+ self.meta_user1_name = 'Info 1'
+ self.meta_user2_name = 'Info 2'
+ self.meta_user3_name = 'Info 3'
+ self.meta_user4_name = 'Info 4'
+ self.meta_user1_value = ''
+ self.meta_user2_value = ''
+ self.meta_user3_value = ''
+ self.meta_user4_value = ''
+ self.meta_creation_date = self.meta_time()
+
+ # Parser data
+ self.parser_element_list = []
+ self.parser_element = ""
+ self.parser_count = 0
+
+ def set_meta(self, metaname, value):
+ """Set meta data in your document.
+
+ Currently implemented metaname options are as follows:
+ 'creator' - The document author
+ """
+ if metaname == 'creator': self.meta_creator = value
+ if metaname == 'editor': self.meta_editor = value
+ if metaname == 'title': self.meta_title = value
+ if metaname == 'subject': self.meta_subject = value
+ if metaname == 'description': self.meta_description = value
+ if metaname == 'user1name': self.meta_user1_name = value
+ if metaname == 'user2name': self.meta_user2_name = value
+ if metaname == 'user3name': self.meta_user3_name = value
+ if metaname == 'user4name': self.meta_user4_name = value
+ if metaname == 'user1value': self.meta_user1_value = value
+ if metaname == 'user2value': self.meta_user2_value = value
+ if metaname == 'user3value': self.meta_user3_value = value
+ if metaname == 'user4value': self.meta_user4_value = value
+ if metaname == 'keyword':
+ if value not in self.meta_keywords:
+ self.meta_keywords.append(value)
+
+ def get_meta_value(self, metaname):
+ "Get meta data value for a given metaname."
+
+ if metaname == 'creator': return self.meta_creator
+ if metaname == 'editor': return self.meta_editor
+ if metaname == 'title': return self.meta_title
+ if metaname == 'subject': return self.meta_subject
+ if metaname == 'description': return self.meta_description
+ if metaname == 'user1name': return self.meta_user1_name
+ if metaname == 'user2name': return self.meta_user2_name
+ if metaname == 'user3name': return self.meta_user3_name
+ if metaname == 'user4name': return self.meta_user4_name
+ if metaname == 'user1value': return self.meta_user1_value
+ if metaname == 'user2value': return self.meta_user2_value
+ if metaname == 'user3value': return self.meta_user3_value
+ if metaname == 'user4value': return self.meta_user4_value
+ if metaname == 'keyword': return self.meta_keywords
+
+ def meta_time(self):
+ "Return time string in meta data format"
+ t = time.localtime()
+ stamp = "%04d-%02d-%02dT%02d:%02d:%02d" % (t[0], t[1], t[2], t[3], t[4], t[5])
+ return stamp
+
+ def parse_start_element(self, name, attrs):
+ if self.debug: print '* Start element:', name
+ self.parser_element_list.append(name)
+ self.parser_element = self.parser_element_list[-1]
+
+ # Need the meta name from the user-defined tags
+ if (self.parser_element == "meta:user-defined"):
+ self.parser_count += 1
+ # Set user-defined name
+ self.set_meta("user%dname" % self.parser_count, attrs['meta:name'])
+
+ # Debugging statements
+ if self.debug: print " List: ", self.parser_element_list
+ if self.debug: print " Attributes: ", attrs
+
+
+ def parse_end_element(self, name):
+ if self.debug: print '* End element:', name
+ if name != self.parser_element:
+ print "Tag Mismatch: '%s' != '%s'" % (name, self.parser_element)
+ self.parser_element_list.pop()
+
+ # Readjust parser_element_list and parser_element
+ if (self.parser_element_list):
+ self.parser_element = self.parser_element_list[-1]
+ else:
+ self.parser_element = ""
+
+ def parse_char_data(self, data):
+ if self.debug: print " Character data: ", repr(data)
+
+ # Collect Meta data fields
+ if (self.parser_element == "dc:title"):
+ self.set_meta("title", data)
+ if (self.parser_element == "dc:description"):
+ self.set_meta("description", data)
+ if (self.parser_element == "dc:subject"):
+ self.set_meta("subject", data)
+ if (self.parser_element == "meta:initial-creator"):
+ self.set_meta("creator", data)
+
+ # Try to maintain the same creation date
+ if (self.parser_element == "meta:creation-date"):
+ self.meta_creation_date = data
+
+ # The user defined fields need to be kept track of, parser_count does that
+ if (self.parser_element == "meta:user-defined"):
+ self.set_meta("user%dvalue" % self.parser_count, data)
+
+ def meta_parse(self, data):
+ "Parse Meta Data from a meta.xml file"
+
+ # Debugging statements
+ if self.debug:
+ # Sometimes it helps to see the document that was read from
+ print data
+ print "\n\n\n"
+
+ # Create parser
+ parser = xml.parsers.expat.ParserCreate()
+ # Set up parser callback functions
+ parser.StartElementHandler = self.parse_start_element
+ parser.EndElementHandler = self.parse_end_element
+ parser.CharacterDataHandler = self.parse_char_data
+
+ # Actually parse the data
+ parser.Parse(data, 1)
+
+ def get_meta(self):
+ "Generate meta.xml file data"
+ self.meta_date = self.meta_time()
+ self.data = ['tag', 'office:document-meta',
+ ['element', 'xmlns:office', 'urn:oasis:names:tc:opendocument:xmlns:office:1.0'],
+ ['element', 'xmlns:xlink', 'http://www.w3.org/1999/xlink'],
+ ['element', 'xmlns:dc', 'http://purl.org/dc/elements/1.1/'],
+ ['element', 'xmlns:meta', 'urn:oasis:names:tc:opendocument:xmlns:meta:1.0'],
+ ['element', 'xmlns:ooo', 'http://openoffice.org/2004/office'],
+ ['element', 'office:version', '1.0'],
+ ['tag', 'office:meta',
+ ['tag', 'meta:generator', # Was: 'OpenOffice.org/2.0$Linux OpenOffice.org_project/680m5$Build-9011'
+ ['data', self.meta_generator]], # Generator is set the the ooolib-python version.
+ ['tag', 'dc:title',
+ ['data', self.meta_title]], # This data is the document title
+ ['tag', 'dc:description',
+ ['data', self.meta_description]], # This data is the document description
+ ['tag', 'dc:subject',
+ ['data', self.meta_subject]], # This data is the document subject
+ ['tag', 'meta:initial-creator',
+ ['data', self.meta_creator]], # This data is the document creator
+ ['tag', 'meta:creation-date',
+ ['data', self.meta_creation_date]], # This is the original creation date of the document
+ ['tag', 'dc:creator',
+ ['data', self.meta_editor]], # This data is the document editor
+ ['tag', 'dc:date',
+ ['data', self.meta_date]], # This is the last modified date of the document
+ ['tag', 'dc:language',
+ ['data', 'en-US']], # We will probably always use en-US for language
+ ['tag', 'meta:editing-cycles',
+ ['data', '1']], # Edit cycles will probably always be 1 for generated documents
+ ['tag', 'meta:editing-duration',
+ ['data', 'PT0S']], # Editing duration is modified - creation date
+ ['tag', 'meta:user-defined',
+ ['element', 'meta:name', self.meta_user1_name],
+ ['data', self.meta_user1_value]],
+ ['tag', 'meta:user-defined',
+ ['element', 'meta:name', self.meta_user2_name],
+ ['data', self.meta_user2_value]],
+ ['tag', 'meta:user-defined',
+ ['element', 'meta:name', self.meta_user3_name],
+ ['data', self.meta_user3_value]],
+ ['tag', 'meta:user-defined',
+ ['element', 'meta:name', self.meta_user4_name],
+ ['data', self.meta_user4_value]]]]
+# ['tagline', 'meta:document-statistic',
+# ['element', 'meta:table-count', len(self.sheets)], # len(self.sheets) ?
+# ['element', 'meta:cell-count', '15']]]] # Not sure how to keep track
+
+ # Generate content.xml XML data
+ xml = XML()
+ self.lines = xml.convert(self.data)
+ self.filedata = '\n'.join(self.lines)
+ # Return generated data
+ return self.filedata
+
+
+
+class CalcStyles:
+ "Calc Style Management - Used to keep track of created styles."
+
+ def __init__(self):
+ self.style_config = {}
+ # Style Counters
+ self.style_table = 1
+ self.style_column = 1
+ self.style_row = 1
+ self.style_cell = 1
+ # Style Properties (Defaults) - To be used later
+ self.property_column_width_default = '0.8925in' # Default Column Width
+ self.property_row_height_default = '0.189in' # Default Row Height
+ # Set Defaults
+ self.property_column_width = '0.8925in' # Default Column Width
+ self.property_row_height = '0.189in' # Default Row Height
+ self.property_cell_bold = False # Bold off be default
+ self.property_cell_italic = False # Italic off be default
+ self.property_cell_underline = False # Underline off be default
+ self.property_cell_fg_color = 'default' # Text Color Default
+ self.property_cell_bg_color = 'default' # Cell Background Default
+ self.property_cell_bg_image = 'none' # Cell Background Default
+ self.property_cell_fontsize = '10' # Cell Font Size Default
+ self.property_cell_valign = 'default' # Vertial Alignment Default
+ self.property_cell_halign = 'default' # Horizantal Alignment Default
+
+ def get_next_style(self, style):
+ "Returns the next style code for the given style"
+ style_code = ""
+ if style == 'table':
+ style_code = 'ta%d' % self.style_table
+ self.style_table+=1
+ if style == 'column':
+ style_code = 'co%d' % self.style_column
+ self.style_column+=1
+ if style == 'row':
+ style_code = 'ro%d' % self.style_row
+ self.style_row+=1
+ if style == 'cell':
+ style_code = 'ce%d' % self.style_cell
+ self.style_cell+=1
+ return style_code
+
+ def set_property(self, style, name, value):
+ "Sets a property which will later be turned into a code"
+ if style == 'table':
+ pass
+ if style == 'column':
+ if name == 'style:column-width': self.property_column_width = value
+ if style == 'row':
+ if name == 'style:row-height': self.property_row_height = value
+ if style == 'cell':
+ if name == 'bold' and type(value) == type(True): self.property_cell_bold = value
+ if name == 'italic' and type(value) == type(True): self.property_cell_italic = value
+ if name == 'underline' and type(value) == type(True): self.property_cell_underline = value
+ if name == 'fontsize': self.property_cell_fontsize = value
+ if name == 'color':
+ self.property_cell_fg_color = 'default'
+ redata = re.search("^(#[\da-fA-F]{6})$", value)
+ if redata: self.property_cell_fg_color = value.lower()
+ if name == 'background':
+ self.property_cell_bg_color = 'default'
+ redata = re.search("^(#[\da-fA-F]{6})$", value)
+ if redata: self.property_cell_bg_color = value.lower()
+ if name == 'backgroundimage':
+ self.property_cell_bg_image = value
+ if name == 'valign':
+ self.property_cell_valign = value
+ if name == 'halign':
+ self.property_cell_halign = value
+
+ def get_style_code(self, style):
+ style_code = ""
+ if style == 'table':
+ style_code = "ta1"
+ if style == 'column':
+ style_data = tuple([style,
+ ('style:column-width', self.property_column_width)])
+ if style_data in self.style_config:
+ # Style Exists, return code
+ style_code = self.style_config[style_data]
+ else:
+ # Style does not exist, create code and return it
+ style_code = self.get_next_style(style)
+ self.style_config[style_data] = style_code
+ if style == 'row':
+ style_data = tuple([style,
+ ('style:row-height', self.property_row_height)])
+ if style_data in self.style_config:
+ # Style Exists, return code
+ style_code = self.style_config[style_data]
+ else:
+ # Style does not exist, create code and return it
+ style_code = self.get_next_style(style)
+ self.style_config[style_data] = style_code
+ if style == 'cell':
+ style_data = [style]
+ # Add additional styles
+ if self.property_cell_bold: style_data.append(('bold', True))
+ if self.property_cell_italic: style_data.append(('italic', True))
+ if self.property_cell_underline: style_data.append(('underline', True))
+ if self.property_cell_fontsize != '10':
+ style_data.append(('fontsize', self.property_cell_fontsize))
+ if self.property_cell_fg_color != 'default':
+ style_data.append(('color', self.property_cell_fg_color))
+ if self.property_cell_bg_color != 'default':
+ style_data.append(('background', self.property_cell_bg_color))
+ if self.property_cell_bg_image != 'none':
+ style_data.append(('backgroundimage', self.property_cell_bg_image))
+ if self.property_cell_valign != 'default':
+ style_data.append(('valign', self.property_cell_valign))
+ if self.property_cell_halign != 'default':
+ style_data.append(('halign', self.property_cell_halign))
+
+ style_data = tuple(style_data)
+ if style_data in self.style_config:
+ # Style Exists, return code
+ style_code = self.style_config[style_data]
+ else:
+ # Style does not exist, create code and return it
+ style_code = self.get_next_style(style)
+ self.style_config[style_data] = style_code
+ return style_code
+
+ def get_automatic_styles(self):
+ "Return 'office:automatic-styles' lists"
+ automatic_styles = ['tag', 'office:automatic-styles']
+
+ for style_data in self.style_config:
+ style_code = self.style_config[style_data]
+ style_data = list(style_data)
+ style = style_data.pop(0)
+
+ if style == 'column':
+ style_list = ['tag', 'style:style',
+ ['element', 'style:name', style_code], # Column 'co1' properties
+ ['element', 'style:family', 'table-column']]
+ tagline = ['tagline', 'style:table-column-properties',
+ ['element', 'fo:break-before', 'auto']] # unsure what break before means
+
+ for set in style_data:
+ name, value = set
+ if name == 'style:column-width':
+ tagline.append(['element', 'style:column-width', value])
+ style_list.append(tagline)
+ automatic_styles.append(style_list)
+
+ if style == 'row':
+ style_list = ['tag', 'style:style',
+ ['element', 'style:name', style_code], # Column 'ro1' properties
+ ['element', 'style:family', 'table-row']]
+ tagline = ['tagline', 'style:table-row-properties']
+
+ for set in style_data:
+ name, value = set
+ if name == 'style:row-height':
+ tagline.append(['element', 'style:row-height', value])
+ tagline.append(['element', 'fo:break-before', 'auto'])
+# tagline.append(['element', 'style:use-optimal-row-height', 'true']) # Overrides settings
+ style_list.append(tagline)
+ automatic_styles.append(style_list)
+
+ if style == 'pagebreak':
+ style_list = ['tag', 'style:style',
+ ['element', 'style:name', style_code], # Column 'ro1' properties
+ ['element', 'style:family', 'table-row']]
+ tagline = ['tagline', 'style:table-row-properties']
+
+ for set in style_data:
+ name, value = set
+ if name == 'style:row-height':
+ tagline.append(['element', 'style:row-height', value])
+ tagline.append(['element', 'fo:break-before', 'page'])
+# tagline.append(['element', 'style:use-optimal-row-height', 'true']) # Overrides settings
+ style_list.append(tagline)
+ automatic_styles.append(style_list)
+
+ if style == 'cell':
+ style_list = ['tag', 'style:style',
+ ['element', 'style:name', style_code], # ce1 style
+ ['element', 'style:family', 'table-cell'], # cell
+ ['element', 'style:parent-style-name', 'Default']] # parent is Default
+ # hack for currency
+ if style_code == 'ce1':
+ style_list.append(['element',
+ 'style:data-style-name',
+ 'N104'])
+
+ # Cell Properties
+ tagline = ['tag', 'style:table-cell-properties']
+ tagline_additional = []
+ for set in style_data:
+ name, value = set
+ if name == 'background':
+ tagline.append(['element', 'fo:background-color', value])
+ if name == 'backgroundimage':
+ tagline.append(['element', 'fo:background-color', 'transparent'])
+ # Additional tags added later
+ bgimagetag = ['tagline', 'style:background-image']
+ bgimagetag.append(['element', 'xlink:href', value])
+ bgimagetag.append(['element', 'xlink:type', 'simple'])
+ bgimagetag.append(['element', 'xlink:actuate', 'onLoad'])
+ tagline_additional.append(bgimagetag)
+ if name == 'valign':
+ if value in ['top', 'bottom', 'middle']:
+ tagline.append(['element', 'style:vertical-align', value])
+ if name == 'halign':
+ tagline.append(['element', 'style:text-align-source', 'fix'])
+ if value in ['filled']:
+ tagline.append(['element', 'style:repeat-content', 'true'])
+ else:
+ tagline.append(['element', 'style:repeat-content', 'false'])
+
+ # Add any additional internal tags
+ while tagline_additional:
+ tagadd = tagline_additional.pop(0)
+ tagline.append(tagadd)
+
+ style_list.append(tagline)
+
+ # Paragraph Properties
+ tagline = ['tagline', 'style:paragraph-properties']
+ tagline_valid = False
+ for set in style_data:
+ name, value = set
+ if name == 'halign':
+ tagline_valid = True
+ if value in ['center']:
+ tagline.append(['element', 'fo:text-align', 'center'])
+ if value in ['end', 'right']:
+ tagline.append(['element', 'fo:text-align', 'end'])
+ if value in ['start', 'filled', 'left']:
+ tagline.append(['element', 'fo:text-align', 'start'])
+ if value in ['justify']:
+ tagline.append(['element', 'fo:text-align', 'justify'])
+ # Conditionally add the tagline
+ if tagline_valid: style_list.append(tagline)
+
+
+ # Text Properties
+ tagline = ['tagline', 'style:text-properties']
+ for set in style_data:
+ name, value = set
+ if name == 'bold':
+ tagline.append(['element', 'fo:font-weight', 'bold'])
+ if name == 'italic':
+ tagline.append(['element', 'fo:font-style', 'italic'])
+ if name == 'underline':
+ tagline.append(['element', 'style:text-underline-style', 'solid'])
+ tagline.append(['element', 'style:text-underline-width', 'auto'])
+ tagline.append(['element', 'style:text-underline-color', 'font-color'])
+ if name == 'color':
+ tagline.append(['element', 'fo:color', value])
+ if name == 'fontsize':
+ tagline.append(['element', 'fo:font-size', '%spt' % value])
+ style_list.append(tagline)
+
+ automatic_styles.append(style_list)
+
+
+ # Attach ta1 style
+ automatic_styles.append(['tag', 'style:style',
+ ['element', 'style:name', 'ta1'],
+ ['element', 'style:family', 'table'],
+ ['element', 'style:master-page-name', 'Default'],
+ ['tagline', 'style:table-properties',
+ ['element', 'table:display', 'true'],
+ ['element', 'style:writing-mode', 'lr-tb']]])
+
+
+ return automatic_styles
+
+
+
+class CalcSheet:
+ "Calc Sheet Class - Used to keep track of the data for an individual sheet."
+
+ def __init__(self, sheetname):
+ "Initialize a sheet"
+ self.sheet_name = sheetname
+ self.sheet_values = {}
+ self.sheet_config = {}
+ self.max_col = 0
+ self.max_row = 0
+
+ def get_sheet_dimensions(self):
+ "Returns the max column and row"
+ return (self.max_col, self.max_row)
+
+ def clean_formula(self, data):
+ "Returns a formula for use in ODF"
+ # Example Translations
+ # '=SUM(A1:A2)'
+ # datavalue = 'oooc:=SUM([.A1:.A2])'
+ # '=IF((A5>A4);A4;"")'
+ # datavalue = 'oooc:=IF(([.A5]&gt;[.A4]);[.A4];&quot;&quot;)'
+ data = str(data)
+ data = clean_string(data)
+ redata = re.search('^=([A-Z]+)(\(.*)$', data)
+ if redata:
+ # funct is the function name. The rest if the string will be the functArgs
+ funct = redata.group(1)
+ functArgs = redata.group(2)
+ # Search for cell lebels and replace them
+ reList = re.findall('([A-Z]+\d+)', functArgs)
+ # sort and keep track so we do not do a cell more than once
+ reList.sort()
+ lastVar = ''
+ while reList:
+ # Replace each cell label
+ curVar = reList.pop()
+ if curVar == lastVar: continue
+ lastVar = curVar
+ functArgs = functArgs.replace(curVar, '[.%s]' % curVar)
+ data = 'oooc:=%s%s' % (funct, functArgs)
+ return data
+
+ def get_name(self):
+ "Returns the sheet name"
+ return self.sheet_name
+
+ def set_name(self, sheetname):
+ "Resets the sheet name"
+ self.sheet_name = sheetname
+
+ def get_sheet_values(self):
+ "Returns the sheet cell values"
+ return self.sheet_values
+
+ def get_sheet_value(self, col, row):
+ "Get the value contents of a cell"
+ cell = (col, row)
+ if cell in self.sheet_values:
+ return self.sheet_values[cell]
+ else:
+ return None
+
+ def get_sheet_config(self):
+ "Returns the sheet cell properties"
+ return self.sheet_config
+
+ def set_sheet_config(self, location, style_code):
+ "Sets Style Code for a given location"
+ self.sheet_config[location] = style_code
+
+ def set_sheet_value(self, cell, datatype, datavalue):
+ """Sets the value for a specific cell
+
+ cell must be in the format (col, row) where row and col are int.
+ Example: B5 would be written as (2, 5)
+ datatype must be one of 'string', 'float', 'formula', 'currency'
+ datavalue should be a string
+ """
+ # Catch invalid data
+ if type(cell) != type(()) or len(cell) != 2:
+ print "Invalid Cell"
+ return
+ (col, row) = cell
+ if type(col) != type(1):
+ print "Invalid Cell"
+ return
+ if type(row) != type(1):
+ print "Invalid Cell"
+ return
+ # Fix String Data
+ if datatype in ['string', 'annotation']:
+ datavalue = clean_string(datavalue)
+ # Fix Link Data. Link's value is a tuple containing (url, description)
+ if (datatype == 'link'):
+ url = clean_string(datavalue[0])
+ desc = clean_string(datavalue[1])
+ datavalue = (url, desc)
+ # Fix Formula Data
+ if datatype == 'formula':
+ datavalue = self.clean_formula(datavalue)
+ # Adjust maximum sizes
+ if col > self.max_col: self.max_col = col
+ if row > self.max_row: self.max_row = row
+ datatype = str(datatype)
+ if (datatype not in ['string', 'float', 'currency', 'formula', 'annotation', 'link']):
+ # Set all unknown cell types to string
+ datatype = 'string'
+ datavalue = str(datavalue)
+
+ # The following lines are taken directly from HPS
+ # self.sheet_values[cell] = (datatype, datavalue)
+ # HPS: Cell content is now a list of tuples instead of a tuple
+ # While storing here, store the cell contents first and the annotation next. While generating the XML reverse this
+ contents = self.sheet_values.get(cell, {'annotation':None,'link':None, 'value':None})
+ if datatype == 'annotation':
+ contents['annotation'] = (datatype, datavalue)
+ elif datatype == 'link':
+ contents['link'] = (datatype, datavalue)
+ else:
+ contents['value'] = (datatype, datavalue)
+
+ self.sheet_values[cell] = contents
+
+
+ def get_lists(self):
+ "Returns nested lists for XML processing"
+ if (self.max_col == 0 and self.max_row == 0):
+ sheet_lists = ['tag', 'table:table',
+ ['element', 'table:name', self.sheet_name], # Set the Sheet Name
+ ['element', 'table:style-name', 'ta1'],
+ ['element', 'table:print', 'false'],
+ ['tagline', 'table:table-column',
+ ['element', 'table:style-name', 'co1'],
+ ['element', 'table:default-cell-style-name', 'Default']],
+ ['tag', 'table:table-row',
+ ['element', 'table:style-name', 'ro1'],
+ ['tagline', 'table:table-cell']]]
+ else:
+ # Base Information
+ sheet_lists = ['tag', 'table:table',
+ ['element', 'table:name', self.sheet_name], # Set the sheet name
+ ['element', 'table:style-name', 'ta1'],
+ ['element', 'table:print', 'false']]
+
+# ['tagline', 'table:table-column',
+# ['element', 'table:style-name', 'co1'],
+# ['element', 'table:number-columns-repeated', self.max_col], # max_col? '2'
+# ['element', 'table:default-cell-style-name', 'Default']],
+
+ # Need to add column information
+ for col in range(1, self.max_col+1):
+ location = ('col', col)
+ style_code = 'co1'
+ if location in self.sheet_config:
+ style_code = self.sheet_config[location]
+ sheet_lists.append(['tagline', 'table:table-column',
+ ['element', 'table:style-name', style_code],
+ ['element', 'table:default-cell-style-name', 'Default']])
+
+
+ # Need to create each row
+ for row in range(1, self.max_row + 1):
+ location = ('row', row)
+ style_code = 'ro1'
+ if location in self.sheet_config:
+ style_code = self.sheet_config[location]
+ rowlist = ['tag', 'table:table-row',
+ ['element', 'table:style-name', style_code]]
+ for col in range(1, self.max_col + 1):
+ cell = (col, row)
+ style_code = 'ce1' # Default all cells to ce1
+ if cell in self.sheet_config:
+ style_code = self.sheet_config[cell] # Lookup cell if available
+ if cell in self.sheet_values:
+ # (datatype, datavalue) = self.sheet_values[cell] # Marked for removal
+ collist = ['tag', 'table:table-cell']
+ if style_code != 'ce1':
+ collist.append(['element', 'table:style-name', style_code])
+
+ # Contents, annotations, and links added by HPS
+ contents = self.sheet_values[cell] # cell contents is a dictionary
+ if contents['value']:
+ (datatype, datavalue) = contents['value']
+ if datatype == 'float':
+ collist.append(['element', 'office:value-type', datatype])
+ collist.append(['element', 'office:value', datavalue])
+ if datatype == 'currency':
+ collist.append(['element', 'table:style-name', "ce1"])
+ collist.append(['element', 'office:value-type', datatype])
+ collist.append(['element', 'office:currency', 'USD'])
+ collist.append(['element', 'office:value', datavalue])
+
+ if datatype == 'string':
+ collist.append(['element', 'office:value-type', datatype])
+ if datatype == 'formula':
+ collist.append(['element', 'table:formula', datavalue])
+ collist.append(['element', 'office:value-type', 'float'])
+ collist.append(['element', 'office:value', '0'])
+ datavalue = '0'
+ else:
+ datavalue = None
+
+ if contents['annotation']:
+ (annotype, annoval) = contents['annotation']
+ collist.append(['tag', 'office:annotation',
+ ['tag', 'text:p', ['data', annoval]]])
+
+ if contents['link']:
+ (linktype, linkval) = contents['link']
+ if datavalue:
+ collist.append(['tag', 'text:p', ['data', datavalue],
+ ['tag', 'text:a', ['element', 'xlink:href', linkval[0]],
+ ['data', linkval[1]]]])
+ else: # no value; just fill the link
+ collist.append(['tag', 'text:p',
+ ['tag', 'text:a', ['element', 'xlink:href', linkval[0]],
+ ['data', linkval[1]]]])
+ else:
+ if datavalue:
+ collist.append(['tag', 'text:p', ['data', datavalue]])
+
+
+
+ else:
+ collist = ['tagline', 'table:table-cell']
+ rowlist.append(collist)
+ sheet_lists.append(rowlist)
+ return sheet_lists
+
+class Calc:
+ "Calc Class - Used to create OpenDocument Format Calc Spreadsheets."
+ def __init__(self, sheetname=None, opendoc=None, debug=False):
+ "Initialize ooolib Calc instance"
+ # Default to no debugging
+ self.debug = debug
+ if not sheetname: sheetname = "Sheet1"
+ self.sheets = [CalcSheet(sheetname)] # The main sheet will be initially called 'Sheet1'
+ self.sheet_index = 0 # We initially start on the first sheet
+ self.styles = CalcStyles()
+ self.meta = Meta('ods')
+ self.styles.get_style_code('column') # Force generation of default column
+ self.styles.get_style_code('row') # Force generation of default row
+ self.styles.get_style_code('table') # Force generation of default table
+ self.styles.get_style_code('cell') # Force generation of default cell
+ self.manifest_files = [] # List of extra files included
+ self.manifest_index = 1 # Index of added manifest files
+
+ # Data Parsing
+ self.parser_element_list = []
+ self.parser_element = ""
+ self.parser_sheet_num = 0
+ self.parser_sheet_row = 0
+ self.parser_sheet_column = 0
+ self.parser_cell_repeats = 0
+ self.parser_cell_string_pending = False
+ self.parser_cell_string_line = ""
+
+ # See if we need to read a document
+ if opendoc:
+ # Verify that the document exists
+ if self.debug: print "Opening Document: %s" % opendoc
+
+ # Okay, now we load the file
+ self.load(opendoc)
+
+ def debug_level(self, level):
+ """Set debug level:
+ True if you want debugging messages
+ False if you do not.
+ """
+ self.debug = level
+
+ def file_mimetype(self, filename):
+ "Determine the filetype from the filename"
+ parts = filename.lower().split('.')
+ ext = parts[-1]
+ if (ext == 'png'): return (ext, "image/png")
+ if (ext == 'gif'): return (ext, "image/gif")
+ return (ext, "image/unknown")
+
+ def add_file(self, filename):
+ """Prepare a file for loading into ooolib
+
+ The filename should be the local filesystem name for
+ the file. The file is then prepared to be included in
+ the creation of the final document. The file needs to
+ remain in place so that it is available when the actual
+ document creation happens.
+ """
+ # mimetype set to (ext, filetype)
+ mimetype = self.file_mimetype(filename)
+ newname = "Pictures/%08d.%s" % (self.manifest_index, mimetype[0])
+ self.manifest_index += 1
+ filetype = mimetype[1]
+ self.manifest_files.append((filename, filetype, newname))
+ return newname
+
+ def set_meta(self, metaname, value):
+ "Set meta data in your document."
+ self.meta.set_meta(metaname, value)
+
+ def get_meta_value(self, metaname):
+ "Get meta data value for a given metaname"
+ return self.meta.get_meta_value(metaname)
+
+ def get_sheet_name(self):
+ "Returns the sheet name"
+ return self.sheets[self.sheet_index].get_name()
+
+ def get_sheet_dimensions(self):
+ "Returns the sheet dimensions in (cols, rows)"
+ return self.sheets[self.sheet_index].get_sheet_dimensions()
+
+ def set_column_property(self, column, name, value):
+ "Set Column Properties"
+ if name == 'width':
+ # column number column needs column-width set to value
+ self.styles.set_property('column', 'style:column-width', value)
+ style_code = self.styles.get_style_code('column')
+ self.sheets[self.sheet_index].set_sheet_config(('col', column), style_code)
+
+ def set_row_property(self, row, name, value):
+ "Set row Properties"
+ if name == 'height':
+ # row number row needs row-height set to value
+ self.styles.set_property('row', 'style:row-height', value)
+ style_code = self.styles.get_style_code('row')
+ self.sheets[self.sheet_index].set_sheet_config(('row', row), style_code)
+
+ def set_cell_property(self, name, value):
+ """Turn and off cell properties
+
+ Actual application of properties is handled by setting a value."""
+ # background images need to be handled a little differently
+ # because they need to also be inserted into the final document
+ if (name == 'backgroundimage'):
+ # Add file and modify value
+ value = self.add_file(value)
+ self.styles.set_property('cell', name, value)
+
+ def get_sheet_index(self):
+ "Return the current sheet index number"
+ return self.sheet_index
+
+ def set_sheet_index(self, index):
+ "Set the sheet index"
+ if type(index) == type(1):
+ if index >= 0 and index < len(self.sheets):
+ self.sheet_index = index
+ return self.sheet_index
+
+ def get_sheet_count(self):
+ "Returns the number of existing sheets"
+ return len(self.sheets)
+
+ def new_sheet(self, sheetname):
+ "Create a new sheet"
+ self.sheet_index = len(self.sheets)
+ self.sheets.append(CalcSheet(sheetname))
+ return self.sheet_index
+
+ def set_cell_value(self, col, row, datatype, value):
+ "Set the value for a given cell"
+ self.sheets[self.sheet_index].set_sheet_value((col, row), datatype, value)
+ style_code = self.styles.get_style_code('cell')
+ self.sheets[self.sheet_index].set_sheet_config((col, row), style_code)
+
+ def get_cell_value(self, col, row):
+ "Get a cell value tuple (type, value) for a given cell"
+ sheetvalue = self.sheets[self.sheet_index].get_sheet_value(col, row)
+ # We stop here if there is no value for sheetvalue
+ if sheetvalue == None: return sheetvalue
+ # Now check to see if we have a value tuple
+ if 'value' in sheetvalue:
+ return sheetvalue['value']
+ else:
+ return None
+
+ def load(self, filename):
+ """Load .ods spreadsheet.
+
+ The load function loads data from a document into the current cells.
+ """
+ # Read in the important files
+
+ # meta.xml
+ data = self._zip_read(filename, "meta.xml")
+ self.meta.meta_parse(data)
+
+ # content.xml
+ data = self._zip_read(filename, "content.xml")
+ self.content_parse(data)
+
+ # settings.xml - I do not remember putting anything here
+ # styles.xml - I do not remember putting anything here
+
+ def parse_content_start_element(self, name, attrs):
+ if self.debug: print '* Start element:', name
+ self.parser_element_list.append(name)
+ self.parser_element = self.parser_element_list[-1]
+
+ # Keep track of the current sheet number
+ if (self.parser_element == 'table:table'):
+ # Move to starting cell
+ self.parser_sheet_row = 0
+ self.parser_sheet_column = 0
+ # Increment the sheet number count
+ self.parser_sheet_num += 1
+ if (self.parser_sheet_num - 1 != self.sheet_index):
+ # We are not on the first sheet and need to create a new sheet.
+ # We will automatically move to the new sheet
+ sheetname = "Sheet%d" % self.parser_sheet_num
+ if 'table:name' in attrs: sheetname = attrs['table:name']
+ self.new_sheet(sheetname)
+ else:
+ # We are on the first sheet and will need to overwrite the default name
+ sheetname = "Sheet%d" % self.parser_sheet_num
+ if 'table:name' in attrs: sheetname = attrs['table:name']
+ self.sheets[self.sheet_index].set_name(sheetname)
+
+ # Update the row numbers
+ if (self.parser_element == 'table:table-row'):
+ self.parser_sheet_row += 1
+ self.parser_sheet_column = 0
+
+ # Okay, now keep track of the sheet cell data
+ if (self.parser_element == 'table:table-cell'):
+ # By default it will repeat zero times
+ self.parser_cell_repeats = 0
+ # We must be in a new column
+ self.parser_sheet_column += 1
+ # Set some default values
+ datatype = ""
+ value = ""
+ # Get values from attrs hash
+ if 'office:value-type' in attrs: datatype = attrs['office:value-type']
+ if 'office:value' in attrs: value = attrs['office:value']
+ if 'table:formula' in attrs:
+ datatype = 'formula'
+ value = attrs['table:formula']
+ if datatype == 'string':
+ datatype = ""
+ self.parser_cell_string_pending = True
+ self.parser_cell_string_line = ""
+ if 'table:number-columns-repeated' in attrs:
+ self.parser_cell_repeats = int(attrs['table:number-columns-repeated']) - 1
+ # Set the cell value
+ if datatype:
+ # I should do this once per cell repeat above 0
+ for i in range(0, self.parser_cell_repeats+1):
+ self.set_cell_value(self.parser_sheet_column+i, self.parser_sheet_row, datatype, value)
+
+ # There are lots of interesting cases with table:table-cell data. One problem is
+ # reading the number of embedded spaces correctly. This code should help us get
+ # the number of spaces out.
+
+ if (self.parser_element == 'text:s'):
+ # This means we have a number of spaces
+ count_num = 0
+ if 'text:c' in attrs:
+ count_alpha = attrs['text:c']
+ if (count_alpha.isdigit()):
+ count_num = int(count_alpha)
+ # I am not sure what to do if we do not have a string pending
+ if (self.parser_cell_string_pending == True):
+ # Append the currect number of spaces to the end
+ self.parser_cell_string_line = "%s%s" % (self.parser_cell_string_line, ' '*count_num)
+
+ if (self.parser_element == 'text:tab-stop'):
+ if (self.parser_cell_string_pending == True):
+ self.parser_cell_string_line = "%s\t" % (self.parser_cell_string_line)
+
+ if (self.parser_element == 'text:line-break'):
+ if (self.parser_cell_string_pending == True):
+ self.parser_cell_string_line = "%s\n" % (self.parser_cell_string_line)
+
+ # Debugging statements
+ if self.debug: print " List: ", self.parser_element_list
+ if self.debug: print " Attributes: ", attrs
+
+
+ def parse_content_end_element(self, name):
+ if self.debug: print '* End element:', name
+ if name != self.parser_element:
+ print "Tag Mismatch: '%s' != '%s'" % (name, self.parser_element)
+ self.parser_element_list.pop()
+
+ # If the element was text:p and we are in string mode
+ if (self.parser_element == 'text:p'):
+ if (self.parser_cell_string_pending):
+ self.parser_cell_string_pending = False
+
+ # Take care of repeated cells
+ if (self.parser_element == 'table:table-cell'):
+ self.parser_sheet_column += self.parser_cell_repeats
+
+ # Readjust parser_element_list and parser_element
+ if (self.parser_element_list):
+ self.parser_element = self.parser_element_list[-1]
+ else:
+ self.parser_element = ""
+
+ def parse_content_char_data(self, data):
+ if self.debug: print " Character data: ", repr(data)
+
+ if (self.parser_element == 'text:p' or self.parser_element == 'text:span'):
+ if (self.parser_cell_string_pending):
+ # Set the string and leave string pending mode
+ # This does feel a little kludgy, but it does the job
+ self.parser_cell_string_line = "%s%s" % (self.parser_cell_string_line, data)
+
+ # I should do this once per cell repeat above 0
+ for i in range(0, self.parser_cell_repeats+1):
+ self.set_cell_value(self.parser_sheet_column+i, self.parser_sheet_row,
+ 'string', self.parser_cell_string_line)
+
+
+ def content_parse(self, data):
+ "Parse Content Data from a content.xml file"
+
+ # Debugging statements
+ if self.debug:
+ # Sometimes it helps to see the document that was read from
+ print data
+ print "\n\n\n"
+
+ # Create parser
+ parser = xml.parsers.expat.ParserCreate()
+ # Set up parser callback functions
+ parser.StartElementHandler = self.parse_content_start_element
+ parser.EndElementHandler = self.parse_content_end_element
+ parser.CharacterDataHandler = self.parse_content_char_data
+
+ # Actually parse the data
+ parser.Parse(data, 1)
+
+ def save(self, filename):
+ """Save .ods spreadsheet.
+
+ The save function saves the current cells and settings into a document.
+ """
+ if self.debug: print "Writing %s" % filename
+ self.savefile = zipfile.ZipFile(filename, "w")
+ if self.debug: print " meta.xml"
+ self._zip_insert(self.savefile, "meta.xml", self.meta.get_meta())
+ if self.debug: print " mimetype"
+ self._zip_insert(self.savefile, "mimetype", "application/vnd.oasis.opendocument.spreadsheet")
+ if self.debug: print " Configurations2/accelerator/current.xml"
+ self._zip_insert(self.savefile, "Configurations2/accelerator/current.xml", "")
+ if self.debug: print " META-INF/manifest.xml"
+ self._zip_insert(self.savefile, "META-INF/manifest.xml", self._ods_manifest())
+ if self.debug: print " content.xml"
+ self._zip_insert(self.savefile, "content.xml", self._ods_content())
+ if self.debug: print " settings.xml"
+ self._zip_insert(self.savefile, "settings.xml", self._ods_settings())
+ if self.debug: print " styles.xml"
+ self._zip_insert(self.savefile, "styles.xml", self._ods_styles())
+
+ # Add additional files if needed
+ for fileset in self.manifest_files:
+ (filename, filetype, newname) = fileset
+ # Read in the file
+ data = self._file_load(filename)
+ if self.debug: print " Inserting '%s' as '%s'" % (filename, newname)
+ self._zip_insert_binary(self.savefile, newname, data)
+
+ def _file_load(self, filename):
+ "Load a file"
+ file = open(filename, "rb")
+ data = file.read()
+ file.close()
+ return data
+
+ def _zip_insert_binary(self, file, filename, data):
+ "Insert a binary file into the zip archive"
+ now = time.localtime(time.time())[:6]
+ info = zipfile.ZipInfo(filename)
+ info.date_time = now
+ info.compress_type = zipfile.ZIP_DEFLATED
+ file.writestr(info, data)
+
+
+ def _zip_insert(self, file, filename, data):
+ "Insert a file into the zip archive"
+
+ # zip seems to struggle with non-ascii characters
+ data = data.encode('utf-8')
+
+ now = time.localtime(time.time())[:6]
+ info = zipfile.ZipInfo(filename)
+ info.date_time = now
+ info.compress_type = zipfile.ZIP_DEFLATED
+ file.writestr(info, data)
+
+ def _zip_read(self, file, filename):
+ "Get the data from a file in the zip archive by filename"
+ file = zipfile.ZipFile(file, "r")
+ data = file.read(filename)
+ # Need to close the file
+ file.close()
+ return data
+
+ def _ods_content(self):
+ "Generate ods content.xml data"
+
+ # This will list all of the sheets in the document
+ self.sheetdata = ['tag', 'office:spreadsheet']
+ for sheet in self.sheets:
+ if self.debug:
+ sheet_name = sheet.get_name()
+ print " Creating Sheet '%s'" % sheet_name
+ sheet_list = sheet.get_lists()
+ self.sheetdata.append(sheet_list)
+ # Automatic Styles
+ self.automatic_styles = self.styles.get_automatic_styles()
+
+ self.data = ['tag', 'office:document-content',
+ ['element', 'xmlns:office', 'urn:oasis:names:tc:opendocument:xmlns:office:1.0'],
+ ['element', 'xmlns:style', 'urn:oasis:names:tc:opendocument:xmlns:style:1.0'],
+ ['element', 'xmlns:text', 'urn:oasis:names:tc:opendocument:xmlns:text:1.0'],
+ ['element', 'xmlns:table', 'urn:oasis:names:tc:opendocument:xmlns:table:1.0'],
+ ['element', 'xmlns:draw', 'urn:oasis:names:tc:opendocument:xmlns:drawing:1.0'],
+ ['element', 'xmlns:fo', 'urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0'],
+ ['element', 'xmlns:xlink', 'http://www.w3.org/1999/xlink'],
+ ['element', 'xmlns:dc', 'http://purl.org/dc/elements/1.1/'],
+ ['element', 'xmlns:meta', 'urn:oasis:names:tc:opendocument:xmlns:meta:1.0'],
+ ['element', 'xmlns:number', 'urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0'],
+ ['element', 'xmlns:svg', 'urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0'],
+ ['element', 'xmlns:chart', 'urn:oasis:names:tc:opendocument:xmlns:chart:1.0'],
+ ['element', 'xmlns:dr3d', 'urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0'],
+ ['element', 'xmlns:math', 'http://www.w3.org/1998/Math/MathML'],
+ ['element', 'xmlns:form', 'urn:oasis:names:tc:opendocument:xmlns:form:1.0'],
+ ['element', 'xmlns:script', 'urn:oasis:names:tc:opendocument:xmlns:script:1.0'],
+ ['element', 'xmlns:ooo', 'http://openoffice.org/2004/office'],
+ ['element', 'xmlns:ooow', 'http://openoffice.org/2004/writer'],
+ ['element', 'xmlns:oooc', 'http://openoffice.org/2004/calc'],
+ ['element', 'xmlns:dom', 'http://www.w3.org/2001/xml-events'],
+ ['element', 'xmlns:xforms', 'http://www.w3.org/2002/xforms'],
+ ['element', 'xmlns:xsd', 'http://www.w3.org/2001/XMLSchema'],
+ ['element', 'xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance'],
+ ['element', 'office:version', '1.0'],
+ ['tagline', 'office:scripts'],
+ ['tag', 'office:font-face-decls',
+ ['tagline', 'style:font-face',
+ ['element', 'style:name', 'DejaVu Sans'],
+ ['element', 'svg:font-family', '&apos;DejaVu Sans&apos;'],
+ ['element', 'style:font-pitch', 'variable']],
+ ['tagline', 'style:font-face',
+ ['element', 'style:name', 'Nimbus Sans L'],
+ ['element', 'svg:font-family', '&apos;Nimbus Sans L&apos;'],
+ ['element', 'style:font-family-generic', 'swiss'],
+ ['element', 'style:font-pitch', 'variable']]],
+
+ # Automatic Styles
+ self.automatic_styles,
+
+ ['tag', 'office:body',
+ self.sheetdata]] # Sheets are generated from the CalcSheet class
+
+ # Generate content.xml XML data
+ xml = XML()
+ self.lines = xml.convert(self.data)
+ self.filedata = '\n'.join(self.lines)
+ # Return generated data
+ return self.filedata
+
+ def _ods_manifest(self):
+ "Generate ods manifest.xml data"
+ self.data = ['tag', 'manifest:manifest',
+ ['element', 'xmlns:manifest', 'urn:oasis:names:tc:opendocument:xmlns:manifest:1.0'],
+ ['tagline', 'manifest:file-entry',
+ ['element', 'manifest:media-type', 'application/vnd.oasis.opendocument.spreadsheet'],
+ ['element', 'manifest:full-path', '/']],
+ ['tagline', 'manifest:file-entry',
+ ['element', 'manifest:media-type', 'application/vnd.sun.xml.ui.configuration'],
+ ['element', 'manifest:full-path', 'Configurations2/']],
+ ['tagline', 'manifest:file-entry',
+ ['element', 'manifest:media-type', ''],
+ ['element', 'manifest:full-path', 'Configurations2/accelerator/']],
+ ['tagline', 'manifest:file-entry',
+ ['element', 'manifest:media-type', ''],
+ ['element', 'manifest:full-path', 'Configurations2/accelerator/current.xml']],
+ ['tagline', 'manifest:file-entry',
+ ['element', 'manifest:media-type', 'text/xml'],
+ ['element', 'manifest:full-path', 'content.xml']],
+ ['tagline', 'manifest:file-entry',
+ ['element', 'manifest:media-type', 'text/xml'],
+ ['element', 'manifest:full-path', 'styles.xml']],
+ ['tagline', 'manifest:file-entry',
+ ['element', 'manifest:media-type', 'text/xml'],
+ ['element', 'manifest:full-path', 'meta.xml']],
+ ['tagline', 'manifest:file-entry',
+ ['element', 'manifest:media-type', 'text/xml'],
+ ['element', 'manifest:full-path', 'settings.xml']]]
+
+ # Add additional files to manifest list
+ for fileset in self.manifest_files:
+ (filename, filetype, newname) = fileset
+ addfile = ['tagline', 'manifest:file-entry',
+ ['element', 'manifest:media-type', filetype],
+ ['element', 'manifest:full-path', newname]]
+ self.data.append(addfile)
+
+ # Generate content.xml XML data
+ xml = XML()
+ self.lines = xml.convert(self.data)
+ self.filedata = '\n'.join(self.lines)
+ # Return generated data
+ return self.filedata
+
+
+ def _ods_settings(self):
+ "Generate ods settings.xml data"
+ self.data = ['tag', 'office:document-settings',
+ ['element', 'xmlns:office', 'urn:oasis:names:tc:opendocument:xmlns:office:1.0'],
+ ['element', 'xmlns:xlink', 'http://www.w3.org/1999/xlink'],
+ ['element', 'xmlns:config', 'urn:oasis:names:tc:opendocument:xmlns:config:1.0'],
+ ['element', 'xmlns:ooo', 'http://openoffice.org/2004/office'],
+ ['element', 'office:version', '1.0'],
+ ['tag', 'office:settings',
+ ['tag', 'config:config-item-set',
+ ['element', 'config:name', 'ooo:view-settings'],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'VisibleAreaTop'],
+ ['element', 'config:type', 'int'],
+ ['data', '0']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'VisibleAreaLeft'],
+ ['element', 'config:type', 'int'],
+ ['data', '0']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'VisibleAreaWidth'],
+ ['element', 'config:type', 'int'],
+ ['data', '6774']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'VisibleAreaHeight'],
+ ['element', 'config:type', 'int'],
+ ['data', '2389']],
+ ['tag', 'config:config-item-map-indexed',
+ ['element', 'config:name', 'Views'],
+ ['tag', 'config:config-item-map-entry',
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'ViewId'],
+ ['element', 'config:type', 'string'],
+ ['data', 'View1']],
+ ['tag', 'config:config-item-map-named',
+ ['element', 'config:name', 'Tables'],
+ ['tag', 'config:config-item-map-entry',
+ ['element', 'config:name', 'Sheet1'],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'CursorPositionX'], # Cursor Position A
+ ['element', 'config:type', 'int'],
+ ['data', '0']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'CursorPositionY'], # Cursor Position 1
+ ['element', 'config:type', 'int'],
+ ['data', '0']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'HorizontalSplitMode'],
+ ['element', 'config:type', 'short'],
+ ['data', '0']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'VerticalSplitMode'],
+ ['element', 'config:type', 'short'],
+ ['data', '0']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'HorizontalSplitPosition'],
+ ['element', 'config:type', 'int'],
+ ['data', '0']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'VerticalSplitPosition'],
+ ['element', 'config:type', 'int'],
+ ['data', '0']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'ActiveSplitRange'],
+ ['element', 'config:type', 'short'],
+ ['data', '2']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'PositionLeft'],
+ ['element', 'config:type', 'int'],
+ ['data', '0']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'PositionRight'],
+ ['element', 'config:type', 'int'],
+ ['data', '0']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'PositionTop'],
+ ['element', 'config:type', 'int'],
+ ['data', '0']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'PositionBottom'],
+ ['element', 'config:type', 'int'],
+ ['data', '0']]]],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'ActiveTable'],
+ ['element', 'config:type', 'string'],
+ ['data', 'Sheet1']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'HorizontalScrollbarWidth'],
+ ['element', 'config:type', 'int'],
+ ['data', '270']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'ZoomType'],
+ ['element', 'config:type', 'short'],
+ ['data', '0']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'ZoomValue'],
+ ['element', 'config:type', 'int'],
+ ['data', '100']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'PageViewZoomValue'],
+ ['element', 'config:type', 'int'],
+ ['data', '60']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'ShowPageBreakPreview'],
+ ['element', 'config:type', 'boolean'],
+ ['data', 'false']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'ShowZeroValues'],
+ ['element', 'config:type', 'boolean'],
+ ['data', 'true']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'ShowNotes'],
+ ['element', 'config:type', 'boolean'],
+ ['data', 'true']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'ShowGrid'],
+ ['element', 'config:type', 'boolean'],
+ ['data', 'true']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'GridColor'],
+ ['element', 'config:type', 'long'],
+ ['data', '12632256']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'ShowPageBreaks'],
+ ['element', 'config:type', 'boolean'],
+ ['data', 'true']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'HasColumnRowHeaders'],
+ ['element', 'config:type', 'boolean'],
+ ['data', 'true']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'HasSheetTabs'],
+ ['element', 'config:type', 'boolean'],
+ ['data', 'true']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'IsOutlineSymbolsSet'],
+ ['element', 'config:type', 'boolean'],
+ ['data', 'true']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'IsSnapToRaster'],
+ ['element', 'config:type', 'boolean'],
+ ['data', 'false']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'RasterIsVisible'],
+ ['element', 'config:type', 'boolean'],
+ ['data', 'false']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'RasterResolutionX'],
+ ['element', 'config:type', 'int'],
+ ['data', '1270']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'RasterResolutionY'],
+ ['element', 'config:type', 'int'],
+ ['data', '1270']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'RasterSubdivisionX'],
+ ['element', 'config:type', 'int'],
+ ['data', '1']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'RasterSubdivisionY'],
+ ['element', 'config:type', 'int'],
+ ['data', '1']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'IsRasterAxisSynchronized'],
+ ['element', 'config:type', 'boolean'],
+ ['data', 'true']]]]],
+ ['tag', 'config:config-item-set',
+ ['element', 'config:name', 'ooo:configuration-settings'],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'ShowZeroValues'],
+ ['element', 'config:type', 'boolean'],
+ ['data', 'true']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'ShowNotes'],
+ ['element', 'config:type', 'boolean'],
+ ['data', 'true']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'ShowGrid'],
+ ['element', 'config:type', 'boolean'],
+ ['data', 'true']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'GridColor'],
+ ['element', 'config:type', 'long'],
+ ['data', '12632256']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'ShowPageBreaks'],
+ ['element', 'config:type', 'boolean'],
+ ['data', 'true']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'LinkUpdateMode'],
+ ['element', 'config:type', 'short'],
+ ['data', '3']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'HasColumnRowHeaders'],
+ ['element', 'config:type', 'boolean'],
+ ['data', 'true']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'HasSheetTabs'],
+ ['element', 'config:type', 'boolean'],
+ ['data', 'true']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'IsOutlineSymbolsSet'],
+ ['element', 'config:type', 'boolean'],
+ ['data', 'true']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'IsSnapToRaster'],
+ ['element', 'config:type', 'boolean'],
+ ['data', 'false']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'RasterIsVisible'],
+ ['element', 'config:type', 'boolean'],
+ ['data', 'false']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'RasterResolutionX'],
+ ['element', 'config:type', 'int'],
+ ['data', '1270']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'RasterResolutionY'],
+ ['element', 'config:type', 'int'],
+ ['data', '1270']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'RasterSubdivisionX'],
+ ['element', 'config:type', 'int'],
+ ['data', '1']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'RasterSubdivisionY'],
+ ['element', 'config:type', 'int'],
+ ['data', '1']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'IsRasterAxisSynchronized'],
+ ['element', 'config:type', 'boolean'],
+ ['data', 'true']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'AutoCalculate'],
+ ['element', 'config:type', 'boolean'],
+ ['data', 'true']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'PrinterName'],
+ ['element', 'config:type', 'string'],
+ ['data', 'Generic Printer']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'PrinterSetup'],
+ ['element', 'config:type', 'base64Binary'],
+ ['data', 'YgH+/0dlbmVyaWMgUHJpbnRlcgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAU0dFTlBSVAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWAAMAqAAAAAAA//8FAFZUAAAkbQAASm9iRGF0YSAxCnByaW50ZXI9R2VuZXJpYyBQcmludGVyCm9yaWVudGF0aW9uPVBvcnRyYWl0CmNvcGllcz0xCnNjYWxlPTEwMAptYXJnaW5kYWp1c3RtZW50PTAsMCwwLDAKY29sb3JkZXB0aD0yNApwc2xldmVsPTAKY29sb3JkZXZpY2U9MApQUERDb250ZXhEYXRhClBhZ2VTaXplOkxldHRlcgAA']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'ApplyUserData'],
+ ['element', 'config:type', 'boolean'],
+ ['data', 'true']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'CharacterCompressionType'],
+ ['element', 'config:type', 'short'],
+ ['data', '0']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'IsKernAsianPunctuation'],
+ ['element', 'config:type', 'boolean'],
+ ['data', 'false']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'SaveVersionOnClose'],
+ ['element', 'config:type', 'boolean'],
+ ['data', 'false']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'UpdateFromTemplate'],
+ ['element', 'config:type', 'boolean'],
+ ['data', 'false']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'AllowPrintJobCancel'],
+ ['element', 'config:type', 'boolean'],
+ ['data', 'true']],
+ ['tag', 'config:config-item',
+ ['element', 'config:name', 'LoadReadonly'],
+ ['element', 'config:type', 'boolean'],
+ ['data', 'false']]]]]
+
+ # Generate content.xml XML data
+ xml = XML()
+ self.lines = xml.convert(self.data)
+ self.filedata = '\n'.join(self.lines)
+ # Return generated data
+ return self.filedata
+
+
+ def _ods_styles(self):
+ "Generate ods styles.xml data"
+ self.data = ['tag', 'office:document-styles',
+ ['element', 'xmlns:office', 'urn:oasis:names:tc:opendocument:xmlns:office:1.0'],
+ ['element', 'xmlns:style', 'urn:oasis:names:tc:opendocument:xmlns:style:1.0'],
+ ['element', 'xmlns:text', 'urn:oasis:names:tc:opendocument:xmlns:text:1.0'],
+ ['element', 'xmlns:table', 'urn:oasis:names:tc:opendocument:xmlns:table:1.0'],
+ ['element', 'xmlns:draw', 'urn:oasis:names:tc:opendocument:xmlns:drawing:1.0'],
+ ['element', 'xmlns:fo', 'urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0'],
+ ['element', 'xmlns:xlink', 'http://www.w3.org/1999/xlink'],
+ ['element', 'xmlns:dc', 'http://purl.org/dc/elements/1.1/'],
+ ['element', 'xmlns:meta', 'urn:oasis:names:tc:opendocument:xmlns:meta:1.0'],
+ ['element', 'xmlns:number', 'urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0'],
+ ['element', 'xmlns:svg', 'urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0'],
+ ['element', 'xmlns:chart', 'urn:oasis:names:tc:opendocument:xmlns:chart:1.0'],
+ ['element', 'xmlns:dr3d', 'urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0'],
+ ['element', 'xmlns:math', 'http://www.w3.org/1998/Math/MathML'],
+ ['element', 'xmlns:form', 'urn:oasis:names:tc:opendocument:xmlns:form:1.0'],
+ ['element', 'xmlns:script', 'urn:oasis:names:tc:opendocument:xmlns:script:1.0'],
+ ['element', 'xmlns:ooo', 'http://openoffice.org/2004/office'],
+ ['element', 'xmlns:ooow', 'http://openoffice.org/2004/writer'],
+ ['element', 'xmlns:oooc', 'http://openoffice.org/2004/calc'],
+ ['element', 'xmlns:dom', 'http://www.w3.org/2001/xml-events'],
+ ['element', 'office:version', '1.0'],
+ ['tag', 'office:font-face-decls',
+ ['tagline', 'style:font-face',
+ ['element', 'style:name', 'DejaVu Sans'],
+ ['element', 'svg:font-family', '&apos;DejaVu Sans&apos;'],
+ ['element', 'style:font-pitch', 'variable']],
+ ['tagline', 'style:font-face',
+ ['element', 'style:name', 'Nimbus Sans L'],
+ ['element', 'svg:font-family', '&apos;Nimbus Sans L&apos;'],
+ ['element', 'style:font-family-generic', 'swiss'],
+ ['element', 'style:font-pitch', 'variable']]],
+ ['tag', 'office:styles',
+ ['tag', 'style:default-style',
+ ['element', 'style:family', 'table-cell'],
+ ['tagline', 'style:table-cell-properties',
+ ['element', 'style:decimal-places', '2']],
+ ['tagline', 'style:paragraph-properties',
+ ['element', 'style:tab-stop-distance', '0.5in']],
+ ['tagline', 'style:text-properties',
+ ['element', 'style:font-name', 'Nimbus Sans L'],
+ ['element', 'fo:language', 'en'],
+ ['element', 'fo:country', 'US'],
+ ['element', 'style:font-name-asian', 'DejaVu Sans'],
+ ['element', 'style:language-asian', 'none'],
+ ['element', 'style:country-asian', 'none'],
+ ['element', 'style:font-name-complex', 'DejaVu Sans'],
+ ['element', 'style:language-complex', 'none'],
+ ['element', 'style:country-complex', 'none']]],
+ ['tag', 'number:number-style',
+ ['element', 'style:name', 'N0'],
+ ['tagline', 'number:number',
+ ['element', 'number:min-integer-digits', '1']]],
+ ['tag', 'number:currency-style',
+ ['element', 'style:name', 'N104P0'],
+ ['element', 'style:volatile', 'true'],
+ ['tag', 'number:currency-symbol',
+ ['element', 'number:language', 'en'],
+ ['element', 'number:country', 'US'],
+ ['data', '$']],
+ ['tagline', 'number:number',
+ ['element', 'number:decimal-places', '2'],
+ ['element', 'number:min-integer-digits', '1'],
+ ['element', 'number:grouping', 'true']]],
+ ['tag', 'number:currency-style',
+ ['element', 'style:name', 'N104'],
+ ['tagline', 'style:text-properties',
+ ['element', 'fo:color', '#ff0000']],
+ ['tag', 'number:text',
+ ['data', '-']],
+ ['tag', 'number:currency-symbol',
+ ['element', 'number:language', 'en'],
+ ['element', 'number:country', 'US'],
+ ['data', '$']],
+ ['tagline', 'number:number',
+ ['element', 'number:decimal-places', '2'],
+ ['element', 'number:min-integer-digits', '1'],
+ ['element', 'number:grouping', 'true']],
+ ['tagline', 'style:map',
+ ['element', 'style:condition', 'value()&gt;=0'],
+ ['element', 'style:apply-style-name', 'N104P0']]],
+ ['tagline', 'style:style',
+ ['element', 'style:name', 'Default'],
+ ['element', 'style:family', 'table-cell']],
+ ['tag', 'style:style',
+ ['element', 'style:name', 'Result'],
+ ['element', 'style:family', 'table-cell'],
+ ['element', 'style:parent-style-name', 'Default'],
+ ['tagline', 'style:text-properties',
+ ['element', 'fo:font-style', 'italic'],
+ ['element', 'style:text-underline-style', 'solid'],
+ ['element', 'style:text-underline-width', 'auto'],
+ ['element', 'style:text-underline-color', 'font-color'],
+ ['element', 'fo:font-weight', 'bold']]],
+ ['tagline', 'style:style',
+ ['element', 'style:name', 'Result2'],
+ ['element', 'style:family', 'table-cell'],
+ ['element', 'style:parent-style-name', 'Result'],
+ ['element', 'style:data-style-name', 'N104']],
+ ['tag', 'style:style',
+ ['element', 'style:name', 'Heading'],
+ ['element', 'style:family', 'table-cell'],
+ ['element', 'style:parent-style-name', 'Default'],
+ ['tagline', 'style:table-cell-properties',
+ ['element', 'style:text-align-source', 'fix'],
+ ['element', 'style:repeat-content', 'false']],
+ ['tagline', 'style:paragraph-properties',
+ ['element', 'fo:text-align', 'center']],
+ ['tagline', 'style:text-properties',
+ ['element', 'fo:font-size', '16pt'],
+ ['element', 'fo:font-style', 'italic'],
+ ['element', 'fo:font-weight', 'bold']]],
+ ['tag', 'style:style',
+ ['element', 'style:name', 'Heading1'],
+ ['element', 'style:family', 'table-cell'],
+ ['element', 'style:parent-style-name', 'Heading'],
+ ['tagline', 'style:table-cell-properties',
+ ['element', 'style:rotation-angle', '90']]]],
+ ['tag', 'office:automatic-styles',
+ ['tag', 'style:page-layout',
+ ['element', 'style:name', 'pm1'],
+ ['tagline', 'style:page-layout-properties',
+ ['element', 'style:writing-mode', 'lr-tb']],
+ ['tag', 'style:header-style',
+ ['tagline', 'style:header-footer-properties',
+ ['element', 'fo:min-height', '0.2957in'],
+ ['element', 'fo:margin-left', '0in'],
+ ['element', 'fo:margin-right', '0in'],
+ ['element', 'fo:margin-bottom', '0.0984in']]],
+ ['tag', 'style:footer-style',
+ ['tagline', 'style:header-footer-properties',
+ ['element', 'fo:min-height', '0.2957in'],
+ ['element', 'fo:margin-left', '0in'],
+ ['element', 'fo:margin-right', '0in'],
+ ['element', 'fo:margin-top', '0.0984in']]]],
+ ['tag', 'style:page-layout',
+ ['element', 'style:name', 'pm2'],
+ ['tagline', 'style:page-layout-properties',
+ ['element', 'style:writing-mode', 'lr-tb']],
+ ['tag', 'style:header-style',
+ ['tag', 'style:header-footer-properties',
+ ['element', 'fo:min-height', '0.2957in'],
+ ['element', 'fo:margin-left', '0in'],
+ ['element', 'fo:margin-right', '0in'],
+ ['element', 'fo:margin-bottom', '0.0984in'],
+ ['element', 'fo:border', '0.0346in solid #000000'],
+ ['element', 'fo:padding', '0.0071in'],
+ ['element', 'fo:background-color', '#c0c0c0'],
+ ['tagline', 'style:background-image']]],
+ ['tag', 'style:footer-style',
+ ['tag', 'style:header-footer-properties',
+ ['element', 'fo:min-height', '0.2957in'],
+ ['element', 'fo:margin-left', '0in'],
+ ['element', 'fo:margin-right', '0in'],
+ ['element', 'fo:margin-top', '0.0984in'],
+ ['element', 'fo:border', '0.0346in solid #000000'],
+ ['element', 'fo:padding', '0.0071in'],
+ ['element', 'fo:background-color', '#c0c0c0'],
+ ['tagline', 'style:background-image']]]]],
+ ['tag', 'office:master-styles',
+ ['tag', 'style:master-page',
+ ['element', 'style:name', 'Default'],
+ ['element', 'style:page-layout-name', 'pm1'],
+ ['tag', 'style:header',
+ ['tag', 'text:p',
+ ['data', '<text:sheet-name>???</text:sheet-name>']]],
+ ['tagline', 'style:header-left',
+ ['element', 'style:display', 'false']],
+ ['tag', 'style:footer',
+ ['tag', 'text:p',
+ ['data', 'Page <text:page-number>1</text:page-number>']]],
+ ['tagline', 'style:footer-left',
+ ['element', 'style:display', 'false']]],
+ ['tag', 'style:master-page',
+ ['element', 'style:name', 'Report'],
+ ['element', 'style:page-layout-name', 'pm2'],
+ ['tag', 'style:header',
+ ['tag', 'style:region-left',
+ ['tag', 'text:p',
+ ['data', '<text:sheet-name>???</text:sheet-name> (<text:title>???</text:title>)']]],
+ ['tag', 'style:region-right',
+ ['tag', 'text:p',
+ ['data', '<text:date style:data-style-name="N2" text:date-value="2006-09-29">09/29/2006</text:date>, <text:time>13:02:56</text:time>']]]],
+ ['tagline', 'style:header-left',
+ ['element', 'style:display', 'false']],
+ ['tag', 'style:footer',
+ ['tag', 'text:p',
+ ['data', 'Page <text:page-number>1</text:page-number> / <text:page-count>99</text:page-count>']]],
+ ['tagline', 'style:footer-left',
+ ['element', 'style:display', 'false']]]]]
+
+
+ # Generate content.xml XML data
+ xml = XML()
+ self.lines = xml.convert(self.data)
+ self.filedata = '\n'.join(self.lines)
+ # Return generated data
+ return self.filedata
+
+class Writer:
+ "Writer Class - Used to create OpenDocument Format Writer Documents."
+ def __init__(self):
+ "Initialize ooolib Writer instance"
+ # Default to no debugging
+ self.debug = False
+ self.meta = Meta('odt')
+
+ def set_meta(self, metaname, value):
+ "Set meta data in your document."
+ self.meta.set_meta(metaname, value)
+
+ def save(self, filename):
+ """Save .odt document
+
+ The save function saves the current .odt document.
+ """
+ if self.debug: print "Writing %s" % filename
+ self.savefile = zipfile.ZipFile(filename, "w")
+ if self.debug: print " meta.xml"
+ self._zip_insert(self.savefile, "meta.xml", self.meta.get_meta())
+ if self.debug: print " mimetype"
+ self._zip_insert(self.savefile, "mimetype", "application/vnd.oasis.opendocument.text")
+ if self.debug: print " META-INF/manifest.xml"
+ self._zip_insert(self.savefile, "META-INF/manifest.xml", self._odt_manifest())
+ if self.debug: print " content.xml"
+ self._zip_insert(self.savefile, "content.xml", self._odt_content())
+ if self.debug: print " settings.xml"
+ # self._zip_insert(self.savefile, "settings.xml", self._odt_settings())
+ if self.debug: print " styles.xml"
+ # self._zip_insert(self.savefile, "styles.xml", self._odt_styles())
+
+ # We need to close the file now that we are done creating it.
+ self.savefile.close()
+
+ def _zip_insert(self, file, filename, data):
+ now = time.localtime(time.time())[:6]
+ info = zipfile.ZipInfo(filename)
+ info.date_time = now
+ info.compress_type = zipfile.ZIP_DEFLATED
+ file.writestr(info, data)
+
+ def _odt_manifest(self):
+ "Generate odt manifest.xml data"
+
+ self.data = ['tag', 'manifest:manifest',
+ ['element', 'xmlns:manifest', 'urn:oasis:names:tc:opendocument:xmlns:manifest:1.0'],
+ ['tagline', 'manifest:file-entry',
+ ['element', 'manifest:media-type', 'application/vnd.oasis.opendocument.text'],
+ ['element', 'manifest:full-path', '/']],
+ ['tagline', 'manifest:file-entry',
+ ['element', 'manifest:media-type', 'text/xml'],
+ ['element', 'manifest:full-path', 'content.xml']],
+ ['tagline', 'manifest:file-entry',
+ ['element', 'manifest:media-type', 'text/xml'],
+ ['element', 'manifest:full-path', 'styles.xml']],
+ ['tagline', 'manifest:file-entry',
+ ['element', 'manifest:media-type', 'text/xml'],
+ ['element', 'manifest:full-path', 'meta.xml']],
+ ['tagline', 'manifest:file-entry',
+ ['element', 'manifest:media-type', 'text/xml'],
+ ['element', 'manifest:full-path', 'settings.xml']]]
+
+ # Generate content.xml XML data
+ xml = XML()
+ self.lines = xml.convert(self.data)
+ self.lines.insert(1, '<!DOCTYPE manifest:manifest PUBLIC "-//OpenOffice.org//DTD Manifest 1.0//EN" "Manifest.dtd">')
+ self.filedata = '\n'.join(self.lines)
+ # Return generated data
+ return self.filedata
+
+ def _odt_content(self):
+ "Generate odt content.xml data"
+
+ self.data = ['tag', 'office:document-content',
+ ['element', 'xmlns:office', 'urn:oasis:names:tc:opendocument:xmlns:office:1.0'],
+ ['element', 'xmlns:style', 'urn:oasis:names:tc:opendocument:xmlns:style:1.0'],
+ ['element', 'xmlns:text', 'urn:oasis:names:tc:opendocument:xmlns:text:1.0'],
+ ['element', 'xmlns:table', 'urn:oasis:names:tc:opendocument:xmlns:table:1.0'],
+ ['element', 'xmlns:draw', 'urn:oasis:names:tc:opendocument:xmlns:drawing:1.0'],
+ ['element', 'xmlns:fo', 'urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0'],
+ ['element', 'xmlns:xlink', 'http://www.w3.org/1999/xlink'],
+ ['element', 'xmlns:dc', 'http://purl.org/dc/elements/1.1/'],
+ ['element', 'xmlns:meta', 'urn:oasis:names:tc:opendocument:xmlns:meta:1.0'],
+ ['element', 'xmlns:number', 'urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0'],
+ ['element', 'xmlns:svg', 'urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0'],
+ ['element', 'xmlns:chart', 'urn:oasis:names:tc:opendocument:xmlns:chart:1.0'],
+ ['element', 'xmlns:dr3d', 'urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0'],
+ ['element', 'xmlns:math', 'http://www.w3.org/1998/Math/MathML'],
+ ['element', 'xmlns:form', 'urn:oasis:names:tc:opendocument:xmlns:form:1.0'],
+ ['element', 'xmlns:script', 'urn:oasis:names:tc:opendocument:xmlns:script:1.0'],
+ ['element', 'xmlns:ooo', 'http://openoffice.org/2004/office'],
+ ['element', 'xmlns:ooow', 'http://openoffice.org/2004/writer'],
+ ['element', 'xmlns:oooc', 'http://openoffice.org/2004/calc'],
+ ['element', 'xmlns:dom', 'http://www.w3.org/2001/xml-events'],
+ ['element', 'xmlns:xforms', 'http://www.w3.org/2002/xforms'],
+ ['element', 'xmlns:xsd', 'http://www.w3.org/2001/XMLSchema'],
+ ['element', 'xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance'],
+ ['element', 'office:version', '1.0'],
+ ['tagline', 'office:scripts'],
+ ['tag', 'office:font-face-decls',
+ ['tagline', 'style:font-face',
+ ['element', 'style:name', 'DejaVu Sans'],
+ ['element', 'svg:font-family', '&apos;DejaVu Sans&apos;'],
+ ['element', 'style:font-pitch', 'variable']],
+ ['tagline', 'style:font-face',
+ ['element', 'style:name', 'Nimbus Roman No9 L'],
+ ['element', 'svg:font-family', '&apos;Nimbus Roman No9 L&apos;'],
+ ['element', 'style:font-family-generic', 'roman'],
+ ['element', 'style:font-pitch', 'variable']],
+ ['tagline', 'style:font-face',
+ ['element', 'style:name', 'Nimbus Sans L'],
+ ['element', 'svg:font-family', '&apos;Nimbus Sans L&apos;'],
+ ['element', 'style:font-family-generic', 'swiss'],
+ ['element', 'style:font-pitch', 'variable']]],
+ ['tagline', 'office:automatic-styles'],
+ ['tag', 'office:body',
+ ['tag', 'office:text',
+ ['tagline', 'office:forms',
+ ['element', 'form:automatic-focus', 'false'],
+ ['element', 'form:apply-design-mode', 'false']],
+ ['tag', 'text:sequence-decls',
+ ['tagline', 'text:sequence-decl',
+ ['element', 'text:display-outline-level', '0'],
+ ['element', 'text:name', 'Illustration']],
+ ['tagline', 'text:sequence-decl',
+ ['element', 'text:display-outline-level', '0'],
+ ['element', 'text:name', 'Table']],
+ ['tagline', 'text:sequence-decl',
+ ['element', 'text:display-outline-level', '0'],
+ ['element', 'text:name', 'Text']],
+ ['tagline', 'text:sequence-decl',
+ ['element', 'text:display-outline-level', '0'],
+ ['element', 'text:name', 'Drawing']]],
+ ['tagline', 'text:p',
+ ['element', 'text:style-name', 'Standard']]]]]
+
+ # Generate content.xml XML data
+ xml = XML()
+ self.lines = xml.convert(self.data)
+ self.filedata = '\n'.join(self.lines)
+ # Return generated data
+ return self.filedata
+
+
diff --git a/contrib/non-profit-audit-reports/readcsv.py b/contrib/non-profit-audit-reports/readcsv.py
new file mode 100755
index 00000000..67fc5663
--- /dev/null
+++ b/contrib/non-profit-audit-reports/readcsv.py
@@ -0,0 +1,31 @@
+#!/usr/bin/python
+# readcsv.py
+# CSV reading technical study
+#
+# Copyright (c) 2012 Tom Marble
+#
+# This program gives you software freedom; you can copy, modify, convey,
+# and/or redistribute it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 3 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 in a file called 'GPLv3'. If not, write to the:
+# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor
+# Boston, MA 02110-1301, USA.
+
+import csv
+
+dialects = csv.list_dialects()
+for dialect in dialects:
+ print 'dialect %s' % str(dialect)
+
+csvfile = open('tests/general-ledger.csv', 'rb')
+reader = csv.reader(csvfile, delimiter=',', quotechar='"')
+for row in reader:
+ print row
diff --git a/contrib/non-profit-audit-reports/tests/Financial/Invoices/Invoice20110510.pdf b/contrib/non-profit-audit-reports/tests/Financial/Invoices/Invoice20110510.pdf
new file mode 100644
index 00000000..e2e06c98
--- /dev/null
+++ b/contrib/non-profit-audit-reports/tests/Financial/Invoices/Invoice20110510.pdf
Binary files differ
diff --git a/contrib/non-profit-audit-reports/tests/Financial/Invoices/Invoice20110510.txt b/contrib/non-profit-audit-reports/tests/Financial/Invoices/Invoice20110510.txt
new file mode 100644
index 00000000..4d5fc907
--- /dev/null
+++ b/contrib/non-profit-audit-reports/tests/Financial/Invoices/Invoice20110510.txt
@@ -0,0 +1,5 @@
+Invoice
+
+Date: May 10, 2011
+
+Donation to the General Fund: $50.00
diff --git a/contrib/non-profit-audit-reports/tests/Projects/Blah/Expenses/hosting/AprilHostingReceipt.pdf b/contrib/non-profit-audit-reports/tests/Projects/Blah/Expenses/hosting/AprilHostingReceipt.pdf
new file mode 100644
index 00000000..b6937670
--- /dev/null
+++ b/contrib/non-profit-audit-reports/tests/Projects/Blah/Expenses/hosting/AprilHostingReceipt.pdf
@@ -0,0 +1,106 @@
+%PDF-1.4
+%쏢
+5 0 obj
+<</Length 6 0 R/Filter /FlateDecode>>
+stream
+xn0E~1q#]4 @;0x؆*tQ!d_Bp:;=F&='(X}]D\+cQٲ K^jyYHTTMq<q#v纹 'nn~]c$33Ah{LC %Ԃ q{þVI!JAA*kʼn1]3{f&0"Lbwɿ"XP~v9BVxu`@Yz=t1o+Wʻk/S-/RWQB"5}m?^f|}8?M*2 o 8?Qsv_RGSdrendstream
+endobj
+6 0 obj
+369
+endobj
+4 0 obj
+<</Type/Page/MediaBox [0 0 612 792]
+/Rotate 0/Parent 3 0 R
+/Resources<</ProcSet[/PDF /Text]
+/ExtGState 11 0 R
+/Font 12 0 R
+>>
+/Contents 5 0 R
+>>
+endobj
+3 0 obj
+<< /Type /Pages /Kids [
+4 0 R
+] /Count 1
+>>
+endobj
+1 0 obj
+<</Type /Catalog /Pages 3 0 R
+/Metadata 13 0 R
+>>
+endobj
+7 0 obj
+<</Type/ExtGState
+/OPM 1>>endobj
+11 0 obj
+<</R7
+7 0 R>>
+endobj
+12 0 obj
+<</R9
+9 0 R/R8
+8 0 R/R10
+10 0 R>>
+endobj
+9 0 obj
+<</BaseFont/Helvetica/Type/Font
+/Subtype/Type1>>
+endobj
+8 0 obj
+<</BaseFont/Courier/Type/Font
+/Subtype/Type1>>
+endobj
+10 0 obj
+<</BaseFont/Helvetica-Bold/Type/Font
+/Subtype/Type1>>
+endobj
+13 0 obj
+<</Type/Metadata
+/Subtype/XML/Length 1393>>stream
+<?xpacket begin='' id='W5M0MpCehiHzreSzNTczkc9d'?>
+<?adobe-xap-filters esc="CRLF"?>
+<x:xmpmeta xmlns:x='adobe:ns:meta/' x:xmptk='XMP toolkit 2.9.1-13, framework 1.6'>
+<rdf:RDF xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#' xmlns:iX='http://ns.adobe.com/iX/1.0/'>
+<rdf:Description rdf:about='1335afed-313b-11ed-0000-eb02a9a83ec4' xmlns:pdf='http://ns.adobe.com/pdf/1.3/' pdf:Producer='GPL Ghostscript 8.71'/>
+<rdf:Description rdf:about='1335afed-313b-11ed-0000-eb02a9a83ec4' xmlns:xmp='http://ns.adobe.com/xap/1.0/'><xmp:ModifyDate>2012-09-07T15:02:10-04:00</xmp:ModifyDate>
+<xmp:CreateDate>2012-09-07T15:02:10-04:00</xmp:CreateDate>
+<xmp:CreatorTool>a2ps version 4.14</xmp:CreatorTool></rdf:Description>
+<rdf:Description rdf:about='1335afed-313b-11ed-0000-eb02a9a83ec4' xmlns:xapMM='http://ns.adobe.com/xap/1.0/mm/' xapMM:DocumentID='1335afed-313b-11ed-0000-eb02a9a83ec4'/>
+<rdf:Description rdf:about='1335afed-313b-11ed-0000-eb02a9a83ec4' xmlns:dc='http://purl.org/dc/elements/1.1/' dc:format='application/pdf'><dc:title><rdf:Alt><rdf:li xml:lang='x-default'>receipt</rdf:li></rdf:Alt></dc:title><dc:creator><rdf:Seq><rdf:li>Bradley M. Kuhn</rdf:li></rdf:Seq></dc:creator></rdf:Description>
+</rdf:RDF>
+</x:xmpmeta>
+
+
+<?xpacket end='w'?>
+endstream
+endobj
+2 0 obj
+<</Producer(GPL Ghostscript 8.71)
+/CreationDate(D:20120907150210-04'00')
+/ModDate(D:20120907150210-04'00')
+/Title(receipt)
+/Author(Bradley M. Kuhn)
+/Creator(a2ps version 4.14)>>endobj
+xref
+0 14
+0000000000 65535 f
+0000000692 00000 n
+0000002544 00000 n
+0000000633 00000 n
+0000000473 00000 n
+0000000015 00000 n
+0000000454 00000 n
+0000000757 00000 n
+0000000942 00000 n
+0000000878 00000 n
+0000001004 00000 n
+0000000798 00000 n
+0000000828 00000 n
+0000001074 00000 n
+trailer
+<< /Size 14 /Root 1 0 R /Info 2 0 R
+/ID [<346C5213A8B2262C0696706A70350365><346C5213A8B2262C0696706A70350365>]
+>>
+startxref
+2736
+%%EOF
diff --git a/contrib/non-profit-audit-reports/tests/Projects/Blah/Expenses/hosting/april-invoice.pdf b/contrib/non-profit-audit-reports/tests/Projects/Blah/Expenses/hosting/april-invoice.pdf
new file mode 100644
index 00000000..7241909a
--- /dev/null
+++ b/contrib/non-profit-audit-reports/tests/Projects/Blah/Expenses/hosting/april-invoice.pdf
Binary files differ
diff --git a/contrib/non-profit-audit-reports/tests/Projects/Foo/Expenses/hosting/AprilHostingReceipt.pdf b/contrib/non-profit-audit-reports/tests/Projects/Foo/Expenses/hosting/AprilHostingReceipt.pdf
new file mode 100644
index 00000000..8441f3e6
--- /dev/null
+++ b/contrib/non-profit-audit-reports/tests/Projects/Foo/Expenses/hosting/AprilHostingReceipt.pdf
Binary files differ
diff --git a/contrib/non-profit-audit-reports/tests/Projects/Foo/Expenses/hosting/AprilHostingReceipt.txt b/contrib/non-profit-audit-reports/tests/Projects/Foo/Expenses/hosting/AprilHostingReceipt.txt
new file mode 100644
index 00000000..e2722c45
--- /dev/null
+++ b/contrib/non-profit-audit-reports/tests/Projects/Foo/Expenses/hosting/AprilHostingReceipt.txt
@@ -0,0 +1,6 @@
+Baz Hosting Services, LLC
+
+Date: April 20, 2011
+
+Charge: $250.00
+
diff --git a/contrib/non-profit-audit-reports/tests/Projects/Foo/Invoices/Invoice20100101.pdf b/contrib/non-profit-audit-reports/tests/Projects/Foo/Invoices/Invoice20100101.pdf
new file mode 100644
index 00000000..11f6286c
--- /dev/null
+++ b/contrib/non-profit-audit-reports/tests/Projects/Foo/Invoices/Invoice20100101.pdf
Binary files differ
diff --git a/contrib/non-profit-audit-reports/tests/non-profit-test-data.ledger b/contrib/non-profit-audit-reports/tests/non-profit-test-data.ledger
new file mode 100644
index 00000000..69aeb571
--- /dev/null
+++ b/contrib/non-profit-audit-reports/tests/non-profit-test-data.ledger
@@ -0,0 +1,26 @@
+
+2010/01/01 Kindly T. Donor
+ Income:Foo:Donation $-100.00
+ ;Invoice: Projects/Foo/Invoices/Invoice20100101.pdf
+ Assets:Checking $100.00
+
+
+2011/03/15 A Later Donation to Project Foo
+ Income:Foo:Donation $-400.00
+ Assets:Checking $400.00
+
+2011/04/20 (1) Baz Hosting Services, LLC
+ Expenses:Foo:Hosting $250.00
+ ;Receipt: Projects/Foo/Expenses/hosting/AprilHostingReceipt.pdf
+ Assets:Checking $-250.00
+
+2011/05/10 Donation to General Fund
+ Income:Donation $-50.00
+ ;Invoice: Financial/Invoices/Invoice20110510.pdf
+ Assets:Checking $50.00
+
+2011/04/20 (2) Baz Hosting Services, LLC
+ Expenses:Blah:Hosting $250.00
+ ;Receipt: Projects/Blah/Expenses/hosting/AprilHostingReceipt.pdf
+ ;Invoice: Projects/Blah/Expenses/hosting/april-invoice.pdf
+ Assets:Checking $-250.00
diff --git a/contrib/non-profit-audit-reports/tests/non-profit-test-data_chart-of-accounts.txt b/contrib/non-profit-audit-reports/tests/non-profit-test-data_chart-of-accounts.txt
new file mode 100644
index 00000000..57e636b9
--- /dev/null
+++ b/contrib/non-profit-audit-reports/tests/non-profit-test-data_chart-of-accounts.txt
@@ -0,0 +1,4 @@
+Assets:Checking
+Expenses:Foo:Hosting
+Income:Donation
+Income:Foo:Donation
diff --git a/contrib/non-profit-audit-reports/tests/non-profit-test-data_general-ledger.ods b/contrib/non-profit-audit-reports/tests/non-profit-test-data_general-ledger.ods
new file mode 100644
index 00000000..80771a6d
--- /dev/null
+++ b/contrib/non-profit-audit-reports/tests/non-profit-test-data_general-ledger.ods
Binary files differ