diff --git a/contract/README.rst b/contract/README.rst new file mode 100644 index 00000000..e369c2dc --- /dev/null +++ b/contract/README.rst @@ -0,0 +1,71 @@ +.. 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 + +================================= +Contracts for recurrent invoicing +================================= + +This module forward-port to v9 the contracts management with recurring +invoicing functions. + +Configuration +============= + +To view discount field set *Discount on lines* in user access rights. + +Usage +===== + +To use this module, you need to: + +#. Go to Sales -> Contracts and select or create a new contract. +#. Check *Generate recurring invoices automatically*. +#. Fill fields and add new lines. You have the possibility to use markers in + the description field to show the start and end date of the invoiced period. +#. A cron is created with daily interval, but if you are in debug mode can + click on *Create invoices* to force this action. +#. Click *Show recurring invoices* link to show all invoices created by the + contract. + +.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas + :alt: Try me on Runbot + :target: https://runbot.odoo-community.org/runbot/110/9.0 + +Known issues / Roadmap +====================== + +* Recovery states and others functional fields in Contracts. + +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 +`here `_. + +Credits +======= + +Contributors +------------ + +* Pedro M. Baeza +* Carlos Dauden +* Angel Moya + +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/contract/__init__.py b/contract/__init__.py index 41ecc2ad..a0fdc10f 100644 --- a/contract/__init__.py +++ b/contract/__init__.py @@ -1,22 +1,2 @@ # -*- coding: utf-8 -*- -############################################################################## -# -# OpenERP, Open Source Management Solution -# Copyright (C) 2004-2010 Tiny SPRL () -# -# 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 . import account_analytic_analysis_recurring +from . import models diff --git a/contract/__openerp__.py b/contract/__openerp__.py index bd6c976b..76011715 100644 --- a/contract/__openerp__.py +++ b/contract/__openerp__.py @@ -1,51 +1,24 @@ # -*- coding: utf-8 -*- -############################################################################## -# -# OpenERP, Open Source Management Solution -# Copyright (C) 2004-2010 Tiny SPRL (). -# -# 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 . -# -############################################################################## - +# © 2004-2010 OpenERP SA +# © 2016 Carlos Dauden +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). { 'name': 'Contracts Management recurring', - 'version': '0.1', + 'version': '9.0.1.0.0', 'category': 'Other', - 'description': """ -This module adds a new feature in contracts to manage recurring invoicing -========================================================================= - -This is a backport of the new V8 feature available in trunk and saas. With -the V8 release this module will be deprecated. - -It also adds a little feature, you can use #START# and #END# in the contract -line description to automatically insert the dates of the invoiced period. - -Backport done By Yannick Buron. -""", - 'author': "OpenERP SA,Odoo Community Association (OCA)", + 'license': 'AGPL-3', + 'author': "OpenERP SA," + "Tecnativa," + "Odoo Community Association (OCA)", 'website': 'http://openerp.com', - 'depends': ['base', 'account_analytic_analysis'], + 'depends': ['base', 'account', 'analytic'], 'data': [ 'security/ir.model.access.csv', - 'account_analytic_analysis_recurring_cron.xml', - 'account_analytic_analysis_recurring_view.xml', + 'data/contract_cron.xml', + 'views/contract.xml', + 'views/account_invoice_view.xml', ], - 'demo': [], - 'test': [], 'installable': True, 'images': [], } diff --git a/contract/account_analytic_analysis_recurring.py b/contract/account_analytic_analysis_recurring.py deleted file mode 100644 index 59800642..00000000 --- a/contract/account_analytic_analysis_recurring.py +++ /dev/null @@ -1,267 +0,0 @@ -# -*- coding: utf-8 -*- -############################################################################## -# -# OpenERP, Open Source Management Solution -# Copyright (C) 2004-2010 Tiny SPRL (). -# -# 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 dateutil.relativedelta import relativedelta -import datetime -import logging -import time - -from openerp.osv import orm, fields -from openerp.tools.translate import _ -from openerp.addons.decimal_precision import decimal_precision as dp - -_logger = logging.getLogger(__name__) - - -class AccountAnalyticInvoiceLine(orm.Model): - _name = "account.analytic.invoice.line" - - def _amount_line( - self, cr, uid, ids, prop, unknow_none, unknow_dict, context=None): - res = {} - for line in self.browse(cr, uid, ids, context=context): - res[line.id] = line.quantity * line.price_unit - if line.analytic_account_id.pricelist_id: - cur = line.analytic_account_id.pricelist_id.currency_id - res[line.id] = self.pool.get('res.currency').round( - cr, uid, cur, res[line.id]) - return res - - _columns = { - 'product_id': fields.many2one( - 'product.product', 'Product', required=True), - 'analytic_account_id': fields.many2one( - 'account.analytic.account', 'Analytic Account'), - 'name': fields.text('Description', required=True), - 'quantity': fields.float('Quantity', required=True), - 'uom_id': fields.many2one( - 'product.uom', 'Unit of Measure', required=True), - 'price_unit': fields.float('Unit Price', required=True), - 'price_subtotal': fields.function( - _amount_line, string='Sub Total', - type="float", digits_compute=dp.get_precision('Account')), - } - _defaults = { - 'quantity': 1, - } - - def product_id_change( - self, cr, uid, ids, product, uom_id, qty=0, name='', - partner_id=False, price_unit=False, pricelist_id=False, - company_id=None, context=None): - context = context or {} - uom_obj = self.pool.get('product.uom') - company_id = company_id or False - context.update( - {'company_id': company_id, - 'force_company': company_id, - 'pricelist_id': pricelist_id}) - - if not product: - return { - 'value': {'price_unit': 0.0}, - 'domain': {'product_uom': []}} - if partner_id: - part = self.pool.get('res.partner').browse( - cr, uid, partner_id, context=context) - if part.lang: - context.update({'lang': part.lang}) - - result = {} - res = self.pool.get('product.product').browse( - cr, uid, product, context=context) - result.update( - {'name': res.partner_ref or False, - 'uom_id': uom_id or res.uom_id.id or False, - 'price_unit': res.list_price or 0.0}) - if res.description: - result['name'] += '\n' + res.description - - res_final = {'value': result} - if result['uom_id'] != res.uom_id.id: - new_price = uom_obj._compute_price( - cr, uid, res.uom_id.id, - res_final['value']['price_unit'], result['uom_id']) - res_final['value']['price_unit'] = new_price - return res_final - - -class AccountAnalyticAccount(orm.Model): - _name = "account.analytic.account" - _inherit = "account.analytic.account" - - _columns = { - 'recurring_invoice_line_ids': fields.one2many( - 'account.analytic.invoice.line', 'analytic_account_id', - 'Invoice Lines'), - 'recurring_invoices': fields.boolean( - 'Generate recurring invoices automatically'), - 'recurring_rule_type': fields.selection( - [('daily', 'Day(s)'), - ('weekly', 'Week(s)'), - ('monthly', 'Month(s)'), - ('yearly', 'Year(s)'), - ], 'Recurrency', - help="Invoice automatically repeat at specified interval"), - 'recurring_interval': fields.integer( - 'Repeat Every', help="Repeat every (Days/Week/Month/Year)"), - 'recurring_next_date': fields.date('Date of Next Invoice'), - } - - _defaults = { - 'recurring_interval': 1, - 'recurring_next_date': lambda *a: time.strftime('%Y-%m-%d'), - 'recurring_rule_type': 'monthly' - } - - def copy(self, cr, uid, id, default=None, context=None): - # Reset next invoice date - default['recurring_next_date'] = \ - self._defaults['recurring_next_date']() - return super(AccountAnalyticAccount, self).copy( - cr, uid, id, default=default, context=context) - - def onchange_recurring_invoices( - self, cr, uid, ids, recurring_invoices, - date_start=False, context=None): - value = {} - if date_start and recurring_invoices: - value = {'value': {'recurring_next_date': date_start}} - return value - - def _prepare_invoice_line(self, cr, uid, line, invoice_id, context=None): - fpos_obj = self.pool['account.fiscal.position'] - lang_obj = self.pool['res.lang'] - product = line.product_id - account_id = product.property_account_income.id - if not account_id: - account_id = product.categ_id.property_account_income_categ.id - contract = line.analytic_account_id - fpos = contract.partner_id.property_account_position or False - account_id = fpos_obj.map_account(cr, uid, fpos, account_id) - taxes = product.taxes_id or False - tax_id = fpos_obj.map_tax(cr, uid, fpos, taxes) - if 'old_date' in context: - lang_ids = lang_obj.search( - cr, uid, [('code', '=', contract.partner_id.lang)], - context=context) - format = lang_obj.browse( - cr, uid, lang_ids, context=context)[0].date_format - line.name = line.name.replace( - '#START#', context['old_date'].strftime(format)) - line.name = line.name.replace( - '#END#', context['next_date'].strftime(format)) - return { - 'name': line.name, - 'account_id': account_id, - 'account_analytic_id': contract.id, - 'price_unit': line.price_unit or 0.0, - 'quantity': line.quantity, - 'uos_id': line.uom_id.id or False, - 'product_id': line.product_id.id or False, - 'invoice_id': invoice_id, - 'invoice_line_tax_id': [(6, 0, tax_id)], - } - - def _prepare_invoice(self, cr, uid, contract, context=None): - if context is None: - context = {} - inv_obj = self.pool['account.invoice'] - journal_obj = self.pool['account.journal'] - if not contract.partner_id: - raise orm.except_orm( - _('No Customer Defined!'), - _("You must first select a Customer for Contract %s!") % - contract.name) - partner = contract.partner_id - fpos = partner.property_account_position or False - journal_ids = journal_obj.search( - cr, uid, - [('type', '=', 'sale'), - ('company_id', '=', contract.company_id.id or False)], - limit=1) - if not journal_ids: - raise orm.except_orm( - _('Error!'), - _('Please define a sale journal for the company "%s".') % - (contract.company_id.name or '',)) - partner_payment_term = partner.property_payment_term.id - inv_data = { - 'reference': contract.code or False, - 'account_id': partner.property_account_receivable.id, - 'type': 'out_invoice', - 'partner_id': partner.id, - 'currency_id': partner.property_product_pricelist.currency_id.id, - 'journal_id': len(journal_ids) and journal_ids[0] or False, - 'date_invoice': contract.recurring_next_date, - 'origin': contract.name, - 'fiscal_position': fpos and fpos.id, - 'payment_term': partner_payment_term, - 'company_id': contract.company_id.id or False, - } - invoice_id = inv_obj.create(cr, uid, inv_data, context=context) - for line in contract.recurring_invoice_line_ids: - invoice_line_vals = self._prepare_invoice_line( - cr, uid, line, invoice_id, context=context) - self.pool['account.invoice.line'].create( - cr, uid, invoice_line_vals, context=context) - inv_obj.button_compute(cr, uid, [invoice_id], context=context) - return invoice_id - - def recurring_create_invoice(self, cr, uid, automatic=False, context=None): - if context is None: - context = {} - current_date = time.strftime('%Y-%m-%d') - contract_ids = self.search( - cr, uid, - [('recurring_next_date', '<=', current_date), - ('state', '=', 'open'), - ('recurring_invoices', '=', True)]) - for contract in self.browse(cr, uid, contract_ids, context=context): - next_date = datetime.datetime.strptime( - contract.recurring_next_date or current_date, "%Y-%m-%d") - interval = contract.recurring_interval - old_date = next_date - if contract.recurring_rule_type == 'daily': - old_date = next_date - relativedelta(days=+interval) - new_date = next_date + relativedelta(days=+interval) - elif contract.recurring_rule_type == 'weekly': - old_date = next_date - relativedelta(weeks=+interval) - new_date = next_date + relativedelta(weeks=+interval) - else: - old_date = next_date + relativedelta(months=+interval) - new_date = next_date + relativedelta(months=+interval) - - context['old_date'] = old_date - context['next_date'] = datetime.datetime.strptime( - contract.recurring_next_date or current_date, "%Y-%m-%d") - # Force company for correct evaluate domain access rules - context['force_company'] = contract.company_id.id - # Re-read contract with correct company - contract = self.browse(cr, uid, contract.id, context=context) - self._prepare_invoice( - cr, uid, contract, context=context - ) - self.write( - cr, uid, [contract.id], - {'recurring_next_date': new_date.strftime('%Y-%m-%d')}, - context=context - ) - return True diff --git a/contract/account_analytic_analysis_recurring_view.xml b/contract/account_analytic_analysis_recurring_view.xml deleted file mode 100644 index e2893476..00000000 --- a/contract/account_analytic_analysis_recurring_view.xml +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - account.analytic.account.invoice.recurring.form.inherit - account.analytic.account - - - - - -
- -
- - -
-
-
- -
-
diff --git a/contract/account_analytic_analysis_recurring_cron.xml b/contract/data/contract_cron.xml similarity index 100% rename from contract/account_analytic_analysis_recurring_cron.xml rename to contract/data/contract_cron.xml diff --git a/contract/i18n/account_analytic_analysis_recurring.pot b/contract/i18n/account_analytic_analysis_recurring.pot deleted file mode 100644 index 7d2f21f1..00000000 --- a/contract/i18n/account_analytic_analysis_recurring.pot +++ /dev/null @@ -1,129 +0,0 @@ -# Translation of OpenERP Server. -# This file contains the translation of the following modules: -# * account_analytic_analysis_recurring -# -msgid "" -msgstr "" -"Project-Id-Version: OpenERP Server 7.0\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2014-02-21 11:41+0000\n" -"PO-Revision-Date: 2014-02-21 11:41+0000\n" -"Last-Translator: <>\n" -"Language-Team: \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Plural-Forms: \n" - -#. module: account_analytic_analysis_recurring -#: field:account.analytic.invoice.line,price_subtotal:0 -msgid "Sub Total" -msgstr "" - -#. module: account_analytic_analysis_recurring -#: field:account.analytic.account,recurring_rule_type:0 -msgid "Recurrency" -msgstr "" - -#. module: account_analytic_analysis_recurring -#: field:account.analytic.invoice.line,price_unit:0 -msgid "Unit Price" -msgstr "" - -#. module: account_analytic_analysis_recurring -#: view:account.analytic.account:0 -msgid ". create invoices" -msgstr "" - -#. module: account_analytic_analysis_recurring -#: view:account.analytic.account:0 -msgid "Account Analytic Lines" -msgstr "" - -#. module: account_analytic_analysis_recurring -#: field:account.analytic.account,recurring_invoice_line_ids:0 -msgid "Invoice Lines" -msgstr "" - -#. module: account_analytic_analysis_recurring -#: field:account.analytic.invoice.line,uom_id:0 -msgid "Unit of Measure" -msgstr "" - -#. module: account_analytic_analysis_recurring -#: selection:account.analytic.account,recurring_rule_type:0 -msgid "Day(s)" -msgstr "" - -#. module: account_analytic_analysis_recurring -#: help:account.analytic.account,recurring_rule_type:0 -msgid "Invoice automatically repeat at specified interval" -msgstr "" - -#. module: account_analytic_analysis_recurring -#: field:account.analytic.invoice.line,product_id:0 -msgid "Product" -msgstr "" - -#. module: account_analytic_analysis_recurring -#: field:account.analytic.invoice.line,name:0 -msgid "Description" -msgstr "" - -#. module: account_analytic_analysis_recurring -#: field:account.analytic.account,recurring_interval:0 -msgid "Repeat Every" -msgstr "" - -#. module: account_analytic_analysis_recurring -#: view:account.analytic.account:0 -msgid "Recurring Invoices" -msgstr "" - -#. module: account_analytic_analysis_recurring -#: field:account.analytic.account,recurring_invoices:0 -msgid "Generate recurring invoices automatically" -msgstr "" - -#. module: account_analytic_analysis_recurring -#: selection:account.analytic.account,recurring_rule_type:0 -msgid "Year(s)" -msgstr "" - -#. module: account_analytic_analysis_recurring -#: selection:account.analytic.account,recurring_rule_type:0 -msgid "Week(s)" -msgstr "" - -#. module: account_analytic_analysis_recurring -#: field:account.analytic.invoice.line,quantity:0 -msgid "Quantity" -msgstr "" - -#. module: account_analytic_analysis_recurring -#: model:ir.model,name:account_analytic_analysis_recurring.model_account_analytic_invoice_line -msgid "account.analytic.invoice.line" -msgstr "" - -#. module: account_analytic_analysis_recurring -#: field:account.analytic.account,recurring_next_date:0 -msgid "Date of Next Invoice" -msgstr "" - -#. module: account_analytic_analysis_recurring -#: field:account.analytic.invoice.line,analytic_account_id:0 -#: model:ir.model,name:account_analytic_analysis_recurring.model_account_analytic_account -msgid "Analytic Account" -msgstr "" - -#. module: account_analytic_analysis_recurring -#: selection:account.analytic.account,recurring_rule_type:0 -msgid "Month(s)" -msgstr "" - -#. module: account_analytic_analysis_recurring -#: help:account.analytic.account,recurring_interval:0 -msgid "Repeat every (Days/Week/Month/Year)" -msgstr "" - - diff --git a/contract/i18n/es.po b/contract/i18n/es.po index 8ea63a97..5557f41c 100644 --- a/contract/i18n/es.po +++ b/contract/i18n/es.po @@ -1,156 +1,293 @@ # Translation of OpenERP Server. # This file contains the translation of the following modules: -# * account_analytic_analysis_recurring +# * account_analytic_analysis_recurring # msgid "" msgstr "" -"Project-Id-Version: OpenERP Server 7.0\n" +"Project-Id-Version: Odoo 9.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2014-08-18 23:13+0000\n" -"PO-Revision-Date: 2014-08-19 01:14+0100\n" -"Last-Translator: Joaquin Gutierrez \n" +"POT-Creation-Date: 2016-03-28 19:26+0000\n" +"PO-Revision-Date: 2016-03-28 21:28+0100\n" +"Last-Translator: Carlos Incaser \n" "Language-Team: \n" +"Language: es_ES\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" -#. module: account_analytic_analysis_recurring -#: view:account.analytic.account:0 -msgid ". create invoices" -msgstr ". crear facturas" +#. module: contract +#: model:ir.ui.view,arch_db:contract.account_analytic_account_recurring_form_form +msgid "#END#: End date of the invoiced period" +msgstr "#END#: Fecha fin del periodo facturado" -#. module: account_analytic_analysis_recurring -#: view:account.analytic.account:0 +#. module: contract +#: model:ir.ui.view,arch_db:contract.account_analytic_account_recurring_form_form +msgid "#START#: Start date of the invoiced period" +msgstr "#START#: Fecha inicio del periodo facturado" + +#. module: contract +#: model:ir.ui.view,arch_db:contract.account_analytic_account_recurring_form_form msgid "Account Analytic Lines" msgstr "Ver líneas contables analíticas" -#. module: account_analytic_analysis_recurring -#: code:_description:0 -#: field:account.analytic.invoice.line,analytic_account_id:0 -#: model:ir.model,name:account_analytic_analysis_recurring.model_account_analytic_account -#, python-format +#. module: contract +#: model:ir.model,name:contract.model_account_analytic_account +#: model:ir.model.fields,field_description:contract.field_account_analytic_invoice_line_analytic_account_id msgid "Analytic Account" msgstr "Cuenta analítica" -#. module: account_analytic_analysis_recurring -#: field:account.analytic.account,recurring_next_date:0 +#. module: contract +#: model:ir.actions.act_window,help:contract.action_account_analytic_overdue_all +msgid "Click to create a new contract." +msgstr "Pinche para crear un contrato nuevo. " + +#. module: contract +#: model:ir.model.fields,field_description:contract.field_account_invoice_contract_id +msgid "Contract" +msgstr "Contrato" + +#. module: contract +#: model:ir.actions.act_window,name:contract.action_account_analytic_overdue_all +#: model:ir.ui.menu,name:contract.menu_action_account_analytic_overdue_all +msgid "Contracts" +msgstr "Contratos" + +#. module: contract +#: model:ir.ui.view,arch_db:contract.account_analytic_account_recurring_form_form +msgid "Create invoices" +msgstr "Crear facturas" + +#. module: contract +#: model:ir.model.fields,field_description:contract.field_account_analytic_invoice_line_create_uid +msgid "Created by" +msgstr "Creado por" + +#. module: contract +#: model:ir.model.fields,field_description:contract.field_account_analytic_invoice_line_create_date +msgid "Created on" +msgstr "Creado en" + +#. module: contract +#: model:ir.model.fields,field_description:contract.field_account_analytic_account_recurring_next_date msgid "Date of Next Invoice" -msgstr "Próximo fecha de factura" +msgstr "Próxima fecha de factura" -#. module: account_analytic_analysis_recurring +#. module: contract +#: model:ir.model.fields,field_description:contract.field_account_analytic_account_date_start +msgid "Date start" +msgstr "Fecha inicio" + +#. module: contract #: selection:account.analytic.account,recurring_rule_type:0 msgid "Day(s)" msgstr "Día(s)" -#. module: account_analytic_analysis_recurring -#: field:account.analytic.invoice.line,name:0 +#. module: contract +#: model:ir.model.fields,field_description:contract.field_account_analytic_invoice_line_name msgid "Description" msgstr "Descripción" -#. module: account_analytic_analysis_recurring -#: code:addons/account_analytic_analysis_recurring/account_analytic_analysis_recurring.py:165 +#. module: contract +#: model:ir.model.fields,field_description:contract.field_account_analytic_invoice_line_discount +msgid "Discount (%)" +msgstr "Descuento (%)" + +#. module: contract +#: code:addons/contract/models/contract.py:59 #, python-format -msgid "Error!" -msgstr "¡Error!" +msgid "Discount should be less or equal to 100" +msgstr "El descuento debería ser menor o igual a 100" + +#. module: contract +#: model:ir.model.fields,help:contract.field_account_analytic_invoice_line_discount +msgid "" +"Discount that is applied in generated invoices. It should be less or equal " +"to 100" +msgstr "" +"Descuento que es aplicado en las facturas generadas. Debería ser menor o " +"igual a 100" + +#. module: contract +#: model:ir.model.fields,field_description:contract.field_account_analytic_invoice_line_display_name +msgid "Display Name" +msgstr "Nombre mostrado" -#. module: account_analytic_analysis_recurring -#: field:account.analytic.account,recurring_invoices:0 +#. module: contract +#: model:ir.model.fields,field_description:contract.field_account_analytic_account_recurring_invoices msgid "Generate recurring invoices automatically" msgstr "Generar facturas recurrentes automáticamente." -#. module: account_analytic_analysis_recurring -#: field:account.analytic.account,recurring_invoice_line_ids:0 +#. module: contract +#: model:ir.ui.view,arch_db:contract.view_account_analytic_account_contract_search +msgid "Group By..." +msgstr "Agrupar por..." + +#. module: contract +#: model:ir.model.fields,field_description:contract.field_account_analytic_invoice_line_id +msgid "ID" +msgstr "ID (identificación)" + +#. module: contract +#: model:ir.model,name:contract.model_account_invoice +msgid "Invoice" +msgstr "Factura" + +#. module: contract +#: model:ir.model.fields,field_description:contract.field_account_analytic_account_recurring_invoice_line_ids msgid "Invoice Lines" msgstr "Líneas de factura" -#. module: account_analytic_analysis_recurring -#: help:account.analytic.account,recurring_rule_type:0 -msgid "Invoice automatically repeat at specified interval" -msgstr "Repetir factura automáticamente en ese intervalo" +#. module: contract +#: model:ir.actions.act_window,name:contract.act_recurring_invoices +msgid "Invoices" +msgstr "Facturas" + +#. module: contract +#: model:ir.model.fields,field_description:contract.field_account_analytic_account_journal_id +msgid "Journal" +msgstr "Diario" + +#. module: contract +#: model:ir.model.fields,field_description:contract.field_account_analytic_invoice_line___last_update +msgid "Last Modified on" +msgstr "Última modificación en" + +#. module: contract +#: model:ir.model.fields,field_description:contract.field_account_analytic_invoice_line_write_uid +msgid "Last Updated by" +msgstr "Última actualización de" -#. module: account_analytic_analysis_recurring +#. module: contract +#: model:ir.model.fields,field_description:contract.field_account_analytic_invoice_line_write_date +msgid "Last Updated on" +msgstr "Última actualización en" + +#. module: contract +#: model:ir.ui.view,arch_db:contract.account_analytic_account_recurring_form_form +msgid "Legend (for the markers inside invoice lines description)" +msgstr "" +"Leyenda (para los marcadores dentro de descripción en lineas de factura)" + +#. module: contract #: selection:account.analytic.account,recurring_rule_type:0 msgid "Month(s)" msgstr "Mes(es)" -#. module: account_analytic_analysis_recurring -#: code:addons/account_analytic_analysis_recurring/account_analytic_analysis_recurring.py:153 -#, python-format -msgid "No Customer Defined!" -msgstr "¡No se ha definido un cliente!" +#. module: contract +#: model:ir.ui.view,arch_db:contract.view_account_analytic_account_contract_search +msgid "Next Invoice" +msgstr "Próxima factura" -#. module: account_analytic_analysis_recurring -#: code:addons/account_analytic_analysis_recurring/account_analytic_analysis_recurring.py:166 +#. module: contract +#: code:addons/contract/models/contract.py:197 #, python-format -msgid "Please define a sale journal for the company \"%s\"." -msgstr "Defina por favor un diario de ventas para esta compañía \"%s\"." +msgid "Please define a sale journal for the company '%s'." +msgstr "Por favor define un diario de ventas para la compañía '%s'." -#. module: account_analytic_analysis_recurring -#: field:account.analytic.invoice.line,product_id:0 +#. module: contract +#: model:ir.model.fields,field_description:contract.field_account_analytic_account_pricelist_id +msgid "Pricelist" +msgstr "Lista de precios" + +#. module: contract +#: model:ir.model.fields,field_description:contract.field_account_analytic_invoice_line_product_id msgid "Product" msgstr "Producto" -#. module: account_analytic_analysis_recurring -#: field:account.analytic.invoice.line,quantity:0 +#. module: contract +#: model:ir.model.fields,field_description:contract.field_account_analytic_invoice_line_quantity msgid "Quantity" msgstr "Cantidad" -#. module: account_analytic_analysis_recurring -#: field:account.analytic.account,recurring_rule_type:0 +#. module: contract +#: model:ir.model.fields,field_description:contract.field_account_analytic_account_recurring_rule_type msgid "Recurrency" msgstr "Recurrencia" -#. module: account_analytic_analysis_recurring -#: view:account.analytic.account:0 +#. module: contract +#: model:ir.ui.view,arch_db:contract.account_analytic_account_recurring_form_form +#: model:ir.ui.view,arch_db:contract.view_account_analytic_account_contract_search msgid "Recurring Invoices" msgstr "Facturas recurrentes" -#. module: account_analytic_analysis_recurring -#: field:account.analytic.account,recurring_interval:0 +#. module: contract +#: model:ir.model.fields,field_description:contract.field_account_analytic_account_recurring_interval msgid "Repeat Every" msgstr "Repetir cada" -#. module: account_analytic_analysis_recurring -#: help:account.analytic.account,recurring_interval:0 +#. module: contract +#: model:ir.model.fields,help:contract.field_account_analytic_account_recurring_interval msgid "Repeat every (Days/Week/Month/Year)" msgstr "Repetir cada (días/semana/mes/año)" -#. module: account_analytic_analysis_recurring -#: field:account.analytic.invoice.line,price_subtotal:0 +#. module: contract +#: model:ir.model.fields,help:contract.field_account_analytic_account_recurring_rule_type +msgid "Specify Interval for automatic invoice generation." +msgstr "Especifica el intervalo para la generación de facturas automática." + +#. module: contract +#: model:ir.model.fields,field_description:contract.field_account_analytic_invoice_line_price_subtotal msgid "Sub Total" msgstr "Subtotal" -#. module: account_analytic_analysis_recurring -#: field:account.analytic.invoice.line,price_unit:0 +#. module: contract +#: model:ir.model.fields,field_description:contract.field_account_analytic_invoice_line_price_unit msgid "Unit Price" msgstr "Precio unidad" -#. module: account_analytic_analysis_recurring -#: field:account.analytic.invoice.line,uom_id:0 +#. module: contract +#: model:ir.model.fields,field_description:contract.field_account_analytic_invoice_line_uom_id msgid "Unit of Measure" msgstr "Unidad de medida" -#. module: account_analytic_analysis_recurring +#. module: contract #: selection:account.analytic.account,recurring_rule_type:0 msgid "Week(s)" msgstr "Semana(s)" -#. module: account_analytic_analysis_recurring +#. module: contract #: selection:account.analytic.account,recurring_rule_type:0 msgid "Year(s)" msgstr "Año(s)" -#. module: account_analytic_analysis_recurring -#: code:addons/account_analytic_analysis_recurring/account_analytic_analysis_recurring.py:154 +#. module: contract +#: code:addons/contract/models/contract.py:189 #, python-format msgid "You must first select a Customer for Contract %s!" msgstr "¡Seleccione un cliente para este contrato %s!" -#. module: account_analytic_analysis_recurring -#: code:_description:0 -#: model:ir.model,name:account_analytic_analysis_recurring.model_account_analytic_invoice_line -#, python-format +#. module: contract +#: model:ir.model,name:contract.model_account_analytic_invoice_line msgid "account.analytic.invoice.line" msgstr "account.analytic.invoice.line" +#. module: contract +#: model:ir.ui.view,arch_db:contract.account_analytic_account_recurring_form_form +msgid "⇒ Show recurring invoices" +msgstr "⇒ Mostrar facturas recurrentes" + +#~ msgid "Invoices related with this contract" +#~ msgstr "Facturas relacionadas con este contrato" + +#~ msgid "" +#~ "Use contracts to follow tasks, issues, timesheets or invoicing based on\n" +#~ " work done, expenses and/or sales orders. Odoo will " +#~ "automatically manage\n" +#~ " the alerts for the renewal of the contracts to the " +#~ "right salesperson." +#~ msgstr "" +#~ "Use contratos para seguir tareas, incidencias, hojas de trabajo o " +#~ "facturación basada\n" +#~ " en trabajo realizado, gastos y/o pedidos de venta. " +#~ "Odoo gestrionará automáticamente\n" +#~ " las alertas para la renovación de los contratos " + +#~ msgid "Error!" +#~ msgstr "¡Error!" + +#~ msgid "Invoice automatically repeat at specified interval" +#~ msgstr "Repetir factura automáticamente en ese intervalo" + +#~ msgid "No Customer Defined!" +#~ msgstr "¡No se ha definido un cliente!" diff --git a/contract/models/__init__.py b/contract/models/__init__.py new file mode 100644 index 00000000..8deef410 --- /dev/null +++ b/contract/models/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +# © 2016 Carlos Dauden +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import contract +from . import invoice diff --git a/contract/models/contract.py b/contract/models/contract.py new file mode 100644 index 00000000..618bf8e5 --- /dev/null +++ b/contract/models/contract.py @@ -0,0 +1,259 @@ +# -*- coding: utf-8 -*- +# © 2004-2010 OpenERP SA +# © 2014 Angel Moya +# © 2015 Pedro M. Baeza +# © 2016 Carlos Dauden +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from dateutil.relativedelta import relativedelta +import logging +import time + +from openerp import api, fields, models +from openerp.addons.decimal_precision import decimal_precision as dp +from openerp.exceptions import ValidationError +from openerp.tools.translate import _ + +_logger = logging.getLogger(__name__) + + +class AccountAnalyticInvoiceLine(models.Model): + _name = 'account.analytic.invoice.line' + + product_id = fields.Many2one( + 'product.product', string='Product', required=True) + analytic_account_id = fields.Many2one( + 'account.analytic.account', string='Analytic Account') + name = fields.Text(string='Description', required=True) + quantity = fields.Float(default=1.0, required=True) + uom_id = fields.Many2one( + 'product.uom', string='Unit of Measure', required=True) + price_unit = fields.Float('Unit Price', required=True) + price_subtotal = fields.Float( + compute='_compute_price_subtotal', + digits_compute=dp.get_precision('Account'), + string='Sub Total') + discount = fields.Float( + string='Discount (%)', + digits=dp.get_precision('Discount'), + help='Discount that is applied in generated invoices.' + ' It should be less or equal to 100') + + @api.multi + @api.depends('quantity', 'price_unit', 'discount') + def _compute_price_subtotal(self): + for line in self: + subtotal = line.quantity * line.price_unit + discount = line.discount / 100 + subtotal *= 1 - discount + if line.analytic_account_id.pricelist_id: + cur = line.analytic_account_id.pricelist_id.currency_id + line.price_subtotal = cur.round(subtotal) + else: + line.price_subtotal = subtotal + + @api.one + @api.constrains('discount') + def _check_discount(self): + if self.discount > 100: + raise ValidationError(_("Discount should be less or equal to 100")) + + @api.multi + @api.onchange('product_id') + def _onchange_product_id(self): + if not self.product_id: + return {'domain': {'uom_id': []}} + + vals = {} + domain = {'uom_id': [ + ('category_id', '=', self.product_id.uom_id.category_id.id)]} + if not self.uom_id or (self.product_id.uom_id.category_id.id != + self.uom_id.category_id.id): + vals['uom_id'] = self.product_id.uom_id + + product = self.product_id.with_context( + lang=self.analytic_account_id.partner_id.lang, + partner=self.analytic_account_id.partner_id.id, + quantity=self.quantity, + date=self.analytic_account_id.recurring_next_date, + pricelist=self.analytic_account_id.pricelist_id.id, + uom=self.uom_id.id + ) + + name = product.name_get()[0][1] + if product.description_sale: + name += '\n' + product.description_sale + vals['name'] = name + + vals['price_unit'] = product.price + self.update(vals) + return {'domain': domain} + + +class AccountAnalyticAccount(models.Model): + _inherit = 'account.analytic.account' + + @api.model + def _default_journal(self): + company_id = self.env.context.get( + 'company_id', self.env.user.company_id.id) + domain = [ + ('type', '=', 'sale'), + ('company_id', '=', company_id)] + return self.env['account.journal'].search(domain, limit=1) + + pricelist_id = fields.Many2one( + comodel_name='product.pricelist', + string='Pricelist') + date_start = fields.Date(default=fields.Date.context_today) + recurring_invoice_line_ids = fields.One2many( + comodel_name='account.analytic.invoice.line', + inverse_name='analytic_account_id', + string='Invoice Lines') + recurring_invoices = fields.Boolean( + string='Generate recurring invoices automatically') + recurring_rule_type = fields.Selection( + [('daily', 'Day(s)'), + ('weekly', 'Week(s)'), + ('monthly', 'Month(s)'), + ('yearly', 'Year(s)'), + ], + default='monthly', + string='Recurrency', + help="Specify Interval for automatic invoice generation.") + recurring_interval = fields.Integer( + default=1, + string='Repeat Every', + help="Repeat every (Days/Week/Month/Year)") + recurring_next_date = fields.Date( + default=fields.Date.context_today, + copy=False, + string='Date of Next Invoice') + journal_id = fields.Many2one( + 'account.journal', + string='Journal', + default=_default_journal, + domain="[('type', '=', 'sale'),('company_id', '=', company_id)]") + + @api.onchange('partner_id') + def _onchange_partner_id(self): + self.pricelist_id = self.partner_id.property_product_pricelist.id + + @api.onchange('recurring_invoices') + def _onchange_recurring_invoices(self): + if self.date_start and self.recurring_invoices: + self.recurring_next_date = self.date_start + + @api.model + def _insert_markers(self, line, date_start, next_date, date_format): + line = line.replace('#START#', date_start.strftime(date_format)) + date_end = next_date - relativedelta(days=1) + line = line.replace('#END#', date_end.strftime(date_format)) + return line + + @api.model + def _prepare_invoice_line(self, line, invoice_id): + invoice_line = self.env['account.invoice.line'].new({ + 'invoice_id': invoice_id, + 'product_id': line.product_id.id, + 'quantity': line.quantity, + 'uom_id': line.uom_id.id, + 'discount': line.discount, + }) + # Get other invoice line values from product onchange + invoice_line._onchange_product_id() + invoice_line_vals = invoice_line._convert_to_write(invoice_line._cache) + + name = line.name + if 'old_date' in self.env.context and 'next_date' in self.env.context: + lang_obj = self.env['res.lang'] + contract = line.analytic_account_id + lang = lang_obj.search( + [('code', '=', contract.partner_id.lang)]) + date_format = lang.date_format or '%m/%d/%Y' + name = self._insert_markers( + name, self.env.context['old_date'], + self.env.context['next_date'], date_format) + + invoice_line_vals.update({ + 'name': name, + 'account_analytic_id': contract.id, + 'price_unit': line.price_unit, + }) + return invoice_line_vals + + @api.model + def _prepare_invoice(self, contract): + if not contract.partner_id: + raise ValidationError( + _("You must first select a Customer for Contract %s!") % + contract.name) + journal = contract.journal_id or self.env['account.journal'].search( + [('type', '=', 'sale'), + ('company_id', '=', contract.company_id.id)], + limit=1) + if not journal: + raise ValidationError( + _("Please define a sale journal for the company '%s'.") % + (contract.company_id.name or '',)) + currency = ( + contract.pricelist_id.currency_id or + contract.partner_id.property_product_pricelist.currency_id or + contract.company_id.currency_id + ) + invoice = self.env['account.invoice'].new({ + 'reference': contract.code, + 'type': 'out_invoice', + 'partner_id': contract.partner_id, + 'currency_id': currency.id, + 'journal_id': journal.id, + 'date_invoice': contract.recurring_next_date, + 'origin': contract.name, + 'company_id': contract.company_id.id, + 'contract_id': contract.id, + }) + # Get other invoice values from partner onchange + invoice._onchange_partner_id() + return invoice._convert_to_write(invoice._cache) + + @api.model + def _create_invoice(self, contract): + invoice_vals = self._prepare_invoice(contract) + invoice = self.env['account.invoice'].create(invoice_vals) + for line in contract.recurring_invoice_line_ids: + invoice_line_vals = self._prepare_invoice_line(line, invoice.id) + self.env['account.invoice.line'].create(invoice_line_vals) + invoice.compute_taxes() + return invoice + + @api.model + def recurring_create_invoice(self, automatic=False): + current_date = time.strftime('%Y-%m-%d') + contracts = self.search( + [('recurring_next_date', '<=', current_date), + ('account_type', '=', 'normal'), + ('recurring_invoices', '=', True)]) + for contract in contracts: + old_date = fields.Date.from_string( + contract.recurring_next_date or fields.Date.today()) + interval = contract.recurring_interval + if contract.recurring_rule_type == 'daily': + new_date = old_date + relativedelta(days=interval) + elif contract.recurring_rule_type == 'weekly': + new_date = old_date + relativedelta(weeks=interval) + else: + new_date = old_date + relativedelta(months=interval) + ctx = self.env.context.copy() + ctx.update({ + 'old_date': old_date, + 'next_date': new_date, + # Force company for correct evaluate domain access rules + 'force_company': contract.company_id.id, + }) + # Re-read contract with correct company + contract = contract.with_context(ctx) + self.with_context(ctx)._create_invoice(contract) + contract.write({ + 'recurring_next_date': new_date.strftime('%Y-%m-%d') + }) + return True diff --git a/contract/models/invoice.py b/contract/models/invoice.py new file mode 100644 index 00000000..8761dfa3 --- /dev/null +++ b/contract/models/invoice.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +# © 2016 Carlos Dauden +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from openerp import fields, models + + +class AccountInvoice(models.Model): + _inherit = 'account.invoice' + + contract_id = fields.Many2one( + 'account.analytic.account', + string='Contract') diff --git a/contract/tests/__init__.py b/contract/tests/__init__.py new file mode 100644 index 00000000..2002a1d8 --- /dev/null +++ b/contract/tests/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# © 2016 Carlos Dauden +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import test_contract diff --git a/contract/tests/test_contract.py b/contract/tests/test_contract.py new file mode 100644 index 00000000..7adb7335 --- /dev/null +++ b/contract/tests/test_contract.py @@ -0,0 +1,125 @@ +# -*- coding: utf-8 -*- +# © 2016 Carlos Dauden +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from dateutil.relativedelta import relativedelta +import datetime + +from openerp.exceptions import ValidationError +from openerp.tests.common import TransactionCase + + +class TestContract(TransactionCase): + # Use case : Prepare some data for current test case + def setUp(self): + super(TestContract, self).setUp() + self.partner = self.env.ref('base.res_partner_2') + self.product = self.env.ref('product.product_product_2') + self.tax = self.env.ref('l10n_generic_coa.sale_tax_template') + self.product.taxes_id = self.tax.ids + self.product.description_sale = 'Test description sale' + self.contract = self.env['account.analytic.account'].create({ + 'name': 'Test Contract', + 'partner_id': self.partner.id, + 'pricelist_id': self.partner.property_product_pricelist.id, + 'recurring_invoices': True, + }) + self.contract_line = self.env['account.analytic.invoice.line'].create({ + 'analytic_account_id': self.contract.id, + 'product_id': self.product.id, + 'name': 'Services from #START# to #END#', + 'quantity': 1, + 'uom_id': self.product.uom_id.id, + 'price_unit': 100, + 'discount': 50, + }) + self.current_date = datetime.date.today() + self.contract_daily = self.contract.copy() + self.contract_daily.recurring_rule_type = 'daily' + self.contract_weekly = self.contract.copy() + self.contract_weekly.recurring_rule_type = 'weekly' + + def test_check_discount(self): + with self.assertRaises(ValidationError): + self.contract_line.write({'discount': 120}) + + def test_contract(self): + self.assertAlmostEqual(self.contract_line.price_subtotal, 50.0) + res = self.contract_line._onchange_product_id() + self.assertIn('uom_id', res['domain']) + self.contract_line.price_unit = 100.0 + + self.contract.partner_id = False + with self.assertRaises(ValidationError): + self.contract.recurring_create_invoice() + self.contract.partner_id = self.partner.id + + self.contract.recurring_create_invoice() + self.invoice_monthly = self.env['account.invoice'].search( + [('contract_id', '=', self.contract.id)]) + self.assertTrue(self.invoice_monthly) + new_date = self.current_date + relativedelta( + months=self.contract.recurring_interval) + self.assertEqual(self.contract.recurring_next_date, + new_date.strftime('%Y-%m-%d')) + + self.inv_line = self.invoice_monthly.invoice_line_ids[0] + self.assertAlmostEqual(self.inv_line.price_subtotal, 50.0) + self.assertTrue(self.inv_line.invoice_line_tax_ids) + + def test_contract_daily(self): + self.contract_daily.pricelist_id = False + self.contract_daily.recurring_create_invoice() + invoice_daily = self.env['account.invoice'].search( + [('contract_id', '=', self.contract_daily.id)]) + self.assertTrue(invoice_daily) + new_date = self.current_date + relativedelta( + days=self.contract_daily.recurring_interval) + self.assertEqual(self.contract_daily.recurring_next_date, + new_date.strftime('%Y-%m-%d')) + + def test_contract_weekly(self): + self.contract_weekly.recurring_create_invoice() + invoices_weekly = self.env['account.invoice'].search( + [('contract_id', '=', self.contract_weekly.id)]) + self.assertTrue(invoices_weekly) + new_date = self.current_date + relativedelta( + weeks=self.contract_weekly.recurring_interval) + self.assertEqual(self.contract_weekly.recurring_next_date, + new_date.strftime('%Y-%m-%d')) + + def test_onchange_partner_id(self): + self.contract._onchange_partner_id() + self.assertEqual(self.contract.pricelist_id, + self.contract.partner_id.property_product_pricelist) + + def test_onchange_recurring_invoices(self): + self.contract.recurring_next_date = False + self.contract._onchange_recurring_invoices() + self.assertEqual(self.contract.recurring_next_date, + self.contract.date_start) + + def test_uom(self): + uom_litre = self.env.ref('product.product_uom_litre') + self.contract_line.uom_id = uom_litre.id + self.contract_line._onchange_product_id() + self.assertEqual(self.contract_line.uom_id, + self.contract_line.product_id.uom_id) + + def test_onchange_product_id(self): + line = self.env['account.analytic.invoice.line'].new() + res = line._onchange_product_id() + self.assertFalse(res['domain']['uom_id']) + + def test_no_pricelist(self): + self.contract.pricelist_id = False + self.contract_line.quantity = 2 + self.assertAlmostEqual(self.contract_line.price_subtotal, 100.0) + + def test_check_journal(self): + contract_no_journal = self.contract.copy() + contract_no_journal.journal_id = False + journal = self.env['account.journal'].search([('type', '=', 'sale')]) + journal.write({'type': 'general'}) + with self.assertRaises(ValidationError): + contract_no_journal.recurring_create_invoice() diff --git a/contract/views/account_invoice_view.xml b/contract/views/account_invoice_view.xml new file mode 100644 index 00000000..09752e9f --- /dev/null +++ b/contract/views/account_invoice_view.xml @@ -0,0 +1,19 @@ + + + + + + + account.invoice.select.contract + account.invoice + + + + + + + + + + + diff --git a/contract/views/contract.xml b/contract/views/contract.xml new file mode 100644 index 00000000..ca5d34ff --- /dev/null +++ b/contract/views/contract.xml @@ -0,0 +1,115 @@ + + + + + + {'search_default_contract_id': + [active_id], + 'default_contract_id': active_id} + + Invoices + account.invoice + + + + + + account.analytic.account.invoice.recurring.form.inherit + account.analytic.account + + + + + +
+ +
+ + + + +
+
+
+ + + + account.analytic.account.journal.list + account.analytic.account + + + + + + + + + + + account.analytic.account.contract.search + account.analytic.account + + + + + + + + + + + + + + + + Contracts + account.analytic.account + form + tree,form + {'search_default_active':1, 'search_default_recurring_invoices':1} + + +

+ Click to create a new contract. +

+
+
+ + +
+