diff --git a/account_cutoff_base/README.rst b/account_cutoff_base/README.rst new file mode 100644 index 00000000000..5e43e44d656 --- /dev/null +++ b/account_cutoff_base/README.rst @@ -0,0 +1,56 @@ +.. 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 + +=================== +Account Cutoff Base +=================== + +This module contains objets, fields and menu entries that are used by +other cut-off modules ; it doesn't provide useful features by itself. You +need to install other cut-off modules to get the useful features: + +* the module *account_cutoff_prepaid* will manage prepaid cut-offs based on + start date and end date, + +* the module *account_cutoff_accrual_picking* will manage the accruals based + on the status of the pickings. + +Usage +===== + +.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas + :alt: Try me on Runbot + :target: https://runbot.odoo-community.org/runbot/89/9.0 + + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues +`_. In case of trouble, please +check there if your issue has already been reported. If you spotted it first, +help us smashing it by providing a detailed and welcomed feedback. + +Credits +======= + +Contributors +------------ + +* Alexis de Lattre + +Maintainer +---------- + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :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. + +To contribute to this module, please visit https://odoo-community.org. diff --git a/account_cutoff_base/__init__.py b/account_cutoff_base/__init__.py index 58f008dc63d..cde864bae21 100644 --- a/account_cutoff_base/__init__.py +++ b/account_cutoff_base/__init__.py @@ -1,24 +1,3 @@ -# -*- encoding: utf-8 -*- -############################################################################## -# -# Account Cut-off Base module for OpenERP -# Copyright (C) 2013 Akretion (http://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 . -# -############################################################################## +# -*- coding: utf-8 -*- -from . import account_cutoff -from . import company +from . import models diff --git a/account_cutoff_base/__openerp__.py b/account_cutoff_base/__openerp__.py index 1e26ca233bf..6c5feee06b4 100644 --- a/account_cutoff_base/__openerp__.py +++ b/account_cutoff_base/__openerp__.py @@ -1,54 +1,21 @@ -# -*- encoding: utf-8 -*- -############################################################################## -# -# Account Cut-off Base module for OpenERP -# Copyright (C) 2013 Akretion (http://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 . -# -############################################################################## - +# -*- coding: utf-8 -*- +# © 2013-2016 Akretion (Alexis de Lattre ) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). { 'name': 'Account Cut-off Base', - 'version': '8.0.0.1.0', + 'version': '9.0.1.0.0', 'category': 'Accounting & Finance', 'license': 'AGPL-3', 'summary': 'Base module for Account Cut-offs', - 'description': """ -This module contains objets, fields and menu entries that are used by other -cut-off modules. So you need to install other cut-off modules to get the -additionnal functionalities : - -* the module *account_cutoff_prepaid* will manage prepaid cut-offs based on - start date and end date, -* the module *account_cutoff_accrual_picking* will manage the accruals based - on the status of the pickings. - -Please contact Alexis de Lattre from Akretion -for any help or question about this module. - """, - 'author': "Akretion,Odoo Community Association (OCA)", + 'author': 'Akretion,Odoo Community Association (OCA)', 'website': 'http://www.akretion.com', 'depends': ['account_accountant'], 'data': [ - 'company_view.xml', - 'account_cutoff_view.xml', - 'security/ir.model.access.csv', 'security/account_cutoff_base_security.xml', + 'security/ir.model.access.csv', + 'views/company.xml', + 'views/account_cutoff.xml', ], - 'installable': False, - 'active': False, + 'installable': True, } diff --git a/account_cutoff_base/account_cutoff.py b/account_cutoff_base/account_cutoff.py deleted file mode 100644 index 6e2d5283fdb..00000000000 --- a/account_cutoff_base/account_cutoff.py +++ /dev/null @@ -1,479 +0,0 @@ -# -*- encoding: utf-8 -*- -############################################################################## -# -# Account Cut-off Base module for OpenERP -# Copyright (C) 2013 Akretion (http://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 . -# -############################################################################## - -from openerp.osv import orm, fields -import openerp.addons.decimal_precision as dp -from openerp.tools.translate import _ -from datetime import datetime - - -class account_cutoff(orm.Model): - _name = 'account.cutoff' - _rec_name = 'cutoff_date' - _order = 'cutoff_date desc' - _inherit = ['mail.thread'] - _description = 'Account Cut-off' - _track = { - 'state': { - 'account_cutoff_base.cutoff_done': - lambda self, cr, uid, obj, ctx=None: obj['state'] == 'done', - } - } - - def copy(self, cr, uid, id, default=None, context=None): - if default is None: - default = {} - default.update({ - 'cutoff_date': '%d-12-31' % datetime.today().year, - 'move_id': False, - 'state': 'draft', - 'line_ids': False, - }) - return super(account_cutoff, self).copy( - cr, uid, id, default=default, context=context) - - def _compute_total_cutoff(self, cr, uid, ids, name, arg, context=None): - res = {} - for cutoff in self.browse(cr, uid, ids, context=context): - res[cutoff.id] = 0 - for line in cutoff.line_ids: - res[cutoff.id] += line.cutoff_amount - return res - - _columns = { - 'cutoff_date': fields.date( - 'Cut-off Date', required=True, readonly=True, - states={'draft': [('readonly', False)]}, - track_visibility='always'), - 'type': fields.selection([ - ('accrued_revenue', 'Accrued Revenue'), - ('accrued_expense', 'Accrued Expense'), - ('prepaid_revenue', 'Prepaid Revenue'), - ('prepaid_expense', 'Prepaid Expense'), - ], 'Type', required=True, readonly=True, - states={'draft': [('readonly', False)]}), - 'move_id': fields.many2one( - 'account.move', 'Cut-off Journal Entry', readonly=True), - 'move_label': fields.char( - 'Label of the Cut-off Journal Entry', - size=64, required=True, readonly=True, - states={'draft': [('readonly', False)]}, - help="This label will be written in the 'Name' field of the " - "Cut-off Account Move Lines and in the 'Reference' field of " - "the Cut-off Account Move."), - 'cutoff_account_id': fields.many2one( - 'account.account', 'Cut-off Account', - domain=[('type', '<>', 'view'), ('type', '<>', 'closed')], - required=True, readonly=True, - states={'draft': [('readonly', False)]}), - 'cutoff_journal_id': fields.many2one( - 'account.journal', 'Cut-off Account Journal', required=True, - readonly=True, states={'draft': [('readonly', False)]}), - 'total_cutoff_amount': fields.function( - _compute_total_cutoff, type='float', string="Total Cut-off Amount", - readonly=True, track_visibility='always'), - 'company_id': fields.many2one( - 'res.company', 'Company', required=True, readonly=True, - states={'draft': [('readonly', False)]}), - 'company_currency_id': fields.related( - 'company_id', 'currency_id', readonly=True, type='many2one', - relation='res.currency', string='Company Currency'), - 'line_ids': fields.one2many( - 'account.cutoff.line', 'parent_id', 'Cut-off Lines', readonly=True, - states={'draft': [('readonly', False)]}), - 'state': fields.selection([ - ('draft', 'Draft'), - ('done', 'Done'), - ], - 'State', select=True, readonly=True, track_visibility='onchange', - help="State of the cutoff. When the Journal Entry is created, " - "the state is set to 'Done' and the fields become read-only."), - } - - def _get_default_journal(self, cr, uid, context=None): - cur_user = self.pool['res.users'].browse(cr, uid, uid, context=context) - return cur_user.company_id.default_cutoff_journal_id.id or None - - def _default_move_label(self, cr, uid, context=None): - if context is None: - context = {} - type = context.get('type') - cutoff_date = context.get('cutoff_date') - if cutoff_date: - cutoff_date_label = ' dated %s' % cutoff_date - else: - cutoff_date_label = '' - label = '' - if type == 'accrued_expense': - label = _('Accrued Expense%s') % cutoff_date_label - elif type == 'accrued_revenue': - label = _('Accrued Revenue%s') % cutoff_date_label - elif type == 'prepaid_revenue': - label = _('Prepaid Revenue%s') % cutoff_date_label - elif type == 'prepaid_expense': - label = _('Prepaid Expense%s') % cutoff_date_label - return label - - def _default_type(self, cr, uid, context=None): - if context is None: - context = {} - return context.get('type') - - def _inherit_default_cutoff_account_id(self, cr, uid, context=None): - '''Function designed to be inherited by other cutoff modules''' - return None - - def _default_cutoff_account_id(self, cr, uid, context=None): - '''This function can't be inherited, so we use a second function''' - return self._inherit_default_cutoff_account_id( - cr, uid, context=context) - - _defaults = { - 'state': 'draft', - 'company_id': lambda self, cr, uid, context: - self.pool['res.users'].browse( - cr, uid, uid, context=context).company_id.id, - 'cutoff_journal_id': _get_default_journal, - 'move_label': _default_move_label, - 'type': _default_type, - 'cutoff_account_id': _default_cutoff_account_id, - } - - _sql_constraints = [( - 'date_type_company_uniq', - 'unique(cutoff_date, company_id, type)', - 'A cutoff of the same type already exists with this cut-off date !' - )] - - def cutoff_date_onchange( - self, cr, uid, ids, type, cutoff_date, move_label, context=None): - if context is None: - context = {} - res = {'value': {}} - if type and cutoff_date: - ctx = context.copy() - ctx.update({'type': type, 'cutoff_date': cutoff_date}) - res['value']['move_label'] = self._default_move_label( - cr, uid, context=ctx) - return res - - def back2draft(self, cr, uid, ids, context=None): - assert len(ids) == 1,\ - 'This function should only be used for a single id at a time' - cur_cutoff = self.browse(cr, uid, ids[0], context=context) - if cur_cutoff.move_id: - self.pool['account.move'].unlink( - cr, uid, [cur_cutoff.move_id.id], context=context) - self.write(cr, uid, ids[0], {'state': 'draft'}, context=context) - return True - - def _prepare_move(self, cr, uid, cur_cutoff, to_provision, context=None): - if context is None: - context = {} - movelines_to_create = [] - amount_total = 0 - move_label = cur_cutoff.move_label - merge_keys = self._get_merge_keys() - for merge_values, amount in to_provision.items(): - vals = { - 'name': move_label, - 'debit': amount < 0 and amount * -1 or 0, - 'credit': amount >= 0 and amount or 0, - } - for k, v in zip(merge_keys, merge_values): - vals[k] = v - movelines_to_create.append((0, 0, vals)) - amount_total += amount - - # add contre-partie - counterpart_amount = amount_total * -1 - movelines_to_create.append((0, 0, { - 'account_id': cur_cutoff.cutoff_account_id.id, - 'debit': counterpart_amount < 0 and counterpart_amount * -1 or 0, - 'credit': counterpart_amount >= 0 and counterpart_amount or 0, - 'name': move_label, - 'analytic_account_id': False, - })) - - # Select period - local_ctx = context.copy() - local_ctx['account_period_prefer_normal'] = True - period_search = self.pool['account.period'].find( - cr, uid, cur_cutoff.cutoff_date, context=local_ctx) - if len(period_search) != 1: - raise orm.except_orm( - 'Error:', "No matching period for date '%s'" - % cur_cutoff.cutoff_date) - period_id = period_search[0] - - res = { - 'journal_id': cur_cutoff.cutoff_journal_id.id, - 'date': cur_cutoff.cutoff_date, - 'period_id': period_id, - 'ref': move_label, - 'line_id': movelines_to_create, - } - return res - - def _prepare_provision_line(self, cr, uid, cutoff_line, - context=None): - """ Convert a cutoff line to elements of a move line - - The returned dictionary must at least contain 'account_id' - and 'amount' (< 0 means debit). - - If you ovverride this, the added fields must also be - added in an override of _get_merge_keys. - """ - return { - 'account_id': cutoff_line.cutoff_account_id.id, - 'analytic_account_id': cutoff_line.analytic_account_id.id, - 'amount': cutoff_line.cutoff_amount, - } - - def _prepare_provision_tax_line(self, cr, uid, cutoff_tax_line, - context=None): - """ Convert a cutoff tax line to elements of a move line - - See _prepare_provision_line for more info. - """ - return { - 'account_id': cutoff_tax_line.cutoff_account_id.id, - 'analytic_account_id': cutoff_tax_line.analytic_account_id.id, - 'amount': cutoff_tax_line.cutoff_amount, - } - - def _get_merge_keys(self): - """ Return merge criteria for provision lines - - The returned list must contain valid field names - for account.move.line. Provision lines with the - same values for these fields will be merged. - The list must at least contain account_id. - """ - return ['account_id', 'analytic_account_id'] - - def _merge_provision_lines(self, cr, uid, provision_lines, context=None): - """ merge provision line - - Returns a dictionary {key, amount} where key is - a tuple containing the values of the properties in _get_merge_keys() - """ - to_provision = {} - merge_keys = self._get_merge_keys() - for provision_line in provision_lines: - key = tuple([provision_line.get(key) for key in merge_keys]) - if key in to_provision: - to_provision[key] += provision_line['amount'] - else: - to_provision[key] = provision_line['amount'] - return to_provision - - def create_move(self, cr, uid, ids, context=None): - assert len(ids) == 1, \ - 'This function should only be used for a single id at a time' - move_obj = self.pool['account.move'] - cur_cutoff = self.browse(cr, uid, ids[0], context=context) - if cur_cutoff.move_id: - raise orm.except_orm( - _('Error:'), - _("The Cut-off Journal Entry already exists. You should " - "delete it before running this function.")) - if not cur_cutoff.line_ids: - raise orm.except_orm( - _('Error:'), - _("There are no lines on this Cut-off, so we can't create " - "a Journal Entry.")) - provision_lines = [] - for line in cur_cutoff.line_ids: - provision_lines.append( - self._prepare_provision_line( - cr, uid, line, context=context)) - for tax_line in line.tax_line_ids: - provision_lines.append( - self._prepare_provision_tax_line( - cr, uid, tax_line, context=context)) - to_provision = self._merge_provision_lines( - cr, uid, provision_lines, context=context) - vals = self._prepare_move( - cr, uid, cur_cutoff, to_provision, context=context) - move_id = move_obj.create(cr, uid, vals, context=context) - move_obj.validate(cr, uid, [move_id], context=context) - self.write(cr, uid, ids[0], { - 'move_id': move_id, - 'state': 'done', - }, context=context) - - action = { - 'name': 'Cut-off Account Move', - 'view_type': 'form', - 'view_mode': 'form,tree', - 'res_id': move_id, - 'view_id': False, - 'res_model': 'account.move', - 'type': 'ir.actions.act_window', - 'nodestroy': False, - 'target': 'current', - } - return action - - -class account_cutoff_line(orm.Model): - _name = 'account.cutoff.line' - _description = 'Account Cut-off Line' - - _columns = { - 'parent_id': fields.many2one( - 'account.cutoff', 'Cut-off', ondelete='cascade'), - 'name': fields.char('Description', size=64), - 'company_currency_id': fields.related( - 'parent_id', 'company_currency_id', type='many2one', - relation='res.currency', string="Company Currency", readonly=True), - 'partner_id': fields.many2one('res.partner', 'Partner', readonly=True), - 'account_id': fields.many2one( - 'account.account', 'Account', - domain=[('type', '<>', 'view'), ('type', '<>', 'closed')], - required=True, readonly=True), - 'cutoff_account_id': fields.many2one( - 'account.account', 'Cut-off Account', - domain=[('type', '<>', 'view'), ('type', '<>', 'closed')], - required=True, readonly=True), - 'cutoff_account_code': fields.related( - 'cutoff_account_id', 'code', type='char', - string='Cut-off Account Code', readonly=True), - 'analytic_account_id': fields.many2one( - 'account.analytic.account', 'Analytic Account', - domain=[('type', 'not in', ('view', 'template'))], - readonly=True), - 'analytic_account_code': fields.related( - 'analytic_account_id', 'code', type='char', - string='Analytic Account Code', readonly=True), - 'currency_id': fields.many2one( - 'res.currency', 'Amount Currency', readonly=True, - help="Currency of the 'Amount' field."), - 'amount': fields.float( - 'Amount', digits_compute=dp.get_precision('Account'), - readonly=True, - help="Amount that is used as base to compute the Cut-off Amount. " - "This Amount is in the 'Amount Currency', which may be different " - "from the 'Company Currency'."), - 'cutoff_amount': fields.float( - 'Cut-off Amount', digits_compute=dp.get_precision('Account'), - readonly=True, - help="Cut-off Amount without taxes in the Company Currency."), - 'tax_ids': fields.many2many( - 'account.tax', id1='cutoff_line_id', id2='tax_id', string='Taxes', - readonly=True), - 'tax_line_ids': fields.one2many( - 'account.cutoff.tax.line', 'parent_id', 'Cut-off Tax Lines', - readonly=True), - } - - -class account_cutoff_tax_line(orm.Model): - _name = 'account.cutoff.tax.line' - _description = 'Account Cut-off Tax Line' - - _columns = { - 'parent_id': fields.many2one( - 'account.cutoff.line', 'Account Cut-off Line', - ondelete='cascade', required=True), - 'tax_id': fields.many2one('account.tax', 'Tax', required=True), - 'cutoff_account_id': fields.many2one( - 'account.account', 'Cut-off Account', - domain=[('type', '<>', 'view'), ('type', '<>', 'closed')], - required=True, readonly=True), - 'analytic_account_id': fields.many2one( - 'account.analytic.account', 'Analytic Account', - domain=[('type', 'not in', ('view', 'template'))], - readonly=True), - 'base': fields.float( - 'Base', digits_compute=dp.get_precision('Account'), - readonly=True, help="Base Amount in the currency of the PO."), - 'amount': fields.float( - 'Tax Amount', digits_compute=dp.get_precision('Account'), - readonly=True, help='Tax Amount in the currency of the PO.'), - 'sequence': fields.integer('Sequence', readonly=True), - 'cutoff_amount': fields.float( - 'Cut-off Tax Amount', digits_compute=dp.get_precision('Account'), - readonly=True, - help="Tax Cut-off Amount in the company currency."), - 'currency_id': fields.related( - 'parent_id', 'currency_id', type='many2one', - relation='res.currency', string='Currency', readonly=True), - 'company_currency_id': fields.related( - 'parent_id', 'company_currency_id', - type='many2one', relation='res.currency', - string="Company Currency", readonly=True), - } - - -class account_cutoff_mapping(orm.Model): - _name = 'account.cutoff.mapping' - _description = 'Account Cut-off Mapping' - _rec_name = 'account_id' - - _columns = { - 'company_id': fields.many2one('res.company', 'Company', required=True), - 'account_id': fields.many2one( - 'account.account', 'Regular Account', - domain=[('type', '<>', 'view'), ('type', '<>', 'closed')], - required=True), - 'cutoff_account_id': fields.many2one( - 'account.account', 'Cut-off Account', - domain=[('type', '<>', 'view'), ('type', '<>', 'closed')], - required=True), - 'cutoff_type': fields.selection([ - ('all', 'All Cut-off Types'), - ('accrued_revenue', 'Accrued Revenue'), - ('accrued_expense', 'Accrued Expense'), - ('prepaid_revenue', 'Prepaid Revenue'), - ('prepaid_expense', 'Prepaid Expense'), - ], 'Cut-off Type', required=True), - } - - _defaults = { - 'company_id': lambda self, cr, uid, context: - self.pool['res.users'].browse( - cr, uid, uid, context=context).company_id.id, - } - - def _get_mapping_dict( - self, cr, uid, company_id, cutoff_type='all', context=None): - '''return a dict with: - key = ID of account, - value = ID of cutoff_account''' - if cutoff_type == 'all': - cutoff_type_filter = ('all') - else: - cutoff_type_filter = ('all', cutoff_type) - mapping_ids = self.search( - cr, uid, [ - ('company_id', '=', company_id), - ('cutoff_type', 'in', cutoff_type_filter), - ], - context=context) - mapping_read = self.read(cr, uid, mapping_ids, context=context) - mapping = {} - for item in mapping_read: - mapping[item['account_id'][0]] = item['cutoff_account_id'][0] - return mapping diff --git a/account_cutoff_base/company.py b/account_cutoff_base/company.py deleted file mode 100644 index 98b5d70fe29..00000000000 --- a/account_cutoff_base/company.py +++ /dev/null @@ -1,35 +0,0 @@ -# -*- encoding: utf-8 -*- -############################################################################## -# -# Account Cut-off Base module for OpenERP -# Copyright (C) 2013 Akretion (http://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 . -# -############################################################################## - - -from openerp.osv import orm, fields - - -class res_company(orm.Model): - _inherit = 'res.company' - - _columns = { - 'default_cutoff_journal_id': fields.many2one( - 'account.journal', 'Default Cut-off Journal'), - 'cutoff_account_mapping_ids': fields.one2many( - 'account.cutoff.mapping', 'company_id', 'Cut-off Account Mapping'), - } diff --git a/account_cutoff_base/models/__init__.py b/account_cutoff_base/models/__init__.py new file mode 100644 index 00000000000..23cb5f9a625 --- /dev/null +++ b/account_cutoff_base/models/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- + +from . import account_cutoff +from . import company diff --git a/account_cutoff_base/models/account_cutoff.py b/account_cutoff_base/models/account_cutoff.py new file mode 100644 index 00000000000..55a68fefab5 --- /dev/null +++ b/account_cutoff_base/models/account_cutoff.py @@ -0,0 +1,359 @@ +# -*- coding: utf-8 -*- +# © 2013-2016 Akretion (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 + + +class AccountCutoff(models.Model): + _name = 'account.cutoff' + _rec_name = 'cutoff_date' + _order = 'cutoff_date desc' + _inherit = ['mail.thread'] + _description = 'Account Cut-off' + + @api.multi + @api.depends('line_ids') + def _compute_total_cutoff(self): + for cutoff in self: + tamount = 0.0 + for line in cutoff.line_ids: + tamount += line.cutoff_amount + cutoff.total_cutoff_amount = tamount + + @api.model + def _default_move_label(self): + type = self._context.get('type') + label = '' + if type == 'accrued_expense': + label = _('Accrued Expense') + elif type == 'accrued_revenue': + label = _('Accrued Revenue') + elif type == 'prepaid_revenue': + label = _('Prepaid Revenue') + elif type == 'prepaid_expense': + label = _('Prepaid Expense') + return label + + @api.model + def _inherit_default_cutoff_account_id(self): + '''Function designed to be inherited by other cutoff modules''' + return None + + @api.model + def _default_cutoff_account_id(self): + '''This function can't be inherited, so we use a second function''' + return self._inherit_default_cutoff_account_id() + + cutoff_date = fields.Date( + string='Cut-off Date', readonly=True, + states={'draft': [('readonly', False)]}, copy=False, + track_visibility='onchange') + type = fields.Selection([ + ('accrued_revenue', 'Accrued Revenue'), + ('accrued_expense', 'Accrued Expense'), + ('prepaid_revenue', 'Prepaid Revenue'), + ('prepaid_expense', 'Prepaid Expense'), + ], string='Type', required=True, readonly=True, + default=lambda self: self._context.get('type'), + states={'draft': [('readonly', False)]}) + move_id = fields.Many2one( + 'account.move', string='Cut-off Journal Entry', readonly=True, + copy=False) + move_label = fields.Char( + string='Label of the Cut-off Journal Entry', readonly=True, + states={'draft': [('readonly', False)]}, default=_default_move_label, + help="This label will be written in the 'Name' field of the " + "Cut-off Account Move Lines and in the 'Reference' field of " + "the Cut-off Account Move.") + cutoff_account_id = fields.Many2one( + 'account.account', string='Cut-off Account', + domain=[('deprecated', '=', False)], + readonly=True, states={'draft': [('readonly', False)]}, + default=_default_cutoff_account_id) + cutoff_journal_id = fields.Many2one( + 'account.journal', string='Cut-off Account Journal', + default=lambda self: self.env.user.company_id. + default_cutoff_journal_id, + readonly=True, states={'draft': [('readonly', False)]}) + total_cutoff_amount = fields.Monetary( + compute='_compute_total_cutoff', string="Total Cut-off Amount", + currency_field='company_currency_id', + readonly=True, track_visibility='onchange') + company_id = fields.Many2one( + 'res.company', string='Company', required=True, readonly=True, + states={'draft': [('readonly', False)]}, + default=lambda self: self.env['res.company']._company_default_get( + 'account.cutoff')) + company_currency_id = fields.Many2one( + related='company_id.currency_id', readonly=True, + string='Company Currency') + line_ids = fields.One2many( + 'account.cutoff.line', 'parent_id', string='Cut-off Lines', + readonly=True, states={'draft': [('readonly', False)]}) + state = fields.Selection([ + ('draft', 'Draft'), + ('done', 'Done'), + ], string='State', index=True, readonly=True, + track_visibility='onchange', default='draft', copy=False, + help="State of the cutoff. When the Journal Entry is created, " + "the state is set to 'Done' and the fields become read-only.") + + _sql_constraints = [( + 'date_type_company_uniq', + 'unique(cutoff_date, company_id, type)', + 'A cutoff of the same type already exists with this cut-off date !' + )] + + @api.multi + def back2draft(self): + self.ensure_one() + if self.move_id: + self.move_id.unlink() + self.state = 'draft' + + def _get_merge_keys(self): + """ Return merge criteria for provision lines + + The returned list must contain valid field names + for account.move.line. Provision lines with the + same values for these fields will be merged. + The list must at least contain account_id. + """ + return ['account_id', 'analytic_account_id'] + + @api.multi + def _prepare_move(self, to_provision): + self.ensure_one() + movelines_to_create = [] + amount_total = 0 + move_label = self.move_label + merge_keys = self._get_merge_keys() + for merge_values, amount in to_provision.items(): + vals = { + 'name': move_label, + 'debit': amount < 0 and amount * -1 or 0, + 'credit': amount >= 0 and amount or 0, + } + for k, v in zip(merge_keys, merge_values): + vals[k] = v + movelines_to_create.append((0, 0, vals)) + amount_total += amount + + # add contre-partie + counterpart_amount = amount_total * -1 + movelines_to_create.append((0, 0, { + 'account_id': self.cutoff_account_id.id, + 'name': move_label, + 'debit': counterpart_amount < 0 and counterpart_amount * -1 or 0, + 'credit': counterpart_amount >= 0 and counterpart_amount or 0, + 'analytic_account_id': False, + })) + + res = { + 'journal_id': self.cutoff_journal_id.id, + 'date': self.cutoff_date, + 'ref': move_label, + 'line_ids': movelines_to_create, + } + return res + + @api.multi + def _prepare_provision_line(self, cutoff_line): + """ Convert a cutoff line to elements of a move line + + The returned dictionary must at least contain 'account_id' + and 'amount' (< 0 means debit). + + If you override this, the added fields must also be + added in an override of _get_merge_keys. + """ + return { + 'account_id': cutoff_line.cutoff_account_id.id, + 'analytic_account_id': cutoff_line.analytic_account_id.id, + 'amount': cutoff_line.cutoff_amount, + } + + @api.multi + def _prepare_provision_tax_line(self, cutoff_tax_line): + """ Convert a cutoff tax line to elements of a move line + + See _prepare_provision_line for more info. + """ + return { + 'account_id': cutoff_tax_line.cutoff_account_id.id, + 'analytic_account_id': cutoff_tax_line.analytic_account_id.id, + 'amount': cutoff_tax_line.cutoff_amount, + } + + @api.multi + def _merge_provision_lines(self, provision_lines): + """ merge provision line + + Returns a dictionary {key, amount} where key is + a tuple containing the values of the properties in _get_merge_keys() + """ + to_provision = {} + merge_keys = self._get_merge_keys() + for provision_line in provision_lines: + key = tuple([provision_line.get(key) for key in merge_keys]) + if key in to_provision: + to_provision[key] += provision_line['amount'] + else: + to_provision[key] = provision_line['amount'] + return to_provision + + @api.multi + def create_move(self): + self.ensure_one() + move_obj = self.env['account.move'] + if self.move_id: + raise UserError(_( + "The Cut-off Journal Entry already exists. You should " + "delete it before running this function.")) + if not self.line_ids: + raise UserError(_( + "There are no lines on this Cut-off, so we can't create " + "a Journal Entry.")) + provision_lines = [] + for line in self.line_ids: + provision_lines.append( + self._prepare_provision_line(line)) + for tax_line in line.tax_line_ids: + provision_lines.append( + self._prepare_provision_tax_line(tax_line)) + to_provision = self._merge_provision_lines(provision_lines) + vals = self._prepare_move(to_provision) + move = move_obj.create(vals) + self.write({'move_id': move.id, 'state': 'done'}) + + action = self.env['ir.actions.act_window'].for_xml_id( + 'account', 'action_move_journal_line') + action.update({ + 'view_mode': 'form,tree', + 'res_id': move.id, + 'view_id': False, + 'views': False, + }) + return action + + +class AccountCutoffLine(models.Model): + _name = 'account.cutoff.line' + _description = 'Account Cut-off Line' + + parent_id = fields.Many2one( + 'account.cutoff', string='Cut-off', ondelete='cascade') + name = fields.Char('Description') + company_currency_id = fields.Many2one( + related='parent_id.company_currency_id', + string="Company Currency", readonly=True) + partner_id = fields.Many2one( + 'res.partner', string='Partner', readonly=True) + account_id = fields.Many2one( + 'account.account', 'Account', + domain=[('deprecated', '=', False)], required=True, readonly=True) + cutoff_account_id = fields.Many2one( + 'account.account', string='Cut-off Account', + domain=[('deprecated', '=', False)], required=True, readonly=True) + cutoff_account_code = fields.Char( + related='cutoff_account_id.code', + string='Cut-off Account Code', readonly=True) + analytic_account_id = fields.Many2one( + 'account.analytic.account', string='Analytic Account', + domain=[('account_type', '!=', 'closed')], readonly=True) + analytic_account_code = fields.Char( + related='analytic_account_id.code', + string='Analytic Account Code', readonly=True) + currency_id = fields.Many2one( + 'res.currency', string='Amount Currency', readonly=True, + help="Currency of the 'Amount' field.") + amount = fields.Monetary( + string='Amount', currency_field='currency_id', readonly=True, + help="Amount that is used as base to compute the Cut-off Amount. " + "This Amount is in the 'Amount Currency', which may be different " + "from the 'Company Currency'.") + cutoff_amount = fields.Monetary( + string='Cut-off Amount', currency_field='company_currency_id', + readonly=True, + help="Cut-off Amount without taxes in the Company Currency.") + tax_ids = fields.Many2many( + 'account.tax', id1='cutoff_line_id', id2='tax_id', string='Taxes', + readonly=True) + tax_line_ids = fields.One2many( + 'account.cutoff.tax.line', 'parent_id', string='Cut-off Tax Lines', + readonly=True) + + +class AccountCutoffTaxLine(models.Model): + _name = 'account.cutoff.tax.line' + _description = 'Account Cut-off Tax Line' + + parent_id = fields.Many2one( + 'account.cutoff.line', string='Account Cut-off Line', + ondelete='cascade', required=True) + tax_id = fields.Many2one('account.tax', string='Tax', required=True) + cutoff_account_id = fields.Many2one( + 'account.account', string='Cut-off Account', + domain=[('deprecated', '=', False)], required=True, readonly=True) + analytic_account_id = fields.Many2one( + 'account.analytic.account', string='Analytic Account', + domain=[('account_type', '!=', 'closed')], readonly=True) + base = fields.Monetary( + string='Base', currency_field='currency_id', + readonly=True, help="Base Amount in the currency of the PO.") + amount = fields.Monetary( + string='Tax Amount', currency_field='currency_id', + readonly=True, help='Tax Amount in the currency of the PO.') + sequence = fields.Integer(string='Sequence', readonly=True) + cutoff_amount = fields.Monetary( + string='Cut-off Tax Amount', currency_field='company_currency_id', + readonly=True, help="Tax Cut-off Amount in the company currency.") + currency_id = fields.Many2one( + related='parent_id.currency_id', string='Currency', readonly=True) + company_currency_id = fields.Many2one( + related='parent_id.company_currency_id', + string="Company Currency", readonly=True) + + +class AccountCutoffMapping(models.Model): + _name = 'account.cutoff.mapping' + _description = 'Account Cut-off Mapping' + _rec_name = 'account_id' + + company_id = fields.Many2one( + 'res.company', string='Company', required=True, + default=lambda self: self.env['res.company']._company_default_get( + 'account.cutoff.mapping')) + account_id = fields.Many2one( + 'account.account', string='Regular Account', + domain=[('deprecated', '=', False)], required=True) + cutoff_account_id = fields.Many2one( + 'account.account', string='Cut-off Account', + domain=[('deprecated', '=', False)], required=True) + cutoff_type = fields.Selection([ + ('all', 'All Cut-off Types'), + ('accrued_revenue', 'Accrued Revenue'), + ('accrued_expense', 'Accrued Expense'), + ('prepaid_revenue', 'Prepaid Revenue'), + ('prepaid_expense', 'Prepaid Expense'), + ], string='Cut-off Type', required=True) + + @api.model + def _get_mapping_dict(self, company_id, cutoff_type='all'): + '''return a dict with: + key = ID of account, + value = ID of cutoff_account''' + if cutoff_type == 'all': + cutoff_type_filter = ('all') + else: + cutoff_type_filter = ('all', cutoff_type) + mappings = self.search([ + ('company_id', '=', company_id), + ('cutoff_type', 'in', cutoff_type_filter), + ]) + mapping = {} + for item in mappings: + mapping[item.account_id.id] = item.cutoff_account_id.id + return mapping diff --git a/account_cutoff_base/models/company.py b/account_cutoff_base/models/company.py new file mode 100644 index 00000000000..8465037bb67 --- /dev/null +++ b/account_cutoff_base/models/company.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +# © 2013-2016 Akretion (Alexis de Lattre ) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) + +from openerp import models, fields + + +class ResCompany(models.Model): + _inherit = 'res.company' + + default_cutoff_journal_id = fields.Many2one( + 'account.journal', string='Default Cut-off Journal') diff --git a/account_cutoff_base/security/account_cutoff_base_security.xml b/account_cutoff_base/security/account_cutoff_base_security.xml index bc2035a9a70..d0bdd8abefc 100644 --- a/account_cutoff_base/security/account_cutoff_base_security.xml +++ b/account_cutoff_base/security/account_cutoff_base_security.xml @@ -1,11 +1,20 @@ - - - - Account Cutoff Multi Company - - - ['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])] - - - + + + + + + Account Cutoff Multi-Company + + ['|', ('company_id', '=', False), ('company_id', 'child_of', [user.company_id.id])] + + + + Account Cutoff Mapping Multi-Company + + ['|', ('company_id', '=', False), ('company_id', 'child_of', [user.company_id.id])] + + + + + diff --git a/account_cutoff_base/account_cutoff_view.xml b/account_cutoff_base/views/account_cutoff.xml similarity index 77% rename from account_cutoff_base/account_cutoff_view.xml rename to account_cutoff_base/views/account_cutoff.xml index 26720b8b1f0..f1f6d15b666 100644 --- a/account_cutoff_base/account_cutoff_view.xml +++ b/account_cutoff_base/views/account_cutoff.xml @@ -1,30 +1,28 @@ - - + + sequence="5" + groups="account.group_account_user,account.group_account_manager"/> account.cutoff.form account.cutoff -
+
@@ -35,15 +33,15 @@ - - + + - - - + + + @@ -68,7 +66,7 @@ - + @@ -82,6 +80,9 @@ + + + @@ -91,7 +92,7 @@ account.cutoff.line.form account.cutoff.line - + @@ -100,11 +101,11 @@ - + - + @@ -127,9 +128,9 @@ - + - + @@ -139,20 +140,20 @@ account.cutoff.tax.line.form account.cutoff.tax.line - + - - - + + + - - + + @@ -170,28 +171,20 @@ - + - - + +
- - - Cut-off Journal Entry Created - account.cutoff - - Cut-off Journal Entry Created - - account.cutoff.mapping.form account.cutoff.mapping -
+ @@ -218,7 +211,6 @@ Cut-off Account Mapping account.cutoff.mapping - form tree,form {'account_cutoff_mapping_main_view': True} @@ -237,4 +229,4 @@ sequence="100"/> - + diff --git a/account_cutoff_base/company_view.xml b/account_cutoff_base/views/company.xml similarity index 73% rename from account_cutoff_base/company_view.xml rename to account_cutoff_base/views/company.xml index bfe1f629f55..fd5067cbf92 100644 --- a/account_cutoff_base/company_view.xml +++ b/account_cutoff_base/views/company.xml @@ -1,12 +1,10 @@ - - + @@ -22,6 +20,5 @@ - - + diff --git a/account_cutoff_prepaid/README.rst b/account_cutoff_prepaid/README.rst index 3a636f88b1c..9ede1c7506c 100644 --- a/account_cutoff_prepaid/README.rst +++ b/account_cutoff_prepaid/README.rst @@ -1,10 +1,14 @@ .. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg - :alt: License: AGPL-3 + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 -Manage prepaid expense and revenue based on start and end dates -=============================================================== +====================== +Account Cutoff Prepaid +====================== -This module adds a **Start Date** and **End Date** field on invoice lines. For +This module allows you to easily compute the prepaid revenue and prepaid expenses and to generate the related cutoff journal items. It uses the **Start Date** and **End Date** fields of journal entries (copied from the same fields of invoice lines). + +For example, if you have an insurance contrat for your company that run from April 1st 2013 to March 31st 2014, you will enter these dates as start and end dates on the supplier invoice line. If your fiscal year ends on December 31st 2013, @@ -14,8 +18,49 @@ Expense* on December 31st 2013 and OpenERP will identify this expense with the 3 months that are after the cut-off date and propose to generate the appropriate cut-off journal entry. -Please contact Alexis de Lattre from Akretion -for any help or question about this module. +Configuration +============= + +On the form view of the company, in the *Configuration* tab, there is a *Cut-offs* section where you can configure: + +* the *Default Cut-off Journal*, which is the journal in which the cut-off journal items will be generated by default, +* the *Default Account for Prepaid Revenue*, +* the *Default Account for Prepaid Expense*. + +Usage +===== + +To compute the prepaid revenue, go to the menu *Accounting > Cut-offs +> Prepaid Revenue* and click on the *Create* button. Enter the cut-off +date, check that the source journals contains all your sale journals +and click on the button *Re-Generate lines*: Odoo will scan all the +journal entries of the source journals and will get all the lines that +have an end date after the cut-off date and, for each line, it will +compute the prepaid revenue. If you agree with the result, click on the +button *Create Journal Entry*: Odoo will generate an account move at the +cut-off date to cut these prepaid revenue. Hint: you can then use the reversal +feature to generate the reverse journal items on the next day. + +If you need to answer a question such as *How much revenue did I already +invoice for my next fiscal year ?*, you will be interested by the +*forecast* feature. For that, on the Prepaid Revenue form, click on +the *Forecast* option and you will see 2 new fields: *Start Date* and +*End Date*. Enter the start date and the end date of your next fiscal +year and click on the button *Re-Generate lines*: you will see all the +revenue that you already have in your source journals for that period. + +.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas + :alt: Try me on Runbot + :target: https://runbot.odoo-community.org/runbot/89/9.0 + + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues +`_. In case of trouble, please +check there if your issue has already been reported. If you spotted it first, +help us smashing it by providing a detailed and welcomed feedback. Credits ======= @@ -23,15 +68,15 @@ Credits Contributors ------------ -* Alexis de Lattre +* Alexis de Lattre * Stéphane Bidoul 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. @@ -39,4 +84,4 @@ 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/account_cutoff_prepaid/__init__.py b/account_cutoff_prepaid/__init__.py index beec5c2af8a..cde864bae21 100644 --- a/account_cutoff_prepaid/__init__.py +++ b/account_cutoff_prepaid/__init__.py @@ -1,26 +1,3 @@ -# -*- encoding: utf-8 -*- -############################################################################## -# -# Account Cut-off Prepaid module for OpenERP -# Copyright (C) 2013 Akretion (http://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 . -# -############################################################################## +# -*- coding: utf-8 -*- -from . import company -from . import product -from . import account -from . import account_cutoff +from . import models diff --git a/account_cutoff_prepaid/__openerp__.py b/account_cutoff_prepaid/__openerp__.py index 0c06ee3ff91..93f748cbead 100644 --- a/account_cutoff_prepaid/__openerp__.py +++ b/account_cutoff_prepaid/__openerp__.py @@ -1,49 +1,27 @@ -# -*- encoding: utf-8 -*- -############################################################################## -# -# Account Cut-off Prepaid module for OpenERP -# Copyright (C) 2013 Akretion (http://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 . -# -############################################################################## - +# -*- coding: utf-8 -*- +# © 2013-2016 Akretion (Alexis de Lattre ) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). { 'name': 'Account Cut-off Prepaid', - 'version': '8.0.0.1.0', + 'version': '9.0.1.0.0', 'category': 'Accounting & Finance', 'license': 'AGPL-3', 'summary': 'Prepaid Expense, Prepaid Revenue', - 'author': "Akretion,Odoo Community Association (OCA)", + 'author': 'Akretion,Odoo Community Association (OCA)', 'website': 'http://www.akretion.com', - 'depends': ['account_cutoff_base'], + 'depends': [ + 'account_cutoff_base', + 'account_invoice_start_end_dates', + ], 'data': [ - 'company_view.xml', - 'product_view.xml', - 'account_invoice_view.xml', - 'account_view.xml', - 'account_cutoff_view.xml', + 'views/company.xml', + 'views/account_cutoff.xml', ], - 'demo': ['product_demo.xml'], 'images': [ 'images/prepaid_revenue_draft.jpg', 'images/prepaid_revenue_journal_entry.jpg', 'images/prepaid_revenue_done.jpg', - ], - 'installable': False, - 'active': False, - 'application': True, + ], + 'installable': True, } diff --git a/account_cutoff_prepaid/account.py b/account_cutoff_prepaid/account.py deleted file mode 100644 index 31f73d6e544..00000000000 --- a/account_cutoff_prepaid/account.py +++ /dev/null @@ -1,150 +0,0 @@ -# -*- encoding: utf-8 -*- -############################################################################## -# -# Account Cut-off Prepaid module for OpenERP -# Copyright (C) 2013 Akretion (http://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 . -# -############################################################################## - - -from openerp.osv import orm, fields -from openerp.tools.translate import _ - - -class account_invoice_line(orm.Model): - _inherit = 'account.invoice.line' - - _columns = { - 'start_date': fields.date('Start Date'), - 'end_date': fields.date('End Date'), - } - - def _check_start_end_dates(self, cr, uid, ids): - for invline in self.browse(cr, uid, ids): - if invline.start_date and not invline.end_date: - raise orm.except_orm( - _('Error:'), - _("Missing End Date for invoice line with " - "Description '%s'.") - % (invline.name)) - if invline.end_date and not invline.start_date: - raise orm.except_orm( - _('Error:'), - _("Missing Start Date for invoice line with " - "Description '%s'.") - % (invline.name)) - if invline.end_date and invline.start_date and \ - invline.start_date > invline.end_date: - raise orm.except_orm( - _('Error:'), - _("Start Date should be before or be the same as " - "End Date for invoice line with Description '%s'.") - % (invline.name)) - # Note : we can't check invline.product_id.must_have_dates - # have start_date and end_date here, because it would - # block automatic invoice generation. So we do the check - # upon validation of the invoice (see below the function - # action_move_create) - return True - - _constraints = [ - (_check_start_end_dates, "Error msg in raise", - ['start_date', 'end_date', 'product_id']), - ] - - def move_line_get_item(self, cr, uid, line, context=None): - res = super(account_invoice_line, self).move_line_get_item( - cr, uid, line, context=context) - res['start_date'] = line.start_date - res['end_date'] = line.end_date - return res - - -class account_move_line(orm.Model): - _inherit = "account.move.line" - - _columns = { - 'start_date': fields.date('Start Date'), - 'end_date': fields.date('End Date'), - } - - def _check_start_end_dates(self, cr, uid, ids): - for moveline in self.browse(cr, uid, ids): - if moveline.start_date and not moveline.end_date: - raise orm.except_orm( - _('Error:'), - _("Missing End Date for move line with Name '%s'.") - % (moveline.name)) - if moveline.end_date and not moveline.start_date: - raise orm.except_orm( - _('Error:'), - _("Missing Start Date for move line with Name '%s'.") - % (moveline.name)) - if moveline.end_date and moveline.start_date and \ - moveline.start_date > moveline.end_date: - raise orm.except_orm( - _('Error:'), - _("Start Date should be before End Date for move line " - "with Name '%s'.") - % (moveline.name)) - # should we check that it's related to an expense / revenue ? - # -> I don't think so - return True - - _constraints = [( - _check_start_end_dates, - "Error msg in raise", - ['start_date', 'end_date'] - )] - - -class account_invoice(orm.Model): - _inherit = 'account.invoice' - - def inv_line_characteristic_hashcode(self, invoice_line): - '''Add start and end dates to hashcode used when the option "Group - Invoice Lines" is active on the Account Journal''' - code = super(account_invoice, self).inv_line_characteristic_hashcode( - invoice_line) - hashcode = '%s-%s-%s' % ( - code, invoice_line.get('start_date', 'False'), - invoice_line.get('end_date', 'False'), - ) - return hashcode - - def line_get_convert(self, cr, uid, x, part, date, context=None): - res = super(account_invoice, self).line_get_convert( - cr, uid, x, part, date, context=context) - res['start_date'] = x.get('start_date', False) - res['end_date'] = x.get('end_date', False) - return res - - def action_move_create(self, cr, uid, ids, context=None): - '''Check that products with must_have_dates=True have - Start and End Dates''' - for invoice in self.browse(cr, uid, ids, context=context): - for invline in invoice.invoice_line: - if invline.product_id and invline.product_id.must_have_dates: - if not invline.start_date or not invline.end_date: - raise orm.except_orm( - _('Error:'), - _("Missing Start Date and End Date for invoice " - "line with Product '%s' which has the " - "property 'Must Have Start and End Dates'.") - % (invline.product_id.name)) - return super(account_invoice, self).action_move_create( - cr, uid, ids, context=context) diff --git a/account_cutoff_prepaid/account_cutoff.py b/account_cutoff_prepaid/account_cutoff.py deleted file mode 100644 index b2ae93bf105..00000000000 --- a/account_cutoff_prepaid/account_cutoff.py +++ /dev/null @@ -1,192 +0,0 @@ -# -*- encoding: utf-8 -*- -############################################################################## -# -# Account Cut-off Prepaid module for OpenERP -# Copyright (C) 2013 Akretion (http://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 . -# -############################################################################## - - -from openerp.osv import orm, fields -from openerp.tools.translate import _ -from datetime import datetime - - -class account_cutoff(orm.Model): - _inherit = 'account.cutoff' - - _columns = { - 'source_journal_ids': fields.many2many( - 'account.journal', id1='cutoff_id', id2='journal_id', - string='Source Journals', readonly=True, - states={'draft': [('readonly', False)]}), - } - - def _get_default_source_journals(self, cr, uid, context=None): - if context is None: - context = {} - journal_obj = self.pool['account.journal'] - res = [] - type = context.get('type') - mapping = { - 'prepaid_expense': ('purchase', 'purchase_refund'), - 'prepaid_revenue': ('sale', 'sale_refund'), - } - if type in mapping: - src_journal_ids = journal_obj.search( - cr, uid, [('type', 'in', mapping[type])]) - if src_journal_ids: - res = src_journal_ids - return res - - _defaults = { - 'source_journal_ids': _get_default_source_journals, - } - - _sql_constraints = [( - 'date_type_company_uniq', - 'unique(cutoff_date, company_id, type)', - 'A cut-off of the same type already exists with this cut-off date !' - )] - - def _prepare_prepaid_lines( - self, cr, uid, ids, aml, cur_cutoff, mapping, context=None): - start_date = datetime.strptime(aml['start_date'], '%Y-%m-%d') - end_date = datetime.strptime(aml['end_date'], '%Y-%m-%d') - cutoff_date_str = cur_cutoff['cutoff_date'] - cutoff_date = datetime.strptime(cutoff_date_str, '%Y-%m-%d') - # Here, we compute the amount of the cutoff - # That's the important part ! - total_days = (end_date - start_date).days + 1 - if aml['start_date'] > cutoff_date_str: - after_cutoff_days = total_days - cutoff_amount = -1 * (aml['credit'] - aml['debit']) - else: - after_cutoff_days = (end_date - cutoff_date).days - if total_days: - cutoff_amount = -1 * (aml['credit'] - aml['debit'])\ - * after_cutoff_days / total_days - else: - raise orm.except_orm( - _('Error:'), - "Should never happen. Total days should always be > 0") - - # we use account mapping here - if aml['account_id'][0] in mapping: - cutoff_account_id = mapping[aml['account_id'][0]] - else: - cutoff_account_id = aml['account_id'][0] - - res = { - 'parent_id': ids[0], - 'move_line_id': aml['id'], - 'partner_id': aml['partner_id'] and aml['partner_id'][0] or False, - 'name': aml['name'], - 'start_date': aml['start_date'], - 'end_date': aml['end_date'], - 'account_id': aml['account_id'][0], - 'cutoff_account_id': cutoff_account_id, - 'analytic_account_id': (aml['analytic_account_id'][0] - if aml['analytic_account_id'] else False), - 'total_days': total_days, - 'after_cutoff_days': after_cutoff_days, - 'amount': aml['credit'] - aml['debit'], - 'currency_id': cur_cutoff['company_currency_id'][0], - 'cutoff_amount': cutoff_amount, - } - return res - - def get_prepaid_lines(self, cr, uid, ids, context=None): - assert len(ids) == 1,\ - 'This function should only be used for a single id at a time' - aml_obj = self.pool['account.move.line'] - line_obj = self.pool['account.cutoff.line'] - mapping_obj = self.pool['account.cutoff.mapping'] - cur_cutoff = self.read( - cr, uid, ids[0], [ - 'line_ids', 'source_journal_ids', 'cutoff_date', 'company_id', - 'type', 'company_currency_id' - ], - context=context) - src_journal_ids = cur_cutoff['source_journal_ids'] - if not src_journal_ids: - raise orm.except_orm( - _('Error:'), _("You should set at least one Source Journal.")) - cutoff_date_str = cur_cutoff['cutoff_date'] - # Delete existing lines - if cur_cutoff['line_ids']: - line_obj.unlink(cr, uid, cur_cutoff['line_ids'], context=context) - - # Search for account move lines in the source journals - aml_ids = aml_obj.search(cr, uid, [ - ('start_date', '!=', False), - ('journal_id', 'in', src_journal_ids), - ('end_date', '>', cutoff_date_str), - ('date', '<=', cutoff_date_str) - ], context=context) - # Create mapping dict - mapping = mapping_obj._get_mapping_dict( - cr, uid, cur_cutoff['company_id'][0], cur_cutoff['type'], - context=context) - - # Loop on selected account move lines to create the cutoff lines - for aml in aml_obj.read( - cr, uid, aml_ids, [ - 'credit', 'debit', 'start_date', 'end_date', 'account_id', - 'analytic_account_id', 'partner_id', 'name' - ], - context=context): - - line_obj.create( - cr, uid, self._prepare_prepaid_lines( - cr, uid, ids, aml, cur_cutoff, mapping, context=context), - context=context) - return True - - def _inherit_default_cutoff_account_id(self, cr, uid, context=None): - if context is None: - context = {} - account_id = super(account_cutoff, self).\ - _inherit_default_cutoff_account_id(cr, uid, context=context) - type = context.get('type') - company = self.pool['res.users'].browse( - cr, uid, uid, context=context).company_id - if type == 'prepaid_revenue': - account_id = company.default_prepaid_revenue_account_id.id or False - elif type == 'prepaid_expense': - account_id = company.default_prepaid_expense_account_id.id or False - return account_id - - -class account_cutoff_line(orm.Model): - _inherit = 'account.cutoff.line' - - _columns = { - 'move_line_id': fields.many2one( - 'account.move.line', 'Accout Move Line', readonly=True), - 'move_date': fields.related( - 'move_line_id', 'date', type='date', - string='Account Move Date', readonly=True), - 'invoice_id': fields.related( - 'move_line_id', 'invoice', type='many2one', - relation='account.invoice', string='Invoice', readonly=True), - 'start_date': fields.date('Start Date', readonly=True), - 'end_date': fields.date('End Date', readonly=True), - 'total_days': fields.integer('Total Number of Days', readonly=True), - 'after_cutoff_days': fields.integer( - 'Number of Days after Cut-off Date', readonly=True), - } diff --git a/account_cutoff_prepaid/account_invoice_view.xml b/account_cutoff_prepaid/account_invoice_view.xml deleted file mode 100644 index 39a6114581e..00000000000 --- a/account_cutoff_prepaid/account_invoice_view.xml +++ /dev/null @@ -1,60 +0,0 @@ - - - - - - - - - - prepaid.cutoff.invoice_form - account.invoice - - - - - - - - - - - - - - prepaid.cutoff.invoice_supplier_form - account.invoice - - - - - - - - - - - - prepaid.cutoff.invoice_line_form - account.invoice.line - - - - - - - - - - - - - - diff --git a/account_cutoff_prepaid/company.py b/account_cutoff_prepaid/company.py deleted file mode 100644 index 168be62a6aa..00000000000 --- a/account_cutoff_prepaid/company.py +++ /dev/null @@ -1,37 +0,0 @@ -# -*- encoding: utf-8 -*- -############################################################################## -# -# Account Cut-off Prepaid module for OpenERP -# Copyright (C) 2013 Akretion (http://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 . -# -############################################################################## - - -from openerp.osv import orm, fields - - -class res_company(orm.Model): - _inherit = 'res.company' - - _columns = { - 'default_prepaid_revenue_account_id': fields.many2one( - 'account.account', 'Default Account for Prepaid Revenue', - domain=[('type', '<>', 'view'), ('type', '<>', 'closed')]), - 'default_prepaid_expense_account_id': fields.many2one( - 'account.account', 'Default Account for Prepaid Expense', - domain=[('type', '<>', 'view'), ('type', '<>', 'closed')]), - } diff --git a/account_cutoff_prepaid/i18n/account_cutoff_prepaid.pot b/account_cutoff_prepaid/i18n/account_cutoff_prepaid.pot index 65b4be015e0..dd5b4b0e474 100644 --- a/account_cutoff_prepaid/i18n/account_cutoff_prepaid.pot +++ b/account_cutoff_prepaid/i18n/account_cutoff_prepaid.pot @@ -6,8 +6,8 @@ msgid "" msgstr "" "Project-Id-Version: OpenERP Server 7.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2013-12-24 15:26+0000\n" -"PO-Revision-Date: 2013-12-24 15:26+0000\n" +"POT-Creation-Date: 2014-03-09 00:05+0000\n" +"PO-Revision-Date: 2014-03-09 00:05+0000\n" "Last-Translator: <>\n" "Language-Team: \n" "MIME-Version: 1.0\n" @@ -21,6 +21,7 @@ msgid "Accout Move Line" msgstr "" #. module: account_cutoff_prepaid +#: field:account.cutoff,end_date:0 #: field:account.cutoff.line,end_date:0 #: field:account.invoice.line,end_date:0 #: field:account.move.line,end_date:0 @@ -37,6 +38,16 @@ msgstr "" msgid "Must Have Start and End Dates" msgstr "" +#. module: account_cutoff_prepaid +#: field:account.cutoff,forecast:0 +msgid "Forecast" +msgstr "" + +#. module: account_cutoff_prepaid +#: view:account.cutoff:0 +msgid "{'invisible': [('forecast', '=', True)], 'required': [('forecast', '=', False)]}" +msgstr "" + #. module: account_cutoff_prepaid #: field:res.company,default_prepaid_expense_account_id:0 msgid "Default Account for Prepaid Expense" @@ -64,6 +75,11 @@ msgstr "" msgid "Missing Start Date for invoice line with Description '%s'." msgstr "" +#. module: account_cutoff_prepaid +#: view:account.cutoff:0 +msgid "{'invisible': [('forecast', '=', True)]}" +msgstr "" + #. module: account_cutoff_prepaid #: field:account.cutoff.line,total_days:0 msgid "Total Number of Days" @@ -103,6 +119,11 @@ msgstr "" msgid "Re-Generate Lines" msgstr "" +#. module: account_cutoff_prepaid +#: help:account.cutoff,forecast:0 +msgid "The Forecast mode allows the user to compute the prepaid revenue/expense between 2 dates in the future." +msgstr "" + #. module: account_cutoff_prepaid #: model:ir.model,name:account_cutoff_prepaid.model_account_cutoff msgid "Account Cut-off" @@ -118,16 +139,6 @@ msgstr "" msgid "Account Cut-off Line" msgstr "" -#. module: account_cutoff_prepaid -#: sql_constraint:account.cutoff:0 -msgid "A cut-off of the same type already exists with this cut-off date !" -msgstr "" - -#. module: account_cutoff_prepaid -#: field:account.cutoff.line,after_cutoff_days:0 -msgid "Number of Days after Cut-off Date" -msgstr "" - #. module: account_cutoff_prepaid #: model:product.template,name:account_cutoff_prepaid.product_insurance_contrat_product_template msgid "Car Insurance" @@ -141,8 +152,8 @@ msgstr "" #: code:addons/account_cutoff_prepaid/account.py:94 #: code:addons/account_cutoff_prepaid/account.py:100 #: code:addons/account_cutoff_prepaid/account.py:144 -#: code:addons/account_cutoff_prepaid/account_cutoff.py:85 -#: code:addons/account_cutoff_prepaid/account_cutoff.py:129 +#: code:addons/account_cutoff_prepaid/account_cutoff.py:132 +#: code:addons/account_cutoff_prepaid/account_cutoff.py:177 #, python-format msgid "Error:" msgstr "" @@ -170,7 +181,7 @@ msgid "Missing Start Date and End Date for invoice line with Product '%s' which msgstr "" #. module: account_cutoff_prepaid -#: code:addons/account_cutoff_prepaid/account_cutoff.py:129 +#: code:addons/account_cutoff_prepaid/account_cutoff.py:177 #, python-format msgid "You should set at least one Source Journal." msgstr "" @@ -181,17 +192,32 @@ msgstr "" msgid "Missing End Date for invoice line with Description '%s'." msgstr "" +#. module: account_cutoff_prepaid +#: constraint:account.cutoff:0 +msgid "The start date is after the end date!" +msgstr "" + #. module: account_cutoff_prepaid #: code:addons/account_cutoff_prepaid/account.py:90 #, python-format msgid "Missing End Date for move line with Name '%s'." msgstr "" +#. module: account_cutoff_prepaid +#: help:account.cutoff.line,prepaid_days:0 +msgid "In regular mode, this is the number of days after the cut-off date. In forecast mode, this is the number of days between the start date and the end date." +msgstr "" + #. module: account_cutoff_prepaid #: model:ir.model,name:account_cutoff_prepaid.model_product_template msgid "Product Template" msgstr "" +#. module: account_cutoff_prepaid +#: view:account.cutoff:0 +msgid "{'required': [('forecast', '=', False)]}" +msgstr "" + #. module: account_cutoff_prepaid #: model:ir.model,name:account_cutoff_prepaid.model_account_invoice_line msgid "Invoice Line" @@ -224,6 +250,12 @@ msgid "Invoice" msgstr "" #. module: account_cutoff_prepaid +#: sql_constraint:account.cutoff:0 +msgid "A cut-off of the same type already exists with the same date(s) !" +msgstr "" + +#. module: account_cutoff_prepaid +#: field:account.cutoff,start_date:0 #: field:account.cutoff.line,start_date:0 #: field:account.invoice.line,start_date:0 #: field:account.move.line,start_date:0 @@ -235,6 +267,11 @@ msgstr "" msgid "Days after Cut-off" msgstr "" +#. module: account_cutoff_prepaid +#: field:account.cutoff.line,prepaid_days:0 +msgid "Prepaid Days" +msgstr "" + #. module: account_cutoff_prepaid #: view:product.template:0 msgid "Sales Properties" diff --git a/account_cutoff_prepaid/i18n/fr.po b/account_cutoff_prepaid/i18n/fr.po index e55a4bd5a69..0749398997e 100644 --- a/account_cutoff_prepaid/i18n/fr.po +++ b/account_cutoff_prepaid/i18n/fr.po @@ -1,55 +1,58 @@ -# Translation of OpenERP Server. +# Translation of Odoo Server. # This file contains the translation of the following modules: -# * account_cutoff_prepaid -# +# * account_cutoff_prepaid +# +# Translators: msgid "" msgstr "" -"Project-Id-Version: OpenERP Server 7.0\n" +"Project-Id-Version: account-closing (8.0)\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2013-12-24 15:26+0000\n" -"PO-Revision-Date: 2014-11-29 17:10+0100\n" -"Last-Translator: Stéphane Bidoul \n" -"Language-Team: \n" +"POT-Creation-Date: 2015-12-18 13:31+0000\n" +"PO-Revision-Date: 2015-12-18 09:04+0000\n" +"Last-Translator: OCA Transbot \n" +"Language-Team: French (http://www.transifex.com/oca/OCA-account-closing-8-0/language/fr/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: \n" -"X-Generator: Poedit 1.5.4\n" +"Content-Transfer-Encoding: \n" +"Language: fr\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" #. module: account_cutoff_prepaid -#: field:account.cutoff.line,move_line_id:0 -msgid "Accout Move Line" -msgstr "Ecriture" - -#. module: account_cutoff_prepaid -#: field:account.cutoff.line,end_date:0 field:account.invoice.line,end_date:0 -#: field:account.move.line,end_date:0 -msgid "End Date" -msgstr "Fin" +#: model:ir.actions.act_window,help:account_cutoff_prepaid.account_cutoff_prepaid_expense_action +msgid "" +"

