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 # 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 # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # it under the terms of the GNU Affero General Public License as
@@ -22,3 +23,4 @@
import easy_reconcile import easy_reconcile
import base_advanced_reconciliation import base_advanced_reconciliation
import advanced_reconciliation import advanced_reconciliation
import res_config # noqa

View File

@@ -2,7 +2,8 @@
############################################################################## ##############################################################################
# #
# Author: Guewen Baconnier # 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 # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # 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. possible.
So basically, if you have an invoice with 3 payments (one per month), the first 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 the first payment, the
month, it will partial reconcile the debit move line with 2 first payments, second month, it will partial reconcile the debit move line with 2 first
the third month, it will make the full reconciliation. payments, the third month, it will make the full reconciliation.
This module is perfectly adapted for E-Commerce business where a big volume of 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 move lines and so, reconciliations, are involved and payments often come from
@@ -73,11 +74,12 @@ many offices.
""", """,
'website': 'http://www.camptocamp.com', 'website': 'http://www.camptocamp.com',
'data': ['easy_reconcile_view.xml'], 'data': ['easy_reconcile_view.xml',
'res_config_view.xml'],
'test': [], 'test': [],
'images': [], 'images': [],
'installable': False, 'installable': False,
'auto_install': False, 'auto_install': False,
'license': 'AGPL-3', 'license': 'AGPL-3',
'application': True, 'application': True,
} }

View File

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

@@ -19,7 +19,7 @@
############################################################################## ##############################################################################
{'name': 'Advanced Reconcile Transaction Ref', {'name': 'Advanced Reconcile Transaction Ref',
'description': """ 'description': """
Advanced reconciliation method for the module account_advanced_reconcile Advanced reconciliation method for the module account_advanced_reconcile
======================================================================== ========================================================================
Reconcile rules with transaction_ref Reconcile rules with transaction_ref
@@ -32,9 +32,8 @@ Reconcile rules with transaction_ref
'depends': ['account_advanced_reconcile'], 'depends': ['account_advanced_reconcile'],
'data': ['easy_reconcile_view.xml'], 'data': ['easy_reconcile_view.xml'],
'demo': [], 'demo': [],
'test': [], # To be ported or migrate to unit tests or scenarios 'test': [], # To be ported or migrate to unit tests or scenarios
'auto_install': False, 'auto_install': False,
'installable': False, 'installable': False,
'images': [] 'images': []
} }
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@@ -19,7 +19,7 @@
############################################################################## ##############################################################################
from openerp.osv import orm from openerp.osv import orm
class easy_reconcile_advanced_transaction_ref(orm.TransientModel): class easy_reconcile_advanced_transaction_ref(orm.TransientModel):
@@ -35,10 +35,10 @@ class easy_reconcile_advanced_transaction_ref(orm.TransientModel):
return not (move_line.get('transaction_ref') and return not (move_line.get('transaction_ref') and
move_line.get('partner_id')) move_line.get('partner_id'))
def _matchers(self, cr, uid, rec, move_line, context=None): def _matchers(self, cr, uid, rec, move_line, context=None):
return (('partner_id', move_line['partner_id']), return (('partner_id', move_line['partner_id']),
('ref', move_line['transaction_ref'].lower().strip())) ('ref', move_line['transaction_ref'].lower().strip()))
def _opposite_matchers(self, cr, uid, rec, move_line, context=None): def _opposite_matchers(self, cr, uid, rec, move_line, context=None):
yield ('partner_id', move_line['partner_id']) yield ('partner_id', move_line['partner_id'])
yield ('ref', (move_line['transaction_ref'] or '').lower().strip()) yield ('ref', (move_line['transaction_ref'] or '').lower().strip())

View File

@@ -19,14 +19,13 @@
# #
############################################################################## ##############################################################################
from itertools import product
from openerp.osv import orm from openerp.osv import orm
class easy_reconcile_advanced(orm.AbstractModel): class EasyReconcileAdvanced(orm.AbstractModel):
_inherit = 'easy.reconcile.advanced' _inherit = 'easy.reconcile.advanced'
def _base_columns(self, rec): def _base_columns(self, rec):
""" Mandatory columns for move lines queries """ Mandatory columns for move lines queries
An extra column aliased as ``key`` should be defined An extra column aliased as ``key`` should be defined
@@ -43,4 +42,4 @@ class easy_reconcile_advanced(orm.AbstractModel):
'account_id', 'account_id',
'move_id', 'move_id',
'transaction_ref') 'transaction_ref')
return ["account_move_line.%s" % col for col in aml_cols] return ["account_move_line.%s" % col for col in aml_cols]

View File

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

View File

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

View File

