commit d985a399ce60e88d90672cb8f9d8d794f39fe301 Author: Cédric Krier Date: Sun Jan 16 22:07:50 2011 +0100 Initial commit 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..67b326b92d --- /dev/null +++ b/INSTALL @@ -0,0 +1,33 @@ +Installing trytond_account_stock_anglo_saxon +============================================ + +Prerequisites +------------- + + * Python 2.5 or later (http://www.python.org/) + * trytond (http://www.tryton.org/) + * trytond_account (http://www.tryton.org/) + * trytond_account_invoice (http://www.tryton.org/) + * trytond_account_product (http://www.tryton.org/) + * trytond_stock_continental (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..dcb2afa6dc --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,10 @@ +include INSTALL +include README +include TODO +include COPYRIGHT +include CHANGELOG +include LICENSE +include *.xml +include *.odt +include *.csv +include doc/* diff --git a/README b/README new file mode 100644 index 0000000000..bb079e7831 --- /dev/null +++ b/README @@ -0,0 +1,36 @@ +trytond_account_stock_anglo_saxon +================================= + +The account_stock_anglo_saxon 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..25cb01e226 --- /dev/null +++ b/__init__.py @@ -0,0 +1,6 @@ +#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 product import * +from stock import * +from invoice import * diff --git a/__tryton__.py b/__tryton__.py new file mode 100644 index 0000000000..d95e7acbba --- /dev/null +++ b/__tryton__.py @@ -0,0 +1,27 @@ +#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 Anglo-Saxon', + 'version': '0.0.1', + 'author': 'B2CK', + 'email': 'info@b2ck.com', + 'website': 'http://www.tryton.org/', + 'description': '''Add anglo-saxon stock accounting for real-time stock valuation. +''', + 'depends': [ + 'ir', + 'res', + 'account', + 'account_invoice', + 'account_product', + 'account_stock_continental', + 'purchase', + 'sale', + ], + 'xml': [ + 'product.xml', + 'account.xml', + ], + 'translation': [ + ], +} diff --git a/account.xml b/account.xml new file mode 100644 index 0000000000..9bffa7a218 --- /dev/null +++ b/account.xml @@ -0,0 +1,17 @@ + + + + + + + COGS + + + + + other + + + + diff --git a/doc/index.rst b/doc/index.rst new file mode 100644 index 0000000000..0423deb5f1 --- /dev/null +++ b/doc/index.rst @@ -0,0 +1,26 @@ +Account Stock Anglo-Saxon Module +################################ + +The account_stock_anglo_saxon module adds anglo-saxon accounting model for +stock valuation. + +A new field is added to Product and Category: + +- Account Cost of Goods Sold: The account which is used to record cost of goods + sold. + +As usual, if the account is not defined on Product, it is the category one that +is used. + +The Account Moves of Invoices are modified. + +On Supplier Invoice, the cost of the Product at reception is debited on the +Account Stock Supplier and only the difference is debited to the Account +Expense. +If the reception is not yet done then the cost is the amount on the invoice. +The opposite is done on Supplier Credit Note. + +On Invoice, the cost of the Product at delivery is credited from the Account +Stock Customer and is debited to the Account Cost of Goods Sold. +If the delivery is not yet done then the current cost is used. +The opposite is done on Credit Note. diff --git a/invoice.py b/invoice.py new file mode 100644 index 0000000000..bf9555b308 --- /dev/null +++ b/invoice.py @@ -0,0 +1,88 @@ +#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 decimal import Decimal +from trytond.model import ModelView, ModelSQL, fields + + +class InvoiceLine(ModelSQL, ModelView): + _name = 'account.invoice.line' + + def _get_anglo_saxon_move_lines(self, line, amount, type_): + ''' + Return account move for anglo-saxon stock accounting + ''' + assert type_.startswith('in_') or type_.startswith('out_'), 'wrong type' + + result = [] + move_line = {} + move_line['name'] = line.description + move_line['amount_second_currency'] = Decimal('0.0') + move_line['second_currency'] = False + + if type_.startswith('in_'): + move_line['debit'] = amount + move_line['credit'] = Decimal('0.0') + account_type = type_[3:] + else: + move_line['debit'] = Decimal('0.0') + move_line['credit'] = amount + account_type = type_[4:] + move_line['account'] = getattr(line.product, + 'account_stock_%s_used' % account_type).id + + result.append(move_line) + move_line = move_line.copy() + move_line['debit'], move_line['credit'] = \ + move_line['credit'], move_line['debit'] + if type_.endswith('supplier'): + move_line['account'] = line.account.id + else: + move_line['account'] = line.product.account_cogs_used.id + result.append(move_line) + return result + + def get_move_line(self, line): + purchase_line_invoice_line_obj = self.pool.get( + 'purchase.line-account.invoice.line') + move_obj = self.pool.get('stock.move') + currency_obj = self.pool.get('currency.currency') + + result = super(InvoiceLine, self).get_move_line(line) + + if line.type != 'line': + return result + if not line.product: + return result + if line.product.type == 'service': + return result + + moves = [] + # other types will get current cost price + if line.invoice.type == 'in_invoice': + moves = [move for purchase_line in line.purchase_lines + for move in purchase_line.moves + if move.state == 'done'] + elif line.invoice.type == 'out_invoice': + moves = [move for sale_line in line.sale_lines + for move in sale_line.moves + if move.state == 'done'] + if line.invoice.type == 'in_invoice': + type_ = 'in_supplier' + elif line.invoice.type == 'out_invoice': + type_ = 'out_customer' + elif line.invoice.type == 'in_credit_note': + type_ = 'out_supplier' + elif line.invoice.type == 'out_credit_note': + type_ = 'in_customer' + + moves.sort(lambda x, y: cmp(x.effective_date, y.effective_date)) + cost = move_obj.update_anglo_saxon_quantity_product_cost( + line.product, moves, line.quantity, line.unit, type_) + cost = currency_obj.round(line.invoice.currency, cost) + + anglo_saxon_move_lines = self._get_anglo_saxon_move_lines(line, cost, + type_) + result.extend(anglo_saxon_move_lines) + return result + +InvoiceLine() diff --git a/product.py b/product.py new file mode 100644 index 0000000000..9647f230e0 --- /dev/null +++ b/product.py @@ -0,0 +1,39 @@ +#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_cogs = fields.Property(fields.Many2One('account.account', + 'Account Cost of Goods Sold', domain=[ + ('kind', '!=', 'view'), + ('company', '=', Eval('company')), + ], + states={ + 'invisible': Not(Bool(Eval('company'))), + })) + +Category() + + +class Template(ModelSQL, ModelView): + _name = 'product.template' + + account_cogs = fields.Property(fields.Many2One('account.account', + 'Account Cost of Good Sold', 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_cogs_used = fields.Function(fields.Many2One('account.account', + 'Account Cost of Goods Sold Used'), 'get_account') + +Template() diff --git a/product.xml b/product.xml new file mode 100644 index 0000000000..f859e81e09 --- /dev/null +++ b/product.xml @@ -0,0 +1,42 @@ + + + + + + + product.category + + + + + + + ]]> + + + + + product.template + + + + + + + ]]> + + + + + diff --git a/setup.py b/setup.py new file mode 100644 index 0000000000..e526c4b381 --- /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_anglo_saxon', + 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_anglo_saxon': '.'}, + packages=[ + 'trytond.modules.account_stock_anglo_saxon', + 'trytond.modules.account.tests', + ], + package_data={ + 'trytond.modules.account_stock_anglo_saxon': 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_anglo_saxon = trytond.modules.account_stock_anglo_saxon + """, + 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..509357085b --- /dev/null +++ b/stock.py @@ -0,0 +1,83 @@ +#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 decimal import Decimal +from trytond.model import ModelView, ModelSQL, fields +from trytond.pyson import Eval + + +class Move(ModelSQL, ModelView): + _name = 'stock.move' + anglo_saxon_quantity = fields.Float('Anglo-Saxon Quantity', + digits=(16, Eval('unit_digits', 2))) + + def __init__(self): + super(Move, self).__init__() + self._sql_constraints += [ + ('check_anglo_saxon_quantity', + 'CHECK(quantity >= anglo_saxon_quantity)', + 'Anglo-Saxon quantity can not be greater than quantity!'), + ] + + def default_anglo_saxon_quantity(self): + return 0.0 + + def _get_anglo_saxon_move(self, moves, quantity): + ''' + Generator of (move, qty) where move is the move to be consumed and qty + is the quantity (in the product default uom) to be consumed on this + move. + ''' + uom_obj = self.pool.get('product.uom') + + consumed_qty = 0.0 + for move in moves: + qty = uom_obj.compute_qty(move.uom, + move.quantity - move.anglo_saxon_quantity, + move.product.default_uom, round=False) + if qty <= 0.0: + continue + if qty > quantity - consumed_qty: + qty = quantity - consumed_qty + if consumed_qty >= quantity: + break + yield (move, qty) + + def update_anglo_saxon_quantity_product_cost(self, product, moves, + quantity, uom, type_): + ''' + Return the cost for quantity based on lines. + Update anglo_saxon_quantity on the concerned moves. + ''' + uom_obj = self.pool.get('product.uom') + + for move in moves: + assert move.product == product, 'wrong product' + assert type_.startswith('in_') or type_.startswith('out_'), 'wrong type' + + total_qty = uom_obj.compute_qty(uom, quantity, product.default_uom, + round=False) + + cost = Decimal('0.0') + consumed_qty = 0.0 + for move, move_qty in self._get_anglo_saxon_move(moves, total_qty): + consumed_qty += move_qty + + if type_.startswith('in_'): + move_cost_price = uom_obj.compute_price(move.uom, + move.unit_price, move.product.default_uom) + else: + move_cost_price = move.cost_price + cost += move_cost_price * Decimal(str(move_qty)) + + self.write(move.id, { + 'anglo_saxon_quantity': ((move.anglo_saxon_quantity or 0.0) + + move_qty), + }) + + if consumed_qty < total_qty: + qty = total_qty - consumed_qty + consumed_qty += qty + cost += product.cost_price * Decimal(str(qty)) + return cost + +Move() diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000000..b94c4a2373 --- /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_anglo_saxon import suite diff --git a/tests/scenario_account_stock_anglo_saxon.rst b/tests/scenario_account_stock_anglo_saxon.rst new file mode 100644 index 0000000000..c2634c72df --- /dev/null +++ b/tests/scenario_account_stock_anglo_saxon.rst @@ -0,0 +1,305 @@ +================================== +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_anglo_saxon', + ... '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')]) + >>> cogs, = Account.find([ + ... ('kind', '=', 'other'), + ... ('company', '=', company.id), + ... ('name', '=', 'COGS'), + ... ]) + >>> 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_cogs = cogs + >>> 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('20.00')) + >>> stock_supplier.reload() + >>> (stock_supplier.debit, stock_supplier.credit) + (Decimal('20.00'), Decimal('20.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')) + >>> stock_customer.reload() + >>> (stock_customer.debit, stock_customer.credit) + (Decimal('12.00'), Decimal('12.00')) + >>> cogs.reload() + >>> (cogs.debit, cogs.credit) + (Decimal('12.00'), Decimal('0.00')) diff --git a/tests/test_account_stock_anglo_saxon.py b/tests/test_account_stock_anglo_saxon.py new file mode 100644 index 0000000000..9ac1d8151e --- /dev/null +++ b/tests/test_account_stock_anglo_saxon.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 AccountStockAngloSaxonTestCase(unittest.TestCase): + ''' + Test Account Stock Anglo Saxon module. + ''' + + def setUp(self): + trytond.tests.test_tryton.install_module('account_stock_anglo_saxon') + + def test0005views(self): + ''' + Test views. + ''' + test_view('account_stock_anglo_saxon') + +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( + AccountStockAngloSaxonTestCase)) + suite.addTests(doctest.DocFileSuite('scenario_account_stock_anglo_saxon.rst', + setUp=doctest_dropdb, tearDown=doctest_dropdb, encoding='utf-8')) + return suite + +if __name__ == '__main__': + unittest.TextTestRunner(verbosity=2).run(suite())