\n" +" Click to start preparing a new prepaid expense.\n" +"

\n" +" This view can be used by accountants in order to collect information about prepaid expenses based on start date and end date. It then allows to generate the corresponding cutoff journal entry in one click.\n" +"

\n" +" " +msgstr "" #. module: account_cutoff_prepaid -#: help:product.template,must_have_dates:0 +#: model:ir.actions.act_window,help:account_cutoff_prepaid.account_cutoff_prepaid_revenue_action msgid "" -"If this option is active, the user will have to enter a Start Date and an " -"End Date on the invoice lines that have this product." +"

\n" +" Click to start preparing a new prepaid revenue.\n" +"

\n" +" This view can be used by accountants in order to collect information about prepaid revenues based on start date and end date. It then allows to generate the corresponding cutoff journal entry in one click.\n" +"

\n" +" " msgstr "" -"Si cette option est active, l'utilisateur devra entrer une date de début et " -"de fin sur les lignes de factures qui utilisent ce produit." #. module: account_cutoff_prepaid -#: field:product.template,must_have_dates:0 -msgid "Must Have Start and End Dates" -msgstr "Dates de début et fin obligatoires" +#: sql_constraint:account.cutoff:0 +msgid "A cut-off of the same type already exists with this cut-off date !" +msgstr "Un report de ce type existe déjà pour cette date de cut-off !" #. module: account_cutoff_prepaid -#: field:res.company,default_prepaid_expense_account_id:0 -msgid "Default Account for Prepaid Expense" -msgstr "Compte par défaut pour les charges à reporter" +#: model:ir.model,name:account_cutoff_prepaid.model_account_cutoff +msgid "Account Cut-off" +msgstr "Etalements" #. module: account_cutoff_prepaid -#: field:account.cutoff,source_journal_ids:0 -msgid "Source Journals" -msgstr "Journaux source" +#: model:ir.model,name:account_cutoff_prepaid.model_account_cutoff_line +msgid "Account Cut-off Line" +msgstr "Ligne de report" #. module: account_cutoff_prepaid #: field:account.cutoff.line,move_date:0 @@ -57,62 +60,34 @@ msgid "Account Move Date" msgstr "Date de la pièce" #. module: account_cutoff_prepaid -#: code:addons/account_cutoff_prepaid/account.py:95 -#, python-format -msgid "Missing Start Date for move line with Name '%s'." -msgstr "Date de début manquante pour l'écriture avec le label '%s'." - -#. module: account_cutoff_prepaid -#: code:addons/account_cutoff_prepaid/account.py:47 -#, python-format -msgid "Missing Start Date for invoice line with Description '%s'." +#: field:account.cutoff.line,move_line_id:0 +msgid "Account Move Line" msgstr "" -"Date de début manquante pour la ligne de facture avec description '%s'." #. module: account_cutoff_prepaid -#: field:account.cutoff.line,total_days:0 -msgid "Total Number of Days" -msgstr "Nombre de jours total" - -#. module: account_cutoff_prepaid -#: constraint:account.invoice.line:0 constraint:account.move.line:0 -msgid "Error msg in raise" -msgstr "Message d'erreur dans raise" - -#. module: account_cutoff_prepaid -#: model:ir.actions.act_window,name:account_cutoff_prepaid.account_cutoff_prepaid_expense_action -#: model:ir.ui.menu,name:account_cutoff_prepaid.account_cutoff_prepaid_expense_menu -msgid "Prepaid Expense" -msgstr "Report de Charges" - -#. module: account_cutoff_prepaid -#: model:ir.actions.act_window,name:account_cutoff_prepaid.account_cutoff_prepaid_revenue_action -#: model:ir.ui.menu,name:account_cutoff_prepaid.account_cutoff_prepaid_revenue_menu -msgid "Prepaid Revenue" -msgstr "Report de Produits" +#: model:product.template,name:account_cutoff_prepaid.product_insurance_contrat_product_template +msgid "Car Insurance" +msgstr "Assurance voiture" #. module: account_cutoff_prepaid -#: code:addons/account_cutoff_prepaid/account.py:101 -#, python-format -msgid "Start Date should be before End Date for move line with Name '%s'." -msgstr "" -"La date de début doit précéder la date de fin pour l'écriture avec label " -"'%s'." +#: model:ir.model,name:account_cutoff_prepaid.model_res_company +msgid "Companies" +msgstr "Sociétés" #. module: account_cutoff_prepaid -#: model:ir.model,name:account_cutoff_prepaid.model_account_move_line -msgid "Journal Items" -msgstr "Ecriture" +#: view:account.cutoff.line:account_cutoff_prepaid.account_cutoff_line_tree +msgid "Days Total" +msgstr "Nombre total de jours" #. module: account_cutoff_prepaid -#: view:account.cutoff:0 -msgid "Re-Generate Lines" -msgstr "Regénérer les lignes" +#: view:account.cutoff.line:account_cutoff_prepaid.account_cutoff_line_tree +msgid "Days after Cut-off" +msgstr "Jours après cut-off" #. module: account_cutoff_prepaid -#: model:ir.model,name:account_cutoff_prepaid.model_account_cutoff -msgid "Account Cut-off" -msgstr "Etalements" +#: field:res.company,default_prepaid_expense_account_id:0 +msgid "Default Account for Prepaid Expense" +msgstr "Compte par défaut pour les charges à reporter" #. module: account_cutoff_prepaid #: field:res.company,default_prepaid_revenue_account_id:0 @@ -120,24 +95,25 @@ msgid "Default Account for Prepaid Revenue" msgstr "Compte par défaut pour les produits à reporter" #. module: account_cutoff_prepaid -#: model:ir.model,name:account_cutoff_prepaid.model_account_cutoff_line -msgid "Account Cut-off Line" -msgstr "Ligne de report" +#: field:res.company,default_prepaid_expense_journal_id:0 +msgid "Default Journal for Prepaid Expenses" +msgstr "" #. module: account_cutoff_prepaid -#: sql_constraint:account.cutoff:0 -msgid "A cut-off of the same type already exists with this cut-off date !" -msgstr "Un report de ce type existe déjà pour cette date de cut-off !" +#: field:res.company,default_prepaid_revenue_journal_id:0 +msgid "Default Journal for Prepaid Revenues" +msgstr "" #. module: account_cutoff_prepaid -#: field:account.cutoff.line,after_cutoff_days:0 -msgid "Number of Days after Cut-off Date" -msgstr "Jours après la date de cut-off" +#: field:account.cutoff.line,end_date:0 field:account.invoice.line,end_date:0 +#: field:account.move.line,end_date:0 +msgid "End Date" +msgstr "Fin" #. module: account_cutoff_prepaid -#: model:product.template,name:account_cutoff_prepaid.product_insurance_contrat_product_template -msgid "Car Insurance" -msgstr "Assurance voiture" +#: constraint:account.invoice.line:0 constraint:account.move.line:0 +msgid "Error msg in raise" +msgstr "Message d'erreur dans raise" #. module: account_cutoff_prepaid #: code:addons/account_cutoff_prepaid/account.py:40 @@ -148,46 +124,33 @@ msgstr "Assurance voiture" #: code:addons/account_cutoff_prepaid/account.py:100 #: code:addons/account_cutoff_prepaid/account.py:144 #: code:addons/account_cutoff_prepaid/account_cutoff.py:85 -#: code:addons/account_cutoff_prepaid/account_cutoff.py:129 +#: code:addons/account_cutoff_prepaid/account_cutoff.py:128 #, python-format msgid "Error:" msgstr "Erreur:" #. module: account_cutoff_prepaid -#: model:ir.model,name:account_cutoff_prepaid.model_res_company -msgid "Companies" -msgstr "Sociétés" - -#. module: account_cutoff_prepaid -#: view:account.cutoff.line:0 -msgid "Days Total" -msgstr "Nombre total de jours" +#: help:product.template,must_have_dates:0 +msgid "" +"If this option is active, the user will have to enter a Start Date and an " +"End Date on the invoice lines that have this product." +msgstr "Si cette option est active, l'utilisateur devra entrer une date de début et de fin sur les lignes de factures qui utilisent ce produit." #. module: account_cutoff_prepaid -#: code:addons/account_cutoff_prepaid/account.py:54 -#, python-format -msgid "" -"Start Date should be before or be the same as End Date for invoice line with " -"Description '%s'." -msgstr "" -"La date de début doit précéder la date de fin pour la ligne de facture avec " -"description '%s'." +#: field:account.cutoff.line,invoice_id:0 +#: model:ir.model,name:account_cutoff_prepaid.model_account_invoice +msgid "Invoice" +msgstr "Facture" #. module: account_cutoff_prepaid -#: code:addons/account_cutoff_prepaid/account.py:145 -#, python-format -msgid "" -"Missing Start Date and End Date for invoice line with Product '%s' which has " -"the property 'Must Have Start and End Dates'." -msgstr "" -"Dates de début et fin manquantes pour la ligne de facture avec produit '%s' " -"qui a la propriété 'Dates de début et fin obligatoires'." +#: model:ir.model,name:account_cutoff_prepaid.model_account_invoice_line +msgid "Invoice Line" +msgstr "Ligne de facture" #. module: account_cutoff_prepaid -#: code:addons/account_cutoff_prepaid/account_cutoff.py:129 -#, python-format -msgid "You should set at least one Source Journal." -msgstr "Vous devez indiquer au moins un journal source." +#: model:ir.model,name:account_cutoff_prepaid.model_account_move_line +msgid "Journal Items" +msgstr "Ecriture" #. module: account_cutoff_prepaid #: code:addons/account_cutoff_prepaid/account.py:41 @@ -202,46 +165,66 @@ msgid "Missing End Date for move line with Name '%s'." msgstr "Date de fin manquante pour l'écriture avec label '%s'." #. module: account_cutoff_prepaid -#: model:ir.model,name:account_cutoff_prepaid.model_product_template -msgid "Product Template" -msgstr "" +#: code:addons/account_cutoff_prepaid/account.py:145 +#, python-format +msgid "" +"Missing Start Date and End Date for invoice line with Product '%s' which has" +" the property 'Must Have Start and End Dates'." +msgstr "Dates de début et fin manquantes pour la ligne de facture avec produit '%s' qui a la propriété 'Dates de début et fin obligatoires'." #. module: account_cutoff_prepaid -#: model:ir.model,name:account_cutoff_prepaid.model_account_invoice_line -msgid "Invoice Line" -msgstr "Ligne de facture" +#: code:addons/account_cutoff_prepaid/account.py:47 +#, python-format +msgid "Missing Start Date for invoice line with Description '%s'." +msgstr "Date de début manquante pour la ligne de facture avec description '%s'." #. module: account_cutoff_prepaid -#: model:ir.actions.act_window,help:account_cutoff_prepaid.account_cutoff_prepaid_expense_action -msgid "" -"