@@ -19,13 +19,11 @@
# #
############################################################################## ##############################################################################
from openerp.osv import fields, osv, orm from openerp.osv import fields, orm
from openerp.tools.translate import _
from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT
from openerp.tools.translate import _ from openerp.tools.translate import _
class easy_reconcile_options(orm.AbstractModel): class EasyReconcileOptions(orm.AbstractModel):
"""Options of a reconciliation profile """Options of a reconciliation profile
Columns shared by the configuration of methods Columns shared by the configuration of methods
@@ -37,26 +35,31 @@ class easy_reconcile_options(orm.AbstractModel):
_name = 'easy.reconcile.options' _name = 'easy.reconcile.options'
def _get_rec_base_date(self, cr, uid, context=None): def _get_rec_base_date(self, cr, uid, context=None):
return [('end_period_last_credit', 'End of period of most recent credit'), return [
('newest', 'Most recent move line'), ('end_period_last_credit', 'End of period of most recent credit'),
('actual', 'Today'), ('newest', 'Most recent move line'),
('end_period', 'End of period of most recent move line'), ('actual', 'Today'),
('newest_credit', 'Date of most recent credit'), ('end_period', 'End of period of most recent move line'),
('newest_debit', 'Date of most recent debit')] ('newest_credit', 'Date of most recent credit'),
('newest_debit', 'Date of most recent debit')
]
_columns = { _columns = {
'write_off': fields.float('Write off allowed'), 'write_off': fields.float('Write off allowed'),
'account_lost_id': fields.many2one( 'account_lost_id': fields.many2one(
'account.account', 'Account Lost'), 'account.account', 'Account Lost'),
'account_profit_id': fields.many2one( 'account_profit_id': fields.many2one(
'account.account', 'Account Profit'), 'account.account', 'Account Profit'),
'journal_id': fields.many2one( 'journal_id': fields.many2one(
'account.journal', 'Journal'), 'account.journal', 'Journal'),
'date_base_on': fields.selection( 'date_base_on': fields.selection(
_get_rec_base_date, _get_rec_base_date,
required=True, required=True,
string='Date of reconciliation'), string='Date of reconciliation'),
'filter': fields.char('Filter', size=128), 'filter': fields.char('Filter', size=128),
'analytic_account_id': fields.many2one(
'account.analytic.account', 'Analytic Account',
help="Analytic account for the write-off"),
} }
_defaults = { _defaults = {
@@ -65,44 +68,42 @@ class easy_reconcile_options(orm.AbstractModel):
} }
class account_easy_reconcile_method(orm.Model): class AccountEasyReconcileMethod(orm.Model):
_name = 'account.easy.reconcile.method' _name = 'account.easy.reconcile.method'
_description = 'reconcile method for account_easy_reconcile' _description = 'reconcile method for account_easy_reconcile'
_inherit = 'easy.reconcile.options' _inherit = 'easy.reconcile.options'
_order = 'sequence' _order = 'sequence'
def _get_all_rec_method(self, cr, uid, context=None): def _get_all_rec_method(self, cr, uid, context=None):
return [ return [
('easy.reconcile.simple.name', 'Simple. Amount and Name'), ('easy.reconcile.simple.name', 'Simple. Amount and Name'),
('easy.reconcile.simple.partner', 'Simple. Amount and Partner'), ('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): def _get_rec_method(self, cr, uid, context=None):
return self._get_all_rec_method(cr, uid, context=None) return self._get_all_rec_method(cr, uid, context=None)
_columns = { _columns = {
'name': fields.selection( 'name': fields.selection(
_get_rec_method, 'Type', required=True), _get_rec_method, 'Type', required=True),
'sequence': fields.integer( 'sequence': fields.integer(
'Sequence', 'Sequence',
required=True, required=True,
help="The sequence field is used to order " help="The sequence field is used to order "
"the reconcile method"), "the reconcile method"),
'task_id': fields.many2one( 'task_id': fields.many2one(
'account.easy.reconcile', 'account.easy.reconcile',
string='Task', string='Task',
required=True, required=True,
ondelete='cascade'), ondelete='cascade'),
'company_id': fields.related('task_id','company_id', 'company_id': fields.related('task_id', 'company_id',
relation='res.company', relation='res.company',
type='many2one', type='many2one',
string='Company', string='Company',
store=True, store=True,
readonly=True), readonly=True),
} }
_defaults = { _defaults = {
@@ -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' _name = 'account.easy.reconcile'
_description = 'account easy reconcile' _description = 'account easy reconcile'
@@ -199,6 +200,8 @@ class account_easy_reconcile(orm.Model):
rec_method.account_lost_id.id), rec_method.account_lost_id.id),
'account_profit_id': (rec_method.account_profit_id and 'account_profit_id': (rec_method.account_profit_id and
rec_method.account_profit_id.id), 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 'journal_id': (rec_method.journal_id and
rec_method.journal_id.id), rec_method.journal_id.id),
'date_base_on': rec_method.date_base_on, 'date_base_on': rec_method.date_base_on,
@@ -235,9 +238,9 @@ class account_easy_reconcile(orm.Model):
all_ml_partial_ids += ml_partial_ids all_ml_partial_ids += ml_partial_ids
reconcile_ids = find_reconcile_ids( reconcile_ids = find_reconcile_ids(
'reconcile_id', all_ml_rec_ids) 'reconcile_id', all_ml_rec_ids)
partial_ids = find_reconcile_ids( partial_ids = find_reconcile_ids(
'reconcile_partial_id', all_ml_partial_ids) 'reconcile_partial_id', all_ml_partial_ids)
self.pool.get('easy.reconcile.history').create( self.pool.get('easy.reconcile.history').create(
cr, cr,
@@ -254,11 +257,11 @@ class account_easy_reconcile(orm.Model):
be called when there is no history on the reconciliation be called when there is no history on the reconciliation
task. task.
""" """
raise osv.except_osv( raise orm.except_orm(
_('Error'), _('Error'),
_('There is no history of reconciled ' _('There is no history of reconciled '
'items on the task: %s.') % rec.name) 'items on the task: %s.') % rec.name)
def _open_move_line_list(sefl, cr, uid, move_line_ids, name, context=None): def _open_move_line_list(sefl, cr, uid, move_line_ids, name, context=None):
return { return {
'name': name, 'name': name,
@@ -270,19 +273,15 @@ class account_easy_reconcile(orm.Model):
'nodestroy': True, 'nodestroy': True,
'target': 'current', 'target': 'current',
'domain': unicode([('id', 'in', move_line_ids)]), 'domain': unicode([('id', 'in', move_line_ids)]),
} }
def open_unreconcile(self, cr, uid, ids, context=None): 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"
assert len(ids) == 1 , \
"You can only open entries from one profile at a time"
obj_move_line = self.pool.get('account.move.line') obj_move_line = self.pool.get('account.move.line')
res = {}
for task in self.browse(cr, uid, ids, context=context): for task in self.browse(cr, uid, ids, context=context):
line_ids = obj_move_line.search( line_ids = obj_move_line.search(
cr, uid, cr, uid,
[('account_id', '=', task.account.id), [('account_id', '=', task.account.id),
('reconcile_id', '=', False), ('reconcile_id', '=', False),
@@ -290,26 +289,24 @@ class account_easy_reconcile(orm.Model):
context=context) context=context)
name = _('Unreconciled items') 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): 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"
assert len(ids) == 1 , \
"You can only open entries from one profile at a time"
obj_move_line = self.pool.get('account.move.line') obj_move_line = self.pool.get('account.move.line')
res = {}
for task in self.browse(cr, uid, ids, context=context): for task in self.browse(cr, uid, ids, context=context):
line_ids = obj_move_line.search( line_ids = obj_move_line.search(
cr, uid, cr, uid,
[('account_id', '=', task.account.id), [('account_id', '=', task.account.id),
('reconcile_id', '=', False), ('reconcile_id', '=', False),
('reconcile_partial_id', '!=', False)], ('reconcile_partial_id', '!=', False)],
context=context) context=context)
name = _('Partial reconciled items') 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): def last_history_reconcile(self, cr, uid, rec_id, context=None):
""" Get the last history record for this reconciliation profile """ Get the last history record for this reconciliation profile
@@ -317,7 +314,7 @@ class account_easy_reconcile(orm.Model):
""" """
if isinstance(rec_id, (tuple, list)): if isinstance(rec_id, (tuple, list)):
assert len(rec_id) == 1, \ assert len(rec_id) == 1, \
"Only 1 id expected" "Only 1 id expected"
rec_id = rec_id[0] rec_id = rec_id[0]
rec = self.browse(cr, uid, rec_id, context=context) rec = self.browse(cr, uid, rec_id, context=context)
if not rec.last_history: if not rec.last_history:
@@ -330,7 +327,7 @@ class account_easy_reconcile(orm.Model):
""" """
if isinstance(rec_id, (tuple, list)): if isinstance(rec_id, (tuple, list)):
assert len(rec_id) == 1, \ assert len(rec_id) == 1, \
"Only 1 id expected" "Only 1 id expected"
rec_id = rec_id[0] rec_id = rec_id[0]
rec = self.browse(cr, uid, rec_id, context=context) rec = self.browse(cr, uid, rec_id, context=context)
if not rec.last_history: if not rec.last_history:

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_lost_id" attrs="{'required':[('write_off','>',0)]}"/>
<field name="account_profit_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="journal_id" attrs="{'required':[('write_off','>',0)]}"/>
<field name="analytic_account_id" groups="analytic.group_analytic_accounting"/>
<field name="date_base_on"/> <field name="date_base_on"/>
</form> </form>
</field> </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_lost_id" attrs="{'required':[('write_off','>',0)]}"/>
<field name="account_profit_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="journal_id" attrs="{'required':[('write_off','>',0)]}"/>
<field name="analytic_account_id" groups="analytic.group_analytic_accounting"/>
<field name="date_base_on"/> <field name="date_base_on"/>
</tree> </tree>
</field> </field>

View File

@@ -23,7 +23,7 @@ from openerp.osv import orm, fields
from openerp.tools.translate import _ from openerp.tools.translate import _
class easy_reconcile_history(orm.Model): class EasyReconcileHistory(orm.Model):
""" Store an history of the runs per profile """ Store an history of the runs per profile
Each history stores the list of reconciliations done""" Each history stores the list of reconciliations done"""
@@ -33,46 +33,43 @@ class easy_reconcile_history(orm.Model):
def _reconcile_line_ids(self, cr, uid, ids, name, args, context=None): def _reconcile_line_ids(self, cr, uid, ids, name, args, context=None):
result = {} result = {}
for history in self.browse(cr, uid, ids, context=context): for history in self.browse(cr, uid, ids, context=context):
result[history.id] = {} result[history.id] = {}
move_line_ids = [] move_line_ids = []
for reconcile in history.reconcile_ids: for reconcile in history.reconcile_ids:
move_line_ids += [line.id move_line_ids += [line.id
for line for line
in reconcile.line_id] in reconcile.line_id]
result[history.id]['reconcile_line_ids'] = move_line_ids result[history.id]['reconcile_line_ids'] = move_line_ids
move_line_ids = [] move_line_ids = []
for reconcile in history.reconcile_partial_ids: for reconcile in history.reconcile_partial_ids:
move_line_ids += [line.id move_line_ids += [line.id
for line for line
in reconcile.line_partial_ids] in reconcile.line_partial_ids]
result[history.id]['partial_line_ids'] = move_line_ids result[history.id]['partial_line_ids'] = move_line_ids
return result return result
_columns = { _columns = {
'easy_reconcile_id': fields.many2one( 'easy_reconcile_id': fields.many2one(
'account.easy.reconcile', 'Reconcile Profile', readonly=True), 'account.easy.reconcile', 'Reconcile Profile', readonly=True),
'date': fields.datetime('Run date', readonly=True), 'date': fields.datetime('Run date', readonly=True),
'reconcile_ids': fields.many2many( 'reconcile_ids': fields.many2many(
'account.move.reconcile', 'account.move.reconcile',
'account_move_reconcile_history_rel', 'account_move_reconcile_history_rel',
string='Reconciliations', readonly=True), string='Reconciliations', readonly=True),
'reconcile_partial_ids': fields.many2many( 'reconcile_partial_ids': fields.many2many(
'account.move.reconcile', 'account.move.reconcile',
'account_move_reconcile_history_partial_rel', 'account_move_reconcile_history_partial_rel',
string='Partial Reconciliations', readonly=True), string='Partial Reconciliations', readonly=True),
'reconcile_line_ids': 'reconcile_line_ids':
fields.function( fields.function(
_reconcile_line_ids, _reconcile_line_ids,
string='Reconciled Items', string='Reconciled Items',
type='many2many', type='many2many',
relation='account.move.line', relation='account.move.line',
readonly=True, readonly=True,
multi='lines'), multi='lines'),
'partial_line_ids': 'partial_line_ids':
fields.function( fields.function(
_reconcile_line_ids, _reconcile_line_ids,
@@ -81,16 +78,17 @@ class easy_reconcile_history(orm.Model):
relation='account.move.line', relation='account.move.line',
readonly=True, readonly=True,
multi='lines'), multi='lines'),
'company_id': fields.related('easy_reconcile_id','company_id', 'company_id': fields.related('easy_reconcile_id', 'company_id',
relation='res.company', relation='res.company',
type='many2one', type='many2one',
string='Company', string='Company',
store=True, store=True,
readonly=True), readonly=True),
} }
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 """ For an history record, open the view of move line with
the reconciled or partially reconciled move lines the reconciled or partially reconciled move lines
@@ -99,19 +97,15 @@ class easy_reconcile_history(orm.Model):
:return: action to open the move lines :return: action to open the move lines
""" """
assert rec_type in ('full', 'partial'), \ assert rec_type in ('full', 'partial'), \
"rec_type must be 'full' or 'partial'" "rec_type must be 'full' or 'partial'"
history = self.browse(cr, uid, history_id, context=context) history = self.browse(cr, uid, history_id, context=context)
if rec_type == 'full': if rec_type == 'full':
field = 'reconcile_line_ids' field = 'reconcile_line_ids'
name = _('Reconciliations') name = _('Reconciliations')
else: else:
field = 'partial_line_ids' field = 'partial_line_ids'
name = _('Partial Reconciliations') name = _('Partial Reconciliations')
move_line_ids = [line.id for line in getattr(history, field)] move_line_ids = [line.id for line in getattr(history, field)]
return { return {
'name': name, 'name': name,
'view_mode': 'tree,form', 'view_mode': 'tree,form',
@@ -122,7 +116,7 @@ class easy_reconcile_history(orm.Model):
'nodestroy': True, 'nodestroy': True,
'target': 'current', 'target': 'current',
'domain': unicode([('id', 'in', move_line_ids)]), 'domain': unicode([('id', 'in', move_line_ids)]),
} }
def open_reconcile(self, cr, uid, history_ids, context=None): def open_reconcile(self, cr, uid, history_ids, context=None):
""" For an history record, open the view of move line """ For an history record, open the view of move line
@@ -136,7 +130,7 @@ class easy_reconcile_history(orm.Model):
assert len(history_ids) == 1, "only 1 ID is accepted" assert len(history_ids) == 1, "only 1 ID is accepted"
history_ids = history_ids[0] history_ids = history_ids[0]
return self._open_move_lines( return self._open_move_lines(
cr, uid, history_ids, rec_type='full', context=None) cr, uid, history_ids, rec_type='full', context=None)
def open_partial(self, cr, uid, history_ids, context=None): def open_partial(self, cr, uid, history_ids, context=None):
""" For an history record, open the view of move line """ For an history record, open the view of move line
@@ -150,4 +144,4 @@ class easy_reconcile_history(orm.Model):
assert len(history_ids) == 1, "only 1 ID is accepted" assert len(history_ids) == 1, "only 1 ID is accepted"
history_ids = history_ids[0] history_ids = history_ids[0]
return self._open_move_lines( return self._open_move_lines(
cr, uid, history_ids, rec_type='partial', context=None) cr, uid, history_ids, rec_type='partial', context=None)

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

View File

@@ -19,15 +19,15 @@
# #
############################################################################## ##############################################################################
{'name' : 'Invoices Reference', {'name': 'Invoices Reference',
'version' : '1.0', 'version': '1.0',
'author' : 'Camptocamp', 'author': 'Camptocamp',
'maintainer': 'Camptocamp', 'maintainer': 'Camptocamp',
'license': 'AGPL-3', 'license': 'AGPL-3',
'category': 'category', 'category': 'category',
'complexity': "easy", 'complexity': "easy",
'depends' : ['account', 'depends': ['account',
], ],
'description': """ 'description': """
Invoices Reference Invoices Reference
================== ==================
@@ -59,13 +59,15 @@ mandatory fields are:
* Description * Description
* Internal Reference ("our reference") * Internal Reference ("our reference")
* External Reference ("customer or supplier 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: Now, on the move lines:
* Name * Name
* Reference * 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. Let's see how the information will be organized with this module.
@@ -143,4 +145,4 @@ Information propagated to the move lines:
], ],
'installable': False, 'installable': False,
'auto_install': False, 'auto_install': False,
} }

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' _inherit = 'account.move'
def create(self, cr, uid, vals, context=None): def create(self, cr, uid, vals, context=None):
@@ -33,15 +33,16 @@ class account_move(orm.Model):
if invoice: if invoice:
assert isinstance(invoice, orm.browse_record) assert isinstance(invoice, orm.browse_record)
invoice_obj = self.pool['account.invoice'] 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 = vals.copy()
vals['ref'] = ref vals['ref'] = ref
move_id = super(account_move, self).\ move_id = super(AccountMove, self).create(cr, uid, vals,
create(cr, uid, vals, context=context) context=context)
return move_id return move_id
class account_invoice(orm.Model): class AccountInvoice(orm.Model):
_inherit = 'account.invoice' _inherit = 'account.invoice'
def _ref_from_invoice(self, cr, uid, invoice, context=None): def _ref_from_invoice(self, cr, uid, invoice, context=None):
@@ -69,32 +70,32 @@ class account_invoice(orm.Model):
cr.execute('UPDATE account_move_line SET ref=%s ' cr.execute('UPDATE account_move_line SET ref=%s '
'WHERE move_id=%s AND (ref is null OR ref = \'\')', 'WHERE move_id=%s AND (ref is null OR ref = \'\')',
(ref, move_id)) (ref, move_id))
cr.execute('UPDATE account_analytic_line SET ref=%s ' cr.execute(
'FROM account_move_line ' 'UPDATE account_analytic_line SET ref=%s '
'WHERE account_move_line.move_id = %s ' 'FROM account_move_line '
'AND account_analytic_line.move_id = account_move_line.id', 'WHERE account_move_line.move_id = %s '
(ref, move_id)) 'AND account_analytic_line.move_id = account_move_line.id',
(ref, move_id))
return True return True
def create(self, cr, uid, vals, context=None): def create(self, cr, uid, vals, context=None):
if (vals.get('supplier_invoice_reference') and not if (vals.get('supplier_invoice_reference') and not
vals.get('reference')): vals.get('reference')):
vals['reference'] = vals['supplier_invoice_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) context=context)
def write(self, cr, uid, ids, vals, context=None): def write(self, cr, uid, ids, vals, context=None):
if vals.get('supplier_invoice_reference'): if vals.get('supplier_invoice_reference'):
if isinstance(ids, (int, long)): if isinstance(ids, (int, long)):
ids = [ids] ids = [ids]
for invoice in self.browse(cr, uid, ids, context=context): for invoice in self.browse(cr, uid, ids, context=context):
local_vals = vals
if not invoice.reference: if not invoice.reference:
locvals = vals.copy() locvals = vals.copy()
locvals['reference'] = vals['supplier_invoice_reference'] 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) locvals, context=context)
return True return True
else: else:
return super(account_invoice, self).write(cr, uid, ids, vals, return super(AccountInvoice, self).write(cr, uid, ids, vals,
context=context) context=context)

View File

@@ -40,4 +40,4 @@ Needs `statement_voucher_killer`
'test': [], 'test': [],
'installable': False, 'installable': False,
'auto_install': True, 'auto_install': True,
} }

View File

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

View File

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

View File

@@ -19,59 +19,66 @@
# #
############################################################################## ##############################################################################
{'name': "Bank statement base completion", {
'version': '1.0.3', 'name': "Bank statement base completion",
'author': 'Camptocamp', 'version': '1.0.3',
'maintainer': 'Camptocamp', 'author': 'Camptocamp',
'category': 'Finance', 'maintainer': 'Camptocamp',
'complexity': 'normal', 'category': 'Finance',
'depends': ['account_statement_ext'], 'complexity': 'normal',
'description': """ 'depends': ['account_statement_ext'],
The goal of this module is to improve the basic bank statement, help dealing with huge volume of 'description': """
reconciliation by providing basic rules to identify the partner of a bank statement line. The goal of this module is to improve the basic bank statement, help dealing
Each bank statement profile can have its own rules to be applied according to a sequence order. 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: 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) 2) Match from statement line label (based on partner name)
3) Match from statement line reference (based on Invoice number) 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 You can easily override this module and add your own rules in your own one. The
fill in the partner, but you can use them to fill in any value of the line (in the future, we will basic rules only fill in the partner, but you can use them to fill in any
add a rule to automatically match and reconcile the line). 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 It adds as well a label on the bank statement line (on which the pre-define
a char field on the partner called 'Bank Statement Label'. Using the pre-define rules, you will be rules can match) and a char field on the partner called 'Bank Statement Label'.
able to match various labels for a partner. 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 The reference of the line is always used by the reconciliation process. We're
there (or write manually) the matching string. This can be: the order Number or an invoice number, supposed to copy there (or write manually) the matching string. This can be:
or anything that will be found in the invoice accounting entry part to make the match. 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. 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
TODO: The rules that look for invoices to find out the partner should take back the payable / receivable the payable / receivable account from there directly instead of retrieving it
account from there directly instead of retrieving it from partner properties ! from partner properties !
""",
""", 'website': 'http://www.camptocamp.com',
'website': 'http://www.camptocamp.com', 'data': [
'data': [ 'statement_view.xml',
'statement_view.xml', 'partner_view.xml',
'partner_view.xml', 'data.xml',
'data.xml', 'security/ir.model.access.csv',
'security/ir.model.access.csv', ],
], 'demo': [],
'demo': [], 'test': [
'test': [ 'test/partner.yml',
'test/partner.yml', 'test/invoice.yml',
'test/invoice.yml', 'test/supplier_invoice.yml',
'test/supplier_invoice.yml', 'test/completion_test.yml'
'test/completion_test.yml' ],
], 'installable': False,
'installable': False, 'images': [],
'images': [], 'auto_install': False,
'auto_install': False, 'license': 'AGPL-3',
'license': 'AGPL-3',
} }

View File

@@ -1,38 +1,39 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
################################################################################# ##########################################################################
# # #
# Copyright (C) 2011 Akretion & Camptocamp # Copyright (C) 2011 Akretion & Camptocamp
# Author : Sébastien BEAU, Joel Grand-Guillaume # # Author : Sébastien BEAU, Joel Grand-Guillaume
# # #
# This program is free software: you can redistribute it and/or modify # # This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as # # it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the # # published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version. # # License, or (at your option) any later version.
# # #
# This program is distributed in the hope that it will be useful, # # This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of # # but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details. # # GNU Affero General Public License for more details.
# # #
# You should have received a copy of the GNU Affero General Public License # # 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/>. # # along with this program. If not, see <http://www.gnu.org/licenses/>.
# # #
################################################################################# ##########################################################################
from openerp.osv.orm import Model from openerp.osv import orm, fields
from openerp.osv import fields
class res_partner(Model): class ResPartner(orm.Model):
""" """Add a bank label on the partner so that we can use it to match
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. this partner when we found this in a statement line.
""" """
_inherit = 'res.partner' _inherit = 'res.partner'
_columns = { _columns = {
'bank_statement_label': fields.char('Bank Statement Label', size=100, 'bank_statement_label': fields.char(
help="Enter the various label found on your bank statement separated by a ; If " 'Bank Statement Label', size=100,
"one of this label is include in the bank statement line, the partner will be automatically " help="Enter the various label found on your bank statement "
"filled (as long as you use this method/rules in your statement profile)."), "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"> <record id="bk_view_partner_form" model="ir.ui.view">
<field name="name">account_bank_statement_import.view.partner.form</field> <field name="name">account_bank_statement_import.view.partner.form</field>
<field name="model">res.partner</field> <field name="model">res.partner</field>
<field name="type">form</field>
<field name="priority">20</field> <field name="priority">20</field>
<field name="inherit_id" ref="account.view_partner_property_form"/> <field name="inherit_id" ref="account.view_partner_property_form"/>
<field name="arch" type="xml"> <field name="arch" type="xml">
@@ -17,6 +16,6 @@
</field> </field>
</record> </record>
</data> </data>
</openerp> </openerp>

View File

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

View File

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

@@ -28,42 +28,46 @@
'depends': [ 'depends': [
'account_statement_ext', 'account_statement_ext',
'account_statement_base_completion' 'account_statement_base_completion'
], ],
'description': """ 'description': """
This module brings basic methods and fields on bank statement to deal with 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 the importation of different bank and offices. A generic abstract method is
example that gives you a basic way of importing bank statement through a standard file is provided. 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 This module improves the bank statement and allows you to import your bank
a standard .csv or .xls file (you'll find it in the 'data' folder). It respects the profile transactions with a standard .csv or .xls file (you'll find it in the 'data'
(provided by the accouhnt_statement_ext module) to pass the entries. That means, folder). It respects the profile (provided by the accouhnt_statement_ext
you'll have to choose a file format for each profile. module) to pass the entries. That means, you'll have to choose a file format
In order to achieve this it uses the `xlrd` Python module which you will need to install for each profile.
separately in your environment. 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 * __ref__: the SO number, INV number or any matching ref found. It'll be used
in the generated entries and will be useful for reconciliation process as reference in the generated entries and will be useful for reconciliation
* date : date of the payment process
* amount : amount paid in the currency of the journal used in the importation profile * __date__: date of the payment
* label : the comunication given by the payment office, used as communication in the * __amount__: amount paid in the currency of the journal used in the
generated entries. importation profile
* __label__: the comunication given by the payment office, used as
The goal is here to populate the statement lines of a bank statement with the infos that the communication in the generated entries.
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.
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', 'website': 'http://www.camptocamp.com',
'data': [ 'data': [
"wizard/import_statement_view.xml", "wizard/import_statement_view.xml",
"statement_view.xml", "statement_view.xml",
], ],
'test': [], 'test': [],
'installable': False, 'installable': False,
'images': [], 'images': [],
'auto_install': False, 'auto_install': False,
'license': 'AGPL-3', 'license': 'AGPL-3',
} }

View File

@@ -18,7 +18,7 @@
# #
############################################################################## ##############################################################################
from openerp.tools.translate import _ from openerp.tools.translate import _
from openerp.osv.osv import except_osv from openerp.osv.orm import except_orm
import tempfile import tempfile
import datetime import datetime
from parser import BankStatementImportParser from parser import BankStatementImportParser
@@ -28,31 +28,36 @@ try:
except: except:
raise Exception(_('Please install python lib xlrd')) raise Exception(_('Please install python lib xlrd'))
def float_or_zero(val): def float_or_zero(val):
""" Conversion function used to manage """ Conversion function used to manage
empty string into float usecase""" empty string into float usecase"""
return float(val) if val else 0.0 return float(val) if val else 0.0
class FileParser(BankStatementImportParser): class FileParser(BankStatementImportParser):
""" """Generic abstract class for defining parser for .csv, .xls or .xlsx file
Generic abstract class for defining parser for .csv, .xls or .xlsx file format. 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: parse_name: The name of the parser
:param char: ftype: extension of the file (could be csv, xls or xlsx) :param char: ftype: extension of the file (could be csv, xls or
:param dict: extra_fields: extra fields to add to the conversion dict. In the format xlsx)
{fieldname: fieldtype} :param dict: extra_fields: extra fields to add to the conversion
:param list: header : specify header fields if the csv file has no header 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) super(FileParser, self).__init__(parse_name, **kwargs)
if ftype in ('csv', 'xls' ,'xlsx'): if ftype in ('csv', 'xls', 'xlsx'):
self.ftype = ftype[0:3] self.ftype = ftype[0:3]
else: else:
raise except_osv(_('User Error'), raise except_orm(
_('Invalid file type %s. Please use csv, xls or xlsx') % ftype) _('User Error'),
_('Invalid file type %s. Please use csv, xls or xlsx') % ftype)
self.conversion_dict = { self.conversion_dict = {
'ref': unicode, 'ref': unicode,
'label': unicode, 'label': unicode,
@@ -64,27 +69,21 @@ class FileParser(BankStatementImportParser):
self.keys_to_validate = self.conversion_dict.keys() self.keys_to_validate = self.conversion_dict.keys()
self.fieldnames = header self.fieldnames = header
self._datemode = 0 # used only for xls documents, self._datemode = 0 # used only for xls documents,
# 0 means Windows mode (1900 based dates). # 0 means Windows mode (1900 based dates).
# Set in _parse_xls, from the contents of the file # Set in _parse_xls, from the contents of the file
def _custom_format(self, *args, **kwargs): 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 return True
def _pre(self, *args, **kwargs): def _pre(self, *args, **kwargs):
""" """No pre-treatment needed for this parser."""
No pre-treatment needed for this parser.
"""
return True return True
def _parse(self, *args, **kwargs): 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 given ftype
""" """
res = None res = None
if self.ftype == 'csv': if self.ftype == 'csv':
res = self._parse_csv() res = self._parse_csv()
@@ -94,31 +93,27 @@ class FileParser(BankStatementImportParser):
return True return True
def _validate(self, *args, **kwargs): def _validate(self, *args, **kwargs):
""" """We check that all the key of the given file (means header) are
We check that all the key of the given file (means header) are present present in the validation key provided. Otherwise, we raise an
in the validation key provided. Otherwise, we raise an Exception. Exception. We skip the validation step if the file header is provided
We skip the validation step if the file header is provided separately separately (in the field: fieldnames).
(in the field: fieldnames).
""" """
if self.fieldnames is None: if self.fieldnames is None:
parsed_cols = self.result_row_list[0].keys() parsed_cols = self.result_row_list[0].keys()
for col in self.keys_to_validate: for col in self.keys_to_validate:
if col not in parsed_cols: if col not in parsed_cols:
raise except_osv(_('Invalid data'), raise except_orm(_('Invalid data'),
_('Column %s not present in file') % col) _('Column %s not present in file') % col)
return True return True
def _post(self, *args, **kwargs): def _post(self, *args, **kwargs):
""" """Cast row type depending on the file format .csv or .xls after
Cast row type depending on the file format .csv or .xls after parsing the file. parsing the file."""
"""
self.result_row_list = self._cast_rows(*args, **kwargs) self.result_row_list = self._cast_rows(*args, **kwargs)
return True return True
def _parse_csv(self): 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 = tempfile.NamedTemporaryFile()
csv_file.write(self.filebuffer) csv_file.write(self.filebuffer)
csv_file.flush() csv_file.flush()
@@ -127,9 +122,7 @@ class FileParser(BankStatementImportParser):
return list(reader) return list(reader)
def _parse_xls(self): 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 = tempfile.NamedTemporaryFile()
wb_file.write(self.filebuffer) wb_file.write(self.filebuffer)
# We ensure that cursor is at beginig of file # We ensure that cursor is at beginig of file
@@ -144,8 +137,7 @@ class FileParser(BankStatementImportParser):
return res return res
def _from_csv(self, result_set, conversion_rules): 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. an .csv file.
""" """
for line in result_set: for line in result_set:
@@ -156,65 +148,60 @@ class FileParser(BankStatementImportParser):
line[rule] = datetime.datetime.strptime(date_string, line[rule] = datetime.datetime.strptime(date_string,
'%Y-%m-%d') '%Y-%m-%d')
except ValueError as err: except ValueError as err:
raise except_osv(_("Date format is not valid."), raise except_orm(
_(" It should be YYYY-MM-DD for column: %s" _("Date format is not valid."),
" value: %s \n \n" _(" It should be YYYY-MM-DD for column: %s"
" \n Please check the line with ref: %s" " value: %s \n \n \n Please check the line with "
" \n \n Detail: %s") % (rule, "ref: %s \n \n Detail: %s") %
line.get(rule, _('Missing')), (rule, line.get(rule, _('Missing')),
line.get('ref', line), line.get('ref', line), repr(err)))
repr(err)))
else: else:
try: try:
line[rule] = conversion_rules[rule](line[rule]) line[rule] = conversion_rules[rule](line[rule])
except Exception as err: except Exception as err:
raise except_osv(_('Invalid data'), raise except_orm(
_("Value %s of column %s is not valid." _('Invalid data'),
"\n Please check the line with ref %s:" _("Value %s of column %s is not valid.\n Please "
"\n \n Detail: %s") % (line.get(rule, _('Missing')), "check the line with ref %s:\n \n Detail: %s") %
rule, (line.get(rule, _('Missing')), rule,
line.get('ref', line), line.get('ref', line), repr(err)))
repr(err)))
return result_set return result_set
def _from_xls(self, result_set, conversion_rules): 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. an .csv, .xls or .xlsx file.
""" """
for line in result_set: for line in result_set:
for rule in conversion_rules: for rule in conversion_rules:
if conversion_rules[rule] == datetime.datetime: if conversion_rules[rule] == datetime.datetime:
try: 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) line[rule] = datetime.datetime(*t_tuple)
except Exception as err: except Exception as err:
raise except_osv(_("Date format is not valid"), raise except_orm(
_("Please modify the cell formatting to date format" _("Date format is not valid"),
" for column: %s" _("Please modify the cell formatting to date format"
" value: %s" " for column: %s value: %s\n Please check the "
"\n Please check the line with ref: %s" "line with ref: %s\n \n Detail: %s") %
"\n \n Detail: %s") % (rule, (rule, line.get(rule, _('Missing')),
line.get(rule, _('Missing')), line.get('ref', line), repr(err)))
line.get('ref', line),
repr(err)))
else: else:
try: try:
line[rule] = conversion_rules[rule](line[rule]) line[rule] = conversion_rules[rule](line[rule])
except Exception as err: except Exception as err:
raise except_osv(_('Invalid data'), raise except_orm(
_("Value %s of column %s is not valid." _('Invalid data'),
"\n Please check the line with ref %s:" _("Value %s of column %s is not valid.\n Please "
"\n \n Detail: %s") % (line.get(rule, _('Missing')), "check the line with ref %s:\n \n Detail: %s") %
rule, (line.get(rule, _('Missing')), rule,
line.get('ref', line), line.get('ref', line), repr(err)))
repr(err)))
return result_set return result_set
def _cast_rows(self, *args, **kwargs): def _cast_rows(self, *args, **kwargs):
""" """Convert the self.result_row_list using the self.conversion_dict
Convert the self.result_row_list using the self.conversion_dict providen. providen. We call here _from_xls or _from_csv depending on the
We call here _from_xls or _from_csv depending on the self.ftype variable. self.ftype variable.
""" """
func = getattr(self, '_from_%s' % self.ftype) func = getattr(self, '_from_%s' % self.ftype)
res = func(self.result_row_list, self.conversion_dict) 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 import datetime
from file_parser import FileParser from file_parser import FileParser
try:
import xlrd
except:
raise Exception(_('Please install python lib xlrd'))
class GenericFileParser(FileParser): 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 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. parser, but will also be useful as it allow to import a basic flat file.
""" """
def __init__(self, parse_name, ftype='csv', **kwargs): 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 @classmethod
def parser_for(cls, parser_name): 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 the providen name is generic_csvxls_so
""" """
return parser_name == '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 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 of every parser to give this dict of vals, so each one can implement his
own way of recording the lines. own way of recording the lines.
:param: line: a dict of vals that represent a line of result_row_list :param: line: a dict of vals that represent a line of
:return: dict of values to give to the create method of statement line, result_row_list
it MUST contain at least: :return: dict of values to give to the create method of statement
line, it MUST contain at least:
{ {
'name':value, 'name':value,
'date':value, 'date':value,

View File

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

View File

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

View File

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

View File

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

View File

@@ -36,12 +36,15 @@ class CreditPartnerStatementImporter(orm.TransientModel):
if context is None: if context is None:
context = {} context = {}
res = {} res = {}
if (context.get('active_model', False) == 'account.statement.profile' and if (context.get('active_model', False) ==
context.get('active_ids', False)): 'account.statement.profile' and
context.get('active_ids', False)):
ids = context['active_ids'] 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] 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', {})) res.update(other_vals.get('value', {}))
return res return res
@@ -55,8 +58,8 @@ class CreditPartnerStatementImporter(orm.TransientModel):
'journal_id': fields.many2one('account.journal', 'journal_id': fields.many2one('account.journal',
'Financial journal to use transaction'), 'Financial journal to use transaction'),
'file_name': fields.char('File Name', size=128), 'file_name': fields.char('File Name', size=128),
'receivable_account_id': fields.many2one('account.account', 'receivable_account_id': fields.many2one(
'Force Receivable/Payable Account'), 'account.account', 'Force Receivable/Payable Account'),
'force_partner_on_bank': fields.boolean( 'force_partner_on_bank': fields.boolean(
'Force partner on bank move', 'Force partner on bank move',
help="Tic that box if you want to use the credit insitute partner " help="Tic that box if you want to use the credit insitute partner "
@@ -71,22 +74,22 @@ class CreditPartnerStatementImporter(orm.TransientModel):
def onchange_profile_id(self, cr, uid, ids, profile_id, context=None): def onchange_profile_id(self, cr, uid, ids, profile_id, context=None):
res = {} res = {}
if profile_id: if profile_id:
c = self.pool.get("account.statement.profile").browse( c = self.pool["account.statement.profile"].browse(
cr, uid, profile_id, context=context) cr, uid, profile_id, context=context)
res = {'value': res = {'value':
{'partner_id': c.partner_id and c.partner_id.id or False, {'partner_id': c.partner_id and c.partner_id.id or False,
'journal_id': c.journal_id and c.journal_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, 'force_partner_on_bank': c.force_partner_on_bank,
'balance_check': c.balance_check, 'balance_check': c.balance_check,
} }
} }
return res return res
def _check_extension(self, filename): def _check_extension(self, filename):
(__, ftype) = os.path.splitext(filename) (__, ftype) = os.path.splitext(filename)
if not ftype: if not ftype:
#We do not use osv exception we do not want to have it logged # We do not use osv exception we do not want to have it logged
raise Exception(_('Please use a file with an extention')) raise Exception(_('Please use a file with an extention'))
return ftype return ftype
@@ -99,18 +102,19 @@ class CreditPartnerStatementImporter(orm.TransientModel):
ftype = self._check_extension(importer.file_name) ftype = self._check_extension(importer.file_name)
context['file_name'] = importer.file_name context['file_name'] = importer.file_name
sid = self.pool.get( sid = self.pool.get(
'account.statement.profile').multi_statement_import( 'account.statement.profile').multi_statement_import(
cr, cr,
uid, uid,
False, False,
importer.profile_id.id, importer.profile_id.id,
importer.input_statement, importer.input_statement,
ftype.replace('.', ''), ftype.replace('.', ''),
context=context context=context
) )
model_obj = self.pool.get('ir.model.data') model_obj = self.pool.get('ir.model.data')
action_obj = self.pool.get('ir.actions.act_window') 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 = action_obj.read(cr, uid, action_id)
res['domain'] = res['domain'][:-1] + ",('id', 'in', %s)]" % sid res['domain'] = res['domain'][:-1] + ",('id', 'in', %s)]" % sid
return res return res

View File

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

View File

@@ -34,7 +34,7 @@ class wizard_cancel_statement(orm.TransientModel):
'Show reconcile warning', 'Show reconcile warning',
help='This is a hidden field set with a default in the context ' help='This is a hidden field set with a default in the context '
'to choose between two different warning messages in the view.' 'to choose between two different warning messages in the view.'
), ),
} }
def do_cancel_button(self, cr, uid, ids, context=None): def do_cancel_button(self, cr, uid, ids, context=None):

View File

@@ -31,17 +31,18 @@
'account_statement_base_import' 'account_statement_base_import'
], ],
'description': """ 'description': """
This module brings commission support to bank statement imports. It computes the sum of a commission This module brings commission support to bank statement imports. It computes the
field on each transaction and creates a statement entry for it. sum of a commission field on each transaction and creates a statement entry for
it.
""", """,
'website': 'http://www.camptocamp.com', 'website': 'http://www.camptocamp.com',
'data': [ 'data': [
"statement_view.xml", "statement_view.xml",
"import_statement_view.xml", "import_statement_view.xml",
], ],
'test': [], 'test': [],
'installable': False, 'installable': False,
'images': [], 'images': [],
'auto_install': False, 'auto_install': False,
'license': 'AGPL-3', 'license': 'AGPL-3',
} }

View File

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

View File

@@ -4,7 +4,6 @@
<record id="statement_importer_view" model="ir.ui.view"> <record id="statement_importer_view" model="ir.ui.view">
<field name="name">credit.statement.import.config.view</field> <field name="name">credit.statement.import.config.view</field>
<field name="model">credit.statement.import</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="inherit_id" ref="account_statement_base_import.statement_importer_view" />
<field name="arch" type="xml"> <field name="arch" type="xml">
<xpath expr="/form/group/field[@name='journal_id']" position="after"> <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="name">account_bank_statement.bank_statement.view_form</field>
<field name="model">account.bank.statement</field> <field name="model">account.bank.statement</field>
<field name="inherit_id" ref="account_statement_base_completion.bank_statement_view_form" /> <field name="inherit_id" ref="account_statement_base_completion.bank_statement_view_form" />
<field name="type">form</field>
<field eval="20" name="priority"/> <field eval="20" name="priority"/>
<field name="arch" type="xml"> <field name="arch" type="xml">
<data> <data>

View File

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

View File

@@ -26,10 +26,12 @@
'category': 'Generic Modules/Others', 'category': 'Generic Modules/Others',
'license': 'AGPL-3', 'license': 'AGPL-3',
'description': """ 'description': """
Improve the basic rule "Match from statement line label (based on partner field 'Bank Statement Label')" provided by the Improve the basic rule "Match from statement line label (based on partner
Bank statement base completion module. The goal is to match the label field from the bank statement line with a partner and field 'Bank Statement Label')" provided by the Bank statement base
an account. completion module. The goal is to match the label field from the bank
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 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. partner and an account.
""", """,
@@ -37,13 +39,12 @@
'website': 'http://www.akretion.com/', 'website': 'http://www.akretion.com/',
'depends': ['account_statement_base_completion'], 'depends': ['account_statement_base_completion'],
'data': [ 'data': [
'partner_view.xml', 'partner_view.xml',
'statement_view.xml', 'statement_view.xml',
'security/ir.model.access.csv', 'security/ir.model.access.csv',
'security/ir_rule.xml', 'security/ir_rule.xml',
], ],
'demo': [], 'demo': [],
'installable': False, 'installable': False,
'active': False, 'active': False,
} }

View File

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

View File

@@ -22,14 +22,16 @@
from openerp.osv import fields, orm from openerp.osv import fields, orm
from collections import defaultdict 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): 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): def __init__(self, value):
self.value = value self.value = value
@@ -38,8 +40,7 @@ class ErrorTooManyLabel(Exception):
class AccountBankSatement(orm.Model): 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. of the bank statement once line have been imported or manually fullfill.
""" """
_inherit = "account.bank.statement" _inherit = "account.bank.statement"
@@ -60,8 +61,7 @@ class AccountStatementCompletionRule(orm.Model):
_inherit = "account.statement.completion.rule" _inherit = "account.statement.completion.rule"
def get_from_label_and_partner_field(self, cr, uid, st_line, context=None): 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. statement line and the table account.statement.label.
If more than one statement label matched, raise the ErrorTooManylabel If more than one statement label matched, raise the ErrorTooManylabel
error. error.
@@ -75,14 +75,14 @@ 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], statement = st_obj.browse(cr, uid, st_line['statement_id'][0],
context=context) context=context)
res = {} res = {}
if not context.get('label_memorizer'): if not context.get('label_memorizer'):
context['label_memorizer'] = defaultdict(list) context['label_memorizer'] = defaultdict(list)
for line in statement.line_ids: for line in statement.line_ids:
cr.execute(""" cr.execute("""
SELECT l.partner_id, SELECT l.partner_id,
l.account_id l.account_id
FROM account_statement_label as l, FROM account_statement_label as l,
@@ -99,14 +99,14 @@ class AccountStatementCompletionRule(orm.Model):
st_l.id = %s st_l.id = %s
""", (line.id,)) """, (line.id,))
for partner, account in cr.fetchall(): for partner, account in cr.fetchall():
context['label_memorizer'][line.id].append({'partner_id': partner, context['label_memorizer'][line.id].append(
'account_id': account}) {'partner_id': partner, 'account_id': account})
if st_line['id'] in context['label_memorizer']: if st_line['id'] in context['label_memorizer']:
label_info = context['label_memorizer'][st_line['id']] label_info = context['label_memorizer'][st_line['id']]
if len(label_info) > 1: if len(label_info) > 1:
raise ErrorTooManyPartner(_('Line named "%s" (Ref:%s) was matched by ' raise ErrorTooManyPartner(
'more than one statement label.') % _('Line named "%s" (Ref:%s) was matched by more than one '
(st_line['name'], st_line['ref'])) 'statement label.') % (st_line['name'], st_line['ref']))
if label_info[0]['partner_id']: if label_info[0]['partner_id']:
res['partner_id'] = label_info[0]['partner_id'] res['partner_id'] = label_info[0]['partner_id']
res['account_id'] = label_info[0]['account_id'] res['account_id'] = label_info[0]['account_id']
@@ -118,14 +118,13 @@ class AccountStatementLabel(orm.Model):
and a specific account and a specific account
""" """
_name = "account.statement.label" _name = "account.statement.label"
_description = "Account Statement Label" _description = "Account Statement Label"
_columns = { _columns = {
'partner_id': fields.many2one('res.partner', 'Partner'), 'partner_id': fields.many2one('res.partner', 'Partner'),
'label': fields.char('Bank Statement Label', size=100), 'label': fields.char('Bank Statement Label', size=100),
'account_id': fields.many2one('account.account', 'Account', 'account_id': fields.many2one('account.account', 'Account',
required = True, required=True,
help='Account corresponding to the label ' help='Account corresponding to the label '
'for a given partner'), 'for a given partner'),
'company_id': fields.related('account_id', 'company_id', 'company_id': fields.related('account_id', 'company_id',
@@ -139,10 +138,9 @@ class AccountStatementLabel(orm.Model):
} }
_defaults = { _defaults = {
'company_id': lambda s,cr,uid,c: 'company_id': lambda s, cr, uid, c:
s.pool.get('res.company')._company_default_get(cr, uid, s.pool.get('res.company')._company_default_get(
'account.statement.label', cr, uid, 'account.statement.label', context=c),
context=c),
} }
_sql_constraints = [ _sql_constraints = [

View File

@@ -28,14 +28,15 @@
'depends': [ 'depends': [
'account_statement_base_completion', 'account_statement_base_completion',
'account_voucher' 'account_voucher'
], ],
'description': """ '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', 'website': 'http://www.camptocamp.com',
'init_xml': [], 'init_xml': [],
'update_xml': [ 'update_xml': [
"statement_view.xml", "statement_view.xml",
], ],
'demo_xml': [], 'demo_xml': [],
'test': [], 'test': [],
@@ -43,4 +44,4 @@
'images': [], 'images': [],
'auto_install': False, 'auto_install': False,
'license': 'AGPL-3', 'license': 'AGPL-3',
} }

View File

@@ -22,4 +22,4 @@
import statement import statement
import report import report
import account import account
import voucher import voucher

View File

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

View File

@@ -19,23 +19,21 @@
# #
############################################################################## ##############################################################################
from openerp.osv.orm import Model from openerp.osv import orm
from openerp.osv import fields
class account_move(Model): class AccountMove(orm.Model):
_inherit = 'account.move' _inherit = 'account.move'
def unlink(self, cr, uid, ids, context=None): 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. allow an easier way of cancelling the bank statement.
""" """
reconcile_to_delete = [] 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 in self.browse(cr, uid, ids, context=context):
for move_line in move.line_id: for move_line in move.line_id:
if move_line.reconcile_id: if move_line.reconcile_id:
reconcile_to_delete.append(move_line.reconcile_id.id) reconcile_to_delete.append(move_line.reconcile_id.id)
reconcile_obj.unlink(cr, uid, reconcile_to_delete, context=context) 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): class BankStatementWebkit(report_sxw.rml_parse):
def __init__(self, cr, uid, name, context): 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.pool = pooler.get_pool(self.cr.dbname)
self.cursor = self.cr self.cursor = self.cr
company = self.pool.get('res.users').browse( company = self.pool.get('res.users').browse(
self.cr, uid, uid, context=context).company_id self.cr, uid, uid, context=context).company_id
header_report_name = ' - '.join((_('BORDEREAU DE REMISE DE CHEQUES'), header_report_name = ' - '.join((
company.name, company.currency_id.name)) _('BORDEREAU DE REMISE DE CHEQUES'),
footer_date_time = self.formatLang(str(datetime.today())[:19], date_time=True) company.name, company.currency_id.name))
footer_date_time = self.formatLang(
str(datetime.today())[:19], date_time=True)
self.localcontext.update({ self.localcontext.update({
'cr': cr, 'cr': cr,
'uid': uid, 'uid': uid,
@@ -50,7 +53,8 @@ class BankStatementWebkit(report_sxw.rml_parse):
('--header-left', header_report_name), ('--header-left', header_report_name),
('--header-spacing', '2'), ('--header-spacing', '2'),
('--footer-left', footer_date_time), ('--footer-left', footer_date_time),
('--footer-right', ' '.join((_('Page'), '[page]', _('of'), '[topage]'))), ('--footer-right',
' '.join((_('Page'), '[page]', _('of'), '[topage]'))),
('--footer-line',), ('--footer-line',),
], ],
}) })
@@ -58,14 +62,14 @@ class BankStatementWebkit(report_sxw.rml_parse):
def _get_bank_statement_data(self, statement): def _get_bank_statement_data(self, statement):
statement_obj = self.pool.get('account.bank.statement.line') statement_obj = self.pool.get('account.bank.statement.line')
statement_line_ids = statement_obj.search( statement_line_ids = statement_obj.search(
self.cr, self.cr,
self.uid, self.uid,
[('statement_id', '=', statement.id)]) [('statement_id', '=', statement.id)])
statement_lines = statement_obj.browse( statement_lines = statement_obj.browse(
self.cr, self.uid, statement_line_ids) self.cr, self.uid, statement_line_ids)
return statement_lines return statement_lines
webkit_report.WebKitParser('report.bank_statement_webkit', webkit_report.WebKitParser(
'account.bank.statement', 'report.bank_statement_webkit', 'account.bank.statement',
'addons/account_statement_ext/report/bank_statement_report.mako', 'addons/account_statement_ext/report/bank_statement_report.mako',
parser=BankStatementWebkit) parser=BankStatementWebkit)

