Merge remote-tracking branch 'upstream/7.0' into 8.0-port-base-import

Conflicts:
	.gitignore
	.travis.yml
	README.md
	__unported__/account_statement_base_completion/__openerp__.py
	__unported__/account_statement_commission/__openerp__.py
	__unported__/account_statement_transactionid_import/__openerp__.py
	__unported__/statement_voucher_killer/__openerp__.py
This commit is contained in:
Laurent Mignon (aka lmi)
2014-08-06 10:28:57 +02:00
83 changed files with 2421 additions and 1640 deletions

661
LICENSE Normal file
View File

@@ -0,0 +1,661 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero 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 Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<http://www.gnu.org/licenses/>.

View File

@@ -2,7 +2,8 @@
##############################################################################
#
# Author: Guewen Baconnier
# Copyright 2012 Camptocamp SA
# Contributor: Leonardo Pistone
# Copyright 2012-2014 Camptocamp SA
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
@@ -22,3 +23,4 @@
import easy_reconcile
import base_advanced_reconciliation
import advanced_reconciliation
import res_config # noqa

View File

@@ -2,7 +2,8 @@
##############################################################################
#
# Author: Guewen Baconnier
# Copyright 2012 Camptocamp SA
# Contributor: Leonardo Pistone
# Copyright 2012-2014 Camptocamp SA
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
@@ -63,9 +64,9 @@ The base class to find the reconciliations is built to be as efficient as
possible.
So basically, if you have an invoice with 3 payments (one per month), the first
month, it will partial reconcile the debit move line with the first payment, the second
month, it will partial reconcile the debit move line with 2 first payments,
the third month, it will make the full reconciliation.
month, it will partial reconcile the debit move line with the first payment, the
second month, it will partial reconcile the debit move line with 2 first
payments, the third month, it will make the full reconciliation.
This module is perfectly adapted for E-Commerce business where a big volume of
move lines and so, reconciliations, are involved and payments often come from
@@ -73,7 +74,8 @@ many offices.
""",
'website': 'http://www.camptocamp.com',
'data': ['easy_reconcile_view.xml'],
'data': ['easy_reconcile_view.xml',
'res_config_view.xml'],
'test': [],
'images': [],
'installable': False,

View File

@@ -2,7 +2,8 @@
##############################################################################
#
# Author: Guewen Baconnier
# Copyright 2012 Camptocamp SA
# Contributor: Leonardo Pistone
# Copyright 2012-2014 Camptocamp SA
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
@@ -18,13 +19,18 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
import logging
from itertools import product
from openerp.osv import orm
from openerp import pooler
from openerp.tools.translate import _
_logger = logging.getLogger(__name__)
class easy_reconcile_advanced(orm.AbstractModel):
_name = 'easy.reconcile.advanced'
_inherit = 'easy.reconcile.base'
@@ -34,11 +40,8 @@ class easy_reconcile_advanced(orm.AbstractModel):
sql_from = self._from(rec)
where, params = self._where(rec)
where += " AND account_move_line.debit > 0 "
where2, params2 = self._get_filter(cr, uid, rec, context=context)
query = ' '.join((select, sql_from, where, where2))
cr.execute(query, params + params2)
return cr.dictfetchall()
@@ -48,11 +51,8 @@ class easy_reconcile_advanced(orm.AbstractModel):
sql_from = self._from(rec)
where, params = self._where(rec)
where += " AND account_move_line.credit > 0 "
where2, params2 = self._get_filter(cr, uid, rec, context=context)
query = ' '.join((select, sql_from, where, where2))
cr.execute(query, params + params2)
return cr.dictfetchall()
@@ -170,14 +170,15 @@ class easy_reconcile_advanced(orm.AbstractModel):
"""
mkey, mvalue = matcher
omkey, omvalue = opposite_matcher
assert mkey == omkey, ("A matcher %s is compared with a matcher %s, "
" the _matchers and _opposite_matchers are probably wrong" %
(mkey, omkey))
assert mkey == omkey, \
(_("A matcher %s is compared with a matcher %s, the _matchers and "
"_opposite_matchers are probably wrong") % (mkey, omkey))
if not isinstance(mvalue, (list, tuple)):
mvalue = mvalue,
if not isinstance(omvalue, (list, tuple)):
omvalue = omvalue,
return easy_reconcile_advanced._compare_matcher_values(mkey, mvalue, omvalue)
return easy_reconcile_advanced._compare_matcher_values(mkey, mvalue,
omvalue)
def _compare_opposite(self, cr, uid, rec, move_line, opposite_move_line,
matchers, context=None):
@@ -206,13 +207,13 @@ class easy_reconcile_advanced(orm.AbstractModel):
return True
def _search_opposites(self, cr, uid, rec, move_line, opposite_move_lines, context=None):
"""
Search the opposite move lines for a move line
def _search_opposites(self, cr, uid, rec, move_line, opposite_move_lines,
context=None):
"""Search the opposite move lines for a move line
:param dict move_line: the move line for which we search opposites
:param list opposite_move_lines: list of dict of move lines values, the move
lines we want to search for
:param list opposite_move_lines: list of dict of move lines values,
the move lines we want to search for
:return: list of matching lines
"""
matchers = self._matchers(cr, uid, rec, move_line, context=context)
@@ -221,10 +222,30 @@ class easy_reconcile_advanced(orm.AbstractModel):
cr, uid, rec, move_line, op, matchers, context=context)]
def _action_rec(self, cr, uid, rec, context=None):
credit_lines = self._query_credit(cr, uid, rec, context=context)
debit_lines = self._query_debit(cr, uid, rec, context=context)
return self._rec_auto_lines_advanced(
cr, uid, rec, credit_lines, debit_lines, context=context)
# we use a new cursor to be able to commit the reconciliation
# often. We have to create it here and not later to avoid problems
# where the new cursor sees the lines as reconciles but the old one
# does not.
if context is None:
context = {}
ctx = context.copy()
ctx['commit_every'] = (
rec.journal_id.company_id.reconciliation_commit_every
)
if ctx['commit_every']:
new_cr = pooler.get_db(cr.dbname).cursor()
else:
new_cr = cr
try:
credit_lines = self._query_credit(new_cr, uid, rec, context=ctx)
debit_lines = self._query_debit(new_cr, uid, rec, context=ctx)
result = self._rec_auto_lines_advanced(
new_cr, uid, rec, credit_lines, debit_lines, context=ctx)
finally:
if ctx['commit_every']:
new_cr.commit()
new_cr.close()
return result
def _skip_line(self, cr, uid, rec, move_line, context=None):
"""
@@ -234,33 +255,41 @@ class easy_reconcile_advanced(orm.AbstractModel):
"""
return False
def _rec_auto_lines_advanced(self, cr, uid, rec, credit_lines, debit_lines, context=None):
def _rec_auto_lines_advanced(self, cr, uid, rec, credit_lines, debit_lines,
context=None):
""" Advanced reconciliation main loop """
reconciled_ids = []
partial_reconciled_ids = []
reconcile_groups = []
for credit_line in credit_lines:
_logger.info("%d credit lines to reconcile", len(credit_lines))
for idx, credit_line in enumerate(credit_lines, start=1):
if idx % 50 == 0:
_logger.info("... %d/%d credit lines inspected ...", idx,
len(credit_lines))
if self._skip_line(cr, uid, rec, credit_line, context=context):
continue
opposite_lines = self._search_opposites(
cr, uid, rec, credit_line, debit_lines, context=context)
if not opposite_lines:
continue
opposite_ids = [l['id'] for l in opposite_lines]
line_ids = opposite_ids + [credit_line['id']]
for group in reconcile_groups:
if any([lid in group for lid in opposite_ids]):
_logger.debug("New lines %s matched with an existing "
"group %s", line_ids, group)
group.update(line_ids)
break
else:
_logger.debug("New group of lines matched %s", line_ids)
reconcile_groups.append(set(line_ids))
lines_by_id = dict([(l['id'], l) for l in credit_lines + debit_lines])
for reconcile_group_ids in reconcile_groups:
_logger.info("Found %d groups to reconcile", len(reconcile_groups))
for group_count, reconcile_group_ids in enumerate(reconcile_groups,
start=1):
_logger.debug("Reconciling group %d/%d with ids %s",
group_count, len(reconcile_groups),
reconcile_group_ids)
group_lines = [lines_by_id[lid] for lid in reconcile_group_ids]
reconciled, full = self._reconcile_lines(
cr, uid, rec, group_lines, allow_partial=True, context=context)
@@ -269,4 +298,10 @@ class easy_reconcile_advanced(orm.AbstractModel):
elif reconciled:
partial_reconciled_ids += reconcile_group_ids
if (context['commit_every'] and
group_count % context['commit_every'] == 0):
cr.commit()
_logger.info("Commit the reconciliations after %d groups",
group_count)
_logger.info("Reconciliation is over")
return reconciled_ids, partial_reconciled_ids

View File

@@ -0,0 +1,59 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# Author: Leonardo Pistone
# Copyright 2014 Camptocamp SA
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from openerp.osv import orm, fields
class AccountConfigSettings(orm.TransientModel):
_inherit = 'account.config.settings'
_columns = {
'reconciliation_commit_every': fields.related(
'company_id',
'reconciliation_commit_every',
type='integer',
string='How often to commit when performing automatic '
'reconciliation.',
help="""Leave zero to commit only at the end of the process."""),
}
def onchange_company_id(self, cr, uid, ids, company_id, context=None):
company_obj = self.pool['res.company']
result = super(AccountConfigSettings, self).onchange_company_id(
cr, uid, ids, company_id, context=None)
if company_id:
company = company_obj.browse(cr, uid, company_id, context=context)
result['value']['reconciliation_commit_every'] = (
company.reconciliation_commit_every
)
return result
class Company(orm.Model):
_inherit = "res.company"
_columns = {
'reconciliation_commit_every': fields.integer(
string='How often to commit when performing automatic '
'reconciliation.',
help="""Leave zero to commit only at the end of the process."""),
}

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<record id="view_account_config" model="ir.ui.view">
<field name="name">account settings</field>
<field name="model">account.config.settings</field>
<field name="inherit_id" ref="account.view_account_config_settings"/>
<field name="arch" type="xml">
<separator string="eInvoicing &amp; Payments" position="before">
<separator string="Reconciliation"/>
<group>
<label for="id" string="Options"/>
<div name="reconciliation_config">
<div>
<label for="reconciliation_commit_every"/>
<field name="reconciliation_commit_every" class="oe_inline"/>
</div>
</div>
</group>
</separator>
</field>
</record>
</data>
</openerp>

View File

@@ -37,4 +37,3 @@ Reconcile rules with transaction_ref
'installable': False,
'images': []
}
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@@ -19,11 +19,10 @@
#
##############################################################################
from itertools import product
from openerp.osv import orm
class easy_reconcile_advanced(orm.AbstractModel):
class EasyReconcileAdvanced(orm.AbstractModel):
_inherit = 'easy.reconcile.advanced'

View File

@@ -36,4 +36,3 @@ class account_easy_reconcile_method(orm.Model):
'Advanced. Partner and Transaction Ref. vs Ref.'),
]
return methods

View File

@@ -23,7 +23,8 @@ from openerp.osv import fields, orm
from operator import itemgetter, attrgetter
class easy_reconcile_base(orm.AbstractModel):
class EasyReconcileBase(orm.AbstractModel):
"""Abstract Model for reconciliation methods"""
_name = 'easy.reconcile.base'
@@ -87,7 +88,6 @@ class easy_reconcile_base(orm.AbstractModel):
# which returns a list, we have to
# accomodate with that
params = [rec.account_id.id]
if rec.partner_ids:
where += " AND account_move_line.partner_id IN %s"
params.append(tuple([l.id for l in rec.partner_ids]))
@@ -115,14 +115,13 @@ class easy_reconcile_base(orm.AbstractModel):
for key, value
in line.iteritems()
if key in keys), lines)
debit, credit = sums['debit'], sums['credit']
writeoff_amount = round(debit - credit, precision)
return bool(writeoff_limit >= abs(writeoff_amount)), debit, credit
def _get_rec_date(self, cr, uid, rec, lines,
based_on='end_period_last_credit', context=None):
period_obj = self.pool.get('account.period')
period_obj = self.pool['account.period']
def last_period(mlines):
period_ids = [ml['period_id'] for ml in mlines]
@@ -153,7 +152,8 @@ class easy_reconcile_base(orm.AbstractModel):
# when date is None
return None
def _reconcile_lines(self, cr, uid, rec, lines, allow_partial=False, context=None):
def _reconcile_lines(self, cr, uid, rec, lines, allow_partial=False,
context=None):
""" Try to reconcile given lines
:param list lines: list of dict of move lines, they must at least
@@ -168,26 +168,23 @@ class easy_reconcile_base(orm.AbstractModel):
"""
if context is None:
context = {}
ml_obj = self.pool.get('account.move.line')
writeoff = rec.write_off
line_ids = [l['id'] for l in lines]
below_writeoff, sum_debit, sum_credit = self._below_writeoff_limit(
cr, uid, rec, lines, writeoff, context=context)
date = self._get_rec_date(
cr, uid, rec, lines, rec.date_base_on, context=context)
rec_ctx = dict(context, date_p=date)
if below_writeoff:
if sum_credit < sum_debit:
writeoff_account_id = rec.account_profit_id.id
else:
writeoff_account_id = rec.account_lost_id.id
period_id = self.pool.get('account.period').find(
cr, uid, dt=date, context=context)[0]
if rec.analytic_account_id:
rec_ctx['analytic_id'] = rec.analytic_account_id.id
ml_obj.reconcile(
cr, uid,
line_ids,
@@ -204,5 +201,4 @@ class easy_reconcile_base(orm.AbstractModel):
type='manual',
context=rec_ctx)
return True, False
return False, False

View File

@@ -19,13 +19,11 @@
#
##############################################################################
from openerp.osv import fields, osv, orm
from openerp.tools.translate import _
from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT
from openerp.osv import fields, orm
from openerp.tools.translate import _
class easy_reconcile_options(orm.AbstractModel):
class EasyReconcileOptions(orm.AbstractModel):
"""Options of a reconciliation profile
Columns shared by the configuration of methods
@@ -37,12 +35,14 @@ class easy_reconcile_options(orm.AbstractModel):
_name = 'easy.reconcile.options'
def _get_rec_base_date(self, cr, uid, context=None):
return [('end_period_last_credit', 'End of period of most recent credit'),
return [
('end_period_last_credit', 'End of period of most recent credit'),
('newest', 'Most recent move line'),
('actual', 'Today'),
('end_period', 'End of period of most recent move line'),
('newest_credit', 'Date of most recent credit'),
('newest_debit', 'Date of most recent debit')]
('newest_debit', 'Date of most recent debit')
]
_columns = {
'write_off': fields.float('Write off allowed'),
@@ -57,6 +57,9 @@ class easy_reconcile_options(orm.AbstractModel):
required=True,
string='Date of reconciliation'),
'filter': fields.char('Filter', size=128),
'analytic_account_id': fields.many2one(
'account.analytic.account', 'Analytic Account',
help="Analytic account for the write-off"),
}
_defaults = {
@@ -65,20 +68,18 @@ class easy_reconcile_options(orm.AbstractModel):
}
class account_easy_reconcile_method(orm.Model):
class AccountEasyReconcileMethod(orm.Model):
_name = 'account.easy.reconcile.method'
_description = 'reconcile method for account_easy_reconcile'
_inherit = 'easy.reconcile.options'
_order = 'sequence'
def _get_all_rec_method(self, cr, uid, context=None):
return [
('easy.reconcile.simple.name', 'Simple. Amount and Name'),
('easy.reconcile.simple.partner', 'Simple. Amount and Partner'),
('easy.reconcile.simple.reference', 'Simple. Amount and Reference'),
('easy.reconcile.simple.reference',
'Simple. Amount and Reference'),
]
def _get_rec_method(self, cr, uid, context=None):
@@ -127,7 +128,7 @@ class account_easy_reconcile_method(orm.Model):
""")
class account_easy_reconcile(orm.Model):
class AccountEasyReconcile(orm.Model):
_name = 'account.easy.reconcile'
_description = 'account easy reconcile'
@@ -199,6 +200,8 @@ class account_easy_reconcile(orm.Model):
rec_method.account_lost_id.id),
'account_profit_id': (rec_method.account_profit_id and
rec_method.account_profit_id.id),
'analytic_account_id': (rec_method.analytic_account_id and
rec_method.analytic_account_id.id),
'journal_id': (rec_method.journal_id and
rec_method.journal_id.id),
'date_base_on': rec_method.date_base_on,
@@ -254,7 +257,7 @@ class account_easy_reconcile(orm.Model):
be called when there is no history on the reconciliation
task.
"""
raise osv.except_osv(
raise orm.except_orm(
_('Error'),
_('There is no history of reconciled '
'items on the task: %s.') % rec.name)
@@ -273,14 +276,10 @@ class account_easy_reconcile(orm.Model):
}
def open_unreconcile(self, cr, uid, ids, context=None):
""" Open the view of move line with the unreconciled move lines
"""
""" Open the view of move line with the unreconciled move lines"""
assert len(ids) == 1, \
"You can only open entries from one profile at a time"
obj_move_line = self.pool.get('account.move.line')
res = {}
for task in self.browse(cr, uid, ids, context=context):
line_ids = obj_move_line.search(
cr, uid,
@@ -290,17 +289,14 @@ class account_easy_reconcile(orm.Model):
context=context)
name = _('Unreconciled items')
return self._open_move_line_list(cr, uid, line_ids, name, context=context)
return self._open_move_line_list(cr, uid, line_ids, name,
context=context)
def open_partial_reconcile(self, cr, uid, ids, context=None):
""" Open the view of move line with the unreconciled move lines
"""
""" Open the view of move line with the unreconciled move lines"""
assert len(ids) == 1, \
"You can only open entries from one profile at a time"
obj_move_line = self.pool.get('account.move.line')
res = {}
for task in self.browse(cr, uid, ids, context=context):
line_ids = obj_move_line.search(
cr, uid,
@@ -309,7 +305,8 @@ class account_easy_reconcile(orm.Model):
('reconcile_partial_id', '!=', False)],
context=context)
name = _('Partial reconciled items')
return self._open_move_line_list(cr, uid, line_ids, name, context=context)
return self._open_move_line_list(cr, uid, line_ids, name,
context=context)
def last_history_reconcile(self, cr, uid, rec_id, context=None):
""" Get the last history record for this reconciliation profile

View File

@@ -130,6 +130,7 @@ The lines should have the same amount (with the write-off) and the same referenc
<field name="account_lost_id" attrs="{'required':[('write_off','>',0)]}"/>
<field name="account_profit_id" attrs="{'required':[('write_off','>',0)]}"/>
<field name="journal_id" attrs="{'required':[('write_off','>',0)]}"/>
<field name="analytic_account_id" groups="analytic.group_analytic_accounting"/>
<field name="date_base_on"/>
</form>
</field>
@@ -147,6 +148,7 @@ The lines should have the same amount (with the write-off) and the same referenc
<field name="account_lost_id" attrs="{'required':[('write_off','>',0)]}"/>
<field name="account_profit_id" attrs="{'required':[('write_off','>',0)]}"/>
<field name="journal_id" attrs="{'required':[('write_off','>',0)]}"/>
<field name="analytic_account_id" groups="analytic.group_analytic_accounting"/>
<field name="date_base_on"/>
</tree>
</field>

View File

@@ -23,7 +23,7 @@ from openerp.osv import orm, fields
from openerp.tools.translate import _
class easy_reconcile_history(orm.Model):
class EasyReconcileHistory(orm.Model):
""" Store an history of the runs per profile
Each history stores the list of reconciliations done"""
@@ -33,10 +33,8 @@ class easy_reconcile_history(orm.Model):
def _reconcile_line_ids(self, cr, uid, ids, name, args, context=None):
result = {}
for history in self.browse(cr, uid, ids, context=context):
result[history.id] = {}
move_line_ids = []
for reconcile in history.reconcile_ids:
move_line_ids += [line.id
@@ -50,7 +48,6 @@ class easy_reconcile_history(orm.Model):
for line
in reconcile.line_partial_ids]
result[history.id]['partial_line_ids'] = move_line_ids
return result
_columns = {
@@ -90,7 +87,8 @@ class easy_reconcile_history(orm.Model):
}
def _open_move_lines(self, cr, uid, history_id, rec_type='full', context=None):
def _open_move_lines(self, cr, uid, history_id, rec_type='full',
context=None):
""" For an history record, open the view of move line with
the reconciled or partially reconciled move lines
@@ -100,18 +98,14 @@ class easy_reconcile_history(orm.Model):
"""
assert rec_type in ('full', 'partial'), \
"rec_type must be 'full' or 'partial'"
history = self.browse(cr, uid, history_id, context=context)
if rec_type == 'full':
field = 'reconcile_line_ids'
name = _('Reconciliations')
else:
field = 'partial_line_ids'
name = _('Partial Reconciliations')
move_line_ids = [line.id for line in getattr(history, field)]
return {
'name': name,
'view_mode': 'tree,form',

View File

@@ -2,14 +2,8 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_easy_reconcile_options_acc_user,easy.reconcile.options,model_easy_reconcile_options,account.group_account_user,1,0,0,0
access_account_easy_reconcile_method_acc_user,account.easy.reconcile.method,model_account_easy_reconcile_method,account.group_account_user,1,0,0,0
access_account_easy_reconcile_acc_user,account.easy.reconcile,model_account_easy_reconcile,account.group_account_user,1,1,0,0
access_easy_reconcile_simple_name_acc_user,easy.reconcile.simple.name,model_easy_reconcile_simple_name,account.group_account_user,1,0,0,0
access_easy_reconcile_simple_partner_acc_user,easy.reconcile.simple.partner,model_easy_reconcile_simple_partner,account.group_account_user,1,0,0,0
access_easy_reconcile_simple_reference_acc_user,easy.reconcile.simple.reference,model_easy_reconcile_simple_reference,account.group_account_user,1,0,0,0
access_easy_reconcile_options_acc_mgr,easy.reconcile.options,model_easy_reconcile_options,account.group_account_user,1,0,0,0
access_account_easy_reconcile_method_acc_mgr,account.easy.reconcile.method,model_account_easy_reconcile_method,account.group_account_user,1,1,1,1
access_account_easy_reconcile_acc_mgr,account.easy.reconcile,model_account_easy_reconcile,account.group_account_user,1,1,1,1
access_easy_reconcile_simple_name_acc_mgr,easy.reconcile.simple.name,model_easy_reconcile_simple_name,account.group_account_user,1,0,0,0
access_easy_reconcile_simple_partner_acc_mgr,easy.reconcile.simple.partner,model_easy_reconcile_simple_partner,account.group_account_user,1,0,0,0
access_easy_reconcile_simple_reference_acc_mgr,easy.reconcile.simple.reference,model_easy_reconcile_simple_reference,account.group_account_user,1,0,0,0
access_easy_reconcile_history_acc_user,easy.reconcile.history,model_easy_reconcile_history,account.group_account_user,1,1,1,0
access_easy_reconcile_history_acc_mgr,easy.reconcile.history,model_easy_reconcile_history,account.group_account_manager,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_easy_reconcile_options_acc_user easy.reconcile.options model_easy_reconcile_options account.group_account_user 1 0 0 0
3 access_account_easy_reconcile_method_acc_user account.easy.reconcile.method model_account_easy_reconcile_method account.group_account_user 1 0 0 0
4 access_account_easy_reconcile_acc_user account.easy.reconcile model_account_easy_reconcile account.group_account_user 1 1 0 0
access_easy_reconcile_simple_name_acc_user easy.reconcile.simple.name model_easy_reconcile_simple_name account.group_account_user 1 0 0 0
access_easy_reconcile_simple_partner_acc_user easy.reconcile.simple.partner model_easy_reconcile_simple_partner account.group_account_user 1 0 0 0
access_easy_reconcile_simple_reference_acc_user easy.reconcile.simple.reference model_easy_reconcile_simple_reference account.group_account_user 1 0 0 0
5 access_easy_reconcile_options_acc_mgr easy.reconcile.options model_easy_reconcile_options account.group_account_user 1 0 0 0
6 access_account_easy_reconcile_method_acc_mgr account.easy.reconcile.method model_account_easy_reconcile_method account.group_account_user 1 1 1 1
7 access_account_easy_reconcile_acc_mgr account.easy.reconcile model_account_easy_reconcile account.group_account_user 1 1 1 1
access_easy_reconcile_simple_name_acc_mgr easy.reconcile.simple.name model_easy_reconcile_simple_name account.group_account_user 1 0 0 0
access_easy_reconcile_simple_partner_acc_mgr easy.reconcile.simple.partner model_easy_reconcile_simple_partner account.group_account_user 1 0 0 0
access_easy_reconcile_simple_reference_acc_mgr easy.reconcile.simple.reference model_easy_reconcile_simple_reference account.group_account_user 1 0 0 0
8 access_easy_reconcile_history_acc_user easy.reconcile.history model_easy_reconcile_history account.group_account_user 1 1 1 0
9 access_easy_reconcile_history_acc_mgr easy.reconcile.history model_easy_reconcile_history account.group_account_manager 1 1 1 1

View File

@@ -22,8 +22,7 @@
from openerp.osv.orm import AbstractModel, TransientModel
class easy_reconcile_simple(AbstractModel):
class EasyReconcileSimple(AbstractModel):
_name = 'easy.reconcile.simple'
_inherit = 'easy.reconcile.base'
@@ -32,20 +31,14 @@ class easy_reconcile_simple(AbstractModel):
_key_field = None
def rec_auto_lines_simple(self, cr, uid, rec, lines, context=None):
if context is None:
context = {}
if self._key_field is None:
raise ValueError("_key_field has to be defined")
count = 0
res = []
while (count < len(lines)):
for i in xrange(count + 1, len(lines)):
writeoff_account_id = False
if lines[count][self._key_field] != lines[i][self._key_field]:
break
check = False
if lines[count]['credit'] > 0 and lines[i]['debit'] > 0:
credit_line = lines[count]
@@ -57,7 +50,6 @@ class easy_reconcile_simple(AbstractModel):
check = True
if not check:
continue
reconciled, dummy = self._reconcile_lines(
cr, uid, rec, [credit_line, debit_line],
allow_partial=False, context=context)
@@ -90,8 +82,7 @@ class easy_reconcile_simple(AbstractModel):
return self.rec_auto_lines_simple(cr, uid, rec, lines, context)
class easy_reconcile_simple_name(TransientModel):
class EasyReconcileSimpleName(TransientModel):
_name = 'easy.reconcile.simple.name'
_inherit = 'easy.reconcile.simple'
@@ -100,8 +91,7 @@ class easy_reconcile_simple_name(TransientModel):
_key_field = 'name'
class easy_reconcile_simple_partner(TransientModel):
class EasyReconcileSimplePartner(TransientModel):
_name = 'easy.reconcile.simple.partner'
_inherit = 'easy.reconcile.simple'
@@ -110,8 +100,7 @@ class easy_reconcile_simple_partner(TransientModel):
_key_field = 'partner_id'
class easy_reconcile_simple_reference(TransientModel):
class EasyReconcileSimpleReference(TransientModel):
_name = 'easy.reconcile.simple.reference'
_inherit = 'easy.reconcile.simple'

View File

@@ -59,13 +59,15 @@ mandatory fields are:
* Description
* Internal Reference ("our reference")
* External Reference ("customer or supplier reference")
* Optionally, a technical transaction reference (credit card payment gateways, SEPA, ...)
* Optionally, a technical transaction reference (credit card payment gateways,
SEPA, ...)
Now, on the move lines:
* Name
* Reference
* Optionally, a technical transaction reference (added by the module `base_transaction_id`)
* Optionally, a technical transaction reference (added by the module
`base_transaction_id`)
Let's see how the information will be organized with this module.

View File

@@ -19,10 +19,10 @@
#
##############################################################################
from openerp.osv import orm, fields
from openerp.osv import orm
class account_move(orm.Model):
class AccountMove(orm.Model):
_inherit = 'account.move'
def create(self, cr, uid, vals, context=None):
@@ -33,15 +33,16 @@ class account_move(orm.Model):
if invoice:
assert isinstance(invoice, orm.browse_record)
invoice_obj = self.pool['account.invoice']
ref = invoice_obj._ref_from_invoice(cr, uid, invoice, context=context)
ref = invoice_obj._ref_from_invoice(
cr, uid, invoice, context=context)
vals = vals.copy()
vals['ref'] = ref
move_id = super(account_move, self).\
create(cr, uid, vals, context=context)
move_id = super(AccountMove, self).create(cr, uid, vals,
context=context)
return move_id
class account_invoice(orm.Model):
class AccountInvoice(orm.Model):
_inherit = 'account.invoice'
def _ref_from_invoice(self, cr, uid, invoice, context=None):
@@ -69,7 +70,8 @@ class account_invoice(orm.Model):
cr.execute('UPDATE account_move_line SET ref=%s '
'WHERE move_id=%s AND (ref is null OR ref = \'\')',
(ref, move_id))
cr.execute('UPDATE account_analytic_line SET ref=%s '
cr.execute(
'UPDATE account_analytic_line SET ref=%s '
'FROM account_move_line '
'WHERE account_move_line.move_id = %s '
'AND account_analytic_line.move_id = account_move_line.id',
@@ -80,7 +82,7 @@ class account_invoice(orm.Model):
if (vals.get('supplier_invoice_reference') and not
vals.get('reference')):
vals['reference'] = vals['supplier_invoice_reference']
return super(account_invoice, self).create(cr, uid, vals,
return super(AccountInvoice, self).create(cr, uid, vals,
context=context)
def write(self, cr, uid, ids, vals, context=None):
@@ -88,13 +90,12 @@ class account_invoice(orm.Model):
if isinstance(ids, (int, long)):
ids = [ids]
for invoice in self.browse(cr, uid, ids, context=context):
local_vals = vals
if not invoice.reference:
locvals = vals.copy()
locvals['reference'] = vals['supplier_invoice_reference']
super(account_invoice, self).write(cr, uid, [invoice.id],
super(AccountInvoice, self).write(cr, uid, [invoice.id],
locvals, context=context)
return True
else:
return super(account_invoice, self).write(cr, uid, ids, vals,
return super(AccountInvoice, self).write(cr, uid, ids, vals,
context=context)

View File

@@ -23,7 +23,8 @@
from openerp.tools.translate import _
from openerp.osv.orm import Model
from openerp.osv import fields
from openerp.addons.account_statement_base_completion.statement import ErrorTooManyPartner
from openerp.addons.account_statement_base_completion.statement import \
ErrorTooManyPartner
class AccountStatementCompletionRule(Model):
@@ -38,10 +39,6 @@ class AccountStatementCompletionRule(Model):
'From bank account number (Normal or IBAN)'))
return res
_columns = {
'function_to_call': fields.selection(_get_functions, 'Method'),
}
def get_from_bank_account(self, cr, uid, st_line, context=None):
"""
Match the partner based on the partner account number field
@@ -65,14 +62,16 @@ class AccountStatementCompletionRule(Model):
[('acc_number', '=', partner_acc_number)],
context=context)
if len(ids) > 1:
raise ErrorTooManyPartner(_('Line named "%s" (Ref:%s) was matched by more than '
'one partner for account number "%s".') % (st_line['name'], st_line['ref'], partner_acc_number))
raise ErrorTooManyPartner(
_('Line named "%s" (Ref:%s) was matched by more than one '
'partner for account number "%s".') %
(st_line['name'], st_line['ref'], partner_acc_number))
if len(ids) == 1:
partner = res_bank_obj.browse(cr, uid, ids[0], context=context).partner_id
partner = res_bank_obj.browse(
cr, uid, ids[0], context=context).partner_id
res['partner_id'] = partner.id
st_vals = st_obj.get_values_for_line(cr,
uid,
profile_id=st_line['profile_id'],
st_vals = st_obj.get_values_for_line(
cr, uid, profile_id=st_line['profile_id'],
master_account_id=st_line['master_account_id'],
partner_id=res.get('partner_id', False),
line_type=st_line['type'],
@@ -86,7 +85,6 @@ class AccountStatementLine(Model):
_inherit = "account.bank.statement.line"
_columns = {
# 'additional_bank_fields' : fields.serialized('Additional infos from bank', help="Used by completion and import system."),
'partner_acc_number': fields.sparse(
type='char',
string='Account Number',

View File

@@ -30,14 +30,19 @@ class bankaccount_completion(common.TransactionCase):
def prepare(self):
self.company_a = self.browse_ref('base.main_company')
self.profile_obj = self.registry("account.statement.profile")
self.account_bank_statement_obj = self.registry("account.bank.statement")
self.account_bank_statement_line_obj = self.registry("account.bank.statement.line")
self.completion_rule_id = self.ref('account_statement_bankaccount_completion.bank_statement_completion_rule_10')
self.journal_id = self.registry("ir.model.data").get_object_reference(self.cr, self. uid, "account", "bank_journal")[1]
self.st_obj = self.registry("account.bank.statement")
self.st_line_obj = self.registry("account.bank.statement.line")
self.completion_rule_id = self.ref(
'account_statement_bankaccount_completion.'
'bank_statement_completion_rule_10')
self.journal_id = self.registry("ir.model.data").get_object_reference(
self.cr, self. uid, "account", "bank_journal")[1]
self.partner_id = self.ref('base.main_partner')
# Create the profile
self.account_id = self.registry("ir.model.data").get_object_reference(self.cr, self.uid, "account", "a_recv")[1]
self.journal_id = self.registry("ir.model.data").get_object_reference(self.cr, self. uid, "account", "bank_journal")[1]
self.account_id = self.registry("ir.model.data").get_object_reference(
self.cr, self.uid, "account", "a_recv")[1]
self.journal_id = self.registry("ir.model.data").get_object_reference(
self.cr, self. uid, "account", "bank_journal")[1]
self.profile_id = self.profile_obj.create(self.cr, self.uid, {
"name": "TEST",
"commission_account_id": self.account_id,
@@ -46,17 +51,17 @@ class bankaccount_completion(common.TransactionCase):
# Create the completion rule
# Create a bank statement
self.statement_id = self.account_bank_statement_obj.create(self.cr, self.uid, {
self.statement_id = self.st_obj.create(
self.cr, self.uid, {
"balance_end_real": 0.0,
"balance_start": 0.0,
"date": time.strftime('%Y-%m-%d'),
"journal_id": self.journal_id,
"profile_id": self.profile_id
})
# Create bank a statement line
self.statement_line_id = self.account_bank_statement_line_obj.create(self.cr, self.uid, {
self.statement_line_id = self.st_line_obj.create(self.cr, self.uid, {
'amount': 1000.0,
'name': 'EXT001',
'ref': 'My ref',
@@ -78,14 +83,20 @@ class bankaccount_completion(common.TransactionCase):
def test_00(self):
"""Test complete partner_id from bank account number
Test the automatic completion of the partner_id based on the account number associated to the
Test the automatic completion of the partner_id based on the account
number associated to the
statement line
"""
self.prepare()
statement_line = self.account_bank_statement_line_obj.browse(self.cr, self.uid, self.statement_line_id)
statement_line = self.st_line_obj.browse(self.cr, self.uid,
self.statement_line_id)
# before import, the
self.assertFalse(statement_line.partner_id, "Partner_id must be blank before completion")
statement_obj = self.account_bank_statement_obj.browse(self.cr, self.uid, self.statement_id)
self.assertFalse(
statement_line.partner_id,
"Partner_id must be blank before completion")
statement_obj = self.st_obj.browse(self.cr, self.uid, self.statement_id)
statement_obj.button_auto_completion()
statement_line = self.account_bank_statement_line_obj.browse(self.cr, self.uid, self.statement_line_id)
self.assertEquals(self.partner_id, statement_line.partner_id['id'], "Missing expected partner id after completion")
statement_line = self.st_line_obj.browse(self.cr, self.uid,
self.statement_line_id)
self.assertEquals(self.partner_id, statement_line.partner_id[
'id'], "Missing expected partner id after completion")

View File

@@ -19,7 +19,8 @@
#
##############################################################################
{'name': "Bank statement base completion",
{
'name': "Bank statement base completion",
'version': '1.0.3',
'author': 'Camptocamp',
'maintainer': 'Camptocamp',
@@ -27,34 +28,40 @@
'complexity': 'normal',
'depends': ['account_statement_ext'],
'description': """
The goal of this module is to improve the basic bank statement, help dealing with huge volume of
reconciliation by providing basic rules to identify the partner of a bank statement line.
Each bank statement profile can have its own rules to be applied according to a sequence order.
The goal of this module is to improve the basic bank statement, help dealing
with huge volume of reconciliation by providing basic rules to identify the
partner of a bank statement line.
Each bank statement profile can have its own rules to be applied according to a
sequence order.
Some basic rules are provided in this module:
1) Match from statement line label (based on partner field 'Bank Statement Label')
1) Match from statement line label (based on partner field 'Bank Statement
Label')
2) Match from statement line label (based on partner name)
3) Match from statement line reference (based on Invoice number)
You can easily override this module and add your own rules in your own one. The basic rules only
fill in the partner, but you can use them to fill in any value of the line (in the future, we will
add a rule to automatically match and reconcile the line).
You can easily override this module and add your own rules in your own one. The
basic rules only fill in the partner, but you can use them to fill in any
value of the line (in the future, we will add a rule to automatically match and
reconcile the line).
It adds as well a label on the bank statement line (on which the pre-define rules can match) and
a char field on the partner called 'Bank Statement Label'. Using the pre-define rules, you will be
able to match various labels for a partner.
It adds as well a label on the bank statement line (on which the pre-define
rules can match) and a char field on the partner called 'Bank Statement Label'.
Using the pre-define rules, you will be able to match various labels for a
partner.
The reference of the line is always used by the reconciliation process. We're supposed to copy
there (or write manually) the matching string. This can be: the order Number or an invoice number,
or anything that will be found in the invoice accounting entry part to make the match.
The reference of the line is always used by the reconciliation process. We're
supposed to copy there (or write manually) the matching string. This can be:
the order Number or an invoice number, or anything that will be found in the
invoice accounting entry part to make the match.
You can use it with our account_advanced_reconcile module to automatize the reconciliation process.
TODO: The rules that look for invoices to find out the partner should take back the payable / receivable
account from there directly instead of retrieving it from partner properties !
You can use it with our account_advanced_reconcile module to automatize the
reconciliation process.
TODO: The rules that look for invoices to find out the partner should take back
the payable / receivable account from there directly instead of retrieving it
from partner properties !
""",
'website': 'http://www.camptocamp.com',
'data': [

View File

@@ -1,38 +1,39 @@
# -*- coding: utf-8 -*-
#################################################################################
# #
##########################################################################
#
# Copyright (C) 2011 Akretion & Camptocamp
# Author : Sébastien BEAU, Joel Grand-Guillaume #
# #
# This program is free software: you can redistribute it and/or modify #
# it under the terms of the GNU Affero 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 Affero General Public License for more details. #
# #
# You should have received a copy of the GNU Affero General Public License #
# along with this program. If not, see <http://www.gnu.org/licenses/>. #
# #
#################################################################################
# Author : Sébastien BEAU, Joel Grand-Guillaume
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##########################################################################
from openerp.osv.orm import Model
from openerp.osv import fields
from openerp.osv import orm, fields
class res_partner(Model):
"""
Add a bank label on the partner so that we can use it to match
class ResPartner(orm.Model):
"""Add a bank label on the partner so that we can use it to match
this partner when we found this in a statement line.
"""
_inherit = 'res.partner'
_columns = {
'bank_statement_label': fields.char('Bank Statement Label', size=100,
help="Enter the various label found on your bank statement separated by a ; If "
"one of this label is include in the bank statement line, the partner will be automatically "
"filled (as long as you use this method/rules in your statement profile)."),
'bank_statement_label': fields.char(
'Bank Statement Label', size=100,
help="Enter the various label found on your bank statement "
"separated by a ; If one of this label is include in the bank "
"statement line, the partner will be automatically filled (as "
"long as you use this method/rules in your statement "
"profile)."),
}

View File

@@ -7,7 +7,6 @@
<record id="bk_view_partner_form" model="ir.ui.view">
<field name="name">account_bank_statement_import.view.partner.form</field>
<field name="model">res.partner</field>
<field name="type">form</field>
<field name="priority">20</field>
<field name="inherit_id" ref="account.view_partner_property_form"/>
<field name="arch" type="xml">

View File

@@ -31,7 +31,7 @@ import psycopg2
from collections import defaultdict
import re
from openerp.tools.translate import _
from openerp.osv import osv, orm, fields
from openerp.osv import orm, fields
from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT
from operator import attrgetter
@@ -40,10 +40,10 @@ _logger = logging.getLogger(__name__)
class ErrorTooManyPartner(Exception):
""" New Exception definition that is raised when more than one partner is
matched by the completion rule.
"""
New Exception definition that is raised when more than one partner is matched by
the completion rule.
"""
def __init__(self, value):
self.value = value
@@ -55,19 +55,18 @@ class ErrorTooManyPartner(Exception):
class AccountStatementProfil(orm.Model):
"""Extend the class to add rules per profile that will match at least the
partner, but it could also be used to match other values as well.
"""
Extend the class to add rules per profile that will match at least the partner,
but it could also be used to match other values as well.
"""
_inherit = "account.statement.profile"
_columns = {
# @Akretion: For now, we don't implement this features, but this would probably be there:
# 'auto_completion': fields.text('Auto Completion'),
# 'transferts_account_id':fields.many2one('account.account', 'Transferts Account'),
# => You can implement it in a module easily, we design it with your needs in mind
# as well!
# @Akretion: For now, we don't implement this features, but this would
# probably be there: 'auto_completion': fields.text('Auto Completion'),
# 'transferts_account_id':fields.many2one('account.account',
# 'Transferts Account'),
# => You can implement it in a module easily, we design it with your
# needs in mind as well!
'rule_ids': fields.many2many(
'account.statement.completion.rule',
@@ -84,8 +83,7 @@ class AccountStatementProfil(orm.Model):
return sorted(prof.rule_ids, key=attrgetter('sequence'))
def _find_values_from_rules(self, cr, uid, calls, line, context=None):
"""
This method will execute all related rules, in their sequence order,
"""This method will execute all related rules, in their sequence order,
to retrieve all the values returned by the first rules that will match.
:param calls: list of lookup function name available in rules
:param dict line: read of the concerned account.bank.statement.line
@@ -97,12 +95,10 @@ class AccountStatementProfil(orm.Model):
...}
"""
if context is None:
context = {}
if not calls:
calls = self._get_rules(cr, uid, line['profile_id'], context=context)
calls = self._get_rules(
cr, uid, line['profile_id'], context=context)
rule_obj = self.pool.get('account.statement.completion.rule')
for call in calls:
method_to_call = getattr(rule_obj, call.function_to_call)
if len(inspect.getargspec(method_to_call).args) == 6:
@@ -116,37 +112,43 @@ class AccountStatementProfil(orm.Model):
class AccountStatementCompletionRule(orm.Model):
"""
This will represent all the completion method that we can have to
fullfill the bank statement lines. You'll be able to extend them in you own module
and choose those to apply for every statement profile.
"""This will represent all the completion method that we can have to
fullfill the bank statement lines. You'll be able to extend them in you own
module and choose those to apply for every statement profile.
The goal of a rule is to fullfill at least the partner of the line, but
if possible also the reference because we'll use it in the reconciliation
process. The reference should contain the invoice number or the SO number
or any reference that will be matched by the invoice accounting move.
"""
_name = "account.statement.completion.rule"
_order = "sequence asc"
def _get_functions(self, cr, uid, context=None):
"""
List of available methods for rules. Override this to add you own.
"""
"""List of available methods for rules. Override this to add you own."""
return [
('get_from_ref_and_invoice', 'From line reference (based on customer invoice number)'),
('get_from_ref_and_supplier_invoice', 'From line reference (based on supplier invoice number)'),
('get_from_label_and_partner_field', 'From line label (based on partner field)'),
('get_from_label_and_partner_name', 'From line label (based on partner name)')]
('get_from_ref_and_invoice',
'From line reference (based on customer invoice number)'),
('get_from_ref_and_supplier_invoice',
'From line reference (based on supplier invoice number)'),
('get_from_label_and_partner_field',
'From line label (based on partner field)'),
('get_from_label_and_partner_name',
'From line label (based on partner name)')
]
def __get_functions(self, cr, uid, context=None):
""" Call method which can be inherited """
return self._get_functions(cr, uid, context=context)
_columns = {
'sequence': fields.integer('Sequence', help="Lower means parsed first."),
'sequence': fields.integer('Sequence',
help="Lower means parsed first."),
'name': fields.char('Name', size=128),
'profile_ids': fields.many2many(
'account.statement.profile',
rel='as_rul_st_prof_rel',
string='Related statement profiles'),
'function_to_call': fields.selection(_get_functions, 'Method'),
'function_to_call': fields.selection(__get_functions, 'Method'),
}
def _find_invoice(self, cr, uid, st_line, inv_type, context=None):
@@ -159,7 +161,8 @@ class AccountStatementCompletionRule(orm.Model):
type_domain = ('out_invoice', 'out_refund')
number_field = 'number'
else:
raise osv.except_osv(_('System error'),
raise orm.except_orm(
_('System error'),
_('Invalid invoice type for completion: %') % inv_type)
inv_id = inv_obj.search(cr, uid,
@@ -170,17 +173,19 @@ class AccountStatementCompletionRule(orm.Model):
if len(inv_id) == 1:
inv = inv_obj.browse(cr, uid, inv_id[0], context=context)
else:
raise ErrorTooManyPartner(_('Line named "%s" (Ref:%s) was matched by more '
'than one partner while looking on %s invoices') %
raise ErrorTooManyPartner(
_('Line named "%s" (Ref:%s) was matched by more than one '
'partner while looking on %s invoices') %
(st_line['name'], st_line['ref'], inv_type))
return inv
return False
def _from_invoice(self, cr, uid, line, inv_type, context):
"""Populate statement line values"""
if not inv_type in ('supplier', 'customer'):
raise osv.except_osv(_('System error'),
_('Invalid invoice type for completion: %') % inv_type)
if inv_type not in ('supplier', 'customer'):
raise orm.except_orm(_('System error'),
_('Invalid invoice type for completion: %') %
inv_type)
res = {}
inv = self._find_invoice(cr, uid, line, inv_type, context=context)
if inv:
@@ -191,7 +196,6 @@ class AccountStatementCompletionRule(orm.Model):
partner_id = inv.commercial_partner_id.id
else:
partner_id = inv.partner_id.id
res = {'partner_id': partner_id,
'account_id': inv.account_id.id,
'type': inv_type}
@@ -202,10 +206,10 @@ class AccountStatementCompletionRule(orm.Model):
# Should be private but data are initialised with no update XML
def get_from_ref_and_supplier_invoice(self, cr, uid, line, context=None):
"""
Match the partner based on the invoice supplier invoice number and the reference of the statement
line. Then, call the generic get_values_for_line method to complete other values.
If more than one partner matched, raise the ErrorTooManyPartner error.
"""Match the partner based on the invoice supplier invoice number and
the reference of the statement line. Then, call the generic
get_values_for_line method to complete other values. If more than one
partner matched, raise the ErrorTooManyPartner error.
:param dict line: read of the concerned account.bank.statement.line
:return:
@@ -220,10 +224,10 @@ class AccountStatementCompletionRule(orm.Model):
# Should be private but data are initialised with no update XML
def get_from_ref_and_invoice(self, cr, uid, line, context=None):
"""
Match the partner based on the invoice number and the reference of the statement
line. Then, call the generic get_values_for_line method to complete other values.
If more than one partner matched, raise the ErrorTooManyPartner error.
"""Match the partner based on the invoice number and the reference of
the statement line. Then, call the generic get_values_for_line method to
complete other values. If more than one partner matched, raise the
ErrorTooManyPartner error.
:param dict line: read of the concerned account.bank.statement.line
:return:
@@ -253,7 +257,7 @@ class AccountStatementCompletionRule(orm.Model):
...}
"""
partner_obj = self.pool.get('res.partner')
partner_obj = self.pool['res.partner']
st_obj = self.pool.get('account.bank.statement.line')
res = {}
# As we have to iterate on each partner for each line,
@@ -263,12 +267,15 @@ class AccountStatementCompletionRule(orm.Model):
# but this option is not really maintanable
if not context.get('label_memoizer'):
context['label_memoizer'] = defaultdict(list)
partner_ids = partner_obj.search(cr,
uid,
[('bank_statement_label', '!=', False)])
partner_ids = partner_obj.search(
cr, uid, [('bank_statement_label', '!=', False)],
context=context)
line_ids = context.get('line_ids', [])
for partner in partner_obj.browse(cr, uid, partner_ids, context=context):
vals = '|'.join(re.escape(x.strip()) for x in partner.bank_statement_label.split(';'))
for partner in partner_obj.browse(cr, uid, partner_ids,
context=context):
vals = '|'.join(
re.escape(x.strip())
for x in partner.bank_statement_label.split(';'))
or_regex = ".*%s.*" % vals
sql = ("SELECT id from account_bank_statement_line"
" WHERE id in %s"
@@ -277,32 +284,29 @@ class AccountStatementCompletionRule(orm.Model):
pairs = cr.fetchall()
for pair in pairs:
context['label_memoizer'][pair[0]].append(partner)
if st_line['id'] in context['label_memoizer']:
found_partner = context['label_memoizer'][st_line['id']]
if len(found_partner) > 1:
msg = (_('Line named "%s" (Ref:%s) was matched by '
'more than one partner while looking on partner label: %s') %
(st_line['name'], st_line['ref'], ','.join([x.name for x in found_partner])))
msg = (_('Line named "%s" (Ref:%s) was matched by more than '
'one partner while looking on partner label: %s') %
(st_line['name'], st_line['ref'],
','.join([x.name for x in found_partner])))
raise ErrorTooManyPartner(msg)
res['partner_id'] = found_partner[0].id
st_vals = st_obj.get_values_for_line(cr,
uid,
profile_id=st_line['profile_id'],
st_vals = st_obj.get_values_for_line(
cr, uid, profile_id=st_line['profile_id'],
master_account_id=st_line['master_account_id'],
partner_id=found_partner[0].id,
line_type=False,
partner_id=found_partner[0].id, line_type=False,
amount=st_line['amount'] if st_line['amount'] else 0.0,
context=context)
res.update(st_vals)
return res
def get_from_label_and_partner_name(self, cr, uid, st_line, context=None):
"""
Match the partner based on the label field of the statement line
and the name of the partner.
Then, call the generic get_values_for_line method to complete other values.
If more than one partner matched, raise the ErrorTooManyPartner error.
"""Match the partner based on the label field of the statement line
and the name of the partner. Then, call the generic get_values_for_line
method to complete other values. If more than one partner matched, raise
the ErrorTooManyPartner error.
:param dict st_line: read of the concerned account.bank.statement.line
:return:
@@ -316,7 +320,8 @@ class AccountStatementCompletionRule(orm.Model):
res = {}
# We memoize allowed partner
if not context.get('partner_memoizer'):
context['partner_memoizer'] = tuple(self.pool['res.partner'].search(cr, uid, []))
context['partner_memoizer'] = tuple(
self.pool['res.partner'].search(cr, uid, []))
if not context['partner_memoizer']:
return res
st_obj = self.pool.get('account.bank.statement.line')
@@ -324,33 +329,41 @@ class AccountStatementCompletionRule(orm.Model):
# example: 'John J. Doe (No 1)' is escaped to 'John J\. Doe \(No 1\)'
# See http://stackoverflow.com/a/400316/1504003 for a list of
# chars to escape. Postgres is POSIX-ARE, compatible with
# POSIX-ERE excepted that '\' must be escaped inside brackets according to:
# POSIX-ERE excepted that '\' must be escaped inside brackets according
# to:
# http://www.postgresql.org/docs/9.0/static/functions-matching.html
# in chapter 9.7.3.6. Limits and Compatibility
sql = """SELECT id FROM (
SELECT id, regexp_matches(%s, regexp_replace(name,'([\.\^\$\*\+\?\(\)\[\{\\\|])', %s, 'g'), 'i') AS name_match FROM res_partner
WHERE id IN %s) AS res_patner_matcher
sql = """
SELECT id FROM (
SELECT id,
regexp_matches(%s,
regexp_replace(name,'([\.\^\$\*\+\?\(\)\[\{\\\|])', %s,
'g'), 'i') AS name_match
FROM res_partner
WHERE id IN %s)
AS res_patner_matcher
WHERE name_match IS NOT NULL"""
cr.execute(sql, (st_line['name'], r"\\\1", context['partner_memoizer']))
cr.execute(
sql, (st_line['name'], r"\\\1", context['partner_memoizer']))
result = cr.fetchall()
if not result:
return res
if len(result) > 1:
raise ErrorTooManyPartner(_('Line named "%s" (Ref:%s) was matched by more '
'than one partner while looking on partner by name') %
raise ErrorTooManyPartner(
_('Line named "%s" (Ref:%s) was matched by more than one '
'partner while looking on partner by name') %
(st_line['name'], st_line['ref']))
res['partner_id'] = result[0][0]
st_vals = st_obj.get_values_for_line(cr,
uid,
profile_id=st_line['profile_id'],
st_vals = st_obj.get_values_for_line(
cr, uid, profile_id=st_line['profile_id'],
master_account_id=st_line['master_account_id'],
partner_id=res['partner_id'],
line_type=False,
partner_id=res['partner_id'], line_type=False,
amount=st_line['amount'] if st_line['amount'] else 0.0,
context=context)
res.update(st_vals)
return res
class AccountStatement(orm.Model):
_inherit = "account.bank.statement"
@@ -363,7 +376,8 @@ class AccountStatement(orm.Model):
], context=context)
if line_without_account:
stat = self.browse(cr, uid, stat_id, context=context)
raise orm.except_orm(_('User error'),
raise orm.except_orm(
_('User error'),
_('You should fill all account on the line of the'
' statement %s') % stat.name)
return super(AccountStatement, self).button_confirm_bank(
@@ -374,10 +388,11 @@ class AccountStatementLine(orm.Model):
"""
Add sparse field on the statement line to allow to store all the
bank infos that are given by a bank/office. You can then add you own in your
module. The idea here is to store all bank/office infos in the additionnal_bank_fields
serialized field when importing the file. If many values, add a tab in the bank
statement line to store your specific one. Have a look in account_statement_base_import
module to see how we've done it.
module. The idea here is to store all bank/office infos in the
additionnal_bank_fields serialized field when importing the file. If many
values, add a tab in the bank statement line to store your specific one.
Have a look in account_statement_base_import module to see how we've done
it.
"""
_inherit = "account.bank.statement.line"
_order = "already_completed desc, date asc"
@@ -407,30 +422,34 @@ class AccountStatementLine(orm.Model):
}
def _get_line_values_from_rules(self, cr, uid, line, rules, context=None):
"""
We'll try to find out the values related to the line based on rules setted on
the profile.. We will ignore line for which already_completed is ticked.
"""We'll try to find out the values related to the line based on rules
setted on the profile.. We will ignore line for which already_completed
is ticked.
:return:
A dict of dict value that can be passed directly to the write method of
the statement line or {}. The first dict has statement line ID as a key:
{117009: {'partner_id': 100997, 'account_id': 489L}}
A dict of dict value that can be passed directly to the write
method of the statement line or {}. The first dict has statement
line ID as a key: {117009: {'partner_id': 100997,
'account_id': 489L}}
"""
profile_obj = self.pool.get('account.statement.profile')
profile_obj = self.pool['account.statement.profile']
if line.get('already_completed'):
return {}
# Ask the rule
vals = profile_obj._find_values_from_rules(cr, uid, rules, line, context)
vals = profile_obj._find_values_from_rules(
cr, uid, rules, line, context)
if vals:
vals['id'] = line['id']
return vals
return {}
def _get_available_columns(self, statement_store, include_serializable=False):
def _get_available_columns(self, statement_store,
include_serializable=False):
"""Return writeable by SQL columns"""
statement_line_obj = self.pool['account.bank.statement.line']
model_cols = statement_line_obj._columns
avail = [k for k, col in model_cols.iteritems() if not hasattr(col, '_fnct')]
avail = [
k for k, col in model_cols.iteritems() if not hasattr(col, '_fnct')]
keys = [k for k in statement_store[0].keys() if k in avail]
# add sparse fields..
if include_serializable:
@@ -468,7 +487,9 @@ class AccountStatementLine(orm.Model):
"""
statement_line_obj = self.pool['account.bank.statement.line']
model_cols = statement_line_obj._columns
sparse_fields = dict([(k, col) for k, col in model_cols.iteritems() if isinstance(col, fields.sparse) and col._type == 'char'])
sparse_fields = dict(
[(k, col) for k, col in model_cols.iteritems() if isinstance(
col, fields.sparse) and col._type == 'char'])
values = []
for statement in statement_store:
to_json_k = set()
@@ -476,7 +497,8 @@ class AccountStatementLine(orm.Model):
for k, col in sparse_fields.iteritems():
if k in st_copy:
to_json_k.add(col.serialization_field)
serialized = st_copy.setdefault(col.serialization_field, {})
serialized = st_copy.setdefault(
col.serialization_field, {})
serialized[k] = st_copy[k]
for k in to_json_k:
st_copy[k] = simplejson.dumps(st_copy[k])
@@ -489,16 +511,21 @@ class AccountStatementLine(orm.Model):
does not exist"""
statement_line_obj = self.pool['account.bank.statement.line']
statement_line_obj.check_access_rule(cr, uid, [], 'create')
statement_line_obj.check_access_rights(cr, uid, 'create', raise_exception=True)
cols = self._get_available_columns(statement_store, include_serializable=True)
statement_line_obj.check_access_rights(
cr, uid, 'create', raise_exception=True)
cols = self._get_available_columns(
statement_store, include_serializable=True)
statement_store = self._prepare_manyinsert(statement_store, cols)
tmp_vals = (', '.join(cols), ', '.join(['%%(%s)s' % i for i in cols]))
sql = "INSERT INTO account_bank_statement_line (%s) VALUES (%s);" % tmp_vals
sql = "INSERT INTO account_bank_statement_line (%s) " \
"VALUES (%s);" % tmp_vals
try:
cr.executemany(sql, tuple(self._serialize_sparse_fields(cols, statement_store)))
cr.executemany(
sql, tuple(self._serialize_sparse_fields(cols,
statement_store)))
except psycopg2.Error as sql_err:
cr.rollback()
raise osv.except_osv(_("ORM bypass error"),
raise orm.except_orm(_("ORM bypass error"),
sql_err.pgerror)
def _update_line(self, cr, uid, vals, context=None):
@@ -512,18 +539,18 @@ class AccountStatementLine(orm.Model):
cols = self._get_available_columns([vals])
vals = self._prepare_insert(vals, cols)
tmp_vals = (', '.join(['%s = %%(%s)s' % (i, i) for i in cols]))
sql = "UPDATE account_bank_statement_line SET %s where id = %%(id)s;" % tmp_vals
sql = "UPDATE account_bank_statement_line " \
"SET %s where id = %%(id)s;" % tmp_vals
try:
cr.execute(sql, vals)
except psycopg2.Error as sql_err:
cr.rollback()
raise osv.except_osv(_("ORM bypass error"),
raise orm.except_orm(_("ORM bypass error"),
sql_err.pgerror)
class AccountBankStatement(orm.Model):
"""
We add a basic button and stuff to support the auto-completion
"""We add a basic button and stuff to support the auto-completion
of the bank statement once line have been imported or manually fullfill.
"""
_inherit = "account.bank.statement"
@@ -532,44 +559,42 @@ class AccountBankStatement(orm.Model):
'completion_logs': fields.text('Completion Log', readonly=True),
}
def write_completion_log(self, cr, uid, stat_id, error_msg, number_imported, context=None):
"""
Write the log in the completion_logs field of the bank statement to let the user
know what have been done. This is an append mode, so we don't overwrite what
already recoded.
def write_completion_log(self, cr, uid, stat_id, error_msg,
number_imported, context=None):
"""Write the log in the completion_logs field of the bank statement to
let the user know what have been done. This is an append mode, so we
don't overwrite what already recoded.
:param int/long stat_id: ID of the account.bank.statement
:param char error_msg: Message to add
:number_imported int/long: Number of lines that have been completed
:return True
"""
user_name = self.pool.get('res.users').read(cr, uid, uid,
['name'], context=context)['name']
user_name = self.pool.get('res.users').read(
cr, uid, uid, ['name'], context=context)['name']
statement = self.browse(cr, uid, stat_id, context=context)
number_line = len(statement.line_ids)
log = self.read(cr, uid, stat_id, ['completion_logs'],
context=context)['completion_logs']
log = log if log else ""
completion_date = datetime.datetime.now().strftime(DEFAULT_SERVER_DATETIME_FORMAT)
message = (_("%s Bank Statement ID %s has %s/%s lines completed by %s \n%s\n%s\n") %
(completion_date, stat_id, number_imported, number_line, user_name,
error_msg, log))
self.write(cr, uid, [stat_id], {'completion_logs': message}, context=context)
completion_date = datetime.datetime.now().strftime(
DEFAULT_SERVER_DATETIME_FORMAT)
message = (_("%s Bank Statement ID %s has %s/%s lines completed by "
"%s \n%s\n%s\n") % (completion_date, stat_id,
number_imported, number_line,
user_name, error_msg, log))
self.write(
cr, uid, [stat_id], {'completion_logs': message}, context=context)
body = (_('Statement ID %s auto-completed for %s/%s lines completed') %
(stat_id, number_imported, number_line)),
self.message_post(cr, uid,
[stat_id],
body=body,
context=context)
self.message_post(cr, uid, [stat_id], body=body, context=context)
return True
def button_auto_completion(self, cr, uid, ids, context=None):
"""
Complete line with values given by rules and tic the already_completed
checkbox so we won't compute them again unless the user untick them!
"""Complete line with values given by rules and tic the
already_completed checkbox so we won't compute them again unless the
user untick them!
"""
if context is None:
context = {}
@@ -577,24 +602,27 @@ class AccountBankStatement(orm.Model):
profile_obj = self.pool.get('account.statement.profile')
compl_lines = 0
stat_line_obj.check_access_rule(cr, uid, [], 'create')
stat_line_obj.check_access_rights(cr, uid, 'create', raise_exception=True)
stat_line_obj.check_access_rights(
cr, uid, 'create', raise_exception=True)
for stat in self.browse(cr, uid, ids, context=context):
msg_lines = []
ctx = context.copy()
ctx['line_ids'] = tuple((x.id for x in stat.line_ids))
b_profile = stat.profile_id
rules = profile_obj._get_rules(cr, uid, b_profile, context=context)
profile_id = b_profile.id # Only for perfo even it gains almost nothing
# Only for perfo even it gains almost nothing
profile_id = b_profile.id
master_account_id = b_profile.receivable_account_id
master_account_id = master_account_id.id if master_account_id else False
master_account_id = master_account_id.id if \
master_account_id else False
res = False
for line in stat_line_obj.read(cr, uid, ctx['line_ids']):
try:
# performance trick
line['master_account_id'] = master_account_id
line['profile_id'] = profile_id
res = stat_line_obj._get_line_values_from_rules(cr, uid, line,
rules, context=ctx)
res = stat_line_obj._get_line_values_from_rules(
cr, uid, line, rules, context=ctx)
if res:
compl_lines += 1
except ErrorTooManyPartner, exc:
@@ -602,17 +630,20 @@ class AccountBankStatement(orm.Model):
except Exception, exc:
msg_lines.append(repr(exc))
error_type, error_value, trbk = sys.exc_info()
st = "Error: %s\nDescription: %s\nTraceback:" % (error_type.__name__, error_value)
st = "Error: %s\nDescription: %s\nTraceback:" % (
error_type.__name__, error_value)
st += ''.join(traceback.format_tb(trbk, 30))
_logger.error(st)
if res:
# stat_line_obj.write(cr, uid, [line.id], vals, context=ctx)
try:
stat_line_obj._update_line(cr, uid, res, context=context)
stat_line_obj._update_line(
cr, uid, res, context=context)
except Exception as exc:
msg_lines.append(repr(exc))
error_type, error_value, trbk = sys.exc_info()
st = "Error: %s\nDescription: %s\nTraceback:" % (error_type.__name__, error_value)
st = "Error: %s\nDescription: %s\nTraceback:" % (
error_type.__name__, error_value)
st += ''.join(traceback.format_tb(trbk, 30))
_logger.error(st)
# we can commit as it is not needed to be atomic

View File

@@ -7,7 +7,6 @@
<field name="model">account.bank.statement</field>
<field name="inherit_id" ref="account.view_bank_statement_form" />
<field eval="16" name="priority"/>
<field name="type">form</field>
<field name="arch" type="xml">
<data>
<xpath expr="/form/sheet/notebook/page/field[@name='line_ids']/form/group/field[@name='sequence']" position="after">
@@ -37,7 +36,6 @@
<field name="name">account_bank_statement_import_base.bank_statement.auto_cmpl</field>
<field name="model">account.bank.statement</field>
<field name="inherit_id" ref="account.view_bank_statement_form" />
<field name="type">form</field>
<field name="arch" type="xml">
<data>
<xpath expr="/form/sheet/notebook/page/field[@name='line_ids']/tree/field[@name='amount']" position="after">
@@ -51,7 +49,6 @@
<field name="name">account.statement.profile.view</field>
<field name="model">account.statement.profile</field>
<field name="inherit_id" ref="account_statement_ext.statement_importer_view_form"/>
<field name="type">form</field>
<field name="arch" type="xml">
<field name="bank_statement_prefix" position="after">
<separator colspan="4" string="Auto-Completion Rules"/>
@@ -64,7 +61,6 @@
<record id="statement_st_completion_rule_view_form" model="ir.ui.view">
<field name="name">account.statement.completion.rule.view</field>
<field name="model">account.statement.completion.rule</field>
<field name="type">form</field>
<field name="arch" type="xml">
<form string="Statement Completion Rule">
<field name="sequence"/>
@@ -79,7 +75,6 @@
<record id="statement_st_completion_rule_view_tree" model="ir.ui.view">
<field name="name">account.statement.completion.rule.view</field>
<field name="model">account.statement.completion.rule</field>
<field name="type">tree</field>
<field name="arch" type="xml">
<tree string="Statement Completion Rule">
<field name="sequence"/>

View File

@@ -23,7 +23,8 @@ from openerp.tests import common
import time
from collections import namedtuple
name_completion_case = namedtuple("name_completion_case", ["partner_name", "line_label", "should_match"])
name_completion_case = namedtuple(
"name_completion_case", ["partner_name", "line_label", "should_match"])
NAMES_COMPLETION_CASES = [
name_completion_case("Acsone", "Line for Acsone SA", True),
name_completion_case("Acsone", "Line for Acsone", True),
@@ -34,9 +35,14 @@ NAMES_COMPLETION_CASES = [
name_completion_case("é@|r{}", "Acsone é@|r{} for line", True),
name_completion_case("Acsone", "A..one for line", False),
name_completion_case("A.one SA", "A.one SA for line", True),
name_completion_case("Acsone SA", "Line for Acsone ([^a-zA-Z0-9 -]) SA test", False),
name_completion_case("Acsone ([^a-zA-Z0-9 -]) SA", "Line for Acsone ([^a-zA-Z0-9 -]) SA test", True),
name_completion_case(r"Acsone (.^$*+?()[{\| -]\) SA", r"Line for Acsone (.^$*+?()[{\| -]\) SA test", True),
name_completion_case(
"Acsone SA", "Line for Acsone ([^a-zA-Z0-9 -]) SA test", False),
name_completion_case(
"Acsone ([^a-zA-Z0-9 -]) SA", "Line for Acsone ([^a-zA-Z0-9 -]) SA "
"test", True),
name_completion_case(
r"Acsone (.^$*+?()[{\| -]\) SA", r"Line for Acsone (.^$*+?()[{\| -]\) "
r"SA test", True),
name_completion_case("Acšone SA", "Line for Acšone SA test", True),
]
@@ -48,8 +54,10 @@ class base_completion(common.TransactionCase):
self.company_a = self.browse_ref('base.main_company')
self.profile_obj = self.registry("account.statement.profile")
self.partner_obj = self.registry("res.partner")
self.account_bank_statement_obj = self.registry("account.bank.statement")
self.account_bank_statement_line_obj = self.registry("account.bank.statement.line")
self.account_bank_statement_obj = self.registry(
"account.bank.statement")
self.account_bank_statement_line_obj = self.registry(
"account.bank.statement.line")
self.journal_id = self.ref("account.bank_journal")
self.partner_id = self.ref('base.main_partner')
self.account_id = self.ref("account.a_recv")
@@ -57,10 +65,12 @@ class base_completion(common.TransactionCase):
def test_name_completion(self):
"""Test complete partner_id from statement line label
Test the automatic completion of the partner_id based if the name of the partner appears in
the statement line label
Test the automatic completion of the partner_id based if the name of the
partner appears in the statement line label
"""
self.completion_rule_id = self.ref('account_statement_base_completion.bank_statement_completion_rule_3')
self.completion_rule_id = self.ref(
'account_statement_base_completion.'
'bank_statement_completion_rule_3')
# Create the profile
self.profile_id = self.profile_obj.create(self.cr, self.uid, {
"name": "TEST",
@@ -68,7 +78,8 @@ class base_completion(common.TransactionCase):
"journal_id": self.journal_id,
"rule_ids": [(6, 0, [self.completion_rule_id])]})
# Create a bank statement
self.statement_id = self.account_bank_statement_obj.create(self.cr, self.uid, {
self.statement_id = self.account_bank_statement_obj.create(
self.cr, self.uid, {
"balance_end_real": 0.0,
"balance_start": 0.0,
"date": time.strftime('%Y-%m-%d'),
@@ -77,21 +88,33 @@ class base_completion(common.TransactionCase):
})
for case in NAMES_COMPLETION_CASES:
self.partner_obj.write(self.cr, self.uid, self.partner_id, {'name': case.partner_name})
statement_line_id = self.account_bank_statement_line_obj.create(self.cr, self.uid, {
self.partner_obj.write(
self.cr, self.uid, self.partner_id, {'name': case.partner_name})
statement_line_id = self.account_bank_statement_line_obj.create(
self.cr, self.uid, {
'amount': 1000.0,
'name': case.line_label,
'ref': 'My ref',
'statement_id': self.statement_id,
})
statement_line = self.account_bank_statement_line_obj.browse(self.cr, self.uid, statement_line_id)
self.assertFalse(statement_line.partner_id, "Partner_id must be blank before completion")
statement_obj = self.account_bank_statement_obj.browse(self.cr, self.uid, self.statement_id)
statement_line = self.account_bank_statement_line_obj.browse(
self.cr, self.uid, statement_line_id)
self.assertFalse(
statement_line.partner_id,
"Partner_id must be blank before completion")
statement_obj = self.account_bank_statement_obj.browse(
self.cr, self.uid, self.statement_id)
statement_obj.button_auto_completion()
statement_line = self.account_bank_statement_line_obj.browse(self.cr, self.uid, statement_line_id)
statement_line = self.account_bank_statement_line_obj.browse(
self.cr, self.uid, statement_line_id)
if case.should_match:
self.assertEquals(self.partner_id, statement_line.partner_id['id'],
"Missing expected partner id after completion (partner_name: %s, line_name: %s)" % (case.partner_name, case.line_label))
self.assertEquals(
self.partner_id, statement_line.partner_id['id'],
"Missing expected partner id after completion "
"(partner_name: %s, line_name: %s)" %
(case.partner_name, case.line_label))
else:
self.assertNotEquals(self.partner_id, statement_line.partner_id['id'],
"Partner id should be empty after completion(partner_name: %s, line_name: %s)" % (case.partner_name, case.line_label))
self.assertNotEquals(
self.partner_id, statement_line.partner_id['id'],
"Partner id should be empty after completion(partner_name: "
"%s, line_name: %s)" % (case.partner_name, case.line_label))

View File

@@ -31,30 +31,34 @@
],
'description': """
This module brings basic methods and fields on bank statement to deal with
the importation of different bank and offices. A generic abstract method is defined and an
example that gives you a basic way of importing bank statement through a standard file is provided.
the importation of different bank and offices. A generic abstract method is
defined and an example that gives you a basic way of importing bank statement
through a standard file is provided.
This module improves the bank statement and allows you to import your bank transactions with
a standard .csv or .xls file (you'll find it in the 'data' folder). It respects the profile
(provided by the accouhnt_statement_ext module) to pass the entries. That means,
you'll have to choose a file format for each profile.
In order to achieve this it uses the `xlrd` Python module which you will need to install
separately in your environment.
This module improves the bank statement and allows you to import your bank
transactions with a standard .csv or .xls file (you'll find it in the 'data'
folder). It respects the profile (provided by the accouhnt_statement_ext
module) to pass the entries. That means, you'll have to choose a file format
for each profile.
In order to achieve this it uses the `xlrd` Python module which you will need
to install separately in your environment.
This module can handle a commission taken by the payment office and has the following format:
This module can handle a commission taken by the payment office and has the
following format:
* ref : the SO number, INV number or any matching ref found. It'll be used as reference
in the generated entries and will be useful for reconciliation process
* date : date of the payment
* amount : amount paid in the currency of the journal used in the importation profile
* label : the comunication given by the payment office, used as communication in the
generated entries.
The goal is here to populate the statement lines of a bank statement with the infos that the
bank or office give you. Fell free to inherit from this module to add your own format.Then,
if you need to complete data from there, add your own account_statement_*_completion module and implement
the needed rules.
* __ref__: the SO number, INV number or any matching ref found. It'll be used
as reference in the generated entries and will be useful for reconciliation
process
* __date__: date of the payment
* __amount__: amount paid in the currency of the journal used in the
importation profile
* __label__: the comunication given by the payment office, used as
communication in the generated entries.
The goal is here to populate the statement lines of a bank statement with the
infos that the bank or office give you. Fell free to inherit from this module
to add your own format. Then, if you need to complete data from there, add your
own account_statement_*_completion module and implement the needed rules.
""",
'website': 'http://www.camptocamp.com',
'data': [

View File

@@ -18,7 +18,7 @@
#
##############################################################################
from openerp.tools.translate import _
from openerp.osv.osv import except_osv
from openerp.osv.orm import except_orm
import tempfile
import datetime
from parser import BankStatementImportParser
@@ -28,30 +28,35 @@ try:
except:
raise Exception(_('Please install python lib xlrd'))
def float_or_zero(val):
""" Conversion function used to manage
empty string into float usecase"""
return float(val) if val else 0.0
class FileParser(BankStatementImportParser):
"""
Generic abstract class for defining parser for .csv, .xls or .xlsx file format.
"""Generic abstract class for defining parser for .csv, .xls or .xlsx file
format.
"""
def __init__(self, parse_name, ftype='csv', extra_fields=None, header=None, **kwargs):
def __init__(self, parse_name, ftype='csv', extra_fields=None, header=None,
**kwargs):
"""
:param char: parse_name: The name of the parser
:param char: ftype: extension of the file (could be csv, xls or xlsx)
:param dict: extra_fields: extra fields to add to the conversion dict. In the format
{fieldname: fieldtype}
:param list: header : specify header fields if the csv file has no header
:param char: ftype: extension of the file (could be csv, xls or
xlsx)
:param dict: extra_fields: extra fields to add to the conversion
dict. In the format {fieldname: fieldtype}
:param list: header : specify header fields if the csv file has no
header
"""
super(FileParser, self).__init__(parse_name, **kwargs)
if ftype in ('csv', 'xls', 'xlsx'):
self.ftype = ftype[0:3]
else:
raise except_osv(_('User Error'),
raise except_orm(
_('User Error'),
_('Invalid file type %s. Please use csv, xls or xlsx') % ftype)
self.conversion_dict = {
'ref': unicode,
@@ -68,23 +73,17 @@ class FileParser(BankStatementImportParser):
# Set in _parse_xls, from the contents of the file
def _custom_format(self, *args, **kwargs):
"""
No other work on data are needed in this parser.
"""
"""No other work on data are needed in this parser."""
return True
def _pre(self, *args, **kwargs):
"""
No pre-treatment needed for this parser.
"""
"""No pre-treatment needed for this parser."""
return True
def _parse(self, *args, **kwargs):
"""
Launch the parsing through .csv, .xls or .xlsx depending on the
"""Launch the parsing through .csv, .xls or .xlsx depending on the
given ftype
"""
res = None
if self.ftype == 'csv':
res = self._parse_csv()
@@ -94,31 +93,27 @@ class FileParser(BankStatementImportParser):
return True
def _validate(self, *args, **kwargs):
"""
We check that all the key of the given file (means header) are present
in the validation key provided. Otherwise, we raise an Exception.
We skip the validation step if the file header is provided separately
(in the field: fieldnames).
"""We check that all the key of the given file (means header) are
present in the validation key provided. Otherwise, we raise an
Exception. We skip the validation step if the file header is provided
separately (in the field: fieldnames).
"""
if self.fieldnames is None:
parsed_cols = self.result_row_list[0].keys()
for col in self.keys_to_validate:
if col not in parsed_cols:
raise except_osv(_('Invalid data'),
raise except_orm(_('Invalid data'),
_('Column %s not present in file') % col)
return True
def _post(self, *args, **kwargs):
"""
Cast row type depending on the file format .csv or .xls after parsing the file.
"""
"""Cast row type depending on the file format .csv or .xls after
parsing the file."""
self.result_row_list = self._cast_rows(*args, **kwargs)
return True
def _parse_csv(self):
"""
:return: list of dict from csv file (line/rows)
"""
""":return: list of dict from csv file (line/rows)"""
csv_file = tempfile.NamedTemporaryFile()
csv_file.write(self.filebuffer)
csv_file.flush()
@@ -127,9 +122,7 @@ class FileParser(BankStatementImportParser):
return list(reader)
def _parse_xls(self):
"""
:return: dict of dict from xls/xlsx file (line/rows)
"""
""":return: dict of dict from xls/xlsx file (line/rows)"""
wb_file = tempfile.NamedTemporaryFile()
wb_file.write(self.filebuffer)
# We ensure that cursor is at beginig of file
@@ -144,8 +137,7 @@ class FileParser(BankStatementImportParser):
return res
def _from_csv(self, result_set, conversion_rules):
"""
Handle the converstion from the dict and handle date format from
"""Handle the converstion from the dict and handle date format from
an .csv file.
"""
for line in result_set:
@@ -156,65 +148,60 @@ class FileParser(BankStatementImportParser):
line[rule] = datetime.datetime.strptime(date_string,
'%Y-%m-%d')
except ValueError as err:
raise except_osv(_("Date format is not valid."),
raise except_orm(
_("Date format is not valid."),
_(" It should be YYYY-MM-DD for column: %s"
" value: %s \n \n"
" \n Please check the line with ref: %s"
" \n \n Detail: %s") % (rule,
line.get(rule, _('Missing')),
line.get('ref', line),
repr(err)))
" value: %s \n \n \n Please check the line with "
"ref: %s \n \n Detail: %s") %
(rule, line.get(rule, _('Missing')),
line.get('ref', line), repr(err)))
else:
try:
line[rule] = conversion_rules[rule](line[rule])
except Exception as err:
raise except_osv(_('Invalid data'),
_("Value %s of column %s is not valid."
"\n Please check the line with ref %s:"
"\n \n Detail: %s") % (line.get(rule, _('Missing')),
rule,
line.get('ref', line),
repr(err)))
raise except_orm(
_('Invalid data'),
_("Value %s of column %s is not valid.\n Please "
"check the line with ref %s:\n \n Detail: %s") %
(line.get(rule, _('Missing')), rule,
line.get('ref', line), repr(err)))
return result_set
def _from_xls(self, result_set, conversion_rules):
"""
Handle the converstion from the dict and handle date format from
"""Handle the converstion from the dict and handle date format from
an .csv, .xls or .xlsx file.
"""
for line in result_set:
for rule in conversion_rules:
if conversion_rules[rule] == datetime.datetime:
try:
t_tuple = xlrd.xldate_as_tuple(line[rule], self._datemode)
t_tuple = xlrd.xldate_as_tuple(line[rule],
self._datemode)
line[rule] = datetime.datetime(*t_tuple)
except Exception as err:
raise except_osv(_("Date format is not valid"),
raise except_orm(
_("Date format is not valid"),
_("Please modify the cell formatting to date format"
" for column: %s"
" value: %s"
"\n Please check the line with ref: %s"
"\n \n Detail: %s") % (rule,
line.get(rule, _('Missing')),
line.get('ref', line),
repr(err)))
" for column: %s value: %s\n Please check the "
"line with ref: %s\n \n Detail: %s") %
(rule, line.get(rule, _('Missing')),
line.get('ref', line), repr(err)))
else:
try:
line[rule] = conversion_rules[rule](line[rule])
except Exception as err:
raise except_osv(_('Invalid data'),
_("Value %s of column %s is not valid."
"\n Please check the line with ref %s:"
"\n \n Detail: %s") % (line.get(rule, _('Missing')),
rule,
line.get('ref', line),
repr(err)))
raise except_orm(
_('Invalid data'),
_("Value %s of column %s is not valid.\n Please "
"check the line with ref %s:\n \n Detail: %s") %
(line.get(rule, _('Missing')), rule,
line.get('ref', line), repr(err)))
return result_set
def _cast_rows(self, *args, **kwargs):
"""
Convert the self.result_row_list using the self.conversion_dict providen.
We call here _from_xls or _from_csv depending on the self.ftype variable.
"""Convert the self.result_row_list using the self.conversion_dict
providen. We call here _from_xls or _from_csv depending on the
self.ftype variable.
"""
func = getattr(self, '_from_%s' % self.ftype)
res = func(self.result_row_list, self.conversion_dict)

View File

@@ -18,32 +18,23 @@
#
##############################################################################
from openerp.tools.translate import _
import base64
import csv
import tempfile
import datetime
from file_parser import FileParser
try:
import xlrd
except:
raise Exception(_('Please install python lib xlrd'))
class GenericFileParser(FileParser):
"""
Standard parser that use a define format in csv or xls to import into a
"""Standard parser that use a define format in csv or xls to import into a
bank statement. This is mostely an example of how to proceed to create a new
parser, but will also be useful as it allow to import a basic flat file.
"""
def __init__(self, parse_name, ftype='csv', **kwargs):
super(GenericFileParser, self).__init__(parse_name, ftype=ftype, **kwargs)
super(GenericFileParser, self).__init__(
parse_name, ftype=ftype, **kwargs)
@classmethod
def parser_for(cls, parser_name):
"""
Used by the new_bank_statement_parser class factory. Return true if
"""Used by the new_bank_statement_parser class factory. Return true if
the providen name is generic_csvxls_so
"""
return parser_name == 'generic_csvxls_so'
@@ -54,9 +45,10 @@ class GenericFileParser(FileParser):
method of statement line in order to record it. It is the responsibility
of every parser to give this dict of vals, so each one can implement his
own way of recording the lines.
:param: line: a dict of vals that represent a line of result_row_list
:return: dict of values to give to the create method of statement line,
it MUST contain at least:
:param: line: a dict of vals that represent a line of
result_row_list
:return: dict of values to give to the create method of statement
line, it MUST contain at least:
{
'name':value,
'date':value,

View File

@@ -21,6 +21,7 @@
import base64
import csv
from datetime import datetime
from openerp.tools.translate import _
def UnicodeDictReader(utf8_data, **kwargs):
@@ -31,10 +32,12 @@ def UnicodeDictReader(utf8_data, **kwargs):
dialect = sniffer.sniff(sample_data, delimiters=',;\t')
csv_reader = csv.DictReader(utf8_data, dialect=dialect, **kwargs)
for row in csv_reader:
yield dict([(key, unicode(value, 'utf-8')) for key, value in row.iteritems()])
yield dict([(key, unicode(value, 'utf-8')) for key, value in
row.iteritems()])
class BankStatementImportParser(object):
"""
Generic abstract class for defining parser for different files and
format to import in a bank statement. Inherit from it to create your
@@ -42,14 +45,16 @@ class BankStatementImportParser(object):
from the FileParser instead.
"""
def __init__(self, parser_name, *args, **kwargs):
def __init__(self, profile, *args, **kwargs):
# The name of the parser as it will be called
self.parser_name = parser_name
self.parser_name = profile.import_type
# The result as a list of row. One row per line of data in the file, but
# not the commission one !
self.result_row_list = None
# The file buffer on which to work on
self.filebuffer = None
# The profile record to access its parameters in any parser method
self.profile = profile
self.balance_start = None
self.balance_end = None
self.statement_name = None
@@ -58,23 +63,19 @@ class BankStatementImportParser(object):
@classmethod
def parser_for(cls, parser_name):
"""
Override this method for every new parser, so that new_bank_statement_parser can
return the good class from his name.
"""Override this method for every new parser, so that
new_bank_statement_parser can return the good class from his name.
"""
return False
def _decode_64b_stream(self):
"""
Decode self.filebuffer in base 64 and override it
"""
"""Decode self.filebuffer in base 64 and override it"""
self.filebuffer = base64.b64decode(self.filebuffer)
return True
def _format(self, decode_base_64=True, **kwargs):
"""
Decode into base 64 if asked and Format the given filebuffer by calling
_custom_format method.
"""Decode into base 64 if asked and Format the given filebuffer by
calling _custom_format method.
"""
if decode_base_64:
self._decode_64b_stream()
@@ -82,44 +83,40 @@ class BankStatementImportParser(object):
return True
def _custom_format(self, *args, **kwargs):
"""
Implement a method in your parser to convert format, encoding and so on before
starting to work on datas. Work on self.filebuffer
"""Implement a method in your parser to convert format, encoding and so
on before starting to work on datas. Work on self.filebuffer
"""
return NotImplementedError
def _pre(self, *args, **kwargs):
"""
Implement a method in your parser to make a pre-treatment on datas before parsing
them, like concatenate stuff, and so... Work on self.filebuffer
"""Implement a method in your parser to make a pre-treatment on datas
before parsing them, like concatenate stuff, and so... Work on
self.filebuffer
"""
return NotImplementedError
def _parse(self, *args, **kwargs):
"""
Implement a method in your parser to save the result of parsing self.filebuffer
in self.result_row_list instance property.
"""Implement a method in your parser to save the result of parsing
self.filebuffer in self.result_row_list instance property.
"""
return NotImplementedError
def _validate(self, *args, **kwargs):
"""
Implement a method in your parser to validate the self.result_row_list instance
property and raise an error if not valid.
"""Implement a method in your parser to validate the
self.result_row_list instance property and raise an error if not valid.
"""
return NotImplementedError
def _post(self, *args, **kwargs):
"""
Implement a method in your parser to make some last changes on the result of parsing
the datas, like converting dates, computing commission, ...
"""Implement a method in your parser to make some last changes on the
result of parsing the datas, like converting dates, computing
commission, ...
"""
return NotImplementedError
def get_st_vals(self):
"""
This method return a dict of vals that ca be passed to
create method of statement.
"""This method return a dict of vals that ca be passed to create method
of statement.
:return: dict of vals that represent additional infos for the statement
"""
return {
@@ -130,11 +127,11 @@ class BankStatementImportParser(object):
}
def get_st_line_vals(self, line, *args, **kwargs):
"""
Implement a method in your parser that must return a dict of vals that can be
passed to create method of statement line in order to record it. It is the responsibility
of every parser to give this dict of vals, so each one can implement his
own way of recording the lines.
"""Implement a method in your parser that must return a dict of vals
that can be passed to create method of statement line in order to record
it. It is the responsibility of every parser to give this dict of vals,
so each one can implement his own way of recording the lines.
:param: line: a dict of vals that represent a line of result_row_list
:return: dict of values to give to the create method of statement line,
it MUST contain at least:
@@ -148,15 +145,14 @@ class BankStatementImportParser(object):
return NotImplementedError
def parse(self, filebuffer, *args, **kwargs):
"""
This will be the method that will be called by wizard, button and so
"""This will be the method that will be called by wizard, button and so
to parse a filebuffer by calling successively all the private method
that need to be define for each parser.
Return:
[] of rows as {'key':value}
Note: The row_list must contain only value that are present in the account.
bank.statement.line object !!!
Note: The row_list must contain only value that are present in the
account.bank.statement.line object !!!
"""
if filebuffer:
self.filebuffer = filebuffer
@@ -217,13 +213,13 @@ def itersubclasses(cls, _seen=None):
yield sub
def new_bank_statement_parser(parser_name, *args, **kwargs):
"""
Return an instance of the good parser class base on the providen name
:param char: parser_name
:return: class instance of parser_name providen.
def new_bank_statement_parser(profile, *args, **kwargs):
"""Return an instance of the good parser class based on the given profile.
:param profile: browse_record of import profile.
:return: class instance for given profile import type.
"""
for cls in itersubclasses(BankStatementImportParser):
if cls.parser_for(parser_name):
return cls(parser_name, *args, **kwargs)
if cls.parser_for(profile.import_type):
return cls(profile, *args, **kwargs)
raise ValueError

View File

@@ -20,24 +20,23 @@
##############################################################################
import sys
import traceback
from openerp.tools.translate import _
import datetime
from openerp.osv.orm import Model
from openerp.osv import fields, osv
from openerp.osv import fields, orm
from parser import new_bank_statement_parser
from openerp.tools.config import config
class AccountStatementProfil(Model):
class AccountStatementProfil(orm.Model):
_inherit = "account.statement.profile"
def get_import_type_selection(self, cr, uid, context=None):
def _get_import_type_selection(self, cr, uid, context=None):
"""This is the method to be inherited for adding the parser"""
return [('generic_csvxls_so', 'Generic .csv/.xls based on SO Name')]
def _get_import_type_selection(self, cr, uid, context=None):
return self.get_import_type_selection(cr, uid, context=context)
def __get_import_type_selection(self, cr, uid, context=None):
""" Call method which can be inherited """
return self._get_import_type_selection(cr, uid, context=context)
_columns = {
'launch_import_completion': fields.boolean(
@@ -45,10 +44,11 @@ class AccountStatementProfil(Model):
help="Tic that box to automatically launch the completion "
"on each imported file using this profile."),
'last_import_date': fields.datetime("Last Import Date"),
# we remove deprecated as it floods logs in standard/warning level sob...
# we remove deprecated as it floods logs in standard/warning level
# sob...
'rec_log': fields.text('log', readonly=True), # Deprecated
'import_type': fields.selection(
_get_import_type_selection,
__get_import_type_selection,
'Type of import',
required=True,
help="Choose here the method by which you want to import bank"
@@ -60,52 +60,51 @@ class AccountStatementProfil(Model):
}
def _write_extra_statement_lines(
self, cr, uid, parser, result_row_list, profile, statement_id, context):
self, cr, uid, parser, result_row_list, profile, statement_id,
context):
"""Insert extra lines after the main statement lines.
After the main statement lines have been created, you can override this method to create
extra statement lines.
After the main statement lines have been created, you can override this
method to create extra statement lines.
:param: browse_record of the current parser
:param: result_row_list: [{'key':value}]
:param: profile: browserecord of account.statement.profile
:param: statement_id: int/long of the current importing statement ID
:param: statement_id: int/long of the current importing
statement ID
:param: context: global context
"""
pass
def write_logs_after_import(self, cr, uid, ids, statement_id, num_lines, context):
"""
Write the log in the logger
def write_logs_after_import(self, cr, uid, ids, statement_id, num_lines,
context):
"""Write the log in the logger
:param int/long statement_id: ID of the concerned account.bank.statement
:param int/long num_lines: Number of line that have been parsed
:return: True
"""
self.message_post(cr,
uid,
ids,
body=_('Statement ID %s have been imported with %s lines.') %
(statement_id, num_lines),
context=context)
self.message_post(
cr, uid, ids,
body=_('Statement ID %s have been imported with %s '
'lines.') % (statement_id, num_lines), context=context)
return True
# Deprecated remove on V8
def prepare_statetement_lines_vals(self, *args, **kwargs):
return self.prepare_statement_lines_vals(*args, **kwargs)
def prepare_statement_lines_vals(
self, cr, uid, parser_vals,
def prepare_statement_lines_vals(self, cr, uid, parser_vals,
statement_id, context):
"""
Hook to build the values of a line from the parser returned values. At
least it fullfill the statement_id. Overide it to add your
own completion if needed.
"""Hook to build the values of a line from the parser returned values.
At least it fullfill the statement_id. Overide it to add your own
completion if needed.
:param dict of vals from parser for account.bank.statement.line (called by
parser.get_st_line_vals)
:param dict of vals from parser for account.bank.statement.line
(called by parser.get_st_line_vals)
:param int/long statement_id: ID of the concerned account.bank.statement
:return: dict of vals that will be passed to create method of statement line.
:return: dict of vals that will be passed to create method of
statement line.
"""
statement_line_obj = self.pool['account.bank.statement.line']
values = parser_vals
@@ -119,28 +118,35 @@ class AccountStatementProfil(Model):
values['period_id'] = period_memoizer[date]
else:
# This is awfully slow...
periods = self.pool.get('account.period').find(cr, uid,
dt=values.get('date'),
context=context)
periods = self.pool.get('account.period').find(
cr, uid, dt=values.get('date'), context=context)
values['period_id'] = periods[0]
period_memoizer[date] = periods[0]
values = statement_line_obj._add_missing_default_values(cr, uid, values, context)
values = statement_line_obj._add_missing_default_values(
cr, uid, values, context)
return values
def prepare_statement_vals(self, cr, uid, profile_id, result_row_list, parser, context):
"""
Hook to build the values of the statement from the parser and
def prepare_statement_vals(self, cr, uid, profile_id, result_row_list,
parser, context=None):
"""Hook to build the values of the statement from the parser and
the profile.
"""
vals = {'profile_id': profile_id}
vals.update(parser.get_st_vals())
if not vals.get('balance_start'):
# Get starting balance from journal balance if parser doesn't
# fill this data, simulating the manual flow
statement_obj = self.pool['account.bank.statement']
profile = self.browse(cr, uid, profile_id, context=context)
temp = statement_obj.onchange_journal_id(
cr, uid, None, profile.journal_id.id, context=context)
vals['balance_start'] = temp['value'].get('balance_start', False)
return vals
def multi_statement_import(self, cr, uid, ids, profile_id, file_stream,
ftype="csv", context=None):
"""
Create multiple bank statements from values given by the parser for the
givenprofile.
"""Create multiple bank statements from values given by the parser for
the given profile.
:param int/long profile_id: ID of the profile used to import the file
:param filebuffer file_stream: binary of the providen file
@@ -149,23 +155,26 @@ class AccountStatementProfil(Model):
"""
prof_obj = self.pool['account.statement.profile']
if not profile_id:
raise osv.except_osv(_("No Profile!"),
_("You must provide a valid profile to import a bank statement!"))
raise orm.except_orm(
_("No Profile!"),
_("You must provide a valid profile to import a bank "
"statement!"))
prof = prof_obj.browse(cr, uid, profile_id, context=context)
parser = new_bank_statement_parser(prof.import_type, ftype=ftype)
parser = new_bank_statement_parser(prof, ftype=ftype)
res = []
for result_row_list in parser.parse(file_stream):
statement_id = self._statement_import(cr, uid, ids, prof, parser,
file_stream, ftype=ftype, context=context)
statement_id = self._statement_import(
cr, uid, ids, prof, parser, file_stream, ftype=ftype,
context=context)
res.append(statement_id)
return res
def _statement_import(self, cr, uid, ids, prof, parser, file_stream, ftype="csv", context=None):
"""
Create a bank statement with the given profile and parser. It will fullfill the bank statement
with the values of the file providen, but will not complete data (like finding the partner, or
the right account). This will be done in a second step with the completion rules.
def _statement_import(self, cr, uid, ids, prof, parser, file_stream,
ftype="csv", context=None):
"""Create a bank statement with the given profile and parser. It will
fullfill the bank statement with the values of the file providen, but
will not complete data (like finding the partner, or the right account).
This will be done in a second step with the completion rules.
:param prof : The profile used to import the file
:param parser: the parser
@@ -173,27 +182,25 @@ class AccountStatementProfil(Model):
:param char: ftype represent the file exstension (csv by default)
:return: ID of the created account.bank.statemênt
"""
statement_obj = self.pool.get('account.bank.statement')
statement_line_obj = self.pool.get('account.bank.statement.line')
attachment_obj = self.pool.get('ir.attachment')
statement_obj = self.pool['account.bank.statement']
statement_line_obj = self.pool['account.bank.statement.line']
attachment_obj = self.pool['ir.attachment']
result_row_list = parser.result_row_list
# Check all key are present in account.bank.statement.line!!
if not result_row_list:
raise osv.except_osv(_("Nothing to import"),
raise orm.except_orm(_("Nothing to import"),
_("The file is empty"))
parsed_cols = parser.get_st_line_vals(result_row_list[0]).keys()
for col in parsed_cols:
if col not in statement_line_obj._columns:
raise osv.except_osv(_("Missing column!"),
_("Column %s you try to import is not "
"present in the bank statement line!") % col)
statement_vals = self.prepare_statement_vals(cr, uid, prof.id, result_row_list, parser, context)
statement_id = statement_obj.create(cr, uid,
statement_vals,
context=context)
raise orm.except_orm(
_("Missing column!"),
_("Column %s you try to import is not present in the bank "
"statement line!") % col)
statement_vals = self.prepare_statement_vals(
cr, uid, prof.id, result_row_list, parser, context)
statement_id = statement_obj.create(
cr, uid, statement_vals, context=context)
try:
# Record every line in the bank statement
statement_store = []
@@ -204,44 +211,44 @@ class AccountStatementProfil(Model):
context)
statement_store.append(values)
# Hack to bypass ORM poor perfomance. Sob...
statement_line_obj._insert_lines(cr, uid, statement_store, context=context)
statement_line_obj._insert_lines(
cr, uid, statement_store, context=context)
self._write_extra_statement_lines(
cr, uid, parser, result_row_list, prof, statement_id, context)
# Trigger store field computation if someone has better idea
start_bal = statement_obj.read(
cr, uid, statement_id, ['balance_start'], context=context)
start_bal = start_bal['balance_start']
statement_obj.write(cr, uid, [statement_id], {'balance_start': start_bal})
statement_obj.write(
cr, uid, [statement_id], {'balance_start': start_bal})
attachment_data = {
'name': 'statement file',
'datas': file_stream,
'datas_fname': "%s.%s" % (datetime.datetime.now().date(), ftype),
'datas_fname': "%s.%s" % (datetime.datetime.now().date(),
ftype),
'res_model': 'account.bank.statement',
'res_id': statement_id,
}
attachment_obj.create(cr, uid, attachment_data, context=context)
# If user ask to launch completion at end of import, do it!
if prof.launch_import_completion:
statement_obj.button_auto_completion(cr, uid, [statement_id], context)
statement_obj.button_auto_completion(
cr, uid, [statement_id], context)
# Write the needed log infos on profile
self.write_logs_after_import(cr, uid, prof.id,
statement_id,
len(result_row_list),
context)
except Exception:
error_type, error_value, trbk = sys.exc_info()
st = "Error: %s\nDescription: %s\nTraceback:" % (error_type.__name__, error_value)
st = "Error: %s\nDescription: %s\nTraceback:" % (
error_type.__name__, error_value)
st += ''.join(traceback.format_tb(trbk, 30))
# TODO we should catch correctly the exception with a python
# Exception and only re-catch some special exception.
# For now we avoid re-catching error in debug mode
if config['debug_mode']:
raise
raise osv.except_osv(_("Statement import error"),
raise orm.except_orm(_("Statement import error"),
_("The statement cannot be created: %s") % st)
return statement_id

View File

@@ -7,7 +7,6 @@
<field name="name">account.statement.profile.view</field>
<field name="model">account.statement.profile</field>
<field name="inherit_id" ref="account_statement_ext.statement_importer_view_form"/>
<field name="type">form</field>
<field name="arch" type="xml">
<field name="bank_statement_prefix" position="after">
<separator colspan="4" string="Import related infos"/>
@@ -31,7 +30,6 @@
<field name="name">account_bank_statement.bank_statement.view_form</field>
<field name="model">account.bank.statement</field>
<field name="inherit_id" ref="account_statement_base_completion.bank_statement_view_form" />
<field name="type">form</field>
<field eval="20" name="priority"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='line_ids']/form//field[@name='account_id']" position="attributes">

View File

@@ -22,19 +22,19 @@
import base64
import inspect
import os
from openerp.tests import common
class test_coda_import(common.TransactionCase):
class TestCodaImport(common.TransactionCase):
def prepare(self):
self.company_a = self.browse_ref('base.main_company')
self.profile_obj = self.registry("account.statement.profile")
self.account_bank_statement_obj = self.registry("account.bank.statement")
# create the 2009 fiscal year since imported coda file reference statement lines in 2009
self.account_bank_statement_obj = self.registry(
"account.bank.statement")
# create the 2009 fiscal year since imported coda file reference
# statement lines in 2009
self.fiscalyear_id = self._create_fiscalyear("2011", self.company_a.id)
self.account_id = self.ref("account.a_recv")
self.journal_id = self.ref("account.bank_journal")
self.import_wizard_obj = self.registry('credit.statement.import')
@@ -71,15 +71,19 @@ class test_coda_import(common.TransactionCase):
'input_statement': base64.b64encode(content),
'file_name': os.path.basename(file_name),
})
res = self.import_wizard_obj.import_statement(self.cr, self.uid, wizard_id)
statement_id = self.account_bank_statement_obj.search(self.cr, self.uid, eval(res['domain']))
return self.account_bank_statement_obj.browse(self.cr, self.uid, statement_id)[0]
res = self.import_wizard_obj.import_statement(
self.cr, self.uid, wizard_id)
statement_id = self.account_bank_statement_obj.search(
self.cr, self.uid, eval(res['domain']))
return self.account_bank_statement_obj.browse(
self.cr, self.uid, statement_id)[0]
def test_simple_xls(self):
"""Test import from xls
"""
self.prepare()
file_name = self._filename_to_abs_filename(os.path.join("..", "data", "statement.xls"))
file_name = self._filename_to_abs_filename(
os.path.join("..", "data", "statement.xls"))
statement = self._import_file(file_name)
self._validate_imported_satement(statement)
@@ -87,7 +91,8 @@ class test_coda_import(common.TransactionCase):
"""Test import from csv
"""
self.prepare()
file_name = self._filename_to_abs_filename(os.path.join("..", "data", "statement.csv"))
file_name = self._filename_to_abs_filename(
os.path.join("..", "data", "statement.csv"))
statement = self._import_file(file_name)
self._validate_imported_satement(statement)

View File

@@ -36,12 +36,15 @@ class CreditPartnerStatementImporter(orm.TransientModel):
if context is None:
context = {}
res = {}
if (context.get('active_model', False) == 'account.statement.profile' and
if (context.get('active_model', False) ==
'account.statement.profile' and
context.get('active_ids', False)):
ids = context['active_ids']
assert len(ids) == 1, 'You cannot use this on more than one profile !'
assert len(
ids) == 1, 'You cannot use this on more than one profile !'
res['profile_id'] = ids[0]
other_vals = self.onchange_profile_id(cr, uid, [], res['profile_id'], context=context)
other_vals = self.onchange_profile_id(
cr, uid, [], res['profile_id'], context=context)
res.update(other_vals.get('value', {}))
return res
@@ -55,8 +58,8 @@ class CreditPartnerStatementImporter(orm.TransientModel):
'journal_id': fields.many2one('account.journal',
'Financial journal to use transaction'),
'file_name': fields.char('File Name', size=128),
'receivable_account_id': fields.many2one('account.account',
'Force Receivable/Payable Account'),
'receivable_account_id': fields.many2one(
'account.account', 'Force Receivable/Payable Account'),
'force_partner_on_bank': fields.boolean(
'Force partner on bank move',
help="Tic that box if you want to use the credit insitute partner "
@@ -71,12 +74,12 @@ class CreditPartnerStatementImporter(orm.TransientModel):
def onchange_profile_id(self, cr, uid, ids, profile_id, context=None):
res = {}
if profile_id:
c = self.pool.get("account.statement.profile").browse(
c = self.pool["account.statement.profile"].browse(
cr, uid, profile_id, context=context)
res = {'value':
{'partner_id': c.partner_id and c.partner_id.id or False,
'journal_id': c.journal_id and c.journal_id.id or False,
'receivable_account_id': c.receivable_account_id and c.receivable_account_id.id or False,
'receivable_account_id': c.receivable_account_id.id,
'force_partner_on_bank': c.force_partner_on_bank,
'balance_check': c.balance_check,
}
@@ -110,7 +113,8 @@ class CreditPartnerStatementImporter(orm.TransientModel):
)
model_obj = self.pool.get('ir.model.data')
action_obj = self.pool.get('ir.actions.act_window')
action_id = model_obj.get_object_reference(cr, uid, 'account', 'action_bank_statement_tree')[1]
action_id = model_obj.get_object_reference(
cr, uid, 'account', 'action_bank_statement_tree')[1]
res = action_obj.read(cr, uid, action_id)
res['domain'] = res['domain'][:-1] + ",('id', 'in', %s)]" % sid
return res

View File

@@ -4,7 +4,6 @@
<record id="statement_importer_view" model="ir.ui.view">
<field name="name">credit.statement.import.config.view</field>
<field name="model">credit.statement.import</field>
<field name="type">form</field>
<field name="arch" type="xml">
<form string="Import statement">
<group colspan="4" >

View File

@@ -31,8 +31,9 @@
'account_statement_base_import'
],
'description': """
This module brings commission support to bank statement imports. It computes the sum of a commission
field on each transaction and creates a statement entry for it.
This module brings commission support to bank statement imports. It computes the
sum of a commission field on each transaction and creates a statement entry for
it.
""",
'website': 'http://www.camptocamp.com',
'data': [

View File

@@ -10,18 +10,18 @@ def float_or_zero(val):
class AccountStatementProfil(orm.Model):
_inherit = "account.statement.profile"
def _write_extra_statement_lines(
self, cr, uid, parser, result_row_list, profile, statement_id, context):
"""Prepare the global commission line if there is one.
"""
def _write_extra_statement_lines(self, cr, uid, parser, result_row_list,
profile, statement_id, context=None):
"""Prepare the global commission line if there is one."""
global_commission_amount = 0
for row in parser.result_row_list:
global_commission_amount += float_or_zero(row.get('commission_amount', '0.0'))
global_commission_amount += float_or_zero(
row.get('commission_amount', '0.0'))
if not global_commission_amount:
return
partner_id = profile.partner_id and profile.partner_id.id or False
commission_account_id = profile.commission_account_id and profile.commission_account_id.id or False
commission_analytic_id = profile.commission_analytic_id and profile.commission_analytic_id.id or False
partner_id = profile.partner_id.id
commission_account_id = profile.commission_account_id.id
commission_analytic_id = profile.commission_analytic_id.id
comm_values = {
'name': 'IN ' + _('Commission line'),
'date': parser.get_st_vals().get('date') or datetime.datetime.now(),
@@ -32,11 +32,12 @@ class AccountStatementProfil(orm.Model):
'account_id': commission_account_id,
'ref': 'commission',
'analytic_account_id': commission_analytic_id,
# !! We set the already_completed so auto-completion will not update those values!
# !! We set the already_completed so auto-completion will not update
# those values!
'already_completed': True,
}
statement_line_obj = self.pool.get('account.bank.statement.line')
statement_line_obj.create(cr, uid, comm_values, context=context)
st_obj = self.pool['account.bank.statement.line']
st_obj.create(cr, uid, comm_values, context=context)
class AccountStatementLineWithCommission(orm.Model):
@@ -53,20 +54,18 @@ class CreditPartnerStatementImporter(orm.TransientModel):
_inherit = "credit.statement.import"
_columns = {
'commission_account_id': fields.many2one('account.account',
'Commission account'),
'commission_analytic_id': fields.many2one('account.analytic.account',
'Commission analytic account'),
'commission_account_id': fields.many2one(
'account.account', 'Commission account'),
'commission_analytic_id': fields.many2one(
'account.analytic.account', 'Commission analytic account'),
}
def onchange_profile_id(self, cr, uid, ids, profile_id, context=None):
res = super(CreditPartnerStatementImporter, self).onchange_profile_id(
cr, uid, ids, profile_id, context=context)
if profile_id:
c = self.pool.get("account.statement.profile").browse(
p = self.pool["account.statement.profile"].browse(
cr, uid, profile_id, context=context)
res['value']['commission_account_id'] = \
c.commission_account_id and c.commission_account_id.id or False
res['value']['commission_a'] = \
c.commission_analytic_id and c.commission_analytic_id.id or False
res['value']['commission_account_id'] = p.commission_account_id.id
res['value']['commission_a'] = p.commission_analytic_id.id
return res

View File

@@ -4,7 +4,6 @@
<record id="statement_importer_view" model="ir.ui.view">
<field name="name">credit.statement.import.config.view</field>
<field name="model">credit.statement.import</field>
<field name="type">form</field>
<field name="inherit_id" ref="account_statement_base_import.statement_importer_view" />
<field name="arch" type="xml">
<xpath expr="/form/group/field[@name='journal_id']" position="after">

View File

@@ -6,7 +6,6 @@
<field name="name">account_bank_statement.bank_statement.view_form</field>
<field name="model">account.bank.statement</field>
<field name="inherit_id" ref="account_statement_base_completion.bank_statement_view_form" />
<field name="type">form</field>
<field eval="20" name="priority"/>
<field name="arch" type="xml">
<data>

View File

@@ -22,4 +22,3 @@
from . import partner
from . import statement

View File

@@ -26,10 +26,12 @@
'category': 'Generic Modules/Others',
'license': 'AGPL-3',
'description': """
Improve the basic rule "Match from statement line label (based on partner field 'Bank Statement Label')" provided by the
Bank statement base completion module. The goal is to match the label field from the bank statement line with a partner and
an account.
For this, you have to create your record in the new class account.statement.label where you can link the label you want with a
Improve the basic rule "Match from statement line label (based on partner
field 'Bank Statement Label')" provided by the Bank statement base
completion module. The goal is to match the label field from the bank
statement line with a partner and an account.
For this, you have to create your record in the new class
account.statement.label where you can link the label you want with a
partner and an account.
""",
@@ -46,4 +48,3 @@
'installable': False,
'active': False,
}

View File

@@ -11,7 +11,6 @@
<record id="bk_view_partner_form" model="ir.ui.view">
<field name="name">account_bank_statement_import.view.partner.form</field>
<field name="model">res.partner</field>
<field name="type">form</field>
<field name="priority">20</field>
<field name="inherit_id" ref="account_statement_base_completion.bk_view_partner_form"/>
<field name="arch" type="xml">

View File

@@ -22,14 +22,16 @@
from openerp.osv import fields, orm
from collections import defaultdict
from openerp.addons.account_statement_base_completion.statement import ErrorTooManyPartner
from openerp.tools.translate import _
from openerp.addons.account_statement_base_completion.statement import \
ErrorTooManyPartner
class ErrorTooManyLabel(Exception):
"""New Exception definition that is raised when more than one label is
matched by the completion rule.
"""
New Exception definition that is raised when more than one label is matched
by the completion rule.
"""
def __init__(self, value):
self.value = value
@@ -38,8 +40,7 @@ class ErrorTooManyLabel(Exception):
class AccountBankSatement(orm.Model):
"""
We add a basic button and stuff to support the auto-completion
"""We add a basic button and stuff to support the auto-completion
of the bank statement once line have been imported or manually fullfill.
"""
_inherit = "account.bank.statement"
@@ -60,8 +61,7 @@ class AccountStatementCompletionRule(orm.Model):
_inherit = "account.statement.completion.rule"
def get_from_label_and_partner_field(self, cr, uid, st_line, context=None):
"""
Match the partner and the account based on the name field of the
"""Match the partner and the account based on the name field of the
statement line and the table account.statement.label.
If more than one statement label matched, raise the ErrorTooManylabel
error.
@@ -75,7 +75,7 @@ class AccountStatementCompletionRule(orm.Model):
...}
"""
st_obj = self.pool.get('account.bank.statement')
st_obj = self.pool['account.bank.statement']
statement = st_obj.browse(cr, uid, st_line['statement_id'][0],
context=context)
res = {}
@@ -99,14 +99,14 @@ class AccountStatementCompletionRule(orm.Model):
st_l.id = %s
""", (line.id,))
for partner, account in cr.fetchall():
context['label_memorizer'][line.id].append({'partner_id': partner,
'account_id': account})
context['label_memorizer'][line.id].append(
{'partner_id': partner, 'account_id': account})
if st_line['id'] in context['label_memorizer']:
label_info = context['label_memorizer'][st_line['id']]
if len(label_info) > 1:
raise ErrorTooManyPartner(_('Line named "%s" (Ref:%s) was matched by '
'more than one statement label.') %
(st_line['name'], st_line['ref']))
raise ErrorTooManyPartner(
_('Line named "%s" (Ref:%s) was matched by more than one '
'statement label.') % (st_line['name'], st_line['ref']))
if label_info[0]['partner_id']:
res['partner_id'] = label_info[0]['partner_id']
res['account_id'] = label_info[0]['account_id']
@@ -118,7 +118,6 @@ class AccountStatementLabel(orm.Model):
and a specific account
"""
_name = "account.statement.label"
_description = "Account Statement Label"
_columns = {
@@ -140,9 +139,8 @@ class AccountStatementLabel(orm.Model):
_defaults = {
'company_id': lambda s, cr, uid, c:
s.pool.get('res.company')._company_default_get(cr, uid,
'account.statement.label',
context=c),
s.pool.get('res.company')._company_default_get(
cr, uid, 'account.statement.label', context=c),
}
_sql_constraints = [

View File

@@ -30,7 +30,8 @@
'account_voucher'
],
'description': """
This module is only needed when using account_statement_base_completion with voucher in order adapt the view correctly.
This module is only needed when using account_statement_base_completion with
voucher in order adapt the view correctly.
""",
'website': 'http://www.camptocamp.com',
'init_xml': [],

View File

@@ -29,14 +29,14 @@
'report_webkit',
'account_voucher'],
'description': """
Improve the basic bank statement, by adding various new features,
and help dealing with huge volume of reconciliation through payment offices such as Paypal, Lazer,
Visa, Amazon...
Improve the basic bank statement, by adding various new features, and help
dealing with huge volume of reconciliation through payment offices such as
Paypal, Lazer, Visa, Amazon...
It is mostly used for E-commerce but can be useful for other use cases as it introduces a
notion of profile on the bank statement to have more control on the generated entries. It serves as
a base for all new features developped to improve the reconciliation process (see our other
set of modules:
It is mostly used for E-commerce but can be useful for other use cases as it
introduces a notion of profile on the bank statement to have more control on
the generated entries. It serves as a base for all new features developped to
improve the reconciliation process (see our other set of modules:
* account_statement_base_completion
* account_statement_base_import
@@ -44,33 +44,38 @@
Features:
1) Improve the bank statement: allows to define profiles (for each
Office or Bank). The bank statement will then generate the entries based on some criteria chosen
in the selected profile. You can setup on the profile:
1) Improve the bank statement: allows to define profiles (for each Office or
Bank). The bank statement will then generate the entries based on some criteria
chosen in the selected profile. You can setup on the profile:
- the journal to use
- use balance check or not
- account commission and Analytic account for commission
- partner concerned by the profile (used in commission and optionaly on generated credit move)
- use a specific credit account (instead of the receivalble/payable default one)
- force Partner on the counter-part move (e.g. 100.- debit, Partner: M.Martin; 100.- credit, Partner: HSBC)
- partner concerned by the profile (used in commission and optionaly on
generated credit move)
- use a specific credit account (instead of the receivalble/payable default
one)
- force Partner on the counter-part move (e.g. 100.- debit, Partner: M.
Martin; 100.- credit, Partner: HSBC)
2) Add a report on bank statement that can be used for checks remittance
3) When an error occurs in a bank statement confirmation, go through all line anyway and summarize
all the erronous line in a same popup instead of raising and crashing on every step.
3) When an error occurs in a bank statement confirmation, go through all line
anyway and summarize all the erronous line in a same popup instead of
raising and crashing on every step.
4) Remove the period on the bank statement, and compute it for each line based on their date instead.
It also adds this feature in the voucher in order to compute the period correctly.
4) Remove the period on the bank statement, and compute it for each line based
on their date instead. It also adds this feature in the voucher in order to
compute the period correctly.
5) Cancelling a bank statement is much more easy and will cancel all related entries, unreconcile them,
and finally delete them.
6) Add the ID in entries view so that you can easily filter on a statement ID to reconcile all related
entries at once (e.g. one statement (ID 100) for paypal on an intermediate account, and then another for
the bank on the bank account. You can then manually reconcile all the line from the first one with
one line of the second by finding them through the statement ID.)
5) Cancelling a bank statement is much more easy and will cancel all related
entries, unreconcile them, and finally delete them.
6) Add the ID in entries view so that you can easily filter on a statement ID
to reconcile all related entries at once (e.g. one statement (ID 100) for
Paypal on an intermediate account, and then another for the bank on the
bank account. You can then manually reconcile all the line from the first
one with one line of the second by finding them through the statement ID.)
""",
'website': 'http://www.camptocamp.com',
'data': ['statement_view.xml',

View File

@@ -19,23 +19,21 @@
#
##############################################################################
from openerp.osv.orm import Model
from openerp.osv import fields
from openerp.osv import orm
class account_move(Model):
class AccountMove(orm.Model):
_inherit = 'account.move'
def unlink(self, cr, uid, ids, context=None):
"""
Delete the reconciliation when we delete the moves. This
"""Delete the reconciliation when we delete the moves. This
allow an easier way of cancelling the bank statement.
"""
reconcile_to_delete = []
reconcile_obj = self.pool.get('account.move.reconcile')
reconcile_obj = self.pool['account.move.reconcile']
for move in self.browse(cr, uid, ids, context=context):
for move_line in move.line_id:
if move_line.reconcile_id:
reconcile_to_delete.append(move_line.reconcile_id.id)
reconcile_obj.unlink(cr, uid, reconcile_to_delete, context=context)
return super(account_move, self).unlink(cr, uid, ids, context=context)
return super(AccountMove, self).unlink(cr, uid, ids, context=context)

View File

@@ -28,15 +28,18 @@ from openerp.addons.report_webkit import webkit_report
class BankStatementWebkit(report_sxw.rml_parse):
def __init__(self, cr, uid, name, context):
super(BankStatementWebkit, self).__init__(cr, uid, name, context=context)
super(BankStatementWebkit, self).__init__(
cr, uid, name, context=context)
self.pool = pooler.get_pool(self.cr.dbname)
self.cursor = self.cr
company = self.pool.get('res.users').browse(
self.cr, uid, uid, context=context).company_id
header_report_name = ' - '.join((_('BORDEREAU DE REMISE DE CHEQUES'),
header_report_name = ' - '.join((
_('BORDEREAU DE REMISE DE CHEQUES'),
company.name, company.currency_id.name))
footer_date_time = self.formatLang(str(datetime.today())[:19], date_time=True)
footer_date_time = self.formatLang(
str(datetime.today())[:19], date_time=True)
self.localcontext.update({
'cr': cr,
'uid': uid,
@@ -50,7 +53,8 @@ class BankStatementWebkit(report_sxw.rml_parse):
('--header-left', header_report_name),
('--header-spacing', '2'),
('--footer-left', footer_date_time),
('--footer-right', ' '.join((_('Page'), '[page]', _('of'), '[topage]'))),
('--footer-right',
' '.join((_('Page'), '[page]', _('of'), '[topage]'))),
('--footer-line',),
],
})
@@ -65,7 +69,7 @@ class BankStatementWebkit(report_sxw.rml_parse):
self.cr, self.uid, statement_line_ids)
return statement_lines
webkit_report.WebKitParser('report.bank_statement_webkit',
'account.bank.statement',
webkit_report.WebKitParser(
'report.bank_statement_webkit', 'account.bank.statement',
'addons/account_statement_ext/report/bank_statement_report.mako',
parser=BankStatementWebkit)

View File

@@ -19,8 +19,7 @@
#
##############################################################################
import openerp.addons.account.account_bank_statement as stat_mod
from openerp.osv.orm import Model
from openerp.osv import fields, osv
from openerp.osv import fields, orm
from openerp.tools.translate import _
@@ -29,8 +28,8 @@ def fixed_write(self, cr, uid, ids, vals, context=None):
""" Fix performance desing of original function
Ideally we should use a real PostgreSQL sequence or serial fields.
I will do it when I have time."""
res = super(stat_mod.account_bank_statement, self).write(cr, uid, ids,
vals, context=context)
res = super(stat_mod.account_bank_statement, self).write(
cr, uid, ids, vals, context=context)
if ids: # will be false for an new empty bank statement
cr.execute("UPDATE account_bank_statement_line"
" SET sequence = account_bank_statement_line.id + 1"
@@ -39,64 +38,56 @@ def fixed_write(self, cr, uid, ids, vals, context=None):
stat_mod.account_bank_statement.write = fixed_write
class AccountStatementProfile(Model):
"""
A Profile will contain all infos related to the type of
class AccountStatementProfile(orm.Model):
"""A Profile will contain all infos related to the type of
bank statement, and related generated entries. It defines the
journal to use, the partner and commision account and so on.
"""
_name = "account.statement.profile"
_inherit = ['mail.thread']
_description = "Statement Profile"
_order = 'sequence'
_columns = {
'name': fields.char('Name', required=True),
'sequence': fields.integer('Sequence', help="Gives a sequence in lists, the first profile will be used as default"),
'sequence': fields.integer(
'Sequence',
help="Gives a sequence in lists, the first profile will be used as "
"default"),
'partner_id': fields.many2one(
'res.partner',
'Bank/Payment Office partner',
help="Put a partner if you want to have it on the "
"commission move (and optionaly on the counterpart "
"of the intermediate/banking move if you tick the "
"corresponding checkbox)."),
help="Put a partner if you want to have it on the commission move "
"(and optionaly on the counterpart of the intermediate/"
"banking move if you tick the corresponding checkbox)."),
'journal_id': fields.many2one(
'account.journal',
'Financial journal to use for transaction',
required=True),
'commission_account_id': fields.many2one(
'account.account',
'Commission account',
required=True),
'commission_analytic_id': fields.many2one(
'account.analytic.account',
'Commission analytic account'),
'receivable_account_id': fields.many2one(
'account.account',
'Force Receivable/Payable Account',
help="Choose a receivable account to force the default "
"debit/credit account (eg. an intermediat bank account "
"instead of default debitors)."),
'force_partner_on_bank': fields.boolean(
'Force partner on bank move',
help="Tick that box if you want to use the credit "
"institute partner in the counterpart of the "
"intermediate/banking move."),
'balance_check': fields.boolean(
'Balance check',
help="Tick that box if you want OpenERP to control "
"the start/end balance before confirming a bank statement. "
"If don't ticked, no balance control will be done."),
'bank_statement_prefix': fields.char('Bank Statement Prefix', size=32),
'bank_statement_ids': fields.one2many('account.bank.statement',
'profile_id',
'Bank Statement Imported'),
@@ -110,53 +101,50 @@ class AccountStatementProfile(Model):
return True
_constraints = [
(_check_partner, "You need to put a partner if you tic the 'Force partner on bank move'!", []),
(_check_partner,
"You need to put a partner if you tic the 'Force partner on bank "
"move'!", []),
]
_sql_constraints = [
('name_uniq', 'unique (name, company_id)', 'The name of the bank statement must be unique !')
('name_uniq', 'unique (name, company_id)',
'The name of the bank statement must be unique !')
]
class AccountBankStatement(Model):
"""
We improve the bank statement class mostly for :
class AccountBankStatement(orm.Model):
"""We improve the bank statement class mostly for :
- Removing the period and compute it from the date of each line.
- Allow to remove the balance check depending on the chosen profile
- Report errors on confirmation all at once instead of crashing onr by one
- Add a profile notion that can change the generated entries on statement
confirmation.
For this, we had to override quite some long method and we'll need to maintain
them up to date. Changes are point up by '#Chg' comment.
For this, we had to override quite some long method and we'll need to
maintain them up to date. Changes are point up by '#Chg' comment.
"""
_inherit = "account.bank.statement"
def _default_period(self, cr, uid, context=None):
"""
Statement default period
"""
"""Statement default period"""
if context is None:
context = {}
period_obj = self.pool.get('account.period')
periods = period_obj.find(cr, uid, dt=context.get('date'), context=context)
periods = period_obj.find(
cr, uid, dt=context.get('date'), context=context)
return periods and periods[0] or False
def _default_profile(self, cr, uid, context=None):
"""
Returns the default statement profile
"""Returns the default statement profile
Default profile is the one with the lowest sequence of user's company
:return profile_id or False
"""
if context is None:
context = {}
user_obj = self.pool.get('res.users')
profile_obj = self.pool.get('account.statement.profile')
user_obj = self.pool['res.users']
profile_obj = self.pool['account.statement.profile']
user = user_obj.browse(cr, uid, uid, context=context)
profile_ids = profile_obj.search(cr, uid, [('company_id', '=', user.company_id.id)], context=context)
profile_ids = profile_obj.search(
cr, uid, [('company_id', '=', user.company_id.id)], context=context)
return profile_ids[0] if profile_ids else False
def _get_statement_from_profile(self, cr, uid, profile_ids, context=None):
@@ -166,7 +154,6 @@ class AccountBankStatement(Model):
when the ORM calls this, self is an account.statement.profile.
Returns a list of account.bank.statement ids to recompute.
"""
triggered = []
for profile in self.browse(cr, uid, profile_ids, context=context):
@@ -232,28 +219,28 @@ class AccountBankStatement(Model):
}
def create(self, cr, uid, vals, context=None):
"""Need to pass the journal_id in vals anytime because of account.cash.statement
need it."""
"""Need to pass the journal_id in vals anytime because of
account.cash.statement need it."""
if 'profile_id' in vals:
profile_obj = self.pool.get('account.statement.profile')
profile = profile_obj.browse(cr, uid, vals['profile_id'], context=context)
profile_obj = self.pool['account.statement.profile']
profile = profile_obj.browse(
cr, uid, vals['profile_id'], context=context)
vals['journal_id'] = profile.journal_id.id
return super(AccountBankStatement, self
).create(cr, uid, vals, context=context)
return super(AccountBankStatement, self).create(
cr, uid, vals, context=context)
def _get_period(self, cr, uid, date, context=None):
"""Return matching period for a date."""
if context is None:
context = {}
period_obj = self.pool.get('account.period')
period_obj = self.pool['account.period']
local_context = context.copy()
local_context['account_period_prefer_normal'] = True
periods = period_obj.find(cr, uid, dt=date, context=local_context)
return periods and periods[0] or False
def _check_company_id(self, cr, uid, ids, context=None):
"""
Adapt this constraint method from the account module to reflect the
"""Adapt this constraint method from the account module to reflect the
move of period_id to the statement line
"""
for statement in self.browse(cr, uid, ids, context=context):
@@ -278,18 +265,18 @@ class AccountBankStatement(Model):
]
def _prepare_move(self, cr, uid, st_line, st_line_number, context=None):
"""Add the period_id from the statement line date to the move preparation.
Originaly, it was taken from the statement period_id
:param browse_record st_line: account.bank.statement.line record to
create the move from.
:param char st_line_number: will be used as the name of the generated account move
"""Add the period_id from the statement line date to the move
preparation. Originaly, it was taken from the statement period_id
:param browse_record st_line: account.bank.statement.line record
to create the move from.
:param char st_line_number: will be used as the name of the
generated account move
:return: dict of value to create() the account.move
"""
if context is None:
context = {}
res = super(AccountBankStatement, self
)._prepare_move(cr, uid, st_line, st_line_number,
context=context)
res = super(AccountBankStatement, self)._prepare_move(
cr, uid, st_line, st_line_number, context=context)
ctx = context.copy()
ctx['company_id'] = st_line.company_id.id
period_id = self._get_period(cr, uid, st_line.date, context=ctx)
@@ -300,19 +287,23 @@ class AccountBankStatement(Model):
self, cr, uid, st_line, move_id, debit, credit, currency_id=False,
amount_currency=False, account_id=False, analytic_id=False,
partner_id=False, context=None):
"""Add the period_id from the statement line date to the move preparation.
Originaly, it was taken from the statement period_id
"""Add the period_id from the statement line date to the move
preparation. Originaly, it was taken from the statement period_id
:param browse_record st_line: account.bank.statement.line record to
create the move from.
:param int/long move_id: ID of the account.move to link the move line
:param browse_record st_line: account.bank.statement.line record
to create the move from.
:param int/long move_id: ID of the account.move to link the move
line
:param float debit: debit amount of the move line
:param float credit: credit amount of the move line
:param int/long currency_id: ID of currency of the move line to create
:param float amount_currency: amount of the debit/credit expressed in the currency_id
:param int/long account_id: ID of the account to use in the move line if different
from the statement line account ID
:param int/long analytic_id: ID of analytic account to put on the move line
:param int/long currency_id: ID of currency of the move line to
create
:param float amount_currency: amount of the debit/credit expressed
in the currency_id
:param int/long account_id: ID of the account to use in the move
line if different from the statement line account ID
:param int/long analytic_id: ID of analytic account to put on the
move line
:param int/long partner_id: ID of the partner to put on the move line
:return: dict of value to create() the account.move.line
"""
@@ -332,12 +323,13 @@ class AccountBankStatement(Model):
return res
def _get_counter_part_partner(self, cr, uid, st_line, context=None):
"""
We change the move line generated from the lines depending on the profile:
- If partner_id is set and force_partner_on_bank is ticked, we'll let the partner of each line
for the debit line, but we'll change it on the credit move line for the choosen partner_id
=> This will ease the reconciliation process with the bank as the partner will match the bank
statement line
"""We change the move line generated from the lines depending on the
profile:
- If partner_id is set and force_partner_on_bank is ticked, we'll let
the partner of each line for the debit line, but we'll change it on
the credit move line for the choosen partner_id
=> This will ease the reconciliation process with the bank as the
partner will match the bank statement line
:param browse_record st_line: account.bank.statement.line record to
create the move from.
:return: int/long of the res.partner to use as counterpart
@@ -351,38 +343,38 @@ class AccountBankStatement(Model):
return bank_partner_id
def _get_st_number_period_profile(self, cr, uid, date, profile_id):
"""
Retrieve the name of bank statement from sequence, according to the period
corresponding to the date passed in args. Add a prefix if set in the profile.
"""Retrieve the name of bank statement from sequence, according to the
period corresponding to the date passed in args. Add a prefix if set in
the profile.
:param: date: date of the statement used to compute the right period
:param: int/long: profile_id: the account.statement.profile ID from which to take the
bank_statement_prefix for the name
:param: int/long: profile_id: the account.statement.profile ID from
which to take the bank_statement_prefix for the name
:return: char: name of the bank statement (st_number)
"""
year = self.pool.get('account.period').browse(
year = self.pool['account.period'].browse(
cr, uid, self._get_period(cr, uid, date)).fiscalyear_id.id
profile = self.pool.get('account.statement.profile').browse(cr, uid, profile_id)
profile = self.pool.get(
'account.statement.profile').browse(cr, uid, profile_id)
c = {'fiscalyear_id': year}
obj_seq = self.pool.get('ir.sequence')
obj_seq = self.pool['ir.sequence']
journal_sequence_id = (profile.journal_id.sequence_id and
profile.journal_id.sequence_id.id or False)
if journal_sequence_id:
st_number = obj_seq.next_by_id(cr, uid, journal_sequence_id, context=c)
st_number = obj_seq.next_by_id(
cr, uid, journal_sequence_id, context=c)
else:
st_number = obj_seq.next_by_code(cr, uid, 'account.bank.statement', context=c)
st_number = obj_seq.next_by_code(
cr, uid, 'account.bank.statement', context=c)
if profile.bank_statement_prefix:
st_number = profile.bank_statement_prefix + st_number
return st_number
def button_confirm_bank(self, cr, uid, ids, context=None):
"""
Completely override the method in order to have
an error message which displays all the messages
instead of having them pop one by one.
We have to copy paste a big block of code, changing the error
stack + managing period from date.
"""Completely override the method in order to have an error message
which displays all the messages instead of having them pop one by one.
We have to copy paste a big block of code, changing the error stack +
managing period from date.
TODO: Log the error in a bank statement field instead of using a popup!
"""
@@ -390,73 +382,78 @@ class AccountBankStatement(Model):
j_type = st.journal_id.type
company_currency_id = st.journal_id.company_id.currency_id.id
if not self.check_status_condition(cr, uid, st.state, journal_type=j_type):
if not self.check_status_condition(cr, uid, st.state,
journal_type=j_type):
continue
self.balance_check(cr, uid, st.id, journal_type=j_type, context=context)
self.balance_check(
cr, uid, st.id, journal_type=j_type, context=context)
if (not st.journal_id.default_credit_account_id) \
or (not st.journal_id.default_debit_account_id):
raise osv.except_osv(_('Configuration Error!'),
_('Please verify that an account is defined in the journal.'))
raise orm.except_orm(
_('Configuration Error!'),
_('Please verify that an account is defined in the '
'journal.'))
if not st.name == '/':
st_number = st.name
else:
# Begin Changes
st_number = self._get_st_number_period_profile(cr, uid, st.date, st.profile_id.id)
# End Changes
st_number = self._get_st_number_period_profile(
cr, uid, st.date, st.profile_id.id)
for line in st.move_line_ids:
if line.state != 'valid':
raise osv.except_osv(_('Error!'),
raise orm.except_orm(
_('Error!'),
_('The account entries lines are not in valid state.'))
# begin changes
errors_stack = []
for st_line in st.line_ids:
try:
if st_line.analytic_account_id:
if not st.journal_id.analytic_journal_id:
raise osv.except_osv(_('No Analytic Journal!'),
_("You have to assign an analytic"
" journal on the '%s' journal!") % st.journal_id.name)
raise orm.except_orm(
_('No Analytic Journal!'),
_("You have to assign an analytic journal on "
"the '%s' journal!") % st.journal_id.name)
if not st_line.amount:
continue
st_line_number = self.get_next_st_line_number(cr, uid, st_number, st_line, context)
self.create_move_from_st_line(cr, uid, st_line.id,
company_currency_id,
st_line_number,
context)
except osv.except_osv, exc:
msg = "Line ID %s with ref %s had following error: %s" % (st_line.id, st_line.ref, exc.value)
st_line_number = self.get_next_st_line_number(
cr, uid, st_number, st_line, context)
self.create_move_from_st_line(
cr, uid, st_line.id, company_currency_id,
st_line_number, context)
except orm.except_orm, exc:
msg = "Line ID %s with ref %s had following error: %s" % (
st_line.id, st_line.ref, exc.value)
errors_stack.append(msg)
except Exception, exc:
msg = "Line ID %s with ref %s had following error: %s" % (st_line.id, st_line.ref, str(exc))
msg = "Line ID %s with ref %s had following error: %s" % (
st_line.id, st_line.ref, str(exc))
errors_stack.append(msg)
if errors_stack:
msg = u"\n".join(errors_stack)
raise osv.except_osv(_('Error'), msg)
# end changes
raise orm.except_orm(_('Error'), msg)
self.write(cr, uid, [st.id],
{'name': st_number,
'balance_end_real': st.balance_end},
context=context)
body = _('Statement %s confirmed, journal items were created.') % st_number
body = _('Statement %s confirmed, journal items were '
'created.') % st_number
self.message_post(cr, uid, [st.id],
body,
context=context)
return self.write(cr, uid, ids, {'state': 'confirm'}, context=context)
def get_account_for_counterpart(self, cr, uid, amount, account_receivable, account_payable):
def get_account_for_counterpart(self, cr, uid, amount, account_receivable,
account_payable):
"""For backward compatibility."""
account_id, type = self.get_account_and_type_for_counterpart(cr, uid, amount,
account_receivable,
account_payable)
account_id, type = self.get_account_and_type_for_counterpart(
cr, uid, amount, account_receivable, account_payable)
return account_id
def _compute_type_from_partner_profile(self, cr, uid, partner_id,
default_type, context=None):
"""Compute the statement line type
from partner profile (customer, supplier)"""
obj_partner = self.pool.get('res.partner')
obj_partner = self.pool['res.partner']
part = obj_partner.browse(cr, uid, partner_id, context=context)
if part.supplier == part.customer:
return default_type
@@ -476,71 +473,83 @@ class AccountBankStatement(Model):
def get_type_for_counterpart(self, cr, uid, amount, partner_id=False):
"""Give the amount and receive the type to use for the line.
The rules are:
- If the customer checkbox is checked on the found partner, type customer
- If the supplier checkbox is checked on the found partner, typewill be supplier
- If both checkbox are checked or none of them, it'll be based on the amount :
- If the customer checkbox is checked on the found partner, type
customer
- If the supplier checkbox is checked on the found partner, typewill
be supplier
- If both checkbox are checked or none of them, it'll be based on the
amount:
If amount is positif the type customer,
If amount is negativ, the type supplier
:param float: amount of the line
:param int/long: partner_id the partner id
:return: type as string: the default type to use: 'customer' or 'supplier'.
:return: type as string: the default type to use: 'customer' or
'supplier'.
"""
s_line_type = self._compute_type_from_amount(cr, uid, amount)
if partner_id:
s_line_type = self._compute_type_from_partner_profile(cr, uid,
partner_id, s_line_type)
s_line_type = self._compute_type_from_partner_profile(
cr, uid, partner_id, s_line_type)
return s_line_type
def get_account_and_type_for_counterpart(self, cr, uid, amount, account_receivable,
account_payable, partner_id=False):
def get_account_and_type_for_counterpart(
self, cr, uid, amount, account_receivable, account_payable,
partner_id=False):
"""
Give the amount, payable and receivable account (that can be found using
get_default_pay_receiv_accounts method) and receive the one to use. This method
should be use when there is no other way to know which one to take.
The rules are:
- If the customer checkbox is checked on the found partner, type and account will be customer and receivable
- If the supplier checkbox is checked on the found partner, type and account will be supplier and payable
- If both checkbox are checked or none of them, it'll be based on the amount :
If amount is positive, the type and account will be customer and receivable,
If amount is negative, the type and account will be supplier and payable
Note that we return the payable or receivable account from agrs and not from the optional partner_id
given!
get_default_pay_receiv_accounts method) and receive the one to use. This
method should be use when there is no other way to know which one to
take. The rules are:
- If the customer checkbox is checked on the found partner, type and
account will be customer and receivable
- If the supplier checkbox is checked on the found partner, type and
account will be supplier and payable
- If both checkbox are checked or none of them, it'll be based on the
amount:
If amount is positive, the type and account will be customer and
receivable,
If amount is negative, the type and account will be supplier and
payable
Note that we return the payable or receivable account from agrs and not
from the optional partner_id given!
:param float: amount of the line
:param int/long: account_receivable the receivable account
:param int/long: account_payable the payable account
:param int/long: partner_id the partner id
:return: dict with [account_id as int/long,type as string]: the default account to be used by
statement line as the counterpart of the journal account depending on the amount and the type
as 'customer' or 'supplier'.
:return: dict with [account_id as int/long,type as string]: the
default account to be used by statement line as the counterpart of
the journal account depending on the amount and the type as
'customer' or 'supplier'.
"""
account_id = False
ltype = self.get_type_for_counterpart(cr, uid, amount, partner_id=partner_id)
ltype = self.get_type_for_counterpart(
cr, uid, amount, partner_id=partner_id)
if ltype == 'supplier':
account_id = account_payable
else:
account_id = account_receivable
if not account_id:
raise osv.except_osv(
raise orm.except_orm(
_('Can not determine account'),
_('Please ensure that minimal properties are set')
)
_('Please ensure that minimal properties are set'))
return [account_id, ltype]
def get_default_pay_receiv_accounts(self, cr, uid, context=None):
"""
We try to determine default payable/receivable accounts to be used as counterpart
from the company default propoerty. This is to be used if there is no otherway to
find the good one, or to find a default value that will be overriden by a completion
method (rules of account_statement_base_completion) afterwards.
We try to determine default payable/receivable accounts to be used as
counterpart from the company default propoerty. This is to be used if
there is no otherway to find the good one, or to find a default value
that will be overriden by a completion method (rules of
account_statement_base_completion) afterwards.
:return: tuple of int/long ID that give account_receivable, account_payable based on
company default.
:return: tuple of int/long ID that give account_receivable,
account_payable based on company default.
"""
property_obj = self.pool.get('ir.property')
account_receivable = property_obj.get(cr, uid, 'property_account_receivable',
'res.partner', context=context)
property_obj = self.pool['ir.property']
account_receivable = property_obj.get(
cr, uid, 'property_account_receivable', 'res.partner',
context=context)
account_payable = property_obj.get(cr, uid, 'property_account_payable',
'res.partner', context=context)
@@ -549,8 +558,8 @@ class AccountBankStatement(Model):
def balance_check(self, cr, uid, st_id, journal_type='bank', context=None):
"""
Balance check depends on the profile. If no check for this profile is required,
return True and do nothing, otherwise call super.
Balance check depends on the profile. If no check for this profile is
required, return True and do nothing, otherwise call super.
:param int/long st_id: ID of the concerned account.bank.statement
:param char: journal_type that concern the bank statement
@@ -565,27 +574,25 @@ class AccountBankStatement(Model):
return True
def onchange_imp_config_id(self, cr, uid, ids, profile_id, context=None):
"""
Compute values on the change of the profile.
"""Compute values on the change of the profile.
:param: int/long: profile_id that changed
:return dict of dict with key = name of the field
"""
if not profile_id:
return {}
import_config = self.pool.get("account.statement.profile").browse(
import_config = self.pool["account.statement.profile"].browse(
cr, uid, profile_id, context=context)
journal_id = import_config.journal_id.id
return {'value': {'journal_id': journal_id,
'balance_check': import_config.balance_check}}
class AccountBankStatementLine(Model):
"""
Override to compute the period from the date of the line, add a method to retrieve
the values for a line from the profile. Override the on_change method to take care of
the profile when fullfilling the bank statement manually. Set the reference to 64
Char long instead 32.
class AccountBankStatementLine(orm.Model):
"""Override to compute the period from the date of the line, add a method
to retrieve the values for a line from the profile. Override the on_change
method to take care of the profile when fullfilling the bank statement
manually. Set the reference to 64 Char long instead 32.
"""
_inherit = "account.bank.statement.line"
@@ -599,7 +606,7 @@ class AccountBankStatementLine(Model):
local_context['account_period_prefer_normal'] = True
try:
periods = period_obj.find(cr, uid, dt=date, context=local_context)
except osv.except_osv:
except orm.except_orm:
# if no period defined, we are certainly at installation time
return False
return periods and periods[0] or False
@@ -617,30 +624,40 @@ class AccountBankStatementLine(Model):
'account_id': _get_default_account,
}
def get_values_for_line(self, cr, uid, profile_id=False, partner_id=False, line_type=False, amount=False, master_account_id=None, context=None):
"""
Return the account_id to be used in the line of a bank statement. It'll base the result as follow:
- If a receivable_account_id is set in the profile, return this value and type = general
def get_values_for_line(self, cr, uid, profile_id=False, partner_id=False,
line_type=False, amount=False,
master_account_id=None, context=None):
"""Return the account_id to be used in the line of a bank statement.
It'll base the result as follow:
- If a receivable_account_id is set in the profile, return this
value and type = general
# TODO
- Elif how_get_type_account is set to force_supplier or force_customer, will take respectively payable and type=supplier,
- Elif how_get_type_account is set to force_supplier or
force_customer, will take respectively payable and type=supplier,
receivable and type=customer otherwise
# END TODO
- Elif line_type is given, take the partner receivable/payable property (payable if type=supplier, receivable
otherwise)
- Elif line_type is given, take the partner receivable/payable
property (payable if type=supplier, receivable otherwise)
- Elif amount is given:
- If the customer checkbox is checked on the found partner, type and account will be customer and receivable
- If the supplier checkbox is checked on the found partner, type and account will be supplier and payable
- If both checkbox are checked or none of them, it'll be based on the amount :
If amount is positive, the type and account will be customer and receivable,
If amount is negative, the type and account will be supplier an payable
- Then, if no partner are given we look and take the property from the company so we always give a value
for account_id. Note that in that case, we return the receivable one.
- If the customer checkbox is checked on the found partner,
type and account will be customer and receivable
- If the supplier checkbox is checked on the found partner,
type and account will be supplier and payable
- If both checkbox are checked or none of them, it'll be based
on the amount :
If amount is positive, the type and account will be
customer and receivable,
If amount is negative, the type and account will be
supplier an payable
- Then, if no partner are given we look and take the property from
the company so we always give a value for account_id. Note that in
that case, we return the receivable one.
:param int/long profile_id of the related bank statement
:param int/long partner_id of the line
:param char line_type: a value from: 'general', 'supplier', 'customer'
:param float: amount of the line
:return: A dict of value that can be passed directly to the write method of
the statement line:
:return: A dict of value that can be passed directly to the write
method of the statement line:
{'partner_id': value,
'account_id' : value,
'type' : value,
@@ -666,9 +683,9 @@ class AccountBankStatementLine(Model):
cr, uid, profile_id, context=context)
if profile.receivable_account_id:
res['account_id'] = profile.receivable_account_id.id
# We return general as default instead of get_type_for_counterpart
# for perfomance reasons as line_type is not a meaningfull value
# as account is forced
# We return general as default instead of
# get_type_for_counterpart for perfomance reasons as line_type
# is not a meaningfull value as account is forced
res['type'] = line_type if line_type else 'general'
return res
# If no account is available on profile you have to do the lookup
@@ -684,36 +701,41 @@ class AccountBankStatementLine(Model):
receiv_account = part.property_account_receivable.id
# If no value, look on the default company property
if not pay_account or not receiv_account:
receiv_account, pay_account = obj_stat.get_default_pay_receiv_accounts(cr, uid, context=None)
account_id, comp_line_type = obj_stat.get_account_and_type_for_counterpart(cr, uid, amount,
receiv_account, pay_account,
receiv_account, pay_account = obj_stat.\
get_default_pay_receiv_accounts(cr, uid, context=None)
account_id, comp_line_type = obj_stat.\
get_account_and_type_for_counterpart(
cr, uid, amount, receiv_account, pay_account,
partner_id=partner_id)
res['account_id'] = account_id if account_id else receiv_account
res['type'] = line_type if line_type else comp_line_type
return res
def onchange_partner_id(self, cr, uid, ids, partner_id, profile_id=None, context=None):
def onchange_partner_id(self, cr, uid, ids, partner_id, profile_id=None,
context=None):
"""
Override of the basic method as we need to pass the profile_id in the on_change_type
call.
Moreover, we now call the get_account_and_type_for_counterpart method now to get the
type to use.
Override of the basic method as we need to pass the profile_id in the
on_change_type call.
Moreover, we now call the get_account_and_type_for_counterpart method
now to get the type to use.
"""
obj_stat = self.pool.get('account.bank.statement')
obj_stat = self.pool['account.bank.statement']
if not partner_id:
return {}
line_type = obj_stat.get_type_for_counterpart(cr, uid, 0.0, partner_id=partner_id)
res_type = self.onchange_type(cr, uid, ids, partner_id, line_type, profile_id, context=context)
line_type = obj_stat.get_type_for_counterpart(
cr, uid, 0.0, partner_id=partner_id)
res_type = self.onchange_type(
cr, uid, ids, partner_id, line_type, profile_id, context=context)
if res_type['value'] and res_type['value'].get('account_id', False):
return {'value': {'type': line_type,
'account_id': res_type['value']['account_id'],
'voucher_id': False}}
return {'value': {'type': line_type}}
def onchange_type(self, cr, uid, line_id, partner_id, line_type, profile_id, context=None):
"""
Keep the same features as in standard and call super. If an account is returned,
call the method to compute line values.
def onchange_type(self, cr, uid, line_id, partner_id, line_type, profile_id,
context=None):
"""Keep the same features as in standard and call super. If an account
is returned, call the method to compute line values.
"""
res = super(AccountBankStatementLine, self
).onchange_type(cr, uid, line_id, partner_id,

View File

@@ -5,7 +5,6 @@
<record id="statement_importer_view_form" model="ir.ui.view">
<field name="name">account.statement.profile.view</field>
<field name="model">account.statement.profile</field>
<field name="type">form</field>
<field name="arch" type="xml">
<form string="Import statement">
<separator string="" colspan="4"/>
@@ -27,7 +26,6 @@
<record id="statement_importer_view_tree" model="ir.ui.view">
<field name="name">account.statement.profile.view</field>
<field name="model">account.statement.profile</field>
<field name="type">tree</field>
<field name="arch" type="xml">
<tree string="Import statement">
<field name="name" />
@@ -76,7 +74,6 @@
<field name="name">account.bank.statement.tree</field>
<field name="model">account.bank.statement</field>
<field name="inherit_id" ref="account.view_bank_statement_tree"/>
<field name="type">tree</field>
<field name="arch" type="xml">
<xpath expr="/tree/field[@name='name']" position="before">
<field name="id"/>
@@ -95,7 +92,6 @@
<field name="name">account.bank.statement.form</field>
<field name="model">account.bank.statement</field>
<field name="inherit_id" ref="account.view_bank_statement_form"/>
<field name="type">form</field>
<field name="arch" type="xml" >
<!-- Add before the group : profile and related infos -->

View File

@@ -29,8 +29,9 @@ class AccountVoucher(Model):
def _get_period(self, cr, uid, context=None):
"""If period not in context, take it from the move lines"""
if not context.get('period_id') and context.get('move_line_ids'):
res = self.pool.get('account.move.line').browse(
cr, uid, context.get('move_line_ids'), context=context)[0].period_id.id
res = self.pool['account.move.line'].browse(
cr, uid, context.get('move_line_ids'),
context=context)[0].period_id.id
context['period_id'] = res
elif context.get('date'):
periods = self.pool.get('account.period').find(

View File

@@ -18,8 +18,9 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from openerp.addons.point_of_sale.point_of_sale import pos_session as std_pos_session
from openerp.osv import orm, osv
from openerp.addons.point_of_sale.point_of_sale import pos_session as \
std_pos_session
from openerp.osv import orm
from openerp.tools.translate import _
if not hasattr(std_pos_session, '_prepare_bank_statement'):
@@ -34,7 +35,8 @@ if not hasattr(std_pos_session, '_prepare_bank_statement'):
# which _inherit pos.session
#
# This change has been proposed for merging to fix lp:125375
def mp_prepare_bank_statement(self, cr, uid, pos_config, journal, context=None):
def mp_prepare_bank_statement(self, cr, uid, pos_config, journal,
context=None):
bank_values = {
'journal_id': journal.id,
'user_id': uid,
@@ -44,11 +46,12 @@ if not hasattr(std_pos_session, '_prepare_bank_statement'):
def mp_create(self, cr, uid, values, context=None):
context = context or {}
config_id = values.get('config_id', False) or context.get('default_config_id', False)
config_id = values.get('config_id', False) or context.get(
'default_config_id', False)
if not config_id:
raise osv.except_osv( _('Error!'),
raise orm.except_orm(
_('Error!'),
_("You should assign a Point of Sale to your session."))
# journal_id is not required on the pos_config because it does not
# exists at the installation. If nothing is configured at the
# installation we do the minimal configuration. Impossible to do in
@@ -57,64 +60,70 @@ if not hasattr(std_pos_session, '_prepare_bank_statement'):
pos_config = jobj.browse(cr, uid, config_id, context=context)
context.update({'company_id': pos_config.shop_id.company_id.id})
if not pos_config.journal_id:
jid = jobj.default_get(cr, uid, ['journal_id'], context=context)['journal_id']
jid = jobj.default_get(
cr, uid, ['journal_id'], context=context)['journal_id']
if jid:
jobj.write(cr, uid, [pos_config.id], {'journal_id': jid}, context=context)
jobj.write(
cr, uid, [pos_config.id], {'journal_id': jid},
context=context)
else:
raise osv.except_osv( _('error!'),
_("Unable to open the session. You have to assign a sale journal to your point of sale."))
raise orm.except_orm(
_('error!'),
_("Unable to open the session. You have to assign a sale "
"journal to your point of sale."))
# define some cash journal if no payment method exists
if not pos_config.journal_ids:
journal_proxy = self.pool.get('account.journal')
cashids = journal_proxy.search(cr, uid, [('journal_user', '=', True), ('type','=','cash')], context=context)
cashids = journal_proxy.search(
cr, uid, [('journal_user', '=', True),
('type', '=', 'cash')], context=context)
if not cashids:
cashids = journal_proxy.search(cr, uid, [('type', '=', 'cash')], context=context)
cashids = journal_proxy.search(
cr, uid, [('type', '=', 'cash')], context=context)
if not cashids:
cashids = journal_proxy.search(cr, uid, [('journal_user','=',True)], context=context)
jobj.write(cr, uid, [pos_config.id], {'journal_ids': [(6,0, cashids)]})
cashids = journal_proxy.search(
cr, uid, [('journal_user', '=', True)], context=context)
jobj.write(
cr, uid, [pos_config.id], {'journal_ids': [(6, 0, cashids)]})
pos_config = jobj.browse(cr, uid, config_id, context=context)
bank_statement_ids = []
for journal in pos_config.journal_ids:
bank_values = self._prepare_bank_statement(cr, uid, pos_config, journal, context)
statement_id = self.pool.get('account.bank.statement').create(cr, uid, bank_values, context=context)
bank_values = self._prepare_bank_statement(
cr, uid, pos_config, journal, context)
statement_id = self.pool.get('account.bank.statement').create(
cr, uid, bank_values, context=context)
bank_statement_ids.append(statement_id)
values.update({
'name': pos_config.sequence_id._next(),
'statement_ids': [(6, 0, bank_statement_ids)],
'config_id': config_id
})
return super(std_pos_session, self).create(cr, uid, values, context=context)
return super(std_pos_session, self).create(cr, uid, values,
context=context)
std_pos_session._prepare_bank_statement = mp_prepare_bank_statement
std_pos_session.create = mp_create
class pos_session(orm.Model):
class PosSession(orm.Model):
_inherit = 'pos.session'
def _prepare_bank_statement(self, cr, uid, pos_config, journal, context=None):
""" Override the function _mp_create. To add the bank profile to the statement
Function That was previously added to pos.session model using monkey patching
def _prepare_bank_statement(self, cr, uid, pos_config, journal,
context=None):
""" Override the function _mp_create. To add the bank profile to the
statement.
Function That was previously added to pos.session model using monkey
patching.
"""
bank_values = super(pos_session, self)._prepare_bank_statement(cr, uid,
pos_config,
journal, context)
user_obj = self.pool.get('res.users')
profile_obj = self.pool.get('account.statement.profile')
bank_values = super(PosSession, self)._prepare_bank_statement(
cr, uid, pos_config, journal, context)
user_obj = self.pool['res.users']
profile_obj = self.pool['account.statement.profile']
user = user_obj.browse(cr, uid, uid, context=context)
defaults = self.pool['account.bank.statement'].default_get(cr, uid,
['profile_id', 'period_id'],
context=context)
profile_ids = profile_obj.search(cr, uid,
[('company_id', '=', user.company_id.id),
defaults = self.pool['account.bank.statement'].default_get(
cr, uid, ['profile_id', 'period_id'], context=context)
profile_ids = profile_obj.search(
cr, uid, [('company_id', '=', user.company_id.id),
('journal_id', '=', bank_values['journal_id'])],
context=context)
if profile_ids:

View File

@@ -30,23 +30,20 @@
'account_voucher'
],
'description': """
This module is deprecated. It was only needed when using account_bank_statement_ext with voucher in order to compute the period
correctly. This is mainly because with account_bank_statement_ext, the period is computed for each line.
Now, we include this in the account_statement_ext module and added a dependencies on account_voucher (mainly cause we can't get
rid of the voucher in version 7.0).
This module is deprecated. It was only needed when using
account_bank_statement_ext with voucher in order to compute the period
correctly. This is mainly because with account_bank_statement_ext, the period
is computed for each line.
Now, we include this in the account_statement_ext module and added a
dependencies on account_voucher (mainly cause we can't get rid of the voucher
in version 7.0).
""",
'website': 'http://www.camptocamp.com',
'init_xml': [],
'update_xml': [
'data': [
"statement_voucher_view.xml",
],
'demo_xml': [],
'test': [],
'installable': False,
'images': [],
'auto_install': False,
'license': 'AGPL-3',
}

View File

@@ -32,10 +32,11 @@ class AccountVoucher(Model):
context = {}
if not context.get('period_id') and context.get('move_line_ids'):
res = self.pool.get('account.move.line').browse(
cr, uid, context.get('move_line_ids'), context=context)[0].period_id.id
cr, uid, context.get('move_line_ids'),
context=context)[0].period_id.id
context['period_id'] = res
elif context.get('date'):
periods = self.pool.get('account.period').find(
periods = self.pool['account.period'].find(
cr, uid, dt=context['date'], context=context)
if periods:
context['period_id'] = periods[0]

View File

@@ -19,4 +19,3 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
###############################################################################

View File

@@ -25,7 +25,9 @@
'version': '0.1',
'category': 'Generic Modules/Others',
'license': 'AGPL-3',
'description': """Module that remove the 'Import invoices' button on bank statement""",
'description': """
Module that remove the 'Import invoices' button on bank statement
""",
'author': 'Akretion',
'website': 'http://www.akretion.com/',
'depends': [

View File

@@ -10,7 +10,6 @@
<field name="model">account.bank.statement</field>
<field name="inherit_id" ref="account_voucher.view_bank_statement_form_invoice" />
<field eval="100" name="priority"/>
<field name="type">form</field>
<field name="arch" type="xml">
<button string="Import Invoices" position="replace">
</button>

View File

@@ -19,4 +19,3 @@
#
##############################################################################
from . import ofx_parser

View File

@@ -21,49 +21,35 @@
import tempfile
import datetime
from openerp.tools.translate import _
from openerp.addons.account_statement_base_import.parser import BankStatementImportParser
from openerp.addons.account_statement_base_import.parser import \
BankStatementImportParser
try:
import ofxparse
except:
raise Exception(_('Please install python lib ofxparse'))
class OfxParser(BankStatementImportParser):
"""
Class for defining parser for OFX file format.
"""
def __init__(self, parser_name, *args, **kwargs):
"""
"""
super(OfxParser, self).__init__(parser_name, *args, **kwargs)
class OfxParser(BankStatementImportParser):
"""Class for defining parser for OFX file format."""
@classmethod
def parser_for(cls, parser_name):
"""
Used by the new_bank_statement_parser class factory. Return true if
"""Used by the new_bank_statement_parser class factory. Return true if
the providen name is 'ofx_so'.
"""
return parser_name == 'ofx_so'
def _custom_format(self, *args, **kwargs):
"""
No other work on data are needed in this parser.
"""
"""No other work on data are needed in this parser."""
return True
def _pre(self, *args, **kwargs):
"""
No pre-treatment needed for this parser.
"""
"""No pre-treatment needed for this parser."""
return True
def _parse(self, *args, **kwargs):
"""
Launch the parsing itself.
"""
"""Launch the parsing itself."""
ofx_file = tempfile.NamedTemporaryFile()
ofx_file.seek(0)
ofx_file.write(self.filebuffer)
@@ -82,26 +68,15 @@ class OfxParser(BankStatementImportParser):
return True
def _validate(self, *args, **kwargs):
"""
Nothing to do here. ofxparse trigger possible format errors.
"""
"""Nothing to do here. ofxparse trigger possible format errors."""
return True
def _post(self, *args, **kwargs):
"""
Nothing is needed to do after parsing.
"""
return True
def _post(self, *args, **kwargs):
"""
Nothing to do.
"""
"""Nothing is needed to do after parsing."""
return True
def get_st_line_vals(self, line, *args, **kwargs):
"""
This method must return a dict of vals that can be passed to create
"""This method must return a dict of vals that can be passed to create
method of statement line in order to record it. It is the
responsibility of every parser to give this dict of vals, so each one
can implement his own way of recording the lines.
@@ -117,4 +92,3 @@ class OfxParser(BankStatementImportParser):
'ref': line.get('ref', '/'),
'label': line.get('label', ''),
}

View File

@@ -19,17 +19,16 @@
#
##############################################################################
from openerp.tools.translate import _
from openerp.osv import fields, orm
from openerp.osv import orm
class AccountStatementProfil(orm.Model):
_inherit = "account.statement.profile"
def get_import_type_selection(self, cr, uid, context=None):
"""
Inherited from parent to add parser.
"""
def _get_import_type_selection(self, cr, uid, context=None):
"""Inherited from parent to add parser."""
selection = super(AccountStatementProfil, self
).get_import_type_selection(cr, uid,
)._get_import_type_selection(cr, uid,
context=context)
selection.append(('ofx_so', _('OFX - Open Financial Exchange')))
return selection

View File

@@ -20,7 +20,7 @@
#
###############################################################################
from openerp.osv import fields, orm, osv
from openerp.osv import fields, orm
class AccountStatementProfile(orm.Model):
@@ -36,12 +36,13 @@ class AccountStatementProfile(orm.Model):
"for the refunds and one for the payments.")
}
class account_bank_statement(orm.Model):
class AccountBankStatement(orm.Model):
_inherit = "account.bank.statement"
def _prepare_move_line_vals(self, cr, uid, st_line, *args, **kwargs):
res = super(account_bank_statement, self)._prepare_move_line_vals(cr, uid, st_line,
*args, **kwargs)
res = super(AccountBankStatement, self)._prepare_move_line_vals(
cr, uid, st_line, *args, **kwargs)
period_id = self._get_period(cr, uid, st_line.statement_id.date,
context=kwargs.get('context'))
if st_line.statement_id.profile_id.one_move:
@@ -52,11 +53,10 @@ class account_bank_statement(orm.Model):
})
return res
return res
def _prepare_move(self, cr, uid, st_line, st_line_number, context=None):
res = super(account_bank_statement, self).\
res = super(AccountBankStatement, self).\
_prepare_move(cr, uid, st_line, st_line_number, context=context)
res.update({
'ref': st_line.statement_id.name,
@@ -65,30 +65,29 @@ class account_bank_statement(orm.Model):
})
return res
def create_move_from_st_line(self, cr, uid, st_line_id, company_currency_id,
st_line_number, context=None):
if context is None:
context = {}
context['from_parent_object'] = True #For compability with module account_constraints
account_move_obj = self.pool.get('account.move')
account_bank_statement_line_obj = self.pool.get('account.bank.statement.line')
st_line = account_bank_statement_line_obj.browse(cr, uid, st_line_id,
context=context)
# For compability with module account_constraints
context['from_parent_object'] = True
account_move_obj = self.pool['account.move']
st_line_obj = self.pool['account.bank.statement.line']
st_line = st_line_obj.browse(cr, uid, st_line_id, context=context)
st = st_line.statement_id
if st.profile_id.one_move:
if not context.get('move_id'):
move_vals = self._prepare_move(cr, uid, st_line, st_line_number, context=context)
context['move_id'] = account_move_obj.create(cr, uid, move_vals, context=context)
move_vals = self._prepare_move(
cr, uid, st_line, st_line_number, context=context)
context['move_id'] = account_move_obj.create(
cr, uid, move_vals, context=context)
self.create_move_line_from_st_line(cr, uid, context['move_id'],
st_line_id, company_currency_id,
context=context)
return context['move_id']
else:
return super(account_bank_statement, self).create_move_from_st_line(cr, uid, st_line_id,
company_currency_id,
st_line_number,
return super(AccountBankStatement, self).create_move_from_st_line(
cr, uid, st_line_id, company_currency_id, st_line_number,
context=context)
def create_move_line_from_st_line(self, cr, uid, move_id, st_line_id,
@@ -96,46 +95,44 @@ class account_bank_statement(orm.Model):
"""Create the account move line from the statement line.
:param int/long move_id: ID of the account.move
:param int/long st_line_id: ID of the account.bank.statement.line to create the move line from.
:param int/long company_currency_id: ID of the res.currency of the company
:param int/long st_line_id: ID of the account.bank.statement.line
to create the move line from.
:param int/long company_currency_id: ID of the res.currency of the
company
:return: ID of the account.move created
"""
if context is None:
context = {}
res_currency_obj = self.pool.get('res.currency')
account_move_line_obj = self.pool.get('account.move.line')
account_bank_statement_line_obj = self.pool.get('account.bank.statement.line')
st_line = account_bank_statement_line_obj.browse(cr, uid, st_line_id, context=context)
res_currency_obj = self.pool['res.currency']
account_move_line_obj = self.pool['account.move.line']
st_line_obj = self.pool['account.bank.statement.line']
st_line = st_line_obj.browse(cr, uid, st_line_id, context=context)
st = st_line.statement_id
context.update({'date': st_line.date})
acc_cur = ((st_line.amount<=0) and st.journal_id.default_debit_account_id) or st_line.account_id
acc_cur = (((st_line.amount <= 0)
and st.journal_id.default_debit_account_id) or
st_line.account_id)
context.update({
'res.currency.compute.account': acc_cur,
})
amount = res_currency_obj.compute(cr, uid, st.currency.id,
company_currency_id,
st_line.amount,
amount = res_currency_obj.compute(
cr, uid, st.currency.id, company_currency_id, st_line.amount,
context=context)
bank_move_vals = self._prepare_bank_move_line(
cr, uid, st_line, move_id, amount, company_currency_id,
context=context)
return account_move_line_obj.create(cr, uid, bank_move_vals,
context=context)
bank_move_vals = self._prepare_bank_move_line(cr, uid, st_line, move_id, amount,
company_currency_id, context=context)
return account_move_line_obj.create(cr, uid, bank_move_vals, context=context)
def _valid_move(self, cr, uid, move_id, context=None):
move_obj = self.pool.get('account.move')
move = move_obj.browse(cr, uid, move_id, context=context)
move_obj = self.pool['account.move']
move_obj.post(cr, uid, [move_id], context=context)
return True
def _prepare_transfer_move_line_vals(self, cr, uid, st, name, amount, move_id, context=None):
"""
Prepare the dict of values to create the transfer move lines.
"""
def _prepare_transfer_move_line_vals(self, cr, uid, st, name, amount,
move_id, context=None):
"""Prepare the dict of values to create the transfer move lines."""
account_id = st.profile_id.journal_id.default_debit_account_id.id
partner_id = st.profile_id.partner_id and profile.partner_id.id or False
if amount < 0.0:
debit = 0.0
credit = -amount
@@ -145,7 +142,7 @@ class account_bank_statement(orm.Model):
vals = {
'name': name,
'date': st.date,
'partner_id': partner_id,
'partner_id': st.profile_id.partner_id.id,
'statement_id': st.id,
'account_id': account_id,
'ref': name,
@@ -157,9 +154,8 @@ class account_bank_statement(orm.Model):
}
return vals
def create_move_transfer_lines(self, cr, uid, move, st, context=None):
move_line_obj = self.pool.get('account.move.line')
move_line_obj = self.pool['account.move.line']
move_id = move.id
refund = 0.0
payment = 0.0
@@ -180,27 +176,22 @@ class account_bank_statement(orm.Model):
if amount:
transfer_lines.append(['Transfer', amount])
for transfer_line in transfer_lines:
vals = self._prepare_transfer_move_line_vals(cr, uid, st,
transfer_line[0],
transfer_line[1],
move_id,
vals = self._prepare_transfer_move_line_vals(
cr, uid, st, transfer_line[0], transfer_line[1], move_id,
context=context)
transfer_line_ids.append(move_line_obj.create(cr, uid, vals, context=context))
transfer_line_ids.append(
move_line_obj.create(cr, uid, vals, context=context))
return transfer_line_ids
def button_confirm_bank(self, cr, uid, ids, context=None):
st_line_obj = self.pool.get('account.bank.statement.line')
move_obj = self.pool.get('account.move')
st_line_obj = self.pool['account.bank.statement.line']
if context is None:
context = {}
for st in self.browse(cr, uid, ids, context=context):
super(account_bank_statement, self).button_confirm_bank(cr, uid, ids,
context=context)
super(AccountBankStatement, self).button_confirm_bank(
cr, uid, ids, context=context)
if st.profile_id.one_move and context.get('move_id', False):
move_id = context['move_id']
move = move_obj.browse(cr, uid, move_id, context=context)
transfe_line_ids = self.create_move_transfer_lines(cr, uid, move, st, context=context)
self._valid_move(cr, uid, move_id, context=context)
lines_ids = [x.id for x in st.line_ids]
st_line_obj.write(cr, uid, lines_ids,
@@ -209,7 +200,6 @@ class account_bank_statement(orm.Model):
return True
def button_cancel(self, cr, uid, ids, context=None):
done = []
for st in self.browse(cr, uid, ids, context=context):
if st.profile_id.one_move and st.line_ids:
for move in st.line_ids[0].move_ids:
@@ -218,8 +208,6 @@ class account_bank_statement(orm.Model):
move.unlink(context=context)
st.write({'state': 'draft'}, context=context)
else:
super(account_bank_statement, self).button_cancel(cr, uid, ids,
context=context)
super(AccountBankStatement, self).button_cancel(
cr, uid, ids, context=context)
return True

View File

@@ -40,8 +40,8 @@
Account Statement Regex Account Completion addon
=========================
- Add a completion method based on a specified regular expression
and update account to use in the bank statement line with the specified account.
Add a completion method based on a specified regular expression and update
account to use in the bank statement line with the specified account.
""",
"data": ['statement_view.xml',
],
@@ -53,4 +53,3 @@ Account Statement Regex Account Completion addon
"auto_install": False,
"application": False,
}
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@@ -35,6 +35,7 @@ import re
class AccountStatementCompletionRule(Model):
"""Add a rule to complete account based on a regular expression"""
_inherit = "account.statement.completion.rule"
@@ -43,13 +44,14 @@ class AccountStatementCompletionRule(Model):
res = super(AccountStatementCompletionRule, self)._get_functions(
cr, uid, context=context)
res.append(('set_account',
'Set account for line labels matching a regular expression'))
'Set account for line labels matching a regular '
'expression'))
return res
_columns = {
'function_to_call': fields.selection(_get_functions, 'Method'),
'regex': fields.char('Regular Expression', size=128),
'account_id': fields.many2one('account.account', string="Account to set"),
'account_id': fields.many2one('account.account',
string="Account to set"),
}
def set_account(self, cr, uid, id, st_line, context=None):

View File

@@ -6,7 +6,6 @@
<field name="name">account.statement.completion.rule.view (account_statement_regex_account_completion)</field>
<field name="model">account.statement.completion.rule</field>
<field name="inherit_id" ref="account_statement_base_completion.statement_st_completion_rule_view_form" />
<field name="type">form</field>
<field name="arch" type="xml">
<field name="function_to_call" position="after">
<group colspan="2">

View File

@@ -36,34 +36,38 @@ ACC_NUMBER = "BE38733040385372"
class test_regex_account_completion(common.TransactionCase):
def prepare(self):
self.account_bank_statement_obj = self.registry("account.bank.statement")
self.account_bank_statement_line_obj = self.registry("account.bank.statement.line")
self.st_obj = self.registry(
"account.bank.statement")
self.st_line_obj = self.registry(
"account.bank.statement.line")
self.account_id = self.ref('account.a_expense')
# create the completion rule
rule_vals = {'function_to_call': 'set_account',
'regex': '^My statement',
'account_id': self.account_id}
completion_rule_id = self.registry("account.statement.completion.rule").create(self.cr, self.uid, rule_vals)
completion_rule_id = self.registry(
"account.statement.completion.rule").create(
self.cr, self.uid, rule_vals)
# Create the profile
journal_id = self.ref("account.bank_journal")
profile_id = self.registry("account.statement.profile").create(self.cr, self.uid, {
profile_id = self.registry("account.statement.profile").create(
self.cr, self.uid, {
"name": "TEST",
"commission_account_id": self.ref("account.a_recv"),
"journal_id": journal_id,
"rule_ids": [(6, 0, [completion_rule_id])]})
"rule_ids": [(6, 0, [completion_rule_id])]
})
# Create a bank statement
self.statement_id = self.account_bank_statement_obj.create(self.cr, self.uid, {
self.statement_id = self.st_obj.create(
self.cr, self.uid, {
"balance_end_real": 0.0,
"balance_start": 0.0,
"date": time.strftime('%Y-%m-%d'),
"journal_id": journal_id,
"profile_id": profile_id
})
# Create two bank statement lines
self.statement_line1_id = self.account_bank_statement_line_obj.create(self.cr, self.uid, {
self.statement_line1_id = self.st_line_obj.create(self.cr, self.uid, {
'amount': 1000.0,
'name': 'My statement',
'ref': 'My ref',
@@ -71,7 +75,7 @@ class test_regex_account_completion(common.TransactionCase):
'partner_acc_number': ACC_NUMBER
})
self.statement_line2_id = self.account_bank_statement_line_obj.create(self.cr, self.uid, {
self.statement_line2_id = self.st_line_obj.create(self.cr, self.uid, {
'amount': 2000.0,
'name': 'My second statement',
'ref': 'My second ref',
@@ -83,9 +87,14 @@ class test_regex_account_completion(common.TransactionCase):
"""Test the automatic completion on account
"""
self.prepare()
statement_obj = self.account_bank_statement_obj.browse(self.cr, self.uid, self.statement_id)
statement_obj = self.st_obj.browse(self.cr, self.uid, self.statement_id)
statement_obj.button_auto_completion()
statement_line1 = self.account_bank_statement_line_obj.browse(self.cr, self.uid, self.statement_line1_id)
self.assertEquals(self.account_id, statement_line1.account_id.id, "The account should be the account of the completion")
statement_line2 = self.account_bank_statement_line_obj.browse(self.cr, self.uid, self.statement_line2_id)
self.assertNotEqual(self.account_id, statement_line2.account_id.id, "The account should be not the account of the completion")
statement_line1 = self.st_line_obj.browse(
self.cr, self.uid, self.statement_line1_id)
self.assertEquals(self.account_id, statement_line1.account_id.id,
"The account should be the account of the completion")
statement_line2 = self.st_line_obj.browse(
self.cr, self.uid, self.statement_line2_id)
self.assertNotEqual(self.account_id, statement_line2.account_id.id,
"The account should be not the account of the "
"completion")

View File

@@ -22,19 +22,19 @@
# #
###############################################################################
from openerp.osv import fields, orm
from openerp.osv import orm
from tools.translate import _
from openerp.addons.account_statement_base_completion.statement import ErrorTooManyPartner
from openerp.addons.account_statement_base_completion.statement import \
ErrorTooManyPartner
class account_statement_completion_rule(orm.Model):
class AccountStatementCompletionRule(orm.Model):
_name = "account.statement.completion.rule"
_inherit = "account.statement.completion.rule"
def _get_functions(self, cr, uid, context=None):
res = super(account_statement_completion_rule, self)._get_functions(
res = super(AccountStatementCompletionRule, self)._get_functions(
cr, uid, context=context)
res.append(
('get_from_ref_and_so', 'From line reference (based on SO number)')
@@ -60,14 +60,12 @@ class account_statement_completion_rule(orm.Model):
...}
"""
st_obj = self.pool.get('account.bank.statement.line')
st_obj = self.pool['account.bank.statement.line']
res = {}
if st_line:
so_obj = self.pool.get('sale.order')
so_id = so_obj.search(cr,
uid,
[('name', '=', st_line['ref'])],
context=context)
so_id = so_obj.search(
cr, uid, [('name', '=', st_line['ref'])], context=context)
if so_id:
if so_id and len(so_id) == 1:
so = so_obj.browse(cr, uid, so_id[0], context=context)
@@ -78,9 +76,7 @@ class account_statement_completion_rule(orm.Model):
'than one partner while looking on SO by ref.') %
(st_line['name'], st_line['ref']))
st_vals = st_obj.get_values_for_line(
cr,
uid,
profile_id=st_line['profile_id'],
cr, uid, profile_id=st_line['profile_id'],
master_account_id=st_line['master_account_id'],
partner_id=res.get('partner_id', False),
line_type='customer',
@@ -88,7 +84,3 @@ class account_statement_completion_rule(orm.Model):
context=context)
res.update(st_vals)
return res
_columns = {
'function_to_call': fields.selection(_get_functions, 'Method'),
}

View File

@@ -22,7 +22,8 @@
from openerp.tools.translate import _
from openerp.osv.orm import Model
from openerp.osv import fields
from openerp.addons.account_statement_base_completion.statement import ErrorTooManyPartner
from openerp.addons.account_statement_base_completion.statement import \
ErrorTooManyPartner
class AccountStatementCompletionRule(Model):
@@ -41,15 +42,12 @@ class AccountStatementCompletionRule(Model):
]
return res
_columns = {
'function_to_call': fields.selection(_get_functions, 'Method'),
}
def get_from_transaction_id_and_so(self, cr, uid, st_line, context=None):
"""
Match the partner based on the transaction ID field of the SO.
Then, call the generic st_line method to complete other values.
In that case, we always fullfill the reference of the line with the SO name.
In that case, we always fullfill the reference of the line with the SO
name.
:param dict st_line: read of the concerned account.bank.statement.line
:return:
A dict of value that can be passed directly to the write method of
@@ -58,23 +56,22 @@ class AccountStatementCompletionRule(Model):
'account_id' : value,
...}
"""
st_obj = self.pool.get('account.bank.statement.line')
st_obj = self.pool['account.bank.statement.line']
res = {}
so_obj = self.pool.get('sale.order')
so_id = so_obj.search(cr,
uid,
[('transaction_id', '=', st_line['transaction_id'])],
so_obj = self.pool['sale.order']
so_id = so_obj.search(
cr, uid, [('transaction_id', '=', st_line['transaction_id'])],
context=context)
if len(so_id) > 1:
raise ErrorTooManyPartner(_('Line named "%s" (Ref:%s) was matched by more than '
raise ErrorTooManyPartner(
_('Line named "%s" (Ref:%s) was matched by more than '
'one partner.') % (st_line['name'], st_line['ref']))
if len(so_id) == 1:
so = so_obj.browse(cr, uid, so_id[0], context=context)
res['partner_id'] = so.partner_id.id
res['ref'] = so.name
st_vals = st_obj.get_values_for_line(cr,
uid,
profile_id=st_line['profile_id'],
st_vals = st_obj.get_values_for_line(
cr, uid, profile_id=st_line['profile_id'],
master_account_id=st_line['master_account_id'],
partner_id=res.get('partner_id', False),
line_type=st_line['type'],
@@ -83,12 +80,13 @@ class AccountStatementCompletionRule(Model):
res.update(st_vals)
return res
def get_from_transaction_id_and_invoice(self, cr, uid, st_line, context=None):
"""
Match the partner based on the transaction ID field of the invoice.
def get_from_transaction_id_and_invoice(self, cr, uid, st_line,
context=None):
"""Match the partner based on the transaction ID field of the invoice.
Then, call the generic st_line method to complete other values.
In that case, we always fullfill the reference of the line with the invoice name.
In that case, we always fullfill the reference of the line with the
invoice name.
:param dict st_line: read of the concerned account.bank.statement.line
:return:
@@ -98,9 +96,9 @@ class AccountStatementCompletionRule(Model):
'account_id' : value,
...}
"""
st_obj = self.pool.get('account.bank.statement.line')
st_obj = self.pool['account.bank.statement.line']
res = {}
invoice_obj = self.pool.get('account.invoice')
invoice_obj = self.pool['account.invoice']
invoice_id = invoice_obj.search(
cr, uid,
[('transaction_id', '=', st_line['transaction_id'])],
@@ -134,11 +132,8 @@ class AccountStatementLine(Model):
_inherit = "account.bank.statement.line"
_columns = {
# 'additionnal_bank_fields' : fields.serialized('Additionnal infos from bank', help="Used by completion and import system."),
'transaction_id': fields.sparse(
type='char',
string='Transaction ID',
size=128,
type='char', string='Transaction ID', size=128,
serialization_field='additionnal_bank_fields',
help="Transaction id from the financial institute"),
}
@@ -151,19 +146,22 @@ class AccountBankStatement(Model):
self, cr, uid, st_line, move_id, debit, credit, currency_id=False,
amount_currency=False, account_id=False, analytic_id=False,
partner_id=False, context=None):
"""Add the period_id from the statement line date to the move preparation.
Originaly, it was taken from the statement period_id
"""Add the period_id from the statement line date to the move
preparation. Originaly, it was taken from the statement period_id
:param browse_record st_line: account.bank.statement.line record to
create the move from.
:param int/long move_id: ID of the account.move to link the move line
:param float debit: debit amount of the move line
:param float credit: credit amount of the move line
:param int/long currency_id: ID of currency of the move line to create
:param float amount_currency: amount of the debit/credit expressed in the currency_id
:param int/long account_id: ID of the account to use in the move line if different
from the statement line account ID
:param int/long analytic_id: ID of analytic account to put on the move line
:param int/long currency_id: ID of currency of the move line to
create
:param float amount_currency: amount of the debit/credit expressed
in the currency_id
:param int/long account_id: ID of the account to use in the move
line if different from the statement line account ID
:param int/long analytic_id: ID of analytic account to put on the
move line
:param int/long partner_id: ID of the partner to put on the move line
:return: dict of value to create() the account.move.line
"""

View File

@@ -19,7 +19,8 @@
#
##############################################################################
{'name': "Bank statement transactionID import",
{
'name': "Bank statement transactionID import",
'version': '1.0',
'author': 'Camptocamp',
'maintainer': 'Camptocamp',
@@ -33,28 +34,25 @@
This module brings generic methods and fields on bank statement to deal with
the importation of different bank and offices that uses transactionID.
This module allows you to import your bank transactions with a standard .csv or .xls file
(you'll find samples in the 'data' folder). It respects the chosen profile
(model provided by the account_statement_ext module) to generate the entries.
This module allows you to import your bank transactions with a standard .csv
or .xls file (you'll find samples in the 'data' folder). It respects the chosen
profile (model provided by the account_statement_ext module) to generate the
entries.
This module can handle a commission taken by the payment office and has the following format:
This module can handle a commission taken by the payment office and has the
following format:
* transaction_id: the transaction ID given by the bank/office. It is used as reference
in the generated entries and is useful for reconciliation process
* transaction_id: the transaction ID given by the bank/office. It is used as
reference in the generated entries and is useful for reconciliation process
* date: date of the payment
* amount: amount paid in the currency of the journal used in the importation profile
* amount: amount paid in the currency of the journal used in the importation
profile
* commission_amount: amount of the comission for each line
* label: the comunication given by the payment office, used as communication in the
generated entries.
* label: the comunication given by the payment office, used as communication in
the generated entries.
""",
'website': 'http://www.camptocamp.com',
'init_xml': [],
'update_xml': [
],
'demo_xml': [],
'test': [],
'installable': False,
'images': [],
'auto_install': False,
'license': 'AGPL-3',
}

View File

@@ -22,44 +22,45 @@ from account_statement_base_import.parser.file_parser import FileParser
class TransactionIDFileParser(FileParser):
"""
TransactionID parser that use a define format in csv or xls to import
"""TransactionID parser that use a define format in csv or xls to import
bank statement.
"""
def __init__(self, parse_name, ftype='csv', extra_fields=None, header=None, **kwargs):
"""
Add transaction_id in header keys
:param char: parse_name: The name of the parser
def __init__(self, profile, ftype='csv', extra_fields=None, header=None,
**kwargs):
"""Add transaction_id in header keys
:param char: profile: Reference to the profile
:param char: ftype: extension of the file (could be csv or xls)
:param dict: extra_fields: extra fields to add to the conversion dict. In the format
{fieldname: fieldtype}
:param list: header : specify header fields if the csv file has no header
:param dict: extra_fields: extra fields to add to the conversion
dict. In the format {fieldname: fieldtype}
:param list: header : specify header fields if the csv file has no
header
"""
extra_fields = {'transaction_id': unicode}
super(TransactionIDFileParser, self).__init__(parse_name, extra_fields=extra_fields,
ftype=ftype, header=header, **kwargs)
super(TransactionIDFileParser, self).__init__(
profile, extra_fields=extra_fields, ftype=ftype, header=header,
**kwargs)
# ref is replaced by transaction_id thus we delete it from check
self.keys_to_validate = [k for k in self.keys_to_validate if k != 'ref']
self.keys_to_validate = [
k for k in self.keys_to_validate if k != 'ref']
del self.conversion_dict['ref']
@classmethod
def parser_for(cls, parser_name):
"""
Used by the new_bank_statement_parser class factory. Return true if
"""Used by the new_bank_statement_parser class factory. Return true if
the providen name is generic_csvxls_transaction
"""
return parser_name == 'generic_csvxls_transaction'
def get_st_line_vals(self, line, *args, **kwargs):
"""
This method must return a dict of vals that can be passed to create
"""This method must return a dict of vals that can be passed to create
method of statement line in order to record it. It is the responsibility
of every parser to give this dict of vals, so each one can implement his
own way of recording the lines.
:param: line: a dict of vals that represent a line of result_row_list
:return: dict of values to give to the create method of statement line,
it MUST contain at least:
:param: line: a dict of vals that represent a line of
result_row_list
:return: dict of values to give to the create method of statement
line, it MUST contain at least:
{
'name':value,
'date':value,
@@ -68,13 +69,15 @@ class TransactionIDFileParser(FileParser):
'label':value,
'commission_amount':value,
}
In this generic parser, the commission is given for every line, so we store it
for each one.
In this generic parser, the commission is given for every line, so we
store it for each one.
"""
return {'name': line.get('label', line.get('ref', '/')),
return {
'name': line.get('label', line.get('ref', '/')),
'date': line.get('date', datetime.datetime.now().date()),
'amount': line.get('amount', 0.0),
'ref': line.get('transaction_id', '/'),
'label': line.get('label', ''),
'transaction_id': line.get('transaction_id', '/'),
'commission_amount': line.get('commission_amount', 0.0)}
'commission_amount': line.get('commission_amount', 0.0)
}

View File

@@ -19,29 +19,16 @@
#
##############################################################################
from openerp.osv.orm import Model
from openerp.osv import fields
from openerp.osv import orm
class AccountStatementProfil(Model):
class AccountStatementProfil(orm.Model):
_inherit = "account.statement.profile"
def get_import_type_selection(self, cr, uid, context=None):
"""
Has to be inherited to add parser
"""
res = super(AccountStatementProfil, self).get_import_type_selection(
def _get_import_type_selection(self, cr, uid, context=None):
"""Has to be inherited to add parser"""
res = super(AccountStatementProfil, self)._get_import_type_selection(
cr, uid, context=context)
res.append(('generic_csvxls_transaction',
'Generic .csv/.xls based on SO transaction ID'))
return res
_columns = {
'import_type': fields.selection(
get_import_type_selection,
'Type of import',
required=True,
help="Choose here the method by which you want to import "
"bank statement for this profile."),
}

View File

@@ -5,7 +5,6 @@
<field name="name">customer.invoice.transaction.inherit</field>
<field name="model">account.invoice</field>
<field name="inherit_id" ref="account.invoice_form"/>
<field name="type">form</field>
<field name="arch" type="xml">
<notebook position="inside">
<page string="Transactions datas">
@@ -19,7 +18,6 @@
<field name="name">account.invoice.tree.inherit</field>
<field name="model">account.invoice</field>
<field name="inherit_id" ref="account.invoice_tree"/>
<field name="type">form</field>
<field name="arch" type="xml">
<field name="origin" position="after">
<field name="transaction_id" select="2"/>

View File

@@ -3,7 +3,6 @@
<record id="view_order_form_transaction" model="ir.ui.view">
<field name="name">sale.order.form.transaction</field>
<field name="model">sale.order</field>
<field name="type">form</field>
<field name="inherit_id" ref="sale.view_order_form"/>
<field name="arch" type="xml">
<field name="payment_term" position="after">

View File

@@ -4,7 +4,6 @@
<field name="name">Hide voucher in invoice</field>
<field name="model">account.invoice</field>
<field name="inherit_id" ref="account_voucher.view_invoice_customer" />
<field name="type">form</field>
<field name="arch" type="xml">
<xpath expr="//button[@name='invoice_pay_customer'][last()]"
position="replace">
@@ -30,7 +29,6 @@
<field name="name">Hide voucher in supplier invoice</field>
<field name="model">account.invoice</field>
<field name="inherit_id" ref="account_voucher.view_invoice_supplier" />
<field name="type">form</field>
<field name="arch" type="xml">
<xpath expr="//button[@name='invoice_pay_customer'][last()]"
position="replace">

View File

@@ -23,12 +23,12 @@
'version': '1.0.0',
'category': 'other',
'description': """
Prevent voucher creation when importing lines into statement.
#############################################################
Prevent voucher creation when importing lines into statement
============================================================
When importing invoice or payment into a bank statement or a payment order, normally a
draft voucher is created on the line. This module will disable this voucher creation.
When importing payment line, date used to populate statement
When importing invoice or payment into a bank statement or a payment order,
normally a draft voucher is created on the line. This module will disable this
voucher creation. When importing payment line, date used to populate statement
line will be take from imported line in this order:
* Date
@@ -44,5 +44,4 @@ line will be take from imported line in this order:
],
'test': [],
'installable': False,
'active': False,
}

View File

@@ -9,7 +9,6 @@
<field name="model">account.bank.statement</field>
<field name="inherit_id" ref="account.view_bank_statement_form" />
<field eval="100" name="priority"/>
<field name="type">form</field>
<field name="arch" type="xml">
<field name="voucher_id" position="replace">
</field>

View File

@@ -19,12 +19,11 @@
#
##############################################################################
from openerp.osv import orm
from openerp.tools import DEFAULT_SERVER_DATE_FORMAT
import time
class AccountStatementFromInvoiceLines(orm.TransientModel):
_inherit = "account.statement.from.invoice.lines"
def populate_statement(self, cr, uid, ids, context=None):
@@ -39,32 +38,32 @@ class AccountStatementFromInvoiceLines(orm.TransientModel):
line_ids = data['line_ids']
if not line_ids:
return {'type': 'ir.actions.act_window_close'}
line_obj = self.pool.get('account.move.line')
statement_obj = self.pool.get('account.bank.statement')
statement_line_obj = self.pool.get('account.bank.statement.line')
currency_obj = self.pool.get('res.currency')
line_date = time.strftime('%Y-%m-%d')
statement = statement_obj.browse(cr, uid, statement_id, context=context)
line_obj = self.pool['account.move.line']
statement_obj = self.pool['account.bank.statement']
statement_line_obj = self.pool['account.bank.statement.line']
currency_obj = self.pool['res.currency']
line_date = time.strftime(DEFAULT_SERVER_DATE_FORMAT)
statement = statement_obj.browse(
cr, uid, statement_id, context=context)
# for each selected move lines
for line in line_obj.browse(cr, uid, line_ids, context=context):
ctx = context.copy()
# take the date for computation of currency => use payment date
ctx['date'] = line_date
amount = 0.0
if line.debit > 0:
amount = line.debit
elif line.credit > 0:
amount = -line.credit
if line.amount_currency:
amount = currency_obj.compute(cr, uid, line.currency_id.id,
statement.currency.id, line.amount_currency, context=ctx)
elif (line.invoice and line.invoice.currency_id.id != statement.currency.id):
amount = currency_obj.compute(cr, uid, line.invoice.currency_id.id,
statement.currency.id, amount, context=ctx)
amount = currency_obj.compute(
cr, uid, line.currency_id.id, statement.currency.id,
line.amount_currency, context=ctx)
elif (line.invoice and
line.invoice.currency_id.id != statement.currency.id):
amount = currency_obj.compute(
cr, uid, line.invoice.currency_id.id, statement.currency.id,
amount, context=ctx)
context.update({'move_line_ids': [line.id],
'invoice_id': line.invoice.id})
s_type = 'general'
@@ -97,25 +96,25 @@ class AccountPaymentPopulateStatement(orm.TransientModel):
def populate_statement(self, cr, uid, ids, context=None):
"""Taken from payment addon as no hook is vailable. No function
no refactoring, just trimming the part that generates voucher"""
line_obj = self.pool.get('payment.line')
statement_obj = self.pool.get('account.bank.statement')
statement_line_obj = self.pool.get('account.bank.statement.line')
currency_obj = self.pool.get('res.currency')
line_obj = self.pool['payment.line']
statement_obj = self.pool['account.bank.statement']
statement_line_obj = self.pool['account.bank.statement.line']
currency_obj = self.pool['res.currency']
if context is None:
context = {}
data = self.read(cr, uid, ids, [], context=context)[0]
line_ids = data['lines']
if not line_ids:
return {'type': 'ir.actions.act_window_close'}
statement = statement_obj.browse(cr, uid, context['active_id'], context=context)
statement = statement_obj.browse(
cr, uid, context['active_id'], context=context)
for line in line_obj.browse(cr, uid, line_ids, context=context):
ctx = context.copy()
ctx['date'] = line.ml_maturity_date # Last value_date earlier,but this field exists no more now
amount = currency_obj.compute(cr, uid, line.currency.id,
statement.currency.id, line.amount_currency, context=ctx)
# Last value_date earlier,but this field exists no more now
ctx['date'] = line.ml_maturity_date
amount = currency_obj.compute(
cr, uid, line.currency.id, statement.currency.id,
line.amount_currency, context=ctx)
if not line.move_line_id.id:
continue
context.update({'move_line_ids': [line.move_line_id.id]})
@@ -123,13 +122,14 @@ class AccountPaymentPopulateStatement(orm.TransientModel):
cr, uid, line, -amount, statement, context=context)
st_line_id = statement_line_obj.create(cr, uid, vals,
context=context)
line_obj.write(cr, uid, [line.id], {'bank_statement_line_id': st_line_id})
line_obj.write(
cr, uid, [line.id], {'bank_statement_line_id': st_line_id})
return {'type': 'ir.actions.act_window_close'}
def _prepare_statement_line_vals(self, cr, uid, payment_line, amount,
statement, context=None):
return {'name': payment_line.order_id.reference or '?',
return {
'name': payment_line.order_id.reference or '?',
'amount': amount,
'type': 'supplier',
'partner_id': payment_line.partner_id.id,