diff --git a/contract/README.rst b/contract/README.rst new file mode 100644 index 000000000..e369c2dce --- /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 new file mode 100644 index 000000000..a0fdc10fe --- /dev/null +++ b/contract/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from . import models diff --git a/contract/__openerp__.py b/contract/__openerp__.py new file mode 100644 index 000000000..76011715c --- /dev/null +++ b/contract/__openerp__.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# © 2004-2010 OpenERP SA +# © 2016 Carlos Dauden +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + 'name': 'Contracts Management recurring', + 'version': '9.0.1.0.0', + 'category': 'Other', + 'license': 'AGPL-3', + 'author': "OpenERP SA," + "Tecnativa," + "Odoo Community Association (OCA)", + 'website': 'http://openerp.com', + 'depends': ['base', 'account', 'analytic'], + 'data': [ + 'security/ir.model.access.csv', + 'data/contract_cron.xml', + 'views/contract.xml', + 'views/account_invoice_view.xml', + ], + 'installable': True, + 'images': [], +} diff --git a/contract/data/contract_cron.xml b/contract/data/contract_cron.xml new file mode 100644 index 000000000..95ae54de8 --- /dev/null +++ b/contract/data/contract_cron.xml @@ -0,0 +1,16 @@ + + + + + + Generate Recurring Invoices from Contracts + 1 + days + -1 + + + + + + + diff --git a/contract/i18n/es.po b/contract/i18n/es.po new file mode 100644 index 000000000..5557f41c6 --- /dev/null +++ b/contract/i18n/es.po @@ -0,0 +1,293 @@ +# Translation of OpenERP Server. +# This file contains the translation of the following modules: +# * account_analytic_analysis_recurring +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo 9.0\n" +"Report-Msgid-Bugs-To: \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" +"X-Generator: Poedit 1.5.4\n" + +#. 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: 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: 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: 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óxima fecha de factura" + +#. 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: contract +#: model:ir.model.fields,field_description:contract.field_account_analytic_invoice_line_name +msgid "Description" +msgstr "Descripción" + +#. 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 "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: 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: 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: 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: 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: contract +#: model:ir.ui.view,arch_db:contract.view_account_analytic_account_contract_search +msgid "Next Invoice" +msgstr "Próxima factura" + +#. module: contract +#: code:addons/contract/models/contract.py:197 +#, python-format +msgid "Please define a sale journal for the company '%s'." +msgstr "Por favor define un diario de ventas para la compañía '%s'." + +#. 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: contract +#: model:ir.model.fields,field_description:contract.field_account_analytic_invoice_line_quantity +msgid "Quantity" +msgstr "Cantidad" + +#. module: contract +#: model:ir.model.fields,field_description:contract.field_account_analytic_account_recurring_rule_type +msgid "Recurrency" +msgstr "Recurrencia" + +#. 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: contract +#: model:ir.model.fields,field_description:contract.field_account_analytic_account_recurring_interval +msgid "Repeat Every" +msgstr "Repetir cada" + +#. 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: 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: contract +#: model:ir.model.fields,field_description:contract.field_account_analytic_invoice_line_price_unit +msgid "Unit Price" +msgstr "Precio unidad" + +#. 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: contract +#: selection:account.analytic.account,recurring_rule_type:0 +msgid "Week(s)" +msgstr "Semana(s)" + +#. module: contract +#: selection:account.analytic.account,recurring_rule_type:0 +msgid "Year(s)" +msgstr "Año(s)" + +#. 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: 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/i18n/nl.po b/contract/i18n/nl.po new file mode 100644 index 000000000..b04b804dd --- /dev/null +++ b/contract/i18n/nl.po @@ -0,0 +1,156 @@ +# 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-07-11 13:24+0000\n" +"PO-Revision-Date: 2014-07-11 13:24+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 +#: view:account.analytic.account:0 +msgid ". create invoices" +msgstr ". create invoices" + +#. module: account_analytic_analysis_recurring +#: view:account.analytic.account:0 +msgid "Account Analytic Lines" +msgstr "Kostenplaatsenboekingen" + +#. 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 +msgid "Analytic Account" +msgstr "Kostenplaats" + +#. module: account_analytic_analysis_recurring +#: field:account.analytic.account,recurring_next_date:0 +msgid "Date of Next Invoice" +msgstr "Datum volgende factuur" + +#. module: account_analytic_analysis_recurring +#: selection:account.analytic.account,recurring_rule_type:0 +msgid "Day(s)" +msgstr "Dag(en)" + +#. module: account_analytic_analysis_recurring +#: field:account.analytic.invoice.line,name:0 +msgid "Description" +msgstr "Omschrijving" + +#. module: account_analytic_analysis_recurring +#: code:addons/account_analytic_analysis_recurring/account_analytic_analysis_recurring.py:130 +#, python-format +msgid "Error!" +msgstr "Fout" + +#. module: account_analytic_analysis_recurring +#: field:account.analytic.account,recurring_invoices:0 +msgid "Generate recurring invoices automatically" +msgstr "Periodieke facturering" + +#. module: account_analytic_analysis_recurring +#: field:account.analytic.account,recurring_invoice_line_ids:0 +msgid "Invoice Lines" +msgstr "Sjablonen factuurregels" + +#. module: account_analytic_analysis_recurring +#: help:account.analytic.account,recurring_rule_type:0 +msgid "Invoice automatically repeat at specified interval" +msgstr "Factureer automatisch met dit interval" + +#. module: account_analytic_analysis_recurring +#: selection:account.analytic.account,recurring_rule_type:0 +msgid "Month(s)" +msgstr "Maand(en)" + +#. module: account_analytic_analysis_recurring +#: code:addons/account_analytic_analysis_recurring/account_analytic_analysis_recurring.py:125 +#, python-format +msgid "No Customer Defined!" +msgstr "Er is geen klant ingesteld." + +#. module: account_analytic_analysis_recurring +#: code:addons/account_analytic_analysis_recurring/account_analytic_analysis_recurring.py:131 +#, python-format +msgid "Please define a sale journal for the company \"%s\"." +msgstr "Er moet een inkoopdagboek worden ingesteld voor bedrijf \"%s\"." + +#. module: account_analytic_analysis_recurring +#: field:account.analytic.invoice.line,product_id:0 +msgid "Product" +msgstr "Product" + +#. module: account_analytic_analysis_recurring +#: field:account.analytic.invoice.line,quantity:0 +msgid "Quantity" +msgstr "Hoeveelheid" + +#. module: account_analytic_analysis_recurring +#: field:account.analytic.account,recurring_rule_type:0 +msgid "Recurrency" +msgstr "Herhaling" + +#. module: account_analytic_analysis_recurring +#: view:account.analytic.account:0 +msgid "Recurring Invoices" +msgstr "Periodieke facturen" + +#. module: account_analytic_analysis_recurring +#: field:account.analytic.account,recurring_interval:0 +msgid "Repeat Every" +msgstr "Herhaal elke:" + +#. module: account_analytic_analysis_recurring +#: help:account.analytic.account,recurring_interval:0 +msgid "Repeat every (Days/Week/Month/Year)" +msgstr "Herhaal elke (dag/week/maand/jaar)" + +#. module: account_analytic_analysis_recurring +#: field:account.analytic.invoice.line,price_subtotal:0 +msgid "Sub Total" +msgstr "Subtotaal" + +#. module: account_analytic_analysis_recurring +#: field:account.analytic.invoice.line,price_unit:0 +msgid "Unit Price" +msgstr "Prijs per eenheid" + +#. module: account_analytic_analysis_recurring +#: field:account.analytic.invoice.line,uom_id:0 +msgid "Unit of Measure" +msgstr "Maateenheid" + +#. module: account_analytic_analysis_recurring +#: selection:account.analytic.account,recurring_rule_type:0 +msgid "Week(s)" +msgstr "Week/weken" + +#. module: account_analytic_analysis_recurring +#: selection:account.analytic.account,recurring_rule_type:0 +msgid "Year(s)" +msgstr "Jaar" + +#. module: account_analytic_analysis_recurring +#: code:addons/account_analytic_analysis_recurring/account_analytic_analysis_recurring.py:125 +#, python-format +msgid "You must first select a Customer for Contract %s!" +msgstr " Er moet eerst een klant worden ingesteld op contract %s!" + +#. module: account_analytic_analysis_recurring +#: code:_description:0 +#: model:ir.model,name:account_analytic_analysis_recurring.model_account_analytic_invoice_line +#, python-format +msgid "account.analytic.invoice.line" +msgstr "account.analytic.invoice.line" + diff --git a/contract/models/__init__.py b/contract/models/__init__.py new file mode 100644 index 000000000..8deef4105 --- /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 000000000..618bf8e55 --- /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 000000000..8761dfa39 --- /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/security/ir.model.access.csv b/contract/security/ir.model.access.csv new file mode 100644 index 000000000..902c5d592 --- /dev/null +++ b/contract/security/ir.model.access.csv @@ -0,0 +1,4 @@ +"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink" +"account_analytic_invoice_line_manager","Recurring manager","model_account_analytic_invoice_line","base.group_sale_manager",1,1,1,1 +"account_analytic_invoice_line_user","Recurring user","model_account_analytic_invoice_line","base.group_sale_salesman",1,0,0,0 + diff --git a/contract/tests/__init__.py b/contract/tests/__init__.py new file mode 100644 index 000000000..2002a1d82 --- /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 000000000..7adb7335e --- /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 000000000..09752e9f7 --- /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 000000000..ca5d34ff4 --- /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. +

+
+
+ + +
+