View File

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

View File

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

View File

@@ -29,12 +29,13 @@ class AccountVoucher(Model):
def _get_period(self, cr, uid, context=None): def _get_period(self, cr, uid, context=None):
"""If period not in context, take it from the move lines""" """If period not in context, take it from the move lines"""
if not context.get('period_id') and context.get('move_line_ids'): if not context.get('period_id') and context.get('move_line_ids'):
res = self.pool.get('account.move.line').browse( res = self.pool['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 context['period_id'] = res
elif context.get('date'): elif context.get('date'):
periods = self.pool.get('account.period').find( periods = self.pool.get('account.period').find(
cr, uid, dt=context['date'], context=context) cr, uid, dt=context['date'], context=context)
if periods: if periods:
context['period_id'] = periods[0] context['period_id'] = periods[0]
return super(AccountVoucher, self)._get_period(cr, uid, context) return super(AccountVoucher, self)._get_period(cr, uid, context)

View File

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

View File

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

View File

@@ -32,11 +32,12 @@ class AccountVoucher(Model):
context = {} context = {}
if not context.get('period_id') and context.get('move_line_ids'): if not context.get('period_id') and context.get('move_line_ids'):
res = self.pool.get('account.move.line').browse( 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 context['period_id'] = res
elif context.get('date'): elif context.get('date'):
periods = self.pool.get('account.period').find( periods = self.pool['account.period'].find(
cr, uid, dt=context['date'], context=context) cr, uid, dt=context['date'], context=context)
if periods: if periods:
context['period_id'] = periods[0] context['period_id'] = periods[0]
return super(AccountVoucher, self)._get_period(cr, uid, context) return super(AccountVoucher, self)._get_period(cr, uid, context)

View File

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

View File

@@ -25,13 +25,15 @@
'version': '0.1', 'version': '0.1',
'category': 'Generic Modules/Others', 'category': 'Generic Modules/Others',
'license': 'AGPL-3', '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', 'author': 'Akretion',
'website': 'http://www.akretion.com/', 'website': 'http://www.akretion.com/',
'depends': [ 'depends': [
'account_voucher', 'account_voucher',
], ],
'data': [ 'data': [
'statement_view.xml', 'statement_view.xml',
], ],
'demo': [], 'demo': [],

View File

@@ -2,7 +2,7 @@
<openerp> <openerp>
<data> <data>
<!-- INHERITED VIEW FOR THE OBJECT : account_bank_statement --> <!-- INHERITED VIEW FOR THE OBJECT : account_bank_statement -->
<record id="account_bank_statement_view_form" model="ir.ui.view"> <record id="account_bank_statement_view_form" model="ir.ui.view">
@@ -10,7 +10,6 @@
<field name="model">account.bank.statement</field> <field name="model">account.bank.statement</field>
<field name="inherit_id" ref="account_voucher.view_bank_statement_form_invoice" /> <field name="inherit_id" ref="account_voucher.view_bank_statement_form_invoice" />
<field eval="100" name="priority"/> <field eval="100" name="priority"/>
<field name="type">form</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<button string="Import Invoices" position="replace"> <button string="Import Invoices" position="replace">
</button> </button>

View File

@@ -34,7 +34,7 @@
}, },
'description': """ 'description': """
Allows to import OFX (Open Financial Exchange) statement files, using Allows to import OFX (Open Financial Exchange) statement files, using
*account_statement_base_import* generic inheritance mechanism to import *account_statement_base_import* generic inheritance mechanism to import
statements. statements.
It requires ofxparse library to work. It requires ofxparse library to work.
@@ -46,4 +46,4 @@
'images': [], 'images': [],
'auto_install': False, 'auto_install': False,
'license': 'AGPL-3', 'license': 'AGPL-3',
} }

