From 79a64e379d67a87681d72edd4903e3585f1bc60a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Sun, 16 Jan 2011 22:05:34 +0100 Subject: [PATCH] Initial commit --- COPYRIGHT | 15 + INSTALL | 32 ++ LICENSE | 340 +++++++++++++++++++ MANIFEST.in | 11 + README | 36 ++ __init__.py | 5 + __tryton__.py | 24 ++ account.xml | 51 +++ doc/index.rst | 37 ++ product.py | 141 ++++++++ product.xml | 70 ++++ setup.py | 62 ++++ stock.py | 149 ++++++++ tests/__init__.py | 4 + tests/scenario_account_stock_continental.rst | 314 +++++++++++++++++ tests/test_account_stock_continental.py | 53 +++ 16 files changed, 1344 insertions(+) create mode 100644 COPYRIGHT create mode 100644 INSTALL create mode 100644 LICENSE create mode 100644 MANIFEST.in create mode 100644 README create mode 100644 __init__.py create mode 100644 __tryton__.py create mode 100644 account.xml create mode 100644 doc/index.rst create mode 100644 product.py create mode 100644 product.xml create mode 100644 setup.py create mode 100644 stock.py create mode 100644 tests/__init__.py create mode 100644 tests/scenario_account_stock_continental.rst create mode 100644 tests/test_account_stock_continental.py diff --git a/COPYRIGHT b/COPYRIGHT new file mode 100644 index 0000000000..de261a68a1 --- /dev/null +++ b/COPYRIGHT @@ -0,0 +1,15 @@ +Copyright (C) 2010-2011 Cédric Krier. +Copyright (C) 2010-2011 B2CK SPRL. + +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 . diff --git a/INSTALL b/INSTALL new file mode 100644 index 0000000000..3a9d106636 --- /dev/null +++ b/INSTALL @@ -0,0 +1,32 @@ +Installing trytond_account_stock_continental +============================================ + +Prerequisites +------------- + + * Python 2.5 or later (http://www.python.org/) + * trytond (http://www.tryton.org/) + * trytond_account (http://www.tryton.org/) + * trytond_account_product (http://www.tryton.org/) + * trytond_stock (http://www.tryton.org/) + +Installation +------------ + +Once you've downloaded and unpacked the trytond_account_stock source release, enter the +directory where the archive was unpacked, and run: + + python setup.py install + +Note that you may need administrator/root privileges for this step, as +this command will by default attempt to install module to the Python +site-packages directory on your system. + +For advanced options, please refer to the easy_install and/or the distutils +documentation: + + http://peak.telecommunity.com/DevCenter/EasyInstall + http://docs.python.org/inst/inst.html + +To use without installation, extract the archive into ``trytond/modules`` with +the directory name account_stock. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000..3912109b5c --- /dev/null +++ b/LICENSE @@ -0,0 +1,340 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General +Public License instead of this License. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000000..a9a697696e --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,11 @@ +include INSTALL +include README +include TODO +include COPYRIGHT +include CHANGELOG +include LICENSE +include *.xml +include *.odt +include *.csv +include doc/* +include tests/*.rst diff --git a/README b/README new file mode 100644 index 0000000000..d85b505f09 --- /dev/null +++ b/README @@ -0,0 +1,36 @@ +trytond_account_stock_continental +================================= + +The account_stock_continental module of the Tryton application platform. +See __tryton__.py + +Installing +---------- + +See INSTALL + +Support +------- + +If you encounter any problems with Tryton, please don't hesitate to ask +questions on the Tryton bug tracker, mailing list, wiki or IRC channel: + + http://bugs.tryton.org/ + http://groups.tryton.org/ + http://wiki.tryton.org/ + irc://irc.freenode.net/tryton + +License +------- + +See LICENSE + +Copyright +--------- + +See COPYRIGHT + + +For more information please visit the Tryton web site: + + http://www.tryton.org/ diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000000..3c5c94db6c --- /dev/null +++ b/__init__.py @@ -0,0 +1,5 @@ +#This file is part of Tryton. The COPYRIGHT file at the top level of +#this repository contains the full copyright notices and license terms. + +from stock import * +from product import * diff --git a/__tryton__.py b/__tryton__.py new file mode 100644 index 0000000000..e7df345175 --- /dev/null +++ b/__tryton__.py @@ -0,0 +1,24 @@ +#This file is part of Tryton. The COPYRIGHT file at the top level of +#this repository contains the full copyright notices and license terms. +{ + 'name': 'Account Stock Continental', + 'version': '1.9.0', + 'author': 'B2CK', + 'email': 'info@b2ck.com', + 'website': 'http://www.tryton.org/', + 'description': '''Add continental stock accounting for real-time stock valuation. +''', + 'depends': [ + 'ir', + 'res', + 'account', + 'account_product', + 'stock', + ], + 'xml': [ + 'product.xml', + 'account.xml', + ], + 'translation': [ + ], +} diff --git a/account.xml b/account.xml new file mode 100644 index 0000000000..d911845b8c --- /dev/null +++ b/account.xml @@ -0,0 +1,51 @@ + + + + + + + Inventories + + + + + + Stock + + + + + other + + + + Stock Supplier + + + + + other + + + + Stock Customer + + + + + other + + + + Stock Lost and Found + + + + + other + + + + diff --git a/doc/index.rst b/doc/index.rst new file mode 100644 index 0000000000..dbe380f1b3 --- /dev/null +++ b/doc/index.rst @@ -0,0 +1,37 @@ +Account Stock Continental Module +################################ + +The account_stock_continental module adds continental accounting model for +stock valuation. + +Five new fields are added to Product and Category: + +- Account Stock: The account which is used to record stock value. +- Account Stock Supplier: The counter part account for supplier stock move. +- Account Stock Customer: The counter part account for customer stock move. +- Account Stock Lost and Found: The counter part account for lost and found + stock move. +- Account Journal Stock Supplier: The journal used for supplier stock move. +- Account Journal Stock Customer: The journal used for customer stock move. +- Account Journal Stock Lost and Found: The journal used for lost and found + stock move. + +As usual, if the "Use Category's accounts" is checked it is the category one +that is used otherwise it is the product one. + +An Account Move is created for each Stock Move for which one Stock Location has +the type "Storage" and an the other has the type "Supplier", "Customer" or +"Lost and Found". + +If the Stock Move has a "Supplier" Location as origin, then the Account +Stock of the Product is debited and the Account Stock Supplier of the Product is +credited. The amount is the Unit Price of the move. +The account move is inverted if it is the destination. + +If the Stock Move has a "Customer" Location as destination, then the Account +Stock of the Product is credited and the Account Stock Customer of the Product +is debited. The amount is the current Cost Price of the Product. +The account move is inverted if it is the origin. + +When the Location has the type "Lost and Found", then the Account Stock Lost +and Found is used instead of the Supplier/Customer. diff --git a/product.py b/product.py new file mode 100644 index 0000000000..0e341a359e --- /dev/null +++ b/product.py @@ -0,0 +1,141 @@ +#This file is part of Tryton. The COPYRIGHT file at the top level of +#this repository contains the full copyright notices and license terms. +from trytond.model import ModelView, ModelSQL, fields +from trytond.pyson import Not, Eval, Bool, Or, In + + +class Category(ModelSQL, ModelView): + _name = 'product.category' + + account_stock = fields.Property(fields.Many2One('account.account', + 'Account Stock', domain=[ + ('kind', '!=', 'view'), + ('company', '=', Eval('company')), + ], states={ + 'invisible': Not(Bool(Eval('company'))), + })) + account_stock_supplier = fields.Property(fields.Many2One('account.account', + 'Account Stock Supplier', domain=[ + ('kind', '!=', 'view'), + ('company', '=', Eval('company')), + ], states={ + 'invisible': Not(Bool(Eval('company'))), + })) + account_stock_customer = fields.Property(fields.Many2One('account.account', + 'Account Stock Customer', domain=[ + ('kind', '!=', 'view'), + ('company', '=', Eval('company')), + ], states={ + 'invisible': Not(Bool(Eval('company'))), + })) + account_stock_lost_found = fields.Property(fields.Many2One( + 'account.account', 'Account Stock Lost and Found', domain=[ + ('kind', '!=', 'view'), + ('company', '=', Eval('company')), + ], states={ + 'invisible': Not(Bool(Eval('company'))), + })) + account_journal_stock_supplier = fields.Property(fields.Many2One( + 'account.journal', 'Journal Stock Supplier', + states={ + 'invisible': Not(Bool(Eval('company'))), + })) + account_journal_stock_customer = fields.Property(fields.Many2One( + 'account.journal', 'Journal Stock Customer', + states={ + 'invisible': Not(Bool(Eval('company'))), + })) + account_journal_stock_lost_found = fields.Property(fields.Many2One( + 'account.journal', 'Journal Stock Lost and Found', + states={ + 'invisible': Not(Bool(Eval('company'))), + })) + +Category() + + +class Template(ModelSQL, ModelView): + _name = 'product.template' + + account_stock = fields.Property(fields.Many2One('account.account', + 'Account Stock', domain=[ + ('kind', '!=', 'view'), + ('company', '=', Eval('company')), + ], states={ + 'invisible': Or(Not(Bool(Eval('company'))), + Bool(Eval('account_category'))), + 'required': In(Eval('type'), ['stockable', 'consumable']), + }, help='This account will be used instead of the one defined ' + 'on the category.', depends=['account_category'])) + account_stock_supplier = fields.Property(fields.Many2One('account.account', + 'Account Stock Supplier', domain=[ + ('kind', '!=', 'view'), + ('company', '=', Eval('company')), + ], states={ + 'invisible': Or(Not(Bool(Eval('company'))), + Bool(Eval('account_category'))), + 'required': In(Eval('type'), ['stockable', 'consumable']), + }, help='This account will be used instead of the one defined ' + 'on the category.', depends=['account_category'])) + account_stock_customer = fields.Property(fields.Many2One('account.account', + 'Account Stock Customer', domain=[ + ('kind', '!=', 'view'), + ('company', '=', Eval('company')), + ], states={ + 'invisible': Or(Not(Bool(Eval('company'))), + Bool(Eval('account_category'))), + 'required': In(Eval('type'), ['stockable', 'consumable']), + }, help='This account will be used instead of the one defined ' + 'on the category.', depends=['account_category'])) + account_stock_lost_found = fields.Property(fields.Many2One( + 'account.account', 'Account Stock Lost and Found', domain=[ + ('kind', '!=', 'view'), + ('company', '=', Eval('company')), + ], states={ + 'invisible': Or(Not(Bool(Eval('company'))), + Bool(Eval('account_category'))), + 'required': In(Eval('type'), ['stockable', 'consumable']), + }, help='This account will be used instead of the one defined ' + 'on the category.', depends=['account_category'])) + account_journal_stock_supplier = fields.Property(fields.Many2One( + 'account.journal', 'Account Journal Stock Supplier', + states={ + 'invisible': Or(Not(Bool(Eval('company'))), + Bool(Eval('account_category'))), + 'required': In(Eval('type'), ['stockable', 'consumable']), + }, help='This journal will be used instead of the one defined ' + 'on the category.', depends=['account_category'])) + account_journal_stock_customer = fields.Property(fields.Many2One( + 'account.journal', 'Account Journal Stock Customer', + states={ + 'invisible': Or(Not(Bool(Eval('company'))), + Bool(Eval('account_category'))), + 'required': In(Eval('type'), ['stockable', 'consumable']), + }, help='This journal will be used instead of the one defined ' + 'on the category.', depends=['account_category'])) + account_journal_stock_lost_found = fields.Property(fields.Many2One( + 'account.journal', 'Account Journal Stock Lost and Found', + states={ + 'invisible': Or(Not(Bool(Eval('company'))), + Bool(Eval('account_category'))), + 'required': In(Eval('type'), ['stockable', 'consumable']), + }, help='This journal will be used instead of the one defined ' + 'on the category.', depends=['account_category'])) + account_stock_used = fields.Function(fields.Many2One('account.account', + 'Account Stock Used'), 'get_account') + account_stock_supplier_used = fields.Function(fields.Many2One( + 'account.account', 'Account Stock Supplier Used'), 'get_account') + account_stock_customer_used = fields.Function(fields.Many2One( + 'account.account', 'Account Stock Customer Used'), 'get_account') + account_stock_lost_found_used = fields.Function(fields.Many2One( + 'account.account', 'Account Stock Lost and Found'), 'get_account') + account_journal_stock_supplier_used = fields.Function(fields.Many2One( + 'account.journal', 'Journal Stock Supplier'), 'get_account') + account_journal_stock_customer_used = fields.Function(fields.Many2One( + 'account.journal', 'Journal Stock Customer'), 'get_account') + account_journal_stock_lost_found_used = fields.Function(fields.Many2One( + 'account.journal', 'Journal Stock Lost and Found'), + 'get_account') + +Template() + diff --git a/product.xml b/product.xml new file mode 100644 index 0000000000..3faf3658e2 --- /dev/null +++ b/product.xml @@ -0,0 +1,70 @@ + + + + + + + product.category + + + + + + + ]]> + + + + + product.template + + + + + + + ]]> + + + + + diff --git a/setup.py b/setup.py new file mode 100644 index 0000000000..67245fd7a0 --- /dev/null +++ b/setup.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python +#This file is part of Tryton. The COPYRIGHT file at the top level of +#this repository contains the full copyright notices and license terms. + +from setuptools import setup +import re + +info = eval(open('__tryton__.py').read()) +major_version, minor_version, _ = info.get('version', '0.0.1').split('.', 2) +major_version = int(major_version) +minor_version = int(minor_version) + +requires = [] +for dep in info.get('depends', []): + if not re.match(r'(ir|res|workflow|webdav)(\W|$)', dep): + requires.append('trytond_%s >= %s.%s, < %s.%s' % + (dep, major_version, minor_version, major_version, + minor_version + 1)) +requires.append('trytond >= %s.%s, < %s.%s' % + (major_version, minor_version, major_version, minor_version + 1)) + +setup(name='trytond_account_stock_continental', + version=info.get('version', '0.0.1'), + description=info.get('description', ''), + author=info.get('author', ''), + author_email=info.get('email', ''), + url=info.get('website', ''), + download_url="http://downloads.tryton.org/" + \ + info.get('version', '0.0.1').rsplit('.', 1)[0] + '/', + package_dir={'trytond.modules.account_stock_continental': '.'}, + packages=[ + 'trytond.modules.account_stock_continental', + 'trytond.modules.account_stock_continental.tests', + ], + package_data={ + 'trytond.modules.account_stock_continental': info.get('xml', []) \ + + info.get('translation', []), + }, + classifiers=[ + 'Development Status :: 5 - Production/Stable', + 'Environment :: Plugins', + 'Intended Audience :: Developers', + 'Intended Audience :: Financial and Insurance Industry', + 'Intended Audience :: Legal Industry', + 'License :: OSI Approved :: GNU General Public License (GPL)', + 'Natural Language :: English', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Topic :: Office/Business', + 'Topic :: Office/Business :: Financial :: Accounting', + ], + license='GPL-3', + install_requires=requires, + zip_safe=False, + entry_points=""" + [trytond.modules] + account_stock_continental = trytond.modules.account_stock_continental + """, + test_suite='tests', + test_loader='trytond.test_loader:Loader', + tests_require='proteus', +) diff --git a/stock.py b/stock.py new file mode 100644 index 0000000000..7b72528257 --- /dev/null +++ b/stock.py @@ -0,0 +1,149 @@ +#This file is part of Tryton. The COPYRIGHT file at the top level of +#this repository contains the full copyright notices and license terms. +from trytond.model import ModelView, ModelSQL, fields +from decimal import Decimal + + +class Move(ModelSQL, ModelView): + _name = 'stock.move' + + account_move = fields.Many2One('account.move', 'Account Move', + readonly=True) + + def _get_account_stock_move_lines(self, move, type_): + ''' + Return move line values for stock move + ''' + uom_obj = self.pool.get('product.uom') + currency_obj = self.pool.get('currency.currency') + assert type_.startswith('in_') or type_.startswith('out_'), 'wrong type' + + move_line = { + 'name': move.rec_name, + } + if type_.endswith('supplier'): + unit_price = move.unit_price + else: + unit_price = uom_obj.compute_price(move.product.default_uom, + move.cost_price, move.uom) + amount = currency_obj.round(move.company.currency, + Decimal(str(move.quantity)) * unit_price) + + if type_.startswith('in_'): + move_line['debit'] = Decimal('0.0') + move_line['credit'] = amount + account_type = type_[3:] + else: + move_line['debit'] = amount + move_line['credit'] = Decimal('0.0') + account_type = type_[4:] + + move_line['account'] = getattr(move.product, + 'account_stock_%s_used' % account_type).id + + return [move_line] + + def _get_account_stock_move_line(self, move, amount): + ''' + Return counterpart move line value for stock move + ''' + move_line = { + 'name': move.rec_name, + 'account': move.product.account_stock_used.id, + } + if amount >= Decimal('0.0'): + move_line['debit'] = Decimal('0.0') + move_line['credit'] = amount + else: + move_line['debit'] = - amount + move_line['credit'] = Decimal('0.0') + return move_line + + def _get_account_stock_move(self, move, move_lines, type_): + ''' + Return account move value for stock move + ''' + date_obj = self.pool.get('ir.date') + period_obj = self.pool.get('account.period') + assert type_.startswith('in_') or type_.startswith('out_'), 'wrong type' + + date = move.effective_date or date_obj.today() + period_id = period_obj.find(move.company.id, date=date) + + if type_.startswith('in_'): + journal_type = type_[3:] + else: + journal_type = type_[4:] + + journal_id = getattr(move.product, + 'account_journal_stock_%s_used' % journal_type).id + + return { + 'journal': journal_id, + 'period': period_id, + 'date': date, + 'lines': [('create', line) for line in move_lines], + } + + def _get_account_stock_move_type(self, move): + ''' + Get account move type + ''' + type_ = (move.from_location.type, move.to_location.type) + if type_ == ('supplier', 'storage'): + return 'in_supplier' + elif type_ == ('storage', 'supplier'): + return 'out_supplier' + elif type_ == ('storage', 'customer'): + return 'out_customer' + elif type_ == ('customer', 'storage'): + return 'in_customer' + elif type_ == ('storage', 'lost_found'): + return 'out_lost_found' + elif type_ == ('lost_found', 'storage'): + return 'in_lost_found' + + def _create_account_stock_move(self, move): + ''' + Create account move for stock move + ''' + account_move_obj = self.pool.get('account.move') + type_ = self._get_account_stock_move_type(move) + if not type_: + return + assert not move.account_move, 'account move field not empty' + account_move_lines = self._get_account_stock_move_lines(move, type_) + + amount = Decimal('0.0') + for line in account_move_lines: + amount += line['debit'] - line['credit'] + account_move_lines.append( + self._get_account_stock_move_line(move, amount)) + + account_move_id = account_move_obj.create( + self._get_account_stock_move(move, account_move_lines, type_)) + self.write(move.id, { + 'account_move': account_move_id, + }) + account_move_obj.post(account_move_id) + return account_move_id + + def create(self, vals): + new_id = super(Move, self).create(vals) + if vals.get('state') == 'done': + move = self.browse(new_id) + self._create_account_stock_move(move) + return new_id + + def write(self, ids, vals): + res = super(Move, self).write(ids, vals) + if vals.get('state') == 'done': + if isinstance(ids, (int, long)): + move = self.browse(ids) + self._create_account_stock_move(move) + else: + for move in self.browse(ids): + self._create_account_stock_move(move) + return res + +Move() diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000000..c32acaac26 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,4 @@ +#This file is part of Tryton. The COPYRIGHT file at the top level of +#this repository contains the full copyright notices and license terms. + +from test_account_stock_continental import suite diff --git a/tests/scenario_account_stock_continental.rst b/tests/scenario_account_stock_continental.rst new file mode 100644 index 0000000000..ec3c55fd00 --- /dev/null +++ b/tests/scenario_account_stock_continental.rst @@ -0,0 +1,314 @@ +================================== +Account Stock Continental Scenario +================================== + +============= +General Setup +============= + +Imports:: + + >>> import datetime + >>> from dateutil.relativedelta import relativedelta + >>> from decimal import Decimal + >>> from proteus import config, Model, Wizard + >>> today = datetime.date.today() + +Create database:: + + >>> config = config.set_trytond(':memory:', database_type='sqlite') + +Install account_stock_continental, sale and purchase:: + + >>> Module = Model.get('ir.module.module') + >>> modules = Module.find([ + ... ('name', 'in', ('account_stock_continental', + ... 'sale', 'purchase')), + ... ]) + >>> Module.button_install([x.id for x in modules], config.context) + >>> Wizard('ir.module.module.install_upgrade').execute('start') + +Create company:: + + >>> Currency = Model.get('currency.currency') + >>> CurrencyRate = Model.get('currency.currency.rate') + >>> Company = Model.get('company.company') + >>> company_config = Wizard('company.company.config') + >>> company_config.execute('company') + >>> company = company_config.form + >>> company.name = 'B2CK' + >>> currencies = Currency.find([('code', '=', 'EUR')]) + >>> if not currencies: + ... currency = Currency(name='Euro', symbol=u'€', code='EUR', + ... rounding=Decimal('0.01'), mon_grouping='[3, 3, 0]', + ... mon_decimal_point=',') + ... currency.save() + ... CurrencyRate(date=today + relativedelta(month=1, day=1), + ... rate=Decimal('1.0'), currency=currency).save() + ... else: + ... currency, = currencies + >>> company.currency = currency + >>> company_config.execute('add') + >>> company, = Company.find() + +Reload the context:: + + >>> User = Model.get('res.user') + >>> config._context = User.get_preferences(True, config.context) + +Create fiscal year:: + + >>> FiscalYear = Model.get('account.fiscalyear') + >>> Sequence = Model.get('ir.sequence') + >>> SequenceStrict = Model.get('ir.sequence.strict') + >>> fiscalyear = FiscalYear(name='%s' % today.year) + >>> fiscalyear.start_date = today + relativedelta(month=1, day=1) + >>> fiscalyear.end_date = today + relativedelta(month=12, day=31) + >>> fiscalyear.company = company + >>> post_move_sequence = Sequence(name='%s' % today.year, + ... code='account.move', + ... company=company) + >>> post_move_sequence.save() + >>> fiscalyear.post_move_sequence = post_move_sequence + >>> invoice_sequence = SequenceStrict(name='%s' % today.year, + ... code='account.invoice', + ... company=company) + >>> invoice_sequence.save() + >>> fiscalyear.out_invoice_sequence = invoice_sequence + >>> fiscalyear.in_invoice_sequence = invoice_sequence + >>> fiscalyear.out_credit_note_sequence = invoice_sequence + >>> fiscalyear.in_credit_note_sequence = invoice_sequence + >>> fiscalyear.save() + >>> FiscalYear.create_period([fiscalyear.id], config.context) + True + +Create chart of accounts:: + + >>> AccountTemplate = Model.get('account.account.template') + >>> Account = Model.get('account.account') + >>> AccountJournal = Model.get('account.journal') + >>> account_template, = AccountTemplate.find([('parent', '=', False)]) + >>> create_chart_account = Wizard('account.account.create_chart_account') + >>> create_chart_account.execute('account') + >>> create_chart_account.form.account_template = account_template + >>> create_chart_account.form.company = company + >>> create_chart_account.execute('create_account') + >>> receivable, = Account.find([ + ... ('kind', '=', 'receivable'), + ... ('company', '=', company.id), + ... ]) + >>> payable, = Account.find([ + ... ('kind', '=', 'payable'), + ... ('company', '=', company.id), + ... ]) + >>> revenue, = Account.find([ + ... ('kind', '=', 'revenue'), + ... ('company', '=', company.id), + ... ]) + >>> expense, = Account.find([ + ... ('kind', '=', 'expense'), + ... ('company', '=', company.id), + ... ]) + >>> stock, stock_supplier, stock_lost_found, stock_customer = Account.find([ + ... ('kind', '=', 'other'), + ... ('company', '=', company.id), + ... ('name', 'like', 'Stock%'), + ... ], order=[('name', 'ASC')]) + >>> create_chart_account.form.account_receivable = receivable + >>> create_chart_account.form.account_payable = payable + >>> create_chart_account.execute('create_properties') + >>> stock_journal, = AccountJournal.find([('code', '=', 'STO')]) + +Create parties:: + + >>> Party = Model.get('party.party') + >>> supplier = Party(name='Supplier') + >>> supplier.save() + >>> customer = Party(name='Customer') + >>> customer.save() + +Create category:: + + >>> ProductCategory = Model.get('product.category') + >>> category = ProductCategory(name='Category') + >>> category.save() + +Create product:: + + >>> ProductUom = Model.get('product.uom') + >>> unit, = ProductUom.find([('name', '=', 'Unit')]) + >>> Product = Model.get('product.product') + >>> product = Product() + >>> product.name = 'product' + >>> product.category = category + >>> product.default_uom = unit + >>> product.type = 'stockable' + >>> product.purchasable = True + >>> product.salable = True + >>> product.list_price = Decimal('10') + >>> product.cost_price = Decimal('5') + >>> product.account_expense = expense + >>> product.account_revenue = revenue + >>> product.account_stock = stock + >>> product.account_stock_supplier = stock_supplier + >>> product.account_stock_customer = stock_customer + >>> product.account_stock_lost_found = stock_lost_found + >>> product.account_journal_stock_supplier = stock_journal + >>> product.account_journal_stock_customer = stock_journal + >>> product.account_journal_stock_lost_found = stock_journal + >>> product.save() + +Create payment term:: + + >>> PaymentTerm = Model.get('account.invoice.payment_term') + >>> PaymentTermLine = Model.get('account.invoice.payment_term.line') + >>> payment_term = PaymentTerm(name='Direct') + >>> payment_term_line = PaymentTermLine(type='remainder') + >>> payment_term.lines.append(payment_term_line) + >>> payment_term.save() + +Purchase 5 products:: + + >>> Purchase = Model.get('purchase.purchase') + >>> PurchaseLine = Model.get('purchase.line') + >>> purchase = Purchase() + >>> purchase.party = supplier + >>> purchase.payment_term = payment_term + >>> purchase.invoice_method = 'shipment' + >>> purchase_line = PurchaseLine() + >>> purchase.lines.append(purchase_line) + >>> purchase_line.product = product + >>> purchase_line.quantity = 5.0 + >>> purchase.save() + >>> Purchase.workflow_trigger_validate(purchase.id, 'quotation', + ... config.context) + >>> Purchase.workflow_trigger_validate(purchase.id, 'confirm', + ... config.context) + >>> purchase.state + u'confirmed' + +Receive 4 products:: + + >>> ShipmentIn = Model.get('stock.shipment.in') + >>> Move = Model.get('stock.move') + >>> shipment = ShipmentIn(supplier=supplier) + >>> move = Move(purchase.moves[0].id) + >>> shipment.incoming_moves.append(move) + >>> move.quantity = 4.0 + >>> shipment.save() + >>> ShipmentIn.workflow_trigger_validate(shipment.id, 'received', + ... config.context) + >>> ShipmentIn.workflow_trigger_validate(shipment.id, 'done', + ... config.context) + >>> shipment.state + u'done' + >>> stock_supplier.reload() + >>> (stock_supplier.debit, stock_supplier.credit) + (Decimal('0.00'), Decimal('20.00')) + >>> stock.reload() + >>> (stock.debit, stock.credit) + (Decimal('20.00'), Decimal('0.00')) + +Open supplier invoice:: + + >>> Invoice = Model.get('account.invoice') + >>> purchase.reload() + >>> invoice, = purchase.invoices + >>> invoice_line, = invoice.lines + >>> invoice_line.unit_price = Decimal('6') + >>> invoice.save() + >>> Invoice.workflow_trigger_validate(invoice.id, 'open', config.context) + >>> invoice.state + u'open' + >>> payable.reload() + >>> (payable.debit, payable.credit) + (Decimal('0.00'), Decimal('24.00')) + >>> expense.reload() + >>> (expense.debit, expense.credit) + (Decimal('24.00'), Decimal('0.00')) + +Update cost price of product:: + + >>> product.cost_price = Decimal('6') + >>> product.save() + +Sale 2 products:: + + >>> Sale = Model.get('sale.sale') + >>> SaleLine = Model.get('sale.line') + >>> sale = Sale() + >>> sale.party = customer + >>> sale.payment_term = payment_term + >>> sale.invoice_method = 'shipment' + >>> sale_line = SaleLine() + >>> sale.lines.append(sale_line) + >>> sale_line.product = product + >>> sale_line.quantity = 2.0 + >>> sale.save() + >>> Sale.workflow_trigger_validate(sale.id, 'quotation', config.context) + >>> Sale.workflow_trigger_validate(sale.id, 'confirm', config.context) + >>> sale.state + u'confirmed' + +Send 2 products:: + + >>> ShipmentOut = Model.get('stock.shipment.out') + >>> shipment, = sale.shipments + >>> ShipmentOut.workflow_trigger_validate(shipment.id, 'assign', + ... config.context) + >>> shipment.state + u'assigned' + >>> shipment.reload() + >>> ShipmentOut.workflow_trigger_validate(shipment.id, 'packed', + ... config.context) + >>> shipment.state + u'packed' + >>> shipment.reload() + >>> ShipmentOut.workflow_trigger_validate(shipment.id, 'done', + ... config.context) + >>> shipment.state + u'done' + >>> stock_customer.reload() + >>> (stock_customer.debit, stock_customer.credit) + (Decimal('12.00'), Decimal('0.00')) + >>> stock.reload() + >>> (stock.debit, stock.credit) + (Decimal('20.00'), Decimal('12.00')) + +Open customer invoice:: + + >>> sale.reload() + >>> invoice, = sale.invoices + >>> Invoice.workflow_trigger_validate(invoice.id, 'open', config.context) + >>> invoice.state + u'open' + >>> receivable.reload() + >>> (receivable.debit, receivable.credit) + (Decimal('20.00'), Decimal('0.00')) + >>> revenue.reload() + >>> (revenue.debit, revenue.credit) + (Decimal('0.00'), Decimal('20.00')) + +Create an Inventory:: + + >>> Inventory = Model.get('stock.inventory') + >>> Location = Model.get('stock.location') + >>> storage, = Location.find([ + ... ('code', '=', 'STO'), + ... ]) + >>> inventory = Inventory() + >>> inventory.location = storage + >>> inventory.save() + >>> complete_inventory = Wizard('stock.inventory.complete', [inventory]) + >>> inventory_line, = inventory.lines + >>> inventory_line.quantity = 1.0 + >>> inventory.save() + >>> Inventory.workflow_trigger_validate(inventory.id, 'done', config.context) + >>> inventory.state + u'done' + >>> stock_lost_found.reload() + >>> (stock_lost_found.debit, stock_lost_found.credit) + (Decimal('6.00'), Decimal('0.00')) + >>> stock.reload() + >>> (stock.debit, stock.credit) + (Decimal('20.00'), Decimal('18.00')) diff --git a/tests/test_account_stock_continental.py b/tests/test_account_stock_continental.py new file mode 100644 index 0000000000..de02e22307 --- /dev/null +++ b/tests/test_account_stock_continental.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python +#This file is part of Tryton. The COPYRIGHT file at the top level of +#this repository contains the full copyright notices and license terms. + +import sys, os +DIR = os.path.abspath(os.path.normpath(os.path.join(__file__, + '..', '..', '..', '..', '..', 'trytond'))) +if os.path.isdir(DIR): + sys.path.insert(0, os.path.dirname(DIR)) + +import unittest +import doctest +import trytond.tests.test_tryton +from trytond.tests.test_tryton import test_view +from trytond.backend.sqlite.database import Database as SQLiteDatabase + + +class AccountStockContinentalTestCase(unittest.TestCase): + ''' + Test Account Stock Continental module. + ''' + + def setUp(self): + trytond.tests.test_tryton.install_module('account_stock_continental') + + def test0005views(self): + ''' + Test views. + ''' + test_view('account_stock_continental') + +def doctest_dropdb(test): + ''' + Remove sqlite memory database + ''' + database = SQLiteDatabase().connect() + cursor = database.cursor(autocommit=True) + try: + database.drop(cursor, ':memory:') + cursor.commit() + finally: + cursor.close() + +def suite(): + suite = trytond.tests.test_tryton.suite() + suite.addTests(unittest.TestLoader().loadTestsFromTestCase( + AccountStockContinentalTestCase)) + suite.addTests(doctest.DocFileSuite('scenario_account_stock_continental.rst', + setUp=doctest_dropdb, tearDown=doctest_dropdb, encoding='utf-8')) + return suite + +if __name__ == '__main__': + unittest.TextTestRunner(verbosity=2).run(suite())