From 3c8f12c19abb66250b8aadd710bb9bfa6a9d306b Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Fri, 22 Jul 2016 17:09:00 +0200 Subject: [PATCH] Start the update to new architecture explained in issue #22 Move from YAML tests to unittest in most modules Use commercial_partner_id for tax receipts (to group tax receipts for a company or a familly) On donation.line, tax_receipt_ok and in_kind are now related stored fields --- donation/README.rst | 19 +- donation/__init__.py | 5 +- donation/__manifest__.py | 43 +-- donation/demo/donation_demo.xml | 130 +++++++ donation/donation_campaign.py | 47 --- donation/donation_demo.xml | 193 ---------- donation/models/__init__.py | 7 + donation/models/account.py | 23 ++ donation/{ => models}/donation.py | 350 ++++++++++-------- donation/models/donation_campaign.py | 31 ++ donation/models/partner.py | 26 ++ donation/models/users.py | 20 + donation/post_install.py | 14 + donation/product.py | 60 --- donation/product_view.xml | 52 --- donation/report/donation_report.py | 27 +- donation/report/donation_report_view.xml | 37 +- donation/security/donation_security.xml | 5 + donation/security/ir.model.access.csv | 3 + donation/test/validate.yml | 63 ---- donation/tests/__init__.py | 3 + donation/tests/test_donation.py | 108 ++++++ .../{account_view.xml => views/account.xml} | 9 +- .../{donation_view.xml => views/donation.xml} | 69 +++- .../donation_campaign.xml} | 11 +- .../{partner_view.xml => views/partner.xml} | 12 +- donation/{users_view.xml => views/users.xml} | 11 +- donation/wizard/donation_validate.py | 24 +- 28 files changed, 690 insertions(+), 712 deletions(-) create mode 100644 donation/demo/donation_demo.xml delete mode 100644 donation/donation_campaign.py delete mode 100644 donation/donation_demo.xml create mode 100644 donation/models/__init__.py create mode 100644 donation/models/account.py rename donation/{ => models}/donation.py (60%) create mode 100644 donation/models/donation_campaign.py create mode 100644 donation/models/partner.py create mode 100644 donation/models/users.py create mode 100644 donation/post_install.py delete mode 100644 donation/product.py delete mode 100644 donation/product_view.xml delete mode 100644 donation/test/validate.yml create mode 100644 donation/tests/__init__.py create mode 100644 donation/tests/test_donation.py rename donation/{account_view.xml => views/account.xml} (71%) rename donation/{donation_view.xml => views/donation.xml} (77%) rename donation/{donation_campaign_view.xml => views/donation_campaign.xml} (83%) rename donation/{partner_view.xml => views/partner.xml} (80%) rename donation/{users_view.xml => views/users.xml} (81%) diff --git a/donation/README.rst b/donation/README.rst index 9a1386f2c..8866ad66b 100644 --- a/donation/README.rst +++ b/donation/README.rst @@ -1,3 +1,8 @@ +.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 + +======== Donation ======== @@ -19,6 +24,10 @@ To use this module, go to the menu Donations > Donations and start to register n To have some statistics about the donations, go to the menu Reporting > Donations > Donations Analysis. +.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas + :alt: Try me on Runbot + :target: https://runbot.odoo-community.org/runbot/180/9.0 + Credits ======= @@ -32,12 +41,14 @@ Contributors Maintainer ---------- -.. image:: http://odoo-community.org/logo.png +.. image:: https://odoo-community.org/logo.png :alt: Odoo Community Association - :target: http://odoo-community.org + :target: https://odoo-community.org This module is maintained by the OCA. -OCA, or the Odoo Community Association, is a nonprofit organization whose mission is to support the collaborative development of Odoo features and promote its widespread use. +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. -To contribute to this module, please visit http://odoo-community.org. +To contribute to this module, please visit https://odoo-community.org. diff --git a/donation/__init__.py b/donation/__init__.py index d9262ed94..ece6f758e 100644 --- a/donation/__init__.py +++ b/donation/__init__.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- -from . import donation_campaign -from . import donation -from . import product +from . import models from . import report from . import wizard +from .post_install import update_account_journal diff --git a/donation/__manifest__.py b/donation/__manifest__.py index d9c3a8b5e..5ac07e047 100644 --- a/donation/__manifest__.py +++ b/donation/__manifest__.py @@ -1,48 +1,29 @@ # -*- coding: utf-8 -*- -############################################################################## -# -# Donation module for Odoo -# Copyright (C) 2014-2015 Barroux Abbey (www.barroux.org) -# Copyright (C) 2014-2015 Akretion France (www.akretion.com) -# -# 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 . -# -############################################################################## - +# © 2014-2016 Barroux Abbey (http://www.barroux.org) +# © 2014-2016 Akretion France (Alexis de Lattre ) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). { 'name': 'Donation', - 'version': '9.0.0.1.0', + 'version': '9.0.1.0.0', 'category': 'Accounting & Finance', 'license': 'AGPL-3', 'summary': 'Manage donations', 'author': 'Barroux Abbey, Akretion, Odoo Community Association (OCA)', 'website': 'http://www.barroux.org', - 'depends': ['account_accountant'], + 'depends': ['donation_base'], 'data': [ 'security/donation_security.xml', - 'donation_view.xml', - 'account_view.xml', - 'product_view.xml', - 'donation_campaign_view.xml', - 'users_view.xml', + 'views/donation.xml', + 'views/account.xml', + 'views/donation_campaign.xml', + 'views/users.xml', + 'views/partner.xml', 'security/ir.model.access.csv', - 'partner_view.xml', 'report/donation_report_view.xml', 'wizard/donation_validate_view.xml', ], - 'demo': ['donation_demo.xml'], - 'test': ['test/validate.yml'], + 'post_init_hook': 'update_account_journal', + 'demo': ['demo/donation_demo.xml'], 'installable': True, } diff --git a/donation/demo/donation_demo.xml b/donation/demo/donation_demo.xml new file mode 100644 index 000000000..e254a8a44 --- /dev/null +++ b/donation/demo/donation_demo.xml @@ -0,0 +1,130 @@ + + + + + + + + Quest + + + + Prospecting + + + + Catalog + + + + + 100 + + + + CHQ BNP 239023 + + each + + + + + + 1 + 100 + + + + + 120 + + + + + + annual + + + + + + 1 + 120 + + + + + 150 + + + + + + none + + + + + + 1 + 150 + + + + + + 1000 + + + + + + each + + + + + + 1 + 1000 + + + + + + + 1200 + + + + + + each + + + + + + 1 + 800 + + + + + + + 1 + 400 + + + + + + + + diff --git a/donation/donation_campaign.py b/donation/donation_campaign.py deleted file mode 100644 index 1c80d4ae3..000000000 --- a/donation/donation_campaign.py +++ /dev/null @@ -1,47 +0,0 @@ -# -*- coding: utf-8 -*- -############################################################################## -# -# Donation module for Odoo -# Copyright (C) 2014-2015 Barroux Abbey (www.barroux.org) -# Copyright (C) 2014-2015 Akretion France (www.akretion.com) -# -# 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 . -# -############################################################################## - -from openerp import models, fields, api - - -class DonationCampaign(models.Model): - _name = 'donation.campaign' - _description = 'Code attributed for a Donation Campaign' - _order = 'code' - _rec_name = 'display_name' - - @api.one - @api.depends('code', 'name') - def _compute_display_name(self): - name = self.name - if self.code: - name = u'[%s] %s' % (self.code, name) - self.display_name = name - - code = fields.Char(string='Code', size=10) - name = fields.Char(string='Name', required=True) - display_name = fields.Char( - string='Display Name', compute='_compute_display_name', - readonly=True, store=True) - start_date = fields.Date( - string='Start Date', default=fields.Date.context_today) - nota = fields.Text(string='Notes') diff --git a/donation/donation_demo.xml b/donation/donation_demo.xml deleted file mode 100644 index a40e127e7..000000000 --- a/donation/donation_demo.xml +++ /dev/null @@ -1,193 +0,0 @@ - - - - - - - - Donation - DON - - - 1 - 0 - service - - - This is a donation product. - - - - In-Kind Donation - KIND-DON - - - 1 - 1 - 0 - service - - - - - - - - Quest - - - - Prospecting - - - - Catalog - - - - Rémi Duplat - - 12 rue de l'espérance - 69100 - Villeurbanne - - vincent.duplat@yahoo.example.com - - - - Lucie Dubois - - 34 rue Pierre Dupont - 69001 - Lyon - - lucie.dubois@yahoo.example.com - - - - Joe Smith - - Craig Pond Trail - 04431 - East Orland - - - joe.smith@gmail.example.com - - - - - - - - - - - - - - - - diff --git a/donation/models/__init__.py b/donation/models/__init__.py new file mode 100644 index 000000000..e635b5817 --- /dev/null +++ b/donation/models/__init__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- + +from . import donation +from . import donation_campaign +from . import account +from . import partner +from . import users diff --git a/donation/models/account.py b/donation/models/account.py new file mode 100644 index 000000000..4b30d850b --- /dev/null +++ b/donation/models/account.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# © 2014-2016 Barroux Abbey (http://www.barroux.org) +# © 2014-2016 Akretion France (Alexis de Lattre ) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from openerp import models, fields, api, _ +from openerp.exceptions import ValidationError + + +class AccountJournal(models.Model): + _inherit = 'account.journal' + + allow_donation = fields.Boolean(string='Donation Payment Method') + + @api.multi + @api.constrains('type', 'allow_donation') + def _check_donation(self): + for journal in self: + if journal.allow_donation and journal.type not in ('bank', 'cash'): + raise ValidationError(_( + "The journal '%s' has the option " + "'Donation Payment Method', so it's type should " + "be 'Cash' or 'Bank and Checks'.") % journal.name) diff --git a/donation/donation.py b/donation/models/donation.py similarity index 60% rename from donation/donation.py rename to donation/models/donation.py index ee95d355d..cee74c51a 100644 --- a/donation/donation.py +++ b/donation/models/donation.py @@ -1,44 +1,13 @@ # -*- coding: utf-8 -*- -############################################################################## -# -# Donation module for Odoo -# Copyright (C) 2014-2015 Barroux Abbey (www.barroux.org) -# Copyright (C) 2014-2015 Akretion France (www.akretion.com) -# -# 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 . -# -############################################################################## +# © 2014-2016 Barroux Abbey (http://www.barroux.org) +# © 2014-2016 Akretion France (Alexis de Lattre ) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from openerp import models, fields, api, _ from openerp.exceptions import UserError, ValidationError import openerp.addons.decimal_precision as dp -class ResUsers(models.Model): - _inherit = 'res.users' - - # begin with context_ to allow user to change it by himself - context_donation_campaign_id = fields.Many2one( - 'donation.campaign', string='Current Donation Campaign') - context_donation_journal_id = fields.Many2one( - 'account.journal', string='Current Donation Payment Method', - domain=[ - ('type', 'in', ('bank', 'cash')), - ('allow_donation', '=', True)], - company_dependent=True) - - class DonationDonation(models.Model): _name = 'donation.donation' _description = 'Donation' @@ -46,30 +15,32 @@ class DonationDonation(models.Model): _rec_name = 'display_name' _inherit = ['mail.thread'] - @api.one + @api.multi @api.depends( 'line_ids', 'line_ids.unit_price', 'line_ids.quantity', 'donation_date', 'currency_id', 'company_id') def _compute_total(self): - total = 0.0 - for line in self.line_ids: - total += line.quantity * line.unit_price - self.amount_total = total - donation_currency =\ - self.currency_id.with_context(date=self.donation_date) - total_company_currency = donation_currency.compute( - total, self.company_id.currency_id) - self.amount_total_company_currency = total_company_currency + for donation in self: + total = 0.0 + for line in donation.line_ids: + total += line.quantity * line.unit_price + donation.amount_total = total + donation_currency =\ + donation.currency_id.with_context(date=donation.donation_date) + total_company_currency = donation_currency.compute( + total, donation.company_id.currency_id) + donation.amount_total_company_currency = total_company_currency # We don't want a depends on partner_id.country_id, because if the partner # moves to another country, we want to keep the old country for - # past donations - @api.one + # past donations to have good statistics + @api.multi @api.depends('partner_id') def _compute_country_id(self): # Use sudo() to by-pass record rules, because the same partner # can have donations in several companies - self.sudo().country_id = self.partner_id.country_id or False + for donation in self: + donation.sudo().country_id = donation.partner_id.country_id @api.model def _default_currency(self): @@ -86,6 +57,9 @@ def _default_currency(self): 'res.partner', string='Donor', required=True, states={'done': [('readonly', True)]}, track_visibility='onchange', ondelete='restrict') + commercial_partner_id = fields.Many2one( + related='partner_id.commercial_partner_id', + string='Parent Donor', readonly=True, store=True) # country_id is here to have stats per country # WARNING : I can't put a related field, because when someone # writes on the country_id of a partner, it will trigger a write @@ -150,16 +124,59 @@ def _default_currency(self): display_name = fields.Char( string='Display Name', compute='_compute_display_name', readonly=True) + tax_receipt_id = fields.Many2one( + 'donation.tax.receipt', string='Tax Receipt', readonly=True, + copy=False) + tax_receipt_option = fields.Selection([ + ('none', 'None'), + ('each', 'For Each Donation'), + ('annual', 'Annual Tax Receipt'), + ], string='Tax Receipt Option', states={'done': [('readonly', True)]}) + tax_receipt_total = fields.Monetary( + compute='_tax_receipt_total', string='Eligible Tax Receipt Sub-total', + store=True, currency_field='currency_id') - @api.one + @api.multi @api.constrains('donation_date') def _check_donation_date(self): - if self.donation_date > fields.Date.context_today(self): - # No error pop-up to user : Odoo 9 BUG ? - raise ValidationError( - _('The date of the donation of %s should be today ' + for donation in self: + if donation.donation_date > fields.Date.context_today(self): + # TODO No error pop-up to user : Odoo 9 BUG ? + raise ValidationError(_( + 'The date of the donation of %s should be today ' 'or in the past, not in the future!') - % self.partner_id.name) + % donation.partner_id.name) + + @api.multi + @api.depends( + 'line_ids', 'line_ids.quantity', 'line_ids.unit_price', + 'line_ids.product_id') + def _tax_receipt_total(self): + for donation in self: + total = 0.0 + # Do not consider other currencies for tax receipts + # because, for the moment, only very very few countries + # accept tax receipts from other countries, and never in another + # currency. If you know such cases, please tell us and we will + # update the code of this module + if donation.currency_id == donation.company_currency_id: + for line in donation.line_ids: + # Filter the lines eligible for a tax receipt. + if line.tax_receipt_ok: + total += line.quantity * line.unit_price + donation.tax_receipt_total = total + + @api.model + def _prepare_tax_receipt(self): + vals = { + 'company_id': self.company_id.id, + 'currency_id': self.currency_id.id, + 'donation_date': self.donation_date, + 'amount': self.tax_receipt_total, + 'type': 'each', + 'partner_id': self.commercial_partner_id.id, + } + return vals @api.model def _prepare_move_line_name(self): @@ -269,70 +286,88 @@ def _prepare_donation_move(self): } return vals - @api.one + @api.multi def validate(self): - if not self.line_ids: - raise UserError( - _("Cannot validate the donation of %s because it doesn't " - "have any lines!") % self.partner_id.name) - - if self.state != 'draft': - raise UserError( - _("Cannot validate the donation of %s because it is not " - "in draft state.") % self.partner_id.name) - - if ( - self.env['res.users'].has_group( - 'account.group_supplier_inv_check_total') and - self.check_total != self.amount_total): - raise UserError( - _("The amount of the donation of %s (%s) is different from " - "the sum of the donation lines (%s).") % ( - self.partner_id.name, self.check_total, - self.amount_total)) - - donation_write_vals = {'state': 'done'} - - if self.amount_total: - move_vals = self._prepare_donation_move() - # when we have a full in-kind donation: no account move - if move_vals: - move = self.env['account.move'].create(move_vals) - move.post() - donation_write_vals['move_id'] = move.id - else: - self.message_post( - _('Full in-kind donation: no account move generated')) - - self.write(donation_write_vals) + check_total = self.env['res.users'].has_group( + 'donation.group_donation_check_total') + for donation in self: + if not donation.line_ids: + raise UserError(_( + "Cannot validate the donation of %s because it doesn't " + "have any lines!") % donation.partner_id.name) + + if donation.state != 'draft': + raise UserError(_( + "Cannot validate the donation of %s because it is not " + "in draft state.") % donation.partner_id.name) + + if check_total and donation.check_total != donation.amount_total: + raise UserError(_( + "The amount of the donation of %s (%s) is different " + "from the sum of the donation lines (%s).") % ( + donation.partner_id.name, donation.check_total, + donation.amount_total)) + + vals = {'state': 'done'} + + if donation.amount_total: + move_vals = donation._prepare_donation_move() + # when we have a full in-kind donation: no account move + if move_vals: + move = self.env['account.move'].create(move_vals) + move.post() + vals['move_id'] = move.id + else: + donation.message_post(_( + 'Full in-kind donation: no account move generated')) + if ( + donation.tax_receipt_option == 'each' and + donation.tax_receipt_total and + not donation.tax_receipt_id): + receipt_vals = donation._prepare_tax_receipt() + receipt = self.env['donation.tax.receipt'].create(receipt_vals) + vals['tax_receipt_id'] = receipt.id + + donation.write(vals) return - @api.one + @api.multi def save_default_values(self): + self.ensure_one() self.env.user.write({ 'context_donation_journal_id': self.journal_id.id, 'context_donation_campaign_id': self.campaign_id.id, }) - return - @api.one + @api.multi def done2cancel(self): '''from Done state to Cancel state''' - if self.move_id: - self.move_id.button_cancel() - self.move_id.unlink() - self.state = 'cancel' - return + for donation in self: + if donation.tax_receipt_id: + raise UserError(_( + "You cannot cancel this donation because " + "it is linked to the tax receipt %s. You should first " + "delete this tax receipt (but it may not be legally " + "allowed).") + % donation.tax_receipt_id.number) + if donation.move_id: + donation.move_id.button_cancel() + donation.move_id.unlink() + donation.state = 'cancel' - @api.one + @api.multi def cancel2draft(self): '''from Cancel state to Draft state''' - if self.move_id: - raise UserError( - _("A cancelled donation should not be linked to an " - "account move")) - self.state = 'draft' - return + for donation in self: + if donation.move_id: + raise UserError(_( + "A cancelled donation should not be linked to " + "an account move")) + if donation.tax_receipt_id: + raise UserError(_( + "A cancelled donation should not be linked to " + "a tax receipt")) + donation.state = 'draft' @api.multi def unlink(self): @@ -348,30 +383,40 @@ def unlink(self): "so you cannot delete it.")) return super(DonationDonation, self).unlink() - @api.one + @api.multi @api.depends('state', 'partner_id', 'move_id') def _compute_display_name(self): - if self.state == 'draft': - name = _('Draft Donation of %s') % self.partner_id.name - elif self.state == 'cancel': - name = _('Cancelled Donation of %s') % self.partner_id.name - else: - name = self.number - self.display_name = name + for donation in self: + if donation.state == 'draft': + name = _('Draft Donation of %s') % donation.partner_id.name + elif donation.state == 'cancel': + name = _('Cancelled Donation of %s') % donation.partner_id.name + else: + name = donation.number + donation.display_name = name - # used by module donation_tax_receipt (and donation_stay) @api.onchange('partner_id') def partner_id_change(self): - return + if self.partner_id: + self.tax_receipt_option = self.partner_id.tax_receipt_option - @api.multi - def _track_subtype(self, init_values): - self.ensure_one() - if 'state' in init_values and self.state == 'done': - return 'donation.donation_done' - elif 'state' in init_values and self.state == 'cancel': - return 'donation.donation_cancel' - return super(DonationDonation, self)._track_subtype(init_values) + @api.onchange('tax_receipt_option') + def tax_receipt_option_change(self): + res = {} + if ( + self.partner_id and + self.partner_id.tax_receipt_option == 'annual' and + self.tax_receipt_option != 'annual'): + res = { + 'warning': { + 'title': _('Error:'), + 'message': + _('You cannot change the Tax Receipt ' + 'Option when it is Annual.'), + }, + } + self.tax_receipt_option = 'annual' + return res class DonationLine(models.Model): @@ -379,22 +424,18 @@ class DonationLine(models.Model): _description = 'Donation Lines' _rec_name = 'product_id' - @api.one - @api.depends('unit_price', 'quantity') - def _compute_amount(self): - amount = self.quantity * self.unit_price - self.amount = amount - - @api.one + @api.multi @api.depends( 'unit_price', 'quantity', 'donation_id.currency_id', 'donation_id.donation_date', 'donation_id.company_id') def _compute_amount_company_currency(self): - amount = self.quantity * self.unit_price - donation_currency = self.donation_id.currency_id.with_context( - date=self.donation_id.donation_date) - self.amount_company_currency = donation_currency.compute( - amount, self.donation_id.company_id.currency_id) + for line in self: + amount = line.quantity * line.unit_price + line.amount = amount + donation_currency = line.donation_id.currency_id.with_context( + date=line.donation_id.donation_date) + line.amount_company_currency = donation_currency.compute( + amount, line.donation_id.company_id.currency_id) donation_id = fields.Many2one( 'donation.donation', string='Donation', ondelete='cascade') @@ -422,49 +463,30 @@ def _compute_amount_company_currency(self): analytic_account_id = fields.Many2one( 'account.analytic.account', string='Analytic Account', domain=[('type', 'not in', ('view', 'template'))], ondelete='restrict') - in_kind = fields.Boolean(string='In Kind') sequence = fields.Integer('Sequence') + # for the fields tax_receipt_ok and in_kind, we made an important change + # between v8 and v9: in v8, it was a reglar field set by an onchange + # in v9, it is a related stored field + tax_receipt_ok = fields.Boolean( + related='product_id.tax_receipt_ok', readonly=True, store=True) + in_kind = fields.Boolean( + related='product_id.in_kind_donation', readonly=True, store=True, + string='In Kind') @api.onchange('product_id') def product_id_change(self): if self.product_id: + # We should change that one day... if self.product_id.list_price: self.unit_price = self.product_id.list_price - self.in_kind = self.product_id.in_kind_donation @api.model def get_analytic_account_id(self): return self.analytic_account_id.id or False -class ResPartner(models.Model): - _inherit = 'res.partner' - - @api.one - @api.depends('donation_ids.partner_id') - def _donation_count(self): - # The current user may not have access rights for donations - try: - self.donation_count = len(self.donation_ids) - except: - self.donation_count = 0 +class DonationTaxReceipt(models.Model): + _inherit = 'donation.tax.receipt' donation_ids = fields.One2many( - 'donation.donation', 'partner_id', string='Donations') - donation_count = fields.Integer( - compute='_donation_count', string="# of Donations", readonly=True) - - -class AccountJournal(models.Model): - _inherit = 'account.journal' - - allow_donation = fields.Boolean(string='Donation Payment Method') - - @api.one - @api.constrains('type', 'allow_donation') - def _check_donation(self): - if self.allow_donation and self.type not in ('bank', 'cash'): - raise UserError( - _("The journal '%s' has the option 'Donation Payment Method', " - "so it's type should be 'Cash' or 'Bank and Checks'.") - % self.name) + 'donation.donation', 'tax_receipt_id', string='Related Donations') diff --git a/donation/models/donation_campaign.py b/donation/models/donation_campaign.py new file mode 100644 index 000000000..82832055b --- /dev/null +++ b/donation/models/donation_campaign.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# © 2014-2016 Barroux Abbey (http://www.barroux.org) +# © 2014-2016 Akretion France (Alexis de Lattre ) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from openerp import models, fields, api + + +class DonationCampaign(models.Model): + _name = 'donation.campaign' + _description = 'Code attributed for a Donation Campaign' + _order = 'code' + _rec_name = 'display_name' + + @api.multi + @api.depends('code', 'name') + def _compute_display_name(self): + for camp in self: + name = camp.name + if camp.code: + name = u'[%s] %s' % (camp.code, name) + camp.display_name = name + + code = fields.Char(string='Code', size=10) + name = fields.Char(string='Name', required=True) + display_name = fields.Char( + string='Display Name', compute='_compute_display_name', + readonly=True, store=True) + start_date = fields.Date( + string='Start Date', default=fields.Date.context_today) + nota = fields.Text(string='Notes') diff --git a/donation/models/partner.py b/donation/models/partner.py new file mode 100644 index 000000000..69244ad9e --- /dev/null +++ b/donation/models/partner.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# © 2014-2016 Barroux Abbey (http://www.barroux.org) +# © 2014-2016 Akretion France (Alexis de Lattre ) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from openerp import models, fields, api + + +class ResPartner(models.Model): + _inherit = 'res.partner' + + @api.multi + @api.depends('donation_ids.partner_id') + def _donation_count(self): + # The current user may not have access rights for donations + for partner in self: + try: + partner.donation_count = len(partner.donation_ids) + except: + partner.donation_count = 0 + + donation_ids = fields.One2many( + 'donation.donation', 'partner_id', string='Donations', + readonly=True) + donation_count = fields.Integer( + compute='_donation_count', string="# of Donations", readonly=True) diff --git a/donation/models/users.py b/donation/models/users.py new file mode 100644 index 000000000..79458e980 --- /dev/null +++ b/donation/models/users.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# © 2014-2016 Barroux Abbey (http://www.barroux.org) +# © 2014-2016 Akretion France (Alexis de Lattre ) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from openerp import models, fields + + +class ResUsers(models.Model): + _inherit = 'res.users' + + # begin with context_ to allow user to change it by himself + context_donation_campaign_id = fields.Many2one( + 'donation.campaign', string='Current Donation Campaign') + context_donation_journal_id = fields.Many2one( + 'account.journal', string='Current Donation Payment Method', + domain=[ + ('type', 'in', ('bank', 'cash')), + ('allow_donation', '=', True)], + company_dependent=True) diff --git a/donation/post_install.py b/donation/post_install.py new file mode 100644 index 000000000..6b60bebcf --- /dev/null +++ b/donation/post_install.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +# © 2016 Akretion (Alexis de Lattre ) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from openerp import SUPERUSER_ID + + +def update_account_journal(cr, pool): + ajo = pool['account.journal'] + journal_ids = ajo.search( + cr, SUPERUSER_ID, [('type', 'in', ('bank', 'cash'))]) + if journal_ids: + ajo.write(cr, SUPERUSER_ID, journal_ids, {'allow_donation': True}) + return diff --git a/donation/product.py b/donation/product.py deleted file mode 100644 index 390136681..000000000 --- a/donation/product.py +++ /dev/null @@ -1,60 +0,0 @@ -# -*- coding: utf-8 -*- -############################################################################## -# -# Donation module for Odoo -# Copyright (C) 2014-2015 Barroux Abbey (www.barroux.org) -# Copyright (C) 2014-2015 Akretion France (www.akretion.com) -# -# 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 . -# -############################################################################## - -from openerp import models, fields, api - - -class ProductTemplate(models.Model): - _inherit = 'product.template' - - donation = fields.Boolean( - string='Is a Donation', - help="Specify if the product can be selected " - "in a donation line.") - in_kind_donation = fields.Boolean( - string="In-Kind Donation") - - @api.onchange('donation') - def _donation_change(self): - if self.donation: - self.type = 'service' - self.sale_ok = False - - @api.onchange('in_kind_donation') - def _in_kind_donation_change(self): - if self.in_kind_donation: - self.donation = True - - -class ProductProduct(models.Model): - _inherit = 'product.product' - - @api.onchange('donation') - def _donation_change(self): - if self.donation: - self.type = 'service' - self.sale_ok = False - - @api.onchange('in_kind_donation') - def _in_kind_donation_change(self): - if self.in_kind_donation: - self.donation = True diff --git a/donation/product_view.xml b/donation/product_view.xml deleted file mode 100644 index ed83ee83d..000000000 --- a/donation/product_view.xml +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - - - donation.product.template.search - product.template - - - - - - - - - - donation.product.template.form - product.template - - -
-
- -
-
- -
-
-
-
- - - Products - product.template - kanban,tree,form - {'search_default_filter_donation': 1, 'default_donation': 1} - - - - - -
-
diff --git a/donation/report/donation_report.py b/donation/report/donation_report.py index 12b236835..39c9fbfd2 100644 --- a/donation/report/donation_report.py +++ b/donation/report/donation_report.py @@ -1,25 +1,7 @@ # -*- coding: utf-8 -*- -############################################################################## -# -# Donation module for Odoo -# Copyright (C) 2014-2015 Barroux Abbey (www.barroux.org) -# Copyright (C) 2014-2015 Akretion France (www.akretion.com) -# @author: Alexis de Lattre -# -# 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 . -# -############################################################################## +# © 2014-2016 Barroux Abbey (http://www.barroux.org) +# © 2014-2016 Akretion France (Alexis de Lattre ) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from openerp import tools from openerp import models, fields @@ -45,6 +27,7 @@ class DonationReport(models.Model): campaign_id = fields.Many2one( 'donation.campaign', string='Donation Campaign', readonly=True) in_kind = fields.Boolean(string='In Kind') + tax_receipt_ok = fields.Boolean(string='Eligible for a Tax Receipt') amount_company_currency = fields.Float( 'Amount Company Currency', readonly=True) @@ -54,6 +37,7 @@ def _select(self): d.donation_date AS donation_date, l.product_id AS product_id, l.in_kind AS in_kind, + l.tax_receipt_ok AS tax_receipt_ok, pt.categ_id AS product_categ_id, d.company_id AS company_id, d.partner_id AS partner_id, @@ -82,6 +66,7 @@ def _group_by(self): group_by = """ GROUP BY l.product_id, l.in_kind, + l.tax_receipt_ok, pt.categ_id, d.donation_date, d.partner_id, diff --git a/donation/report/donation_report_view.xml b/donation/report/donation_report_view.xml index 837a725d1..0475b9ba0 100644 --- a/donation/report/donation_report_view.xml +++ b/donation/report/donation_report_view.xml @@ -1,12 +1,11 @@ - + @@ -15,14 +14,24 @@ - - - - - - - - + + + + + + + + + @@ -66,4 +75,4 @@ parent="donation_report_title_menu" sequence="10"/> - + diff --git a/donation/security/donation_security.xml b/donation/security/donation_security.xml index 560017a58..eb37b4114 100644 --- a/donation/security/donation_security.xml +++ b/donation/security/donation_security.xml @@ -34,6 +34,11 @@ + + Donation Check Total + + + diff --git a/donation/security/ir.model.access.csv b/donation/security/ir.model.access.csv index d13a15ecc..71eee0db1 100644 --- a/donation/security/ir.model.access.csv +++ b/donation/security/ir.model.access.csv @@ -12,3 +12,6 @@ access_account_move_donation,Full access on account.move,account.model_account_m access_account_move_line_donation,Full access on account.move.line,account.model_account_move_line,group_donation_user,1,1,1,1 access_account_analytic_line_donation,Full access on account.analytic.line to donation user,analytic.model_account_analytic_line,group_donation_user,1,1,1,1 access_donation_report,Full access on donation.report to Donation Viewer,model_donation_report,group_donation_viewer,1,1,1,1 +access_donation_tax_receipt_viewer,Read access on donation.tax.receipt to Donation Viewer grp,model_donation_tax_receipt,donation.group_donation_viewer,1,0,0,0 +access_donation_tax_receipt,Create/Write access on donation.tax.receipt to Donation User,model_donation_tax_receipt,donation.group_donation_user,1,0,1,0 +access_donation_tax_receipt_full,Full access on donation.tax.receipt to Donation Manager,model_donation_tax_receipt,donation.group_donation_manager,1,1,1,1 diff --git a/donation/test/validate.yml b/donation/test/validate.yml deleted file mode 100644 index 10db82b97..000000000 --- a/donation/test/validate.yml +++ /dev/null @@ -1,63 +0,0 @@ -- - Validate donation1 -- - !function {model: donation.donation, name: validate}: - - eval: "[obj(ref('donation1')).id]" -- - Donation1 Check that an account move has been generated -- - !assert {model: donation.donation, id: donation1, string: Donation1 was not correctly validated}: - - state == 'done' - - move_id.date == time.strftime('%Y-%m-01') - - move_id.journal_id.id == ref('account.check_journal') - - move_id.state == 'posted' - - move_id.ref == 'CHQ BNP 239023' -- - Validate donation2 -- - !function {model: donation.donation, name: validate}: - - eval: "[obj(ref('donation2')).id]" -- - Donation2 Check that an account move has been generated -- - !assert {model: donation.donation, id: donation2, string: Donation2 was not correctly validated}: - - state == 'done' - - move_id.date == time.strftime('%Y-%m-01') - - move_id.journal_id.id == ref('account.bank_journal') - - move_id.state == 'posted' -- - Validate donation3 -- - !function {model: donation.donation, name: validate}: - - eval: "[obj(ref('donation3')).id]" -- - Donation3 Check that an account move has been generated -- - !assert {model: donation.donation, id: donation3, string: Donation3 was not correctly validated}: - - state == 'done' - - move_id.date == time.strftime('%Y-%m-01') - - move_id.journal_id.id == ref('account.cash_journal') - - move_id.state == 'posted' -- - Validate donation4 -- - !function {model: donation.donation, name: validate}: - - eval: "[obj(ref('donation4')).id]" -- - Donation4 Check that no account move has been generated (full in-kind donation) -- - !python {id: donation4, model: donation.donation}: | - assert self.state == 'done', 'Donation has not been set to done (donation4)' - assert not self.move_id, 'Donation should not have an account move (donation4)' -- - Validate donation5 -- - !function {model: donation.donation, name: validate}: - - eval: "[obj(ref('donation5')).id]" -- - Donation5 Check that an account move has been generated (partial in-kind donation) -- - !python {id: donation5, model: donation.donation}: | - assert self.state == 'done', 'Donation has not been set to done (donation5)' - assert self.move_id, 'No account move generated (donation5)' - assert self.move_id.amount == 400, 'Wrong amount on account move' diff --git a/donation/tests/__init__.py b/donation/tests/__init__.py new file mode 100644 index 000000000..7679605f5 --- /dev/null +++ b/donation/tests/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import test_donation diff --git a/donation/tests/test_donation.py b/donation/tests/test_donation.py new file mode 100644 index 000000000..142ee8ea5 --- /dev/null +++ b/donation/tests/test_donation.py @@ -0,0 +1,108 @@ +# -*- coding: utf-8 -*- +# © 2015-2016 Akretion (Alexis de Lattre ) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from openerp.tests.common import TransactionCase +import time + + +class TestDonation(TransactionCase): + + def test_donation(self): + for i in range(1, 6): + donation = self.env.ref('donation.donation%d' % i) + self.assertEquals(donation.state, 'draft') + donation.validate() + self.assertEquals(donation.state, 'done') + if i == 4: # full in-kind donation + self.assertFalse(donation.move_id) + else: + self.assertEquals(donation.move_id.state, 'posted') + self.assertEquals(donation.payment_ref, donation.move_id.ref) + self.assertEquals( + donation.journal_id, donation.move_id.journal_id) + self.assertEquals( + donation.donation_date, donation.move_id.date) + if ( + donation.tax_receipt_option == 'each' and + donation.tax_receipt_total): + self.assertTrue(donation.tax_receipt_id) + tax_receipt = donation.tax_receipt_id + self.assertEquals(tax_receipt.type, 'each') + self.assertEquals( + donation.commercial_partner_id, tax_receipt.partner_id) + self.assertEquals( + donation.donation_date, tax_receipt.donation_date) + self.assertEquals( + donation.tax_receipt_total, tax_receipt.amount) + + def test_annual_tax_receipt(self): + partner_familly = self.env['res.partner'].create({ + 'name': u'Famille Joly', + 'tax_receipt_option': 'annual', + }) + partner_husband = self.env['res.partner'].create({ + 'parent_id': partner_familly.id, + 'name': u'Xavier Joly'}) + partner_wife = self.env['res.partner'].create({ + 'parent_id': partner_familly.id, + 'name': u'Stéphanie Joly'}) + + dons = self.create_donation_annual_receipt( + partner_husband, 40, 10, 'CHQ FB 93283290') + dons += self.create_donation_annual_receipt( + partner_husband, 140, 60, 'CHQ FB OPIE02') + dons += self.create_donation_annual_receipt( + partner_wife, 20, 5, 'CHQ FB AZERTY1242') + dons.validate() + last_day_year = time.strftime('%Y-12-31') + wizard = self.env['tax.receipt.annual.create'].create({ + 'start_date': time.strftime('%Y-01-01'), + 'end_date': last_day_year}) + action = wizard.generate_annual_receipts() + tax_receipt_ids = action['domain'][0][2] + self.assertTrue(tax_receipt_ids) + dtro = self.env['donation.tax.receipt'] + tax_receipts = dtro.search([ + ('partner_id', '=', partner_familly.id), + ('type', '=', 'annual'), + ('id', 'in', tax_receipt_ids)]) + self.assertEquals(len(tax_receipts), 1) + tax_receipt = tax_receipts[0] + self.assertEquals(tax_receipt.amount, 200) + self.assertTrue(tax_receipt.number) + + self.assertEquals(tax_receipt.date, last_day_year) + self.assertEquals(tax_receipt.donation_date, last_day_year) + self.assertEquals( + tax_receipt.currency_id, dons[0].company_id.currency_id) + + def create_donation_annual_receipt( + self, partner, amount_tax_receipt, amount_no_tax_receipt, + payment_ref): + journal = self.env['account.journal'].search([( + 'type', '=', 'bank')], limit=1) + donation = self.env['donation.donation'].create({ + 'journal_id': journal.id, + 'partner_id': partner.id, + 'currency_id': self.env.ref('base.main_company').currency_id.id, + 'tax_receipt_option': 'annual', + 'donation_date': time.strftime('%Y-01-01'), + 'payment_ref': payment_ref, + 'line_ids': [ + (0, 0, { + 'product_id': + self.env.ref('donation_base.product_product_donation').id, + 'quantity': 1, + 'unit_price': amount_tax_receipt, + }), + (0, 0, { + 'product_id': self.env.ref( + 'donation_base.product_product_donation_notaxreceipt' + ).id, + 'quantity': 1, + 'unit_price': amount_no_tax_receipt, + }), + ] + }) + return donation diff --git a/donation/account_view.xml b/donation/views/account.xml similarity index 71% rename from donation/account_view.xml rename to donation/views/account.xml index 25fa0600a..c756aca1f 100644 --- a/donation/account_view.xml +++ b/donation/views/account.xml @@ -1,6 +1,11 @@ + - + @@ -16,4 +21,4 @@ - + diff --git a/donation/donation_view.xml b/donation/views/donation.xml similarity index 77% rename from donation/donation_view.xml rename to donation/views/donation.xml index fe46dcaba..f4aab7750 100644 --- a/donation/donation_view.xml +++ b/donation/views/donation.xml @@ -1,12 +1,11 @@ - + @@ -39,13 +38,16 @@ + groups="donation.group_donation_check_total"/> + + + @@ -107,10 +109,12 @@ + + @@ -150,6 +154,7 @@ + @@ -168,6 +173,7 @@ + @@ -204,20 +210,47 @@ - - - Validated - donation.donation - - Donation validated + + Products + product.template + kanban,tree,form + {'search_default_filter_donation': 1, 'default_donation': 1} - - Cancelled - donation.donation - - Donation cancelled + + + + donation.donation.tax.receipt.form + donation.tax.receipt + + + + + + + + + + + + + + + + + - + diff --git a/donation/donation_campaign_view.xml b/donation/views/donation_campaign.xml similarity index 83% rename from donation/donation_campaign_view.xml rename to donation/views/donation_campaign.xml index 3584c4f06..fbda61fc3 100644 --- a/donation/donation_campaign_view.xml +++ b/donation/views/donation_campaign.xml @@ -1,12 +1,11 @@ - + @@ -46,4 +45,4 @@ parent="donation_config_menu" sequence="30"/> - + diff --git a/donation/partner_view.xml b/donation/views/partner.xml similarity index 80% rename from donation/partner_view.xml rename to donation/views/partner.xml index f775c3d7b..5dd96c986 100644 --- a/donation/partner_view.xml +++ b/donation/views/partner.xml @@ -1,13 +1,11 @@ - - + @@ -34,4 +32,4 @@ - + diff --git a/donation/users_view.xml b/donation/views/users.xml similarity index 81% rename from donation/users_view.xml rename to donation/views/users.xml index 8b51e91b1..42176fbff 100644 --- a/donation/users_view.xml +++ b/donation/views/users.xml @@ -1,12 +1,11 @@ - + @@ -36,4 +35,4 @@ - + diff --git a/donation/wizard/donation_validate.py b/donation/wizard/donation_validate.py index 9bb857e95..66ca4bb64 100644 --- a/donation/wizard/donation_validate.py +++ b/donation/wizard/donation_validate.py @@ -1,25 +1,7 @@ # -*- coding: utf-8 -*- -############################################################################## -# -# Donation module for Odoo -# Copyright (C) 2014-2015 Barroux Abbey (www.barroux.org) -# Copyright (C) 2014-2015 Akretion France (www.akretion.com) -# @author Alexis de Lattre -# -# 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 . -# -############################################################################## +# © 2014-2016 Barroux Abbey (http://www.barroux.org) +# © 2014-2016 Akretion France (Alexis de Lattre ) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from openerp import models, api