View File

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

View File

@@ -21,49 +21,35 @@
import tempfile import tempfile
import datetime import datetime
from openerp.tools.translate import _ 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: try:
import ofxparse import ofxparse
except: except:
raise Exception(_('Please install python lib ofxparse')) raise Exception(_('Please install python lib ofxparse'))
class OfxParser(BankStatementImportParser): class OfxParser(BankStatementImportParser):
""" """Class for defining parser for OFX file format."""
Class for defining parser for OFX file format.
"""
def __init__(self, parser_name, *args, **kwargs):
"""
"""
super(OfxParser, self).__init__(parser_name, *args, **kwargs)
@classmethod @classmethod
def parser_for(cls, parser_name): 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'. the providen name is 'ofx_so'.
""" """
return parser_name == 'ofx_so' return parser_name == 'ofx_so'
def _custom_format(self, *args, **kwargs): 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 return True
def _pre(self, *args, **kwargs): def _pre(self, *args, **kwargs):
""" """No pre-treatment needed for this parser."""
No pre-treatment needed for this parser.
"""
return True return True
def _parse(self, *args, **kwargs): def _parse(self, *args, **kwargs):
""" """Launch the parsing itself."""
Launch the parsing itself.
"""
ofx_file = tempfile.NamedTemporaryFile() ofx_file = tempfile.NamedTemporaryFile()
ofx_file.seek(0) ofx_file.seek(0)
ofx_file.write(self.filebuffer) ofx_file.write(self.filebuffer)
@@ -82,26 +68,15 @@ class OfxParser(BankStatementImportParser):
return True return True
def _validate(self, *args, **kwargs): 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 return True
def _post(self, *args, **kwargs): def _post(self, *args, **kwargs):
""" """Nothing is needed to do after parsing."""
Nothing is needed to do after parsing.
"""
return True
def _post(self, *args, **kwargs):
"""
Nothing to do.
"""
return True return True
def get_st_line_vals(self, line, *args, **kwargs): 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 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 responsibility of every parser to give this dict of vals, so each one
can implement his own way of recording the lines. can implement his own way of recording the lines.
@@ -117,4 +92,3 @@ class OfxParser(BankStatementImportParser):
'ref': line.get('ref', '/'), 'ref': line.get('ref', '/'),
'label': line.get('label', ''), 'label': line.get('label', ''),
} }