\n" -" Click to start preparing a new prepaid expense.\n" -"

\n" -" This view can be used by accountants in order to collect information " -"about prepaid expenses based on start date and end date. It then allows to " -"generate the corresponding cutoff journal entry in one click.\n" -"

\n" -" " +#: code:addons/account_cutoff_prepaid/account.py:95 +#, python-format +msgid "Missing Start Date for move line with Name '%s'." +msgstr "Date de début manquante pour l'écriture avec le label '%s'." + +#. module: account_cutoff_prepaid +#: field:account.cutoff.line,move_line_period_id:0 +msgid "Move Line Period" msgstr "" #. module: account_cutoff_prepaid -#: model:ir.actions.act_window,help:account_cutoff_prepaid.account_cutoff_prepaid_revenue_action -msgid "" -"

\n" -" Click to start preparing a new prepaid revenue.\n" -"

\n" -" This view can be used by accountants in order to collect information " -"about prepaid revenues based on start date and end date. It then allows to " -"generate the corresponding cutoff journal entry in one click.\n" -"

\n" -" " +#: field:product.template,must_have_dates:0 +msgid "Must Have Start and End Dates" +msgstr "Dates de début et fin obligatoires" + +#. module: account_cutoff_prepaid +#: field:account.cutoff.line,after_cutoff_days:0 +msgid "Number of Days after Cut-off Date" +msgstr "Jours après la date de cut-off" + +#. module: account_cutoff_prepaid +#: model:ir.actions.act_window,name:account_cutoff_prepaid.account_cutoff_prepaid_expense_action +#: model:ir.ui.menu,name:account_cutoff_prepaid.account_cutoff_prepaid_expense_menu +msgid "Prepaid Expense" +msgstr "Report de Charges" + +#. module: account_cutoff_prepaid +#: model:ir.actions.act_window,name:account_cutoff_prepaid.account_cutoff_prepaid_revenue_action +#: model:ir.ui.menu,name:account_cutoff_prepaid.account_cutoff_prepaid_revenue_menu +msgid "Prepaid Revenue" +msgstr "Report de Produits" + +#. module: account_cutoff_prepaid +#: model:ir.model,name:account_cutoff_prepaid.model_product_template +msgid "Product Template" msgstr "" #. module: account_cutoff_prepaid -#: field:account.cutoff.line,invoice_id:0 -#: model:ir.model,name:account_cutoff_prepaid.model_account_invoice -msgid "Invoice" -msgstr "Facture" +#: view:account.cutoff:account_cutoff_prepaid.account_cutoff_form +msgid "Re-Generate Lines" +msgstr "Regénérer les lignes" + +#. module: account_cutoff_prepaid +#: field:account.cutoff,source_journal_ids:0 +msgid "Source Journals" +msgstr "Journaux source" #. module: account_cutoff_prepaid #: field:account.cutoff.line,start_date:0 @@ -251,11 +234,26 @@ msgid "Start Date" msgstr "Début" #. module: account_cutoff_prepaid -#: view:account.cutoff.line:0 -msgid "Days after Cut-off" -msgstr "Jours après cut-off" +#: code:addons/account_cutoff_prepaid/account.py:101 +#, python-format +msgid "Start Date should be before End Date for move line with Name '%s'." +msgstr "La date de début doit précéder la date de fin pour l'écriture avec label '%s'." #. module: account_cutoff_prepaid -#: view:product.template:0 -msgid "Sales Properties" -msgstr "" +#: code:addons/account_cutoff_prepaid/account.py:54 +#, python-format +msgid "" +"Start Date should be before or be the same as End Date for invoice line with" +" Description '%s'." +msgstr "La date de début doit précéder la date de fin pour la ligne de facture avec description '%s'." + +#. module: account_cutoff_prepaid +#: field:account.cutoff.line,total_days:0 +msgid "Total Number of Days" +msgstr "Nombre de jours total" + +#. module: account_cutoff_prepaid +#: code:addons/account_cutoff_prepaid/account_cutoff.py:128 +#, python-format +msgid "You should set at least one Source Journal." +msgstr "Vous devez indiquer au moins un journal source." diff --git a/account_cutoff_prepaid/migrations/7.0.0.2/pre-migration.py b/account_cutoff_prepaid/migrations/7.0.0.2/pre-migration.py new file mode 100644 index 00000000000..94179a01425 --- /dev/null +++ b/account_cutoff_prepaid/migrations/7.0.0.2/pre-migration.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +# © 2014-2016 Akretion (Alexis de Lattre ) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + + +def migrate(cr, version): + if not version: + return + + cr.execute( + 'ALTER TABLE "account_cutoff_line" RENAME "after_cutoff_days" ' + 'TO "prepaid_days"') diff --git a/account_cutoff_prepaid/models/__init__.py b/account_cutoff_prepaid/models/__init__.py new file mode 100644 index 00000000000..ca4ab3e09c0 --- /dev/null +++ b/account_cutoff_prepaid/models/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- + +from . import company +from . import account_cutoff diff --git a/account_cutoff_prepaid/models/account_cutoff.py b/account_cutoff_prepaid/models/account_cutoff.py new file mode 100644 index 00000000000..566e78a8a00 --- /dev/null +++ b/account_cutoff_prepaid/models/account_cutoff.py @@ -0,0 +1,182 @@ +# -*- coding: utf-8 -*- +# © 2013-2016 Akretion (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 + + +class AccountCutoff(models.Model): + _inherit = 'account.cutoff' + + @api.model + def _get_default_source_journals(self): + res = [] + type = self._context.get('type') + mapping = { + 'prepaid_expense': 'purchase', + 'prepaid_revenue': 'sale', + } + if type in mapping: + src_journals = self.env['account.journal'].search( + [('type', '=', mapping[type])]) + if src_journals: + res = src_journals.ids + return res + + source_journal_ids = fields.Many2many( + 'account.journal', id1='cutoff_id', id2='journal_id', + string='Source Journals', readonly=True, + default=_get_default_source_journals, + states={'draft': [('readonly', False)]}) + forecast = fields.Boolean( + string='Forecast', + readonly=True, states={'draft': [('readonly', False)]}, + help="The Forecast mode allows the user to compute " + "the prepaid revenue/expense between 2 dates in the future.") + start_date = fields.Date(string='Start Date') + end_date = fields.Date(string='End Date') + + _sql_constraints = [( + 'date_type_forecast_company_uniq', + 'unique(' + 'cutoff_date, company_id, type, forecast, start_date, end_date)', + 'A cut-off of the same type already exists with the same date(s) !' + )] + + @api.multi + @api.constrains('start_date', 'end_date', 'forecast') + def _check_start_end_dates(self): + for prepaid in self: + if prepaid.forecast and prepaid.start_date and prepaid.end_date \ + and prepaid.start_date > prepaid.end_date: + raise ValidationError(_( + 'The start date is after the end date!')) + + @api.onchange('forecast') + def forecast_onchange(self): + return {'warning': { + 'title': _('Warning'), + 'message': _( + "Don't forget to Re-Generate Lines after entering or " + "leaving forecast mode.")}} + + @api.multi + def _prepare_prepaid_lines(self, aml, mapping): + self.ensure_one() + start_date_dt = fields.Date.from_string(aml.start_date) + end_date_dt = fields.Date.from_string(aml.end_date) + # Here, we compute the amount of the cutoff + # That's the important part ! + total_days = (end_date_dt - start_date_dt).days + 1 + if self.forecast: + out_days = 0 + forecast_start_date_dt = fields.Date.from_string(self.start_date) + forecast_end_date_dt = fields.Date.from_string(self.end_date) + if end_date_dt > forecast_end_date_dt: + out_days += (end_date_dt - forecast_end_date_dt).days + if start_date_dt < forecast_start_date_dt: + out_days += (forecast_start_date_dt - start_date_dt).days + prepaid_days = total_days - out_days + else: + cutoff_date_str = self.cutoff_date + cutoff_date_dt = fields.Date.from_string(cutoff_date_str) + if start_date_dt > cutoff_date_dt: + prepaid_days = total_days + else: + prepaid_days = (end_date_dt - cutoff_date_dt).days + assert total_days > 0,\ + 'Should never happen. Total days should always be > 0' + cutoff_amount = (aml.debit - aml.credit) *\ + prepaid_days / float(total_days) + # we use account mapping here + if aml.account_id.id in mapping: + cutoff_account_id = mapping[aml.account_id.id] + else: + cutoff_account_id = aml.account_id.id + + res = { + 'parent_id': self.id, + 'move_line_id': aml.id, + 'partner_id': aml.partner_id.id or False, + 'name': aml.name, + 'start_date': start_date_dt, + 'end_date': end_date_dt, + 'account_id': aml.account_id.id, + 'cutoff_account_id': cutoff_account_id, + 'analytic_account_id': aml.analytic_account_id.id or False, + 'total_days': total_days, + 'prepaid_days': prepaid_days, + 'amount': aml.credit - aml.debit, + 'currency_id': self.company_currency_id.id, + 'cutoff_amount': cutoff_amount, + } + return res + + @api.multi + def get_prepaid_lines(self): + self.ensure_one() + aml_obj = self.env['account.move.line'] + line_obj = self.env['account.cutoff.line'] + mapping_obj = self.env['account.cutoff.mapping'] + if not self.source_journal_ids: + raise UserError( + _("You should set at least one Source Journal.")) + cutoff_date_str = self.cutoff_date + # Delete existing lines + self.line_ids.unlink() + + if self.forecast: + domain = [ + ('start_date', '<=', self.end_date), + ('end_date', '>=', self.start_date), + ('journal_id', 'in', self.source_journal_ids.ids) + ] + else: + domain = [ + ('start_date', '!=', False), + ('journal_id', 'in', self.source_journal_ids.ids), + ('end_date', '>', cutoff_date_str), + ('date', '<=', cutoff_date_str) + ] + + # Search for account move lines in the source journals + amls = aml_obj.search(domain) + # Create mapping dict + mapping = mapping_obj._get_mapping_dict(self.company_id.id, self.type) + + # Loop on selected account move lines to create the cutoff lines + for aml in amls: + line_obj.create(self._prepare_prepaid_lines(aml, mapping)) + return True + + @api.model + def _inherit_default_cutoff_account_id(self): + account_id = super(AccountCutoff, self).\ + _inherit_default_cutoff_account_id() + type = self._context.get('type') + company = self.env.user.company_id + if type == 'prepaid_revenue': + account_id = company.default_prepaid_revenue_account_id.id or False + elif type == 'prepaid_expense': + account_id = company.default_prepaid_expense_account_id.id or False + return account_id + + +class AccountCutoffLine(models.Model): + _inherit = 'account.cutoff.line' + + move_line_id = fields.Many2one( + 'account.move.line', string='Account Move Line', readonly=True) + move_date = fields.Date( + related='move_line_id.date', string='Account Move Date', readonly=True) + invoice_id = fields.Many2one( + related='move_line_id.invoice_id', string='Invoice', readonly=True) + start_date = fields.Date(string='Start Date', readonly=True) + end_date = fields.Date(string='End Date', readonly=True) + total_days = fields.Integer('Total Number of Days', readonly=True) + prepaid_days = fields.Integer( + string='Prepaid Days', readonly=True, + help="In regular mode, this is the number of days after the " + "cut-off date. In forecast mode, this is the number of days " + "between the start date and the end date.") diff --git a/account_cutoff_prepaid/models/company.py b/account_cutoff_prepaid/models/company.py new file mode 100644 index 00000000000..d7abc3e0e6b --- /dev/null +++ b/account_cutoff_prepaid/models/company.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# © 2013-2016 Akretion (Alexis de Lattre ) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from openerp import models, fields + + +class ResCompany(models.Model): + _inherit = 'res.company' + + default_prepaid_revenue_account_id = fields.Many2one( + 'account.account', string='Default Account for Prepaid Revenue', + domain=[('deprecated', '=', False)]) + default_prepaid_expense_account_id = fields.Many2one( + 'account.account', string='Default Account for Prepaid Expense', + domain=[('deprecated', '=', False)]) diff --git a/account_cutoff_prepaid/product.py b/account_cutoff_prepaid/product.py deleted file mode 100644 index 67a3c410231..00000000000 --- a/account_cutoff_prepaid/product.py +++ /dev/null @@ -1,36 +0,0 @@ -# -*- encoding: utf-8 -*- -############################################################################## -# -# Account Cut-off Prepaid module for OpenERP -# Copyright (C) 2013 Akretion (http://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 . -# -############################################################################## - - -from openerp.osv import orm, fields - - -class product_template(orm.Model): - _inherit = 'product.template' - - _columns = { - 'must_have_dates': fields.boolean( - 'Must Have Start and End Dates', - help="If this option is active, the user will have to enter " - "a Start Date and an End Date on the invoice lines that have " - "this product."), - } diff --git a/account_cutoff_prepaid/product_demo.xml b/account_cutoff_prepaid/product_demo.xml deleted file mode 100644 index 077d7f73215..00000000000 --- a/account_cutoff_prepaid/product_demo.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - Car Insurance - CARINSUR - service - - - - - - - - - diff --git a/account_cutoff_prepaid/tests/__init__.py b/account_cutoff_prepaid/tests/__init__.py index bbec59cdee9..0f94ebab696 100644 --- a/account_cutoff_prepaid/tests/__init__.py +++ b/account_cutoff_prepaid/tests/__init__.py @@ -1,23 +1,3 @@ -# -*- encoding: utf-8 -*- -############################################################################## -# -# Account Cut-off Prepaid test module for OpenERP -# Copyright (C) 2014 ACSONE SA/NV (http://acsone.eu) -# @author Stéphane Bidoul -# -# 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 . -# -############################################################################## +# -*- coding: utf-8 -*- from . import test_account_cutoff_prepaid diff --git a/account_cutoff_prepaid/tests/test_account_cutoff_prepaid.py b/account_cutoff_prepaid/tests/test_account_cutoff_prepaid.py index 056ec87cae7..969d7e15cc1 100644 --- a/account_cutoff_prepaid/tests/test_account_cutoff_prepaid.py +++ b/account_cutoff_prepaid/tests/test_account_cutoff_prepaid.py @@ -1,82 +1,78 @@ -# -*- encoding: utf-8 -*- -############################################################################## -# -# Account Cut-off Prepaid test module for OpenERP -# Copyright (C) 2014 ACSONE SA/NV (http://acsone.eu) -# @author Stéphane Bidoul -# -# 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 . -# -############################################################################## +# -*- coding: utf-8 -*- +# © 2014 ACSONE SA/NV (http://acsone.eu) +# @author Stéphane Bidoul +# © 2016 Akretion (Alexis de Lattre ) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -import time - -from dateutil.parser import parse as parse_date -import openerp.tests.common as common -from openerp import workflow +import time +from openerp import fields +from openerp.tests.common import TransactionCase -class TestCutoffPrepaid(common.TransactionCase): +class TestCutoffPrepaid(TransactionCase): def setUp(self): super(TestCutoffPrepaid, self).setUp() - self.inv_model = self.registry('account.invoice') - self.cutoff_model = self.registry('account.cutoff') + self.inv_model = self.env['account.invoice'] + self.cutoff_model = self.env['account.cutoff'] + self.account_model = self.env['account.account'] + self.journal_model = self.env['account.journal'] + self.account_expense = self.account_model.search([( + 'user_type_id', + '=', + self.env.ref('account.data_account_type_expenses').id)], limit=1) + self.account_payable = self.account_model.search([( + 'user_type_id', + '=', + self.env.ref('account.data_account_type_payable').id)], limit=1) + self.account_cutoff = self.account_model.search([( + 'user_type_id', + '=', + self.env.ref('account.data_account_type_current_liabilities').id)], + limit=1) + self.cutoff_journal = self.journal_model.search([], limit=1) + self.purchase_journal = self.journal_model.search([( + 'type', '=', 'purchase')], limit=1) def _date(self, date): """ convert MM-DD to current year date YYYY-MM-DD """ return time.strftime('%Y-' + date) def _days(self, start_date, end_date): - start_date = parse_date(self._date(start_date)) - end_date = parse_date(self._date(end_date)) + start_date = fields.Date.from_string(self._date(start_date)) + end_date = fields.Date.from_string(self._date(end_date)) return (end_date - start_date).days + 1 def _create_invoice(self, date, amount, start_date, end_date): - inv_id = self.inv_model.create(self.cr, self.uid, { - 'journal_id': self.ref('account.expenses_journal'), + invoice = self.inv_model.create({ 'date_invoice': self._date(date), - 'account_id': self.ref('account.a_recv'), - 'partner_id': self.ref('base.res_partner_17'), + 'account_id': self.account_payable.id, + 'partner_id': self.env.ref('base.res_partner_2').id, + 'journal_id': self.purchase_journal.id, 'type': 'in_invoice', - 'invoice_line': [(0, 0, { + 'invoice_line_ids': [(0, 0, { 'name': 'expense', 'price_unit': amount, 'quantity': 1, - 'account_id': self.ref('account.a_expense'), + 'account_id': self.account_expense.id, 'start_date': self._date(start_date), 'end_date': self._date(end_date), })], }) - workflow.trg_validate(self.uid, 'account.invoice', inv_id, - 'invoice_open', self.cr) - inv = self.inv_model.browse(self.cr, self.uid, inv_id) - self.assertEqual(amount, inv.amount_untaxed) - return inv_id + invoice.signal_workflow('invoice_open') + self.assertEqual(amount, invoice.amount_untaxed) + return invoice def _create_cutoff(self, date): - cutoff_id = self.cutoff_model.create(self.cr, self.uid, { + cutoff = self.cutoff_model.create({ 'cutoff_date': self._date(date), 'type': 'prepaid_revenue', - 'cutoff_journal_id': self.ref('account.miscellaneous_journal'), - 'cutoff_account_id': self.ref('account.o_expense'), - 'source_journal_ids': [ - (6, 0, [self.ref('account.expenses_journal')]), - ], + 'cutoff_journal_id': self.cutoff_journal.id, + 'cutoff_account_id': self.account_cutoff.id, + 'source_journal_ids': [(6, 0, [self.purchase_journal.id])], }) - return cutoff_id + return cutoff def test_0(self): """ basic test with cutoff before, after and in the middle """ @@ -86,19 +82,16 @@ def test_0(self): self._create_invoice('01-15', amount, start_date='04-01', end_date='06-30') # cutoff after one month of invoice period -> 2 months cutoff - cutoff_id = self._create_cutoff('04-30') - self.cutoff_model.get_prepaid_lines(self.cr, self.uid, [cutoff_id]) - cutoff = self.cutoff_model.browse(self.cr, self.uid, cutoff_id) + cutoff = self._create_cutoff('04-30') + cutoff.get_prepaid_lines() self.assertEqual(amount_2months, cutoff.total_cutoff_amount) # cutoff at end of invoice period -> no cutoff - cutoff_id = self._create_cutoff('06-30') - self.cutoff_model.get_prepaid_lines(self.cr, self.uid, [cutoff_id]) - cutoff = self.cutoff_model.browse(self.cr, self.uid, cutoff_id) + cutoff = self._create_cutoff('06-30') + cutoff.get_prepaid_lines() self.assertEqual(0, cutoff.total_cutoff_amount) # cutoff before invoice period -> full value cutoff - cutoff_id = self._create_cutoff('01-31') - self.cutoff_model.get_prepaid_lines(self.cr, self.uid, [cutoff_id]) - cutoff = self.cutoff_model.browse(self.cr, self.uid, cutoff_id) + cutoff = self._create_cutoff('01-31') + cutoff.get_prepaid_lines() self.assertEqual(amount, cutoff.total_cutoff_amount) def tests_1(self): @@ -110,12 +103,11 @@ def tests_1(self): self._create_invoice('01-16', amount, start_date='04-01', end_date='06-30') # cutoff before invoice period -> full value cutoff - cutoff_id = self._create_cutoff('01-31') - self.cutoff_model.get_prepaid_lines(self.cr, self.uid, [cutoff_id]) - self.cutoff_model.create_move(self.cr, self.uid, [cutoff_id]) - cutoff = self.cutoff_model.browse(self.cr, self.uid, cutoff_id) + cutoff = self._create_cutoff('01-31') + cutoff.get_prepaid_lines() + cutoff.create_move() self.assertEqual(amount * 2, cutoff.total_cutoff_amount) self.assert_(cutoff.move_id, "move not generated") # two invoices, but two lines (because the two cutoff lines # have been grouped into one line plus one counterpart) - self.assertEqual(len(cutoff.move_id.line_id), 2) + self.assertEqual(len(cutoff.move_id.line_ids), 2) diff --git a/account_cutoff_prepaid/account_cutoff_view.xml b/account_cutoff_prepaid/views/account_cutoff.xml similarity index 57% rename from account_cutoff_prepaid/account_cutoff_view.xml rename to account_cutoff_prepaid/views/account_cutoff.xml index 0f8576a2c94..33bc204ba54 100644 --- a/account_cutoff_prepaid/account_cutoff_view.xml +++ b/account_cutoff_prepaid/views/account_cutoff.xml @@ -1,20 +1,61 @@ - - + - + + + account.cutoff.prepaid.tree + account.cutoff + + + + + + + + + + account.cutoff.prepaid.form account.cutoff + + + + + + + {'invisible': [('forecast', '=', True)], 'required': [('forecast', '=', False)]} + 0 + + + {'invisible': [('forecast', '=', True)]} + + + 0 + {'required': [('forecast', '=', False)]} + + + 0 + {'required': [('forecast', '=', False)]} + + + 0 + {'required': [('forecast', '=', False)]} + + @@ -24,7 +65,21 @@ - + + prepaid.account.cutoff.prepaid.search + account.cutoff + + + + + + + + + + + + account.cutoff.line.prepaid.form account.cutoff.line @@ -41,12 +96,11 @@
- + - account.cutoff.line.prepaid.tree account.cutoff.line @@ -59,7 +113,7 @@ - + @@ -68,7 +122,6 @@ Prepaid Expense account.cutoff - form tree,form [('type', '=', 'prepaid_expense')] {'type': 'prepaid_expense'} @@ -91,7 +144,6 @@ Prepaid Revenue account.cutoff - form tree,form [('type', '=', 'prepaid_revenue')] {'type': 'prepaid_revenue'} @@ -112,4 +164,4 @@ - + diff --git a/account_cutoff_prepaid/company_view.xml b/account_cutoff_prepaid/views/company.xml similarity index 75% rename from account_cutoff_prepaid/company_view.xml rename to account_cutoff_prepaid/views/company.xml index 6d91e4859b5..712cf6696c6 100644 --- a/account_cutoff_prepaid/company_view.xml +++ b/account_cutoff_prepaid/views/company.xml @@ -1,12 +1,10 @@ - - + @@ -24,4 +22,4 @@ - + diff --git a/account_invoice_start_end_dates/README.rst b/account_invoice_start_end_dates/README.rst new file mode 100644 index 00000000000..a6b8d3c407a --- /dev/null +++ b/account_invoice_start_end_dates/README.rst @@ -0,0 +1,61 @@ +.. 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 + +======================= +Invoice Start/End Dates +======================= + +This module adds the fields *Start Date* and *End Date* on invoice lines. When you validate the invoice, the information is copied from invoice lines to account move lines (if you enabled the grouping option on the related journal, Odoo will not group invoice lines that have different start/end dates). + +It also adds an option *Must Have Start and End Dates* on the product form (in the *Accounting* tab) ; if you enable this option, you will get an error message if you try to validate an invoice that constains such a product on one of its lines and doesn't have start/end dates on that line. + +If you use this module, you may also be interested in 2 other modules: + +* the module *sale_start_end_dates* from the sale-workflow OCA project: this module adds the fields *Start Date* and *End Date* on sale order lines and copies the information from sale order lines to invoice lines. + +* the module *account_cutoff_prepaid* (same repository): this module allows easy computation of prepaid expenses and prepaid revenues. + +Usage +===== + +.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas + :alt: Try me on Runbot + :target: https://runbot.odoo-community.org/runbot/89/9.0 + + +Known issues / Roadmap +====================== + +* Add the start/end date field on the Qweb invoice report. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues +`_. In case of trouble, please +check there if your issue has already been reported. If you spotted it first, +help us smashing it by providing a detailed and welcomed feedback. + +Credits +======= + +Contributors +------------ + +* Alexis de Lattre + +Maintainer +---------- + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :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. + +To contribute to this module, please visit https://odoo-community.org. diff --git a/account_invoice_start_end_dates/__init__.py b/account_invoice_start_end_dates/__init__.py new file mode 100644 index 00000000000..cde864bae21 --- /dev/null +++ b/account_invoice_start_end_dates/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import models diff --git a/account_invoice_start_end_dates/__openerp__.py b/account_invoice_start_end_dates/__openerp__.py new file mode 100644 index 00000000000..7414168afd2 --- /dev/null +++ b/account_invoice_start_end_dates/__openerp__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# © 2016 Akretion (Alexis de Lattre ) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + 'name': 'Account Invoice Start End Dates', + 'version': '9.0.1.0.0', + 'category': 'Accounting & Finance', + 'license': 'AGPL-3', + 'summary': 'Adds start/end dates on invoice lines and move lines', + 'author': 'Akretion,Odoo Community Association (OCA)', + 'website': 'http://www.akretion.com', + 'depends': ['account'], + 'data': [ + 'views/account_invoice.xml', + 'views/account_move.xml', + 'views/product.xml', + ], + 'demo': ['demo/product_demo.xml'], + 'installable': True, +} diff --git a/account_invoice_start_end_dates/demo/product_demo.xml b/account_invoice_start_end_dates/demo/product_demo.xml new file mode 100644 index 00000000000..348d2114bd7 --- /dev/null +++ b/account_invoice_start_end_dates/demo/product_demo.xml @@ -0,0 +1,34 @@ + + + + + + + + Car Insurance + CARINSUR + service + + + + + + + + + Maintenance contract + MAINTENANCE + service + + + + + + + + + + diff --git a/account_invoice_start_end_dates/models/__init__.py b/account_invoice_start_end_dates/models/__init__.py new file mode 100644 index 00000000000..bfb477db2c9 --- /dev/null +++ b/account_invoice_start_end_dates/models/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- + +from . import product +from . import account_invoice +from . import account_move_line diff --git a/account_invoice_start_end_dates/models/account_invoice.py b/account_invoice_start_end_dates/models/account_invoice.py new file mode 100644 index 00000000000..11125710a80 --- /dev/null +++ b/account_invoice_start_end_dates/models/account_invoice.py @@ -0,0 +1,91 @@ +# -*- coding: utf-8 -*- +# © 2013-2016 Akretion (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, UserError + + +class AccountInvoiceLine(models.Model): + _inherit = 'account.invoice.line' + + start_date = fields.Date('Start Date') + end_date = fields.Date('End Date') + must_have_dates = fields.Boolean( + related='product_id.must_have_dates', readonly=True) + + @api.multi + @api.constrains('start_date', 'end_date') + def _check_start_end_dates(self): + for invline in self: + if invline.start_date and not invline.end_date: + raise ValidationError( + _("Missing End Date for invoice line with " + "Description '%s'.") + % (invline.name)) + if invline.end_date and not invline.start_date: + raise ValidationError( + _("Missing Start Date for invoice line with " + "Description '%s'.") + % (invline.name)) + if invline.end_date and invline.start_date and \ + invline.start_date > invline.end_date: + raise ValidationError( + _("Start Date should be before or be the same as " + "End Date for invoice line with Description '%s'.") + % (invline.name)) + # Note : we can't check invline.product_id.must_have_dates + # have start_date and end_date here, because it would + # block automatic invoice generation/import. So we do the check + # upon validation of the invoice (see below the function + # action_move_create) + + +class AccountInvoice(models.Model): + _inherit = 'account.invoice' + + def inv_line_characteristic_hashcode(self, invoice_line): + '''Add start and end dates to hashcode used when the option "Group + Invoice Lines" is active on the Account Journal''' + code = super(AccountInvoice, self).inv_line_characteristic_hashcode( + invoice_line) + hashcode = '%s-%s-%s' % ( + code, + invoice_line.get('start_date', 'False'), + invoice_line.get('end_date', 'False'), + ) + return hashcode + + @api.model + def line_get_convert(self, line, part): + '''Copy from invoice to move lines''' + res = super(AccountInvoice, self).line_get_convert(line, part) + res['start_date'] = line.get('start_date', False) + res['end_date'] = line.get('end_date', False) + return res + + @api.model + def invoice_line_move_line_get(self): + '''Copy from invoice line to move lines''' + res = super(AccountInvoice, self).invoice_line_move_line_get() + ailo = self.env['account.invoice.line'] + for move_line_dict in res: + iline = ailo.browse(move_line_dict['invl_id']) + move_line_dict['start_date'] = iline.start_date + move_line_dict['end_date'] = iline.end_date + return res + + @api.multi + def action_move_create(self): + '''Check that products with must_have_dates=True have + Start and End Dates''' + for invoice in self: + for iline in invoice.invoice_line_ids: + if iline.product_id and iline.product_id.must_have_dates: + if not iline.start_date or not iline.end_date: + raise UserError(_( + "Missing Start Date and End Date for invoice " + "line with Product '%s' which has the " + "property 'Must Have Start and End Dates'.") + % (iline.product_id.name)) + return super(AccountInvoice, self).action_move_create() diff --git a/account_invoice_start_end_dates/models/account_move_line.py b/account_invoice_start_end_dates/models/account_move_line.py new file mode 100644 index 00000000000..d36cc49080b --- /dev/null +++ b/account_invoice_start_end_dates/models/account_move_line.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# © 2013-2016 Akretion (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 AccountMoveLine(models.Model): + _inherit = "account.move.line" + + start_date = fields.Date('Start Date', index=True) + end_date = fields.Date('End Date', index=True) + + @api.multi + @api.constrains('start_date', 'end_date') + def _check_start_end_dates(self): + for moveline in self: + if moveline.start_date and not moveline.end_date: + raise ValidationError( + _("Missing End Date for move line with Name '%s'.") + % (moveline.name)) + if moveline.end_date and not moveline.start_date: + raise ValidationError( + _("Missing Start Date for move line with Name '%s'.") + % (moveline.name)) + if moveline.end_date and moveline.start_date and \ + moveline.start_date > moveline.end_date: + raise ValidationError(_( + "Start Date should be before End Date for move line " + "with Name '%s'.") + % (moveline.name)) + # should we check that it's related to an expense / revenue ? + # -> I don't think so diff --git a/account_invoice_start_end_dates/models/product.py b/account_invoice_start_end_dates/models/product.py new file mode 100644 index 00000000000..4b329ed35be --- /dev/null +++ b/account_invoice_start_end_dates/models/product.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +# © 2013-2016 Akretion (Alexis de Lattre ) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from openerp import models, fields + + +class ProductTemplate(models.Model): + _inherit = 'product.template' + + must_have_dates = fields.Boolean( + string='Must Have Start and End Dates', + help="If this option is active, the user will have to enter " + "a Start Date and an End Date on the invoice lines that have " + "this product.") diff --git a/account_invoice_start_end_dates/tests/__init__.py b/account_invoice_start_end_dates/tests/__init__.py new file mode 100644 index 00000000000..85a30b83cdc --- /dev/null +++ b/account_invoice_start_end_dates/tests/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import test_invoice_start_end_dates diff --git a/account_invoice_start_end_dates/tests/test_invoice_start_end_dates.py b/account_invoice_start_end_dates/tests/test_invoice_start_end_dates.py new file mode 100644 index 00000000000..83bc787be5e --- /dev/null +++ b/account_invoice_start_end_dates/tests/test_invoice_start_end_dates.py @@ -0,0 +1,95 @@ +# -*- coding: utf-8 -*- +# © 2016 Akretion (Alexis de Lattre ) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + + +import time +from openerp.tools import float_compare +from openerp.tests.common import TransactionCase + + +class TestInvoiceStartEndDates(TransactionCase): + + def setUp(self): + super(TestInvoiceStartEndDates, self).setUp() + self.inv_model = self.env['account.invoice'] + self.account_model = self.env['account.account'] + self.journal_model = self.env['account.journal'] + self.account_revenue = self.account_model.search([( + 'user_type_id', + '=', + self.env.ref('account.data_account_type_revenue').id)], limit=1) + self.account_receivable = self.account_model.search([( + 'user_type_id', + '=', + self.env.ref('account.data_account_type_receivable').id)], limit=1) + self.cutoff_journal = self.journal_model.search([], limit=1) + self.sale_journal = self.journal_model.search([( + 'type', '=', 'sale')], limit=1) + # enable grouping on sale journal + self.sale_journal.group_invoice_lines = True + self.maint_product = self.env.ref( + 'account_invoice_start_end_dates.product_maintenance_contrat') + + def _date(self, date): + """ convert MM-DD to current year date YYYY-MM-DD """ + return time.strftime('%Y-' + date) + + def test_invoice_with_grouping(self): + invoice = self.inv_model.create({ + 'date_invoice': self._date('01-01'), + 'account_id': self.account_receivable.id, + 'partner_id': self.env.ref('base.res_partner_2').id, + 'journal_id': self.sale_journal.id, + 'type': 'out_invoice', + 'invoice_line_ids': [ + (0, 0, { + 'product_id': self.maint_product.id, + 'name': 'Maintenance IPBX 12 mois', + 'price_unit': 2400, + 'quantity': 1, + 'account_id': self.account_revenue.id, + 'start_date': self._date('01-01'), + 'end_date': self._date('12-31'), + }), + (0, 0, { + 'product_id': self.maint_product.id, + 'name': 'Maintenance téléphones 12 mois', + 'price_unit': 12, + 'quantity': 10, + 'account_id': self.account_revenue.id, + 'start_date': self._date('01-01'), + 'end_date': self._date('12-31'), + }), + (0, 0, { + 'product_id': self.maint_product.id, + 'name': 'Maintenance Fax 6 mois', + 'price_unit': 120.75, + 'quantity': 1, + 'account_id': self.account_revenue.id, + 'start_date': self._date('01-01'), + 'end_date': self._date('06-30'), + }), + (0, 0, { + 'product_id': + self.env.ref('product.product_product_17').id, + 'name': 'HD IPBX', + 'price_unit': 215.5, + 'quantity': 1, + 'account_id': self.account_revenue.id, + }), + ], + }) + invoice.signal_workflow('invoice_open') + self.assertTrue(invoice.move_id) + iline_res = { + (self._date('01-01'), self._date('12-31')): 2520, + (self._date('01-01'), self._date('06-30')): 120.75, + (False, False): 215.5, + } + precision = self.env['decimal.precision'].precision_get('Account') + for mline in invoice.move_id.line_ids: + if mline.account_id == self.account_revenue: + amount = iline_res.pop((mline.start_date, mline.end_date)) + self.assertEquals(float_compare( + amount, mline.credit, precision_digits=precision), 0) diff --git a/account_invoice_start_end_dates/views/account_invoice.xml b/account_invoice_start_end_dates/views/account_invoice.xml new file mode 100644 index 00000000000..95882a4e1cc --- /dev/null +++ b/account_invoice_start_end_dates/views/account_invoice.xml @@ -0,0 +1,75 @@ + + + + + + + + + prepaid.cutoff.invoice_form + account.invoice + + + + + + + + + + + + prepaid.cutoff.invoice_supplier_form + account.invoice + + + + + + + + + + + + prepaid.cutoff.invoice_line_form + account.invoice.line + + + + + + + + + + + + + + prepaid.cutoff.invoice_line_tree + account.invoice.line + + + + + + + + + + + + + diff --git a/account_cutoff_prepaid/account_view.xml b/account_invoice_start_end_dates/views/account_move.xml similarity index 73% rename from account_cutoff_prepaid/account_view.xml rename to account_invoice_start_end_dates/views/account_move.xml index c87524c01b6..43b306d3abc 100644 --- a/account_cutoff_prepaid/account_view.xml +++ b/account_invoice_start_end_dates/views/account_move.xml @@ -1,17 +1,15 @@ - - + - prepaid.cutoff.start.end.date.view_move_line_form + invoice.start.end.dates.view_move_line_form account.move.line @@ -22,9 +20,8 @@ - - prepaid.cutoff.start.end.date.view_move_line_form2 + invoice.start.end.dates.view_move_line_form2 account.move.line @@ -35,19 +32,18 @@ - prepaid.cutoff.start.end.date.view_move_form account.move - + - + - + diff --git a/account_cutoff_prepaid/product_view.xml b/account_invoice_start_end_dates/views/product.xml similarity index 68% rename from account_cutoff_prepaid/product_view.xml rename to account_invoice_start_end_dates/views/product.xml index fdbb9bc5aa4..b9dc9b1847e 100644 --- a/account_cutoff_prepaid/product_view.xml +++ b/account_invoice_start_end_dates/views/product.xml @@ -1,12 +1,10 @@ - - + @@ -17,7 +15,7 @@ - + @@ -25,4 +23,4 @@ - + diff --git a/setup/account_cutoff_base/odoo_addons/__init__.py b/setup/account_cutoff_base/odoo_addons/__init__.py new file mode 100644 index 00000000000..de40ea7ca05 --- /dev/null +++ b/setup/account_cutoff_base/odoo_addons/__init__.py @@ -0,0 +1 @@ +__import__('pkg_resources').declare_namespace(__name__) diff --git a/setup/account_cutoff_base/odoo_addons/account_cutoff_base b/setup/account_cutoff_base/odoo_addons/account_cutoff_base new file mode 120000 index 00000000000..71fd112a74b --- /dev/null +++ b/setup/account_cutoff_base/odoo_addons/account_cutoff_base @@ -0,0 +1 @@ +../../../account_cutoff_base \ No newline at end of file diff --git a/setup/account_cutoff_base/setup.py b/setup/account_cutoff_base/setup.py new file mode 100644 index 00000000000..28c57bb6403 --- /dev/null +++ b/setup/account_cutoff_base/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/setup/account_cutoff_prepaid/odoo_addons/__init__.py b/setup/account_cutoff_prepaid/odoo_addons/__init__.py new file mode 100644 index 00000000000..de40ea7ca05 --- /dev/null +++ b/setup/account_cutoff_prepaid/odoo_addons/__init__.py @@ -0,0 +1 @@ +__import__('pkg_resources').declare_namespace(__name__) diff --git a/setup/account_cutoff_prepaid/odoo_addons/account_cutoff_prepaid b/setup/account_cutoff_prepaid/odoo_addons/account_cutoff_prepaid new file mode 120000 index 00000000000..a802fa084c8 --- /dev/null +++ b/setup/account_cutoff_prepaid/odoo_addons/account_cutoff_prepaid @@ -0,0 +1 @@ +../../../account_cutoff_prepaid \ No newline at end of file diff --git a/setup/account_cutoff_prepaid/setup.py b/setup/account_cutoff_prepaid/setup.py new file mode 100644 index 00000000000..28c57bb6403 --- /dev/null +++ b/setup/account_cutoff_prepaid/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/setup/account_invoice_start_end_dates/odoo_addons/__init__.py b/setup/account_invoice_start_end_dates/odoo_addons/__init__.py new file mode 100644 index 00000000000..de40ea7ca05 --- /dev/null +++ b/setup/account_invoice_start_end_dates/odoo_addons/__init__.py @@ -0,0 +1 @@ +__import__('pkg_resources').declare_namespace(__name__) diff --git a/setup/account_invoice_start_end_dates/odoo_addons/account_invoice_start_end_dates b/setup/account_invoice_start_end_dates/odoo_addons/account_invoice_start_end_dates new file mode 120000 index 00000000000..1ac02d6ca3f --- /dev/null +++ b/setup/account_invoice_start_end_dates/odoo_addons/account_invoice_start_end_dates @@ -0,0 +1 @@ +../../../account_invoice_start_end_dates \ No newline at end of file diff --git a/setup/account_invoice_start_end_dates/setup.py b/setup/account_invoice_start_end_dates/setup.py new file mode 100644 index 00000000000..28c57bb6403 --- /dev/null +++ b/setup/account_invoice_start_end_dates/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)