View File

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

View File

@@ -20,11 +20,11 @@
# #
############################################################################### ###############################################################################
from openerp.osv import fields, orm, osv from openerp.osv import fields, orm
class AccountStatementProfile(orm.Model): class AccountStatementProfile(orm.Model):
_inherit = "account.statement.profile" _inherit = "account.statement.profile"
_columns = { _columns = {
'one_move': fields.boolean( 'one_move': fields.boolean(
'Group Journal Items', 'Group Journal Items',
@@ -36,12 +36,13 @@ class AccountStatementProfile(orm.Model):
"for the refunds and one for the payments.") "for the refunds and one for the payments.")
} }
class account_bank_statement(orm.Model):
class AccountBankStatement(orm.Model):
_inherit = "account.bank.statement" _inherit = "account.bank.statement"
def _prepare_move_line_vals(self, cr, uid, st_line, *args, **kwargs): 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, res = super(AccountBankStatement, self)._prepare_move_line_vals(
*args, **kwargs) cr, uid, st_line, *args, **kwargs)
period_id = self._get_period(cr, uid, st_line.statement_id.date, period_id = self._get_period(cr, uid, st_line.statement_id.date,
context=kwargs.get('context')) context=kwargs.get('context'))
if st_line.statement_id.profile_id.one_move: if st_line.statement_id.profile_id.one_move:
@@ -49,93 +50,89 @@ class account_bank_statement(orm.Model):
'period_id': period_id, 'period_id': period_id,
'date': st_line.statement_id.date, 'date': st_line.statement_id.date,
'name': st_line.ref, 'name': st_line.ref,
}) })
return res return res
return res return res
def _prepare_move(self, cr, uid, st_line, st_line_number, context=None): 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) _prepare_move(cr, uid, st_line, st_line_number, context=context)
res.update({ res.update({
'ref': st_line.statement_id.name, 'ref': st_line.statement_id.name,
'name': st_line.statement_id.name, 'name': st_line.statement_id.name,
'date': st_line.statement_id.date, 'date': st_line.statement_id.date,
}) })
return res return res
def create_move_from_st_line(self, cr, uid, st_line_id, company_currency_id, def create_move_from_st_line(self, cr, uid, st_line_id, company_currency_id,
st_line_number, context=None): st_line_number, context=None):
if context is None: if context is None:
context = {} context = {}
context['from_parent_object'] = True #For compability with module account_constraints # For compability with module account_constraints
account_move_obj = self.pool.get('account.move') context['from_parent_object'] = True
account_bank_statement_line_obj = self.pool.get('account.bank.statement.line') account_move_obj = self.pool['account.move']
st_line = account_bank_statement_line_obj.browse(cr, uid, st_line_id, st_line_obj = self.pool['account.bank.statement.line']
context=context) st_line = st_line_obj.browse(cr, uid, st_line_id, context=context)
st = st_line.statement_id st = st_line.statement_id
if st.profile_id.one_move: if st.profile_id.one_move:
if not context.get('move_id'): if not context.get('move_id'):
move_vals = self._prepare_move(cr, uid, st_line, st_line_number, context=context) move_vals = self._prepare_move(
context['move_id'] = account_move_obj.create(cr, uid, move_vals, context=context) cr, uid, st_line, st_line_number, context=context)
self.create_move_line_from_st_line(cr, uid, context['move_id'], context['move_id'] = account_move_obj.create(
st_line_id, company_currency_id, cr, uid, move_vals, context=context)
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'] return context['move_id']
else: else:
return super(account_bank_statement, self).create_move_from_st_line(cr, uid, st_line_id, return super(AccountBankStatement, self).create_move_from_st_line(
company_currency_id, cr, uid, st_line_id, company_currency_id, st_line_number,
st_line_number, context=context)
context=context)
def create_move_line_from_st_line(self, cr, uid, move_id, st_line_id, def create_move_line_from_st_line(self, cr, uid, move_id, st_line_id,
company_currency_id, context=None): company_currency_id, context=None):
"""Create the account move line from the statement line. """Create the account move line from the statement line.
:param int/long move_id: ID of the account.move :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 st_line_id: ID of the account.bank.statement.line
:param int/long company_currency_id: ID of the res.currency of the company to create the move line from.
:return: ID of the account.move created :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: if context is None:
context = {} context = {}
res_currency_obj = self.pool.get('res.currency') res_currency_obj = self.pool['res.currency']
account_move_line_obj = self.pool.get('account.move.line') account_move_line_obj = self.pool['account.move.line']
account_bank_statement_line_obj = self.pool.get('account.bank.statement.line') st_line_obj = self.pool['account.bank.statement.line']
st_line = account_bank_statement_line_obj.browse(cr, uid, st_line_id, context=context) st_line = st_line_obj.browse(cr, uid, st_line_id, context=context)
st = st_line.statement_id st = st_line.statement_id
context.update({'date': st_line.date}) 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({ context.update({
'res.currency.compute.account': acc_cur, 'res.currency.compute.account': acc_cur,
}) })
amount = res_currency_obj.compute(cr, uid, st.currency.id, amount = res_currency_obj.compute(
company_currency_id, cr, uid, st.currency.id, company_currency_id, st_line.amount,
st_line.amount, context=context)
context=context) bank_move_vals = self._prepare_bank_move_line(
cr, uid, st_line, move_id, amount, company_currency_id,
bank_move_vals = self._prepare_bank_move_line(cr, uid, st_line, move_id, amount, context=context)
company_currency_id, context=context) return account_move_line_obj.create(cr, uid, bank_move_vals,
return account_move_line_obj.create(cr, uid, bank_move_vals, context=context) context=context)
def _valid_move(self, cr, uid, move_id, context=None): def _valid_move(self, cr, uid, move_id, context=None):
move_obj = self.pool.get('account.move') move_obj = self.pool['account.move']
move = move_obj.browse(cr, uid, move_id, context=context)
move_obj.post(cr, uid, [move_id], context=context) move_obj.post(cr, uid, [move_id], context=context)
return True return True
def _prepare_transfer_move_line_vals(self, cr, uid, st, name, amount,
def _prepare_transfer_move_line_vals(self, cr, uid, st, name, amount, move_id, context=None): move_id, context=None):
""" """Prepare the dict of values to create the transfer move lines."""
Prepare the dict of values to create the transfer move lines.
"""
account_id = st.profile_id.journal_id.default_debit_account_id.id 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: if amount < 0.0:
debit = 0.0 debit = 0.0
credit = -amount credit = -amount
@@ -145,7 +142,7 @@ class account_bank_statement(orm.Model):
vals = { vals = {
'name': name, 'name': name,
'date': st.date, 'date': st.date,
'partner_id': partner_id, 'partner_id': st.profile_id.partner_id.id,
'statement_id': st.id, 'statement_id': st.id,
'account_id': account_id, 'account_id': account_id,
'ref': name, 'ref': name,
@@ -157,19 +154,18 @@ class account_bank_statement(orm.Model):
} }
return vals return vals
def create_move_transfer_lines(self, cr, uid, move, st, context=None): 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 move_id = move.id
refund = 0.0 refund = 0.0
payment = 0.0 payment = 0.0
transfer_lines = [] transfer_lines = []
transfer_line_ids = [] transfer_line_ids = []
#Calculate the part of the refund amount and the payment amount # Calculate the part of the refund amount and the payment amount
for move_line in move.line_id: for move_line in move.line_id:
refund -= move_line.debit refund -= move_line.debit
payment += move_line.credit payment += move_line.credit
#Create 2 Transfer lines or One global tranfer line # Create 2 Transfer lines or One global tranfer line
if st.profile_id.split_transfer_line: if st.profile_id.split_transfer_line:
if refund: if refund:
transfer_lines.append(['Refund Transfer', refund]) transfer_lines.append(['Refund Transfer', refund])
@@ -180,46 +176,38 @@ class account_bank_statement(orm.Model):
if amount: if amount:
transfer_lines.append(['Transfer', amount]) transfer_lines.append(['Transfer', amount])
for transfer_line in transfer_lines: for transfer_line in transfer_lines:
vals = self._prepare_transfer_move_line_vals(cr, uid, st, vals = self._prepare_transfer_move_line_vals(
transfer_line[0], cr, uid, st, transfer_line[0], transfer_line[1], move_id,
transfer_line[1], context=context)
move_id, transfer_line_ids.append(
context=context) 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 return transfer_line_ids
def button_confirm_bank(self, cr, uid, ids, context=None): def button_confirm_bank(self, cr, uid, ids, context=None):
st_line_obj = self.pool.get('account.bank.statement.line') st_line_obj = self.pool['account.bank.statement.line']
move_obj = self.pool.get('account.move')
if context is None: if context is None:
context = {} context = {}
for st in self.browse(cr, uid, ids, context=context): for st in self.browse(cr, uid, ids, context=context):
super(account_bank_statement, self).button_confirm_bank(cr, uid, ids, super(AccountBankStatement, self).button_confirm_bank(
context=context) cr, uid, ids, context=context)
if st.profile_id.one_move and context.get('move_id', False): if st.profile_id.one_move and context.get('move_id', False):
move_id = context['move_id'] 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) self._valid_move(cr, uid, move_id, context=context)
lines_ids = [x.id for x in st.line_ids] lines_ids = [x.id for x in st.line_ids]
st_line_obj.write(cr, uid, lines_ids, st_line_obj.write(cr, uid, lines_ids,
{'move_ids': [(4, move_id, False)]}, {'move_ids': [(4, move_id, False)]},
context=context) context=context)
return True return True
def button_cancel(self, cr, uid, ids, context=None): def button_cancel(self, cr, uid, ids, context=None):
done = []
for st in self.browse(cr, uid, ids, context=context): for st in self.browse(cr, uid, ids, context=context):
if st.profile_id.one_move and st.line_ids: if st.profile_id.one_move and st.line_ids:
for move in st.line_ids[0].move_ids: for move in st.line_ids[0].move_ids:
if move.state != 'draft': if move.state != 'draft':
move.button_cancel(context=context) move.button_cancel(context=context)
move.unlink(context=context) move.unlink(context=context)
st.write({'state':'draft'}, context=context) st.write({'state': 'draft'}, context=context)
else: else:
super(account_bank_statement, self).button_cancel(cr, uid, ids, super(AccountBankStatement, self).button_cancel(
context=context) cr, uid, ids, context=context)
return True return True

View File

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

View File

@@ -35,6 +35,7 @@ import re
class AccountStatementCompletionRule(Model): class AccountStatementCompletionRule(Model):
"""Add a rule to complete account based on a regular expression""" """Add a rule to complete account based on a regular expression"""
_inherit = "account.statement.completion.rule" _inherit = "account.statement.completion.rule"
@@ -43,13 +44,14 @@ class AccountStatementCompletionRule(Model):
res = super(AccountStatementCompletionRule, self)._get_functions( res = super(AccountStatementCompletionRule, self)._get_functions(
cr, uid, context=context) cr, uid, context=context)
res.append(('set_account', res.append(('set_account',
'Set account for line labels matching a regular expression')) 'Set account for line labels matching a regular '
'expression'))
return res return res
_columns = { _columns = {
'function_to_call': fields.selection(_get_functions, 'Method'),
'regex': fields.char('Regular Expression', size=128), '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): 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="name">account.statement.completion.rule.view (account_statement_regex_account_completion)</field>
<field name="model">account.statement.completion.rule</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="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="arch" type="xml">
<field name="function_to_call" position="after"> <field name="function_to_call" position="after">
<group colspan="2"> <group colspan="2">

View File

@@ -36,34 +36,38 @@ ACC_NUMBER = "BE38733040385372"
class test_regex_account_completion(common.TransactionCase): class test_regex_account_completion(common.TransactionCase):
def prepare(self): def prepare(self):
self.account_bank_statement_obj = self.registry("account.bank.statement") self.st_obj = self.registry(
self.account_bank_statement_line_obj = self.registry("account.bank.statement.line") "account.bank.statement")
self.st_line_obj = self.registry(
"account.bank.statement.line")
self.account_id = self.ref('account.a_expense') self.account_id = self.ref('account.a_expense')
# create the completion rule # create the completion rule
rule_vals = {'function_to_call': 'set_account', rule_vals = {'function_to_call': 'set_account',
'regex': '^My statement', 'regex': '^My statement',
'account_id': self.account_id} '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 # Create the profile
journal_id = self.ref("account.bank_journal") 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(
"name": "TEST", self.cr, self.uid, {
"commission_account_id": self.ref("account.a_recv"), "name": "TEST",
"journal_id": journal_id, "commission_account_id": self.ref("account.a_recv"),
"rule_ids": [(6, 0, [completion_rule_id])]}) "journal_id": journal_id,
"rule_ids": [(6, 0, [completion_rule_id])]
})
# Create a bank statement # Create a bank statement
self.statement_id = self.account_bank_statement_obj.create(self.cr, self.uid, { self.statement_id = self.st_obj.create(
"balance_end_real": 0.0, self.cr, self.uid, {
"balance_start": 0.0, "balance_end_real": 0.0,
"date": time.strftime('%Y-%m-%d'), "balance_start": 0.0,
"journal_id": journal_id, "date": time.strftime('%Y-%m-%d'),
"profile_id": profile_id "journal_id": journal_id,
}) "profile_id": profile_id
})
# Create two bank statement lines # 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, 'amount': 1000.0,
'name': 'My statement', 'name': 'My statement',
'ref': 'My ref', 'ref': 'My ref',
@@ -71,7 +75,7 @@ class test_regex_account_completion(common.TransactionCase):
'partner_acc_number': ACC_NUMBER '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, 'amount': 2000.0,
'name': 'My second statement', 'name': 'My second statement',
'ref': 'My second ref', 'ref': 'My second ref',
@@ -83,9 +87,14 @@ class test_regex_account_completion(common.TransactionCase):
"""Test the automatic completion on account """Test the automatic completion on account
""" """
self.prepare() 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_obj.button_auto_completion()
statement_line1 = self.account_bank_statement_line_obj.browse(self.cr, self.uid, self.statement_line1_id) statement_line1 = self.st_line_obj.browse(
self.assertEquals(self.account_id, statement_line1.account_id.id, "The account should be the account of the completion") self.cr, self.uid, self.statement_line1_id)
statement_line2 = self.account_bank_statement_line_obj.browse(self.cr, self.uid, self.statement_line2_id) self.assertEquals(self.account_id, statement_line1.account_id.id,
self.assertNotEqual(self.account_id, statement_line2.account_id.id, "The account should be not the account of the completion") "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 tools.translate import _
from openerp.addons.account_statement_base_completion.statement import \
from openerp.addons.account_statement_base_completion.statement import ErrorTooManyPartner ErrorTooManyPartner
class account_statement_completion_rule(orm.Model): class AccountStatementCompletionRule(orm.Model):
_name = "account.statement.completion.rule" _name = "account.statement.completion.rule"
_inherit = "account.statement.completion.rule" _inherit = "account.statement.completion.rule"
def _get_functions(self, cr, uid, context=None): 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) cr, uid, context=context)
res.append( res.append(
('get_from_ref_and_so', 'From line reference (based on SO number)') ('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 = {} res = {}
if st_line: if st_line:
so_obj = self.pool.get('sale.order') so_obj = self.pool.get('sale.order')
so_id = so_obj.search(cr, so_id = so_obj.search(
uid, cr, uid, [('name', '=', st_line['ref'])], context=context)
[('name', '=', st_line['ref'])],
context=context)
if so_id: if so_id:
if so_id and len(so_id) == 1: if so_id and len(so_id) == 1:
so = so_obj.browse(cr, uid, so_id[0], context=context) 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.') % 'than one partner while looking on SO by ref.') %
(st_line['name'], st_line['ref'])) (st_line['name'], st_line['ref']))
st_vals = st_obj.get_values_for_line( st_vals = st_obj.get_values_for_line(
cr, cr, uid, profile_id=st_line['profile_id'],
uid,
profile_id=st_line['profile_id'],
master_account_id=st_line['master_account_id'], master_account_id=st_line['master_account_id'],
partner_id=res.get('partner_id', False), partner_id=res.get('partner_id', False),
line_type='customer', line_type='customer',
@@ -88,7 +84,3 @@ class account_statement_completion_rule(orm.Model):
context=context) context=context)
res.update(st_vals) res.update(st_vals)
return res return res
_columns = {
'function_to_call': fields.selection(_get_functions, 'Method'),
}

View File

@@ -28,7 +28,7 @@
'depends': [ 'depends': [
'account_statement_base_completion', 'account_statement_base_completion',
'base_transaction_id' 'base_transaction_id'
], ],
'description': """ 'description': """
Add a completion method based on transaction ID providen by the bank/office. Add a completion method based on transaction ID providen by the bank/office.
@@ -44,8 +44,8 @@
'website': 'http://www.camptocamp.com', 'website': 'http://www.camptocamp.com',
'init_xml': [], 'init_xml': [],
'update_xml': [ 'update_xml': [
"statement_view.xml", "statement_view.xml",
"data.xml", "data.xml",
], ],
'demo_xml': [], 'demo_xml': [],
'test': [ 'test': [
@@ -58,4 +58,4 @@
'images': [], 'images': [],
'auto_install': True, 'auto_install': True,
'license': 'AGPL-3', 'license': 'AGPL-3',
} }

View File

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

View File

@@ -19,42 +19,40 @@
# #
############################################################################## ##############################################################################
{'name': "Bank statement transactionID import", {
'version': '1.0', 'name': "Bank statement transactionID import",
'author': 'Camptocamp', 'version': '1.0',
'maintainer': 'Camptocamp', 'author': 'Camptocamp',
'category': 'Finance', 'maintainer': 'Camptocamp',
'complexity': 'normal', 'category': 'Finance',
'depends': [ 'complexity': 'normal',
'account_statement_base_import', 'depends': [
'account_statement_transactionid_completion' 'account_statement_base_import',
], 'account_statement_transactionid_completion'
'description': """ ],
'description': """
This module brings generic methods and fields on bank statement to deal with This module brings generic methods and fields on bank statement to deal with
the importation of different bank and offices that uses transactionID. 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 This module allows you to import your bank transactions with a standard .csv
(you'll find samples in the 'data' folder). It respects the chosen profile or .xls file (you'll find samples in the 'data' folder). It respects the chosen
(model provided by the account_statement_ext module) to generate the entries. 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 * transaction_id: the transaction ID given by the bank/office. It is used as
in the generated entries and is useful for reconciliation process reference in the generated entries and is useful for reconciliation process
* date: date of the payment * 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 * commission_amount: amount of the comission for each line
* label: the comunication given by the payment office, used as communication in the * label: the comunication given by the payment office, used as communication in
generated entries. the generated entries.
""", """,
'website': 'http://www.camptocamp.com', 'website': 'http://www.camptocamp.com',
'init_xml': [], 'installable': False,
'update_xml': [ 'auto_install': False,
], 'license': 'AGPL-3',
'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): 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. bank statement.
""" """
def __init__(self, parse_name, ftype='csv', extra_fields=None, header=None, **kwargs): def __init__(self, profile, ftype='csv', extra_fields=None, header=None,
""" **kwargs):
Add transaction_id in header keys """Add transaction_id in header keys
:param char: parse_name: The name of the parser :param char: profile: Reference to the profile
:param char: ftype: extension of the file (could be csv or xls) :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 :param dict: extra_fields: extra fields to add to the conversion
{fieldname: fieldtype} dict. In the format {fieldname: fieldtype}
:param list: header : specify header fields if the csv file has no header :param list: header : specify header fields if the csv file has no
""" header
"""
extra_fields = {'transaction_id': unicode} extra_fields = {'transaction_id': unicode}
super(TransactionIDFileParser, self).__init__(parse_name, extra_fields=extra_fields, super(TransactionIDFileParser, self).__init__(
ftype=ftype, header=header, **kwargs) profile, extra_fields=extra_fields, ftype=ftype, header=header,
**kwargs)
# ref is replaced by transaction_id thus we delete it from check # 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'] del self.conversion_dict['ref']
@classmethod @classmethod
def parser_for(cls, parser_name): 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 the providen name is generic_csvxls_transaction
""" """
return parser_name == 'generic_csvxls_transaction' return parser_name == 'generic_csvxls_transaction'
def get_st_line_vals(self, line, *args, **kwargs): 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 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 of every parser to give this dict of vals, so each one can implement his
own way of recording the lines. own way of recording the lines.
:param: line: a dict of vals that represent a line of result_row_list :param: line: a dict of vals that represent a line of
:return: dict of values to give to the create method of statement line, result_row_list
it MUST contain at least: :return: dict of values to give to the create method of statement
line, it MUST contain at least:
{ {
'name':value, 'name':value,
'date':value, 'date':value,
@@ -68,13 +69,15 @@ class TransactionIDFileParser(FileParser):
'label':value, 'label':value,
'commission_amount':value, 'commission_amount':value,
} }
In this generic parser, the commission is given for every line, so we store it In this generic parser, the commission is given for every line, so we
for each one. store it for each one.
""" """
return {'name': line.get('label', line.get('ref', '/')), return {
'date': line.get('date', datetime.datetime.now().date()), 'name': line.get('label', line.get('ref', '/')),
'amount': line.get('amount', 0.0), 'date': line.get('date', datetime.datetime.now().date()),
'ref': line.get('transaction_id', '/'), 'amount': line.get('amount', 0.0),
'label': line.get('label', ''), 'ref': line.get('transaction_id', '/'),
'transaction_id': line.get('transaction_id', '/'), 'label': line.get('label', ''),
'commission_amount': line.get('commission_amount', 0.0)} 'transaction_id': line.get('transaction_id', '/'),
'commission_amount': line.get('commission_amount', 0.0)
}

View File

@@ -19,29 +19,16 @@
# #
############################################################################## ##############################################################################
from openerp.osv.orm import Model from openerp.osv import orm
from openerp.osv import fields
class AccountStatementProfil(Model): class AccountStatementProfil(orm.Model):
_inherit = "account.statement.profile" _inherit = "account.statement.profile"
def get_import_type_selection(self, cr, uid, context=None): def _get_import_type_selection(self, cr, uid, context=None):
""" """Has to be inherited to add parser"""
Has to be inherited to add parser res = super(AccountStatementProfil, self)._get_import_type_selection(
""" cr, uid, context=context)
res = super(AccountStatementProfil, self).get_import_type_selection(
cr, uid, context=context)
res.append(('generic_csvxls_transaction', res.append(('generic_csvxls_transaction',
'Generic .csv/.xls based on SO transaction ID')) 'Generic .csv/.xls based on SO transaction ID'))
return res 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

@@ -29,7 +29,7 @@
'account', 'account',
'sale', 'sale',
'stock' 'stock'
], ],
'description': """ 'description': """
Adds transaction id to invoice and sale models and views. Adds transaction id to invoice and sale models and views.
On Sales order, you can specify the transaction ID used On Sales order, you can specify the transaction ID used
@@ -47,11 +47,11 @@
'update_xml': [ 'update_xml': [
'invoice_view.xml', 'invoice_view.xml',
'sale_view.xml' 'sale_view.xml'
], ],
'demo_xml': [], 'demo_xml': [],
'test': [], 'test': [],
'installable': False, 'installable': False,
'images': [], 'images': [],
'auto_install': False, 'auto_install': False,
'license': 'AGPL-3', 'license': 'AGPL-3',
} }

View File

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

View File

@@ -35,9 +35,9 @@ class SaleOrder(Model):
} }
def _prepare_invoice(self, cr, uid, order, lines, context=None): def _prepare_invoice(self, cr, uid, order, lines, context=None):
#we put the transaction id in the generated invoices # we put the transaction id in the generated invoices
invoice_vals = super(SaleOrder, self)._prepare_invoice( invoice_vals = super(SaleOrder, self)._prepare_invoice(
cr, uid, order, lines, context=context) cr, uid, order, lines, context=context)
invoice_vals.update({ invoice_vals.update({
'transaction_id': order.transaction_id}) 'transaction_id': order.transaction_id})
return invoice_vals return invoice_vals

View File

@@ -3,7 +3,6 @@
<record id="view_order_form_transaction" model="ir.ui.view"> <record id="view_order_form_transaction" model="ir.ui.view">
<field name="name">sale.order.form.transaction</field> <field name="name">sale.order.form.transaction</field>
<field name="model">sale.order</field> <field name="model">sale.order</field>
<field name="type">form</field>
<field name="inherit_id" ref="sale.view_order_form"/> <field name="inherit_id" ref="sale.view_order_form"/>
<field name="arch" type="xml"> <field name="arch" type="xml">
<field name="payment_term" position="after"> <field name="payment_term" position="after">
@@ -12,10 +11,10 @@
</field> </field>
</record> </record>
<act_window <act_window
domain="[('origin', '=', name)]" domain="[('origin', '=', name)]"
id="prm_act" id="prm_act"
name="Packing" name="Packing"
res_model="stock.picking" res_model="stock.picking"
src_model="sale.order"/> src_model="sale.order"/>
</data> </data>
</openerp> </openerp>

View File

@@ -29,14 +29,14 @@ class StockPicking(Model):
self, cr, uid, ids, journal_id=False, group=False, self, cr, uid, ids, journal_id=False, group=False,
type='out_invoice', context=None): type='out_invoice', context=None):
res = super(StockPicking, self).action_invoice_create( res = super(StockPicking, self).action_invoice_create(
cr, uid, ids, journal_id, group, type, context) cr, uid, ids, journal_id, group, type, context)
for pick_id in res: for pick_id in res:
pick = self.browse(cr, uid, pick_id, context=context) pick = self.browse(cr, uid, pick_id, context=context)
if pick.sale_id and pick.sale_id.transaction_id: if pick.sale_id and pick.sale_id.transaction_id:
self.pool.get('account.invoice').write( self.pool.get('account.invoice').write(
cr, cr,
uid, uid,
res[pick_id], res[pick_id],
{'transaction_id': pick.sale_id.transaction_id}, {'transaction_id': pick.sale_id.transaction_id},
context=context) context=context)
return res return res

View File

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

View File

@@ -23,12 +23,12 @@
'version': '1.0.0', 'version': '1.0.0',
'category': 'other', 'category': 'other',
'description': """ '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 When importing invoice or payment into a bank statement or a payment order,
draft voucher is created on the line. This module will disable this voucher creation. normally a draft voucher is created on the line. This module will disable this
When importing payment line, date used to populate statement voucher creation. When importing payment line, date used to populate statement
line will be take from imported line in this order: line will be take from imported line in this order:
* Date * Date
@@ -41,8 +41,7 @@ line will be take from imported line in this order:
'depends': ['account_voucher', 'account_payment'], 'depends': ['account_voucher', 'account_payment'],
'data': [ 'data': [
'statement_view.xml', 'statement_view.xml',
], ],
'test': [], 'test': [],
'installable': False, 'installable': False,
'active': False,
} }

View File

@@ -2,14 +2,13 @@
<openerp> <openerp>
<data> <data>
<!-- INHERITED VIEW FOR THE OBJECT : account_bank_statement --> <!-- INHERITED VIEW FOR THE OBJECT : account_bank_statement -->
<record id="account_bank_statement_view_form" model="ir.ui.view"> <record id="account_bank_statement_view_form" model="ir.ui.view">
<field name="model">account.bank.statement</field> <field name="model">account.bank.statement</field>
<field name="inherit_id" ref="account.view_bank_statement_form" /> <field name="inherit_id" ref="account.view_bank_statement_form" />
<field eval="100" name="priority"/> <field eval="100" name="priority"/>
<field name="type">form</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<field name="voucher_id" position="replace"> <field name="voucher_id" position="replace">
</field> </field>

View File

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