diff --git a/contract/__manifest__.py b/contract/__manifest__.py index 7bdc150c..8ab77964 100644 --- a/contract/__manifest__.py +++ b/contract/__manifest__.py @@ -4,17 +4,19 @@ # Copyright 2016-2018 Tecnativa - Carlos Dauden # Copyright 2017 Tecnativa - Vicent Cubells # Copyright 2016-2017 LasLabs Inc. +# Copyright 2018 ACSONE SA/NV # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). { - 'name': 'Contracts Management - Recurring', - 'version': '12.0.1.0.0', + 'name': 'Recurring - Contracts Management', + 'version': '12.0.2.0.0', 'category': 'Contract Management', 'license': 'AGPL-3', 'author': "OpenERP SA, " - "Tecnativa, " - "LasLabs, " - "Odoo Community Association (OCA)", + "Tecnativa, " + "LasLabs, " + "ACSONE SA/NV, " + "Odoo Community Association (OCA)", 'website': 'https://github.com/oca/contract', 'depends': ['base', 'account', 'analytic'], 'data': [ @@ -24,9 +26,12 @@ 'report/contract_views.xml', 'data/contract_cron.xml', 'data/mail_template.xml', - 'views/account_analytic_account_view.xml', - 'views/account_analytic_contract_view.xml', + 'views/abstract_contract_line.xml', + 'views/contract.xml', + 'views/contract_template_line.xml', + 'views/contract_template.xml', 'views/account_invoice_view.xml', + 'views/contract_line.xml', 'views/res_partner_view.xml', ], 'installable': True, diff --git a/contract/migrations/12.0.2.0.0/post-migration.py b/contract/migrations/12.0.2.0.0/post-migration.py new file mode 100644 index 00000000..a197d9b7 --- /dev/null +++ b/contract/migrations/12.0.2.0.0/post-migration.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Copyright 2018 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + + +def migrate(cr, version): + """Copy recurrence info from contract to contract lines.""" + + cr.execute( + """UPDATE account_analytic_invoice_line AS contract_line + SET recurring_rule_type=contract.recurring_rule_type, + recurring_invoicing_type=contract.recurring_invoicing_type, + recurring_interval=contract.recurring_interval, + recurring_next_date=contract.recurring_next_date, + date_start=contract.date_start, + date_end=contract.date_end + FROM account_analytic_account AS contract + WHERE contract.id=contract_line.contract_id""" + ) diff --git a/contract/models/__init__.py b/contract/models/__init__.py index 406cc389..0bbc06e9 100644 --- a/contract/models/__init__.py +++ b/contract/models/__init__.py @@ -1,8 +1,10 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from . import account_analytic_contract -from . import account_analytic_account -from . import account_analytic_contract_line -from . import account_analytic_invoice_line +from . import abstract_contract +from . import abstract_contract_line +from . import contract_template +from . import contract +from . import contract_template_line +from . import contract_line from . import account_invoice from . import res_partner diff --git a/contract/models/abstract_contract.py b/contract/models/abstract_contract.py new file mode 100644 index 00000000..06dca173 --- /dev/null +++ b/contract/models/abstract_contract.py @@ -0,0 +1,69 @@ +# Copyright 2004-2010 OpenERP SA +# Copyright 2014 Angel Moya +# Copyright 2015 Pedro M. Baeza +# Copyright 2016-2018 Carlos Dauden +# Copyright 2016-2017 LasLabs Inc. +# Copyright 2018 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, models, fields + + +class AbstractAccountAnalyticContract(models.AbstractModel): + _name = 'account.abstract.analytic.contract' + _description = 'Abstract Account Analytic Contract' + + # These fields will not be synced to the contract + NO_SYNC = ['name', 'partner_id'] + + name = fields.Char(required=True) + # Needed for avoiding errors on several inherited behaviors + partner_id = fields.Many2one( + comodel_name="res.partner", string="Partner (always False)" + ) + pricelist_id = fields.Many2one( + comodel_name='product.pricelist', string='Pricelist' + ) + contract_type = fields.Selection( + selection=[('sale', 'Customer'), ('purchase', 'Supplier')], + default='sale', + ) + + journal_id = fields.Many2one( + 'account.journal', + string='Journal', + default=lambda s: s._default_journal(), + domain="[('type', '=', contract_type)," + "('company_id', '=', company_id)]", + ) + company_id = fields.Many2one( + 'res.company', + string='Company', + required=True, + default=lambda self: self.env.user.company_id, + ) + + @api.onchange('contract_type') + def _onchange_contract_type(self): + if self.contract_type == 'purchase': + self.recurring_invoice_line_ids.filtered('automatic_price').update( + {'automatic_price': False} + ) + self.journal_id = self.env['account.journal'].search( + [ + ('type', '=', self.contract_type), + ('company_id', '=', self.company_id.id), + ], + limit=1, + ) + + @api.model + def _default_journal(self): + company_id = self.env.context.get( + 'company_id', self.env.user.company_id.id + ) + domain = [ + ('type', '=', self.contract_type), + ('company_id', '=', company_id), + ] + return self.env['account.journal'].search(domain, limit=1) diff --git a/contract/models/abstract_contract_line.py b/contract/models/abstract_contract_line.py new file mode 100644 index 00000000..8596389b --- /dev/null +++ b/contract/models/abstract_contract_line.py @@ -0,0 +1,184 @@ +# Copyright 2004-2010 OpenERP SA +# Copyright 2014 Angel Moya +# Copyright 2015 Pedro M. Baeza +# Copyright 2016-2018 Carlos Dauden +# Copyright 2016-2017 LasLabs Inc. +# Copyright 2018 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, models, fields +from odoo.addons import decimal_precision as dp +from odoo.exceptions import ValidationError +from odoo.tools.translate import _ + + +class AccountAbstractAnalyticContractLine(models.AbstractModel): + _name = 'account.abstract.analytic.contract.line' + _description = 'Account Abstract Analytic Contract Line' + + product_id = fields.Many2one( + 'product.product', string='Product', required=True + ) + + name = fields.Text(string='Description', required=True) + quantity = fields.Float(default=1.0, required=True) + uom_id = fields.Many2one( + 'uom.uom', string='Unit of Measure', required=True + ) + automatic_price = fields.Boolean( + string="Auto-price?", + help="If this is marked, the price will be obtained automatically " + "applying the pricelist to the product. If not, you will be " + "able to introduce a manual price", + ) + specific_price = fields.Float(string='Specific Price') + price_unit = fields.Float( + string='Unit Price', + compute="_compute_price_unit", + inverse="_inverse_price_unit", + ) + price_subtotal = fields.Float( + compute='_compute_price_subtotal', + digits=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', + ) + sequence = fields.Integer( + string="Sequence", + default=10, + help="Sequence of the contract line when displaying contracts", + ) + recurring_rule_type = fields.Selection( + [ + ('daily', 'Day(s)'), + ('weekly', 'Week(s)'), + ('monthly', 'Month(s)'), + ('monthlylastday', 'Month(s) last day'), + ('yearly', 'Year(s)'), + ], + default='monthly', + string='Recurrence', + help="Specify Interval for automatic invoice generation.", + required=True, + ) + recurring_invoicing_type = fields.Selection( + [('pre-paid', 'Pre-paid'), ('post-paid', 'Post-paid')], + default='pre-paid', + string='Invoicing type', + help="Specify if process date is 'from' or 'to' invoicing date", + required=True, + ) + recurring_interval = fields.Integer( + default=1, + string='Repeat Every', + help="Repeat every (Days/Week/Month/Year)", + required=True, + ) + + partner_id = fields.Many2one( + comodel_name="res.partner", string="Partner (always False)" + ) + pricelist_id = fields.Many2one( + comodel_name='product.pricelist', string='Pricelist' + ) + recurring_next_date = fields.Date( + copy=False, string='Date of Next Invoice' + ) + + @api.depends( + 'automatic_price', + 'specific_price', + 'product_id', + 'quantity', + 'pricelist_id', + 'partner_id', + ) + def _compute_price_unit(self): + """Get the specific price if no auto-price, and the price obtained + from the pricelist otherwise. + """ + for line in self: + if line.automatic_price: + product = line.product_id.with_context( + quantity=line.env.context.get( + 'contract_line_qty', line.quantity + ), + pricelist=line.pricelist_id.id, + partner=line.partner_id.id, + date=line.env.context.get('old_date', fields.Date.today()), + ) + line.price_unit = product.price + else: + line.price_unit = line.specific_price + + # Tip in https://github.com/odoo/odoo/issues/23891#issuecomment-376910788 + @api.onchange('price_unit') + def _inverse_price_unit(self): + """Store the specific price in the no auto-price records.""" + for line in self.filtered(lambda x: not x.automatic_price): + line.specific_price = line.price_unit + + @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.pricelist_id: + cur = line.pricelist_id.currency_id + line.price_subtotal = cur.round(subtotal) + else: + line.price_subtotal = subtotal + + @api.multi + @api.constrains('discount') + def _check_discount(self): + for line in self: + if line.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 + + date = self.recurring_next_date or fields.Date.today() + partner = self.partner_id or self.env.user.partner_id + + product = self.product_id.with_context( + lang=partner.lang, + partner=partner.id, + quantity=self.quantity, + date=date, + pricelist=self.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} diff --git a/contract/models/account_analytic_account.py b/contract/models/account_analytic_account.py deleted file mode 100644 index 5e66f24f..00000000 --- a/contract/models/account_analytic_account.py +++ /dev/null @@ -1,361 +0,0 @@ -# Copyright 2004-2010 OpenERP SA -# Copyright 2014 Angel Moya -# Copyright 2015 Pedro M. Baeza -# Copyright 2016-2018 Carlos Dauden -# Copyright 2016-2017 LasLabs Inc. -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). - -from dateutil.relativedelta import relativedelta - -from odoo import api, fields, models -from odoo.exceptions import ValidationError -from odoo.tools.translate import _ - - -class AccountAnalyticAccount(models.Model): - _name = 'account.analytic.account' - _inherit = ['account.analytic.account', - 'account.analytic.contract', - ] - - contract_template_id = fields.Many2one( - string='Contract Template', - comodel_name='account.analytic.contract', - ) - recurring_invoice_line_ids = fields.One2many( - string='Invoice Lines', - comodel_name='account.analytic.invoice.line', - inverse_name='analytic_account_id', - copy=True, - ) - date_start = fields.Date( - string='Date Start', - default=fields.Date.context_today, - ) - date_end = fields.Date( - string='Date End', - index=True, - ) - recurring_invoices = fields.Boolean( - string='Generate recurring invoices automatically', - ) - recurring_next_date = fields.Date( - default=fields.Date.context_today, - copy=False, - string='Date of Next Invoice', - ) - user_id = fields.Many2one( - comodel_name='res.users', - string='Responsible', - index=True, - default=lambda self: self.env.user, - ) - create_invoice_visibility = fields.Boolean( - compute='_compute_create_invoice_visibility', - ) - - @api.depends('recurring_next_date', 'date_end') - def _compute_create_invoice_visibility(self): - for contract in self: - contract.create_invoice_visibility = ( - not contract.date_end or - contract.recurring_next_date <= contract.date_end - ) - - @api.onchange('contract_template_id') - def _onchange_contract_template_id(self): - """Update the contract fields with that of the template. - - Take special consideration with the `recurring_invoice_line_ids`, - which must be created using the data from the contract lines. Cascade - deletion ensures that any errant lines that are created are also - deleted. - """ - contract = self.contract_template_id - if not contract: - return - for field_name, field in contract._fields.items(): - if field.name == 'recurring_invoice_line_ids': - lines = self._convert_contract_lines(contract) - self.recurring_invoice_line_ids = lines - elif not any(( - field.compute, field.related, field.automatic, - field.readonly, field.company_dependent, - field.name in self.NO_SYNC, - )): - self[field_name] = self.contract_template_id[field_name] - - @api.onchange('date_start') - def _onchange_date_start(self): - if self.date_start: - self.recurring_next_date = self.date_start - - @api.onchange('partner_id') - def _onchange_partner_id(self): - self.pricelist_id = self.partner_id.property_product_pricelist.id - - @api.constrains('partner_id', 'recurring_invoices') - def _check_partner_id_recurring_invoices(self): - for contract in self.filtered('recurring_invoices'): - if not contract.partner_id: - raise ValidationError( - _("You must supply a customer for the contract '%s'") % - contract.name - ) - - @api.constrains('recurring_next_date', 'date_start') - def _check_recurring_next_date_start_date(self): - for contract in self.filtered('recurring_next_date'): - if contract.date_start > contract.recurring_next_date: - raise ValidationError( - _("You can't have a next invoicing date before the start " - "of the contract '%s'") % contract.name - ) - - @api.constrains('recurring_next_date', 'recurring_invoices') - def _check_recurring_next_date_recurring_invoices(self): - for contract in self.filtered('recurring_invoices'): - if not contract.recurring_next_date: - raise ValidationError( - _("You must supply a next invoicing date for contract " - "'%s'") % contract.name - ) - - @api.constrains('date_start', 'recurring_invoices') - def _check_date_start_recurring_invoices(self): - for contract in self.filtered('recurring_invoices'): - if not contract.date_start: - raise ValidationError( - _("You must supply a start date for contract '%s'") % - contract.name - ) - - @api.constrains('date_start', 'date_end') - def _check_start_end_dates(self): - for contract in self.filtered('date_end'): - if contract.date_start > contract.date_end: - raise ValidationError( - _("Contract '%s' start date can't be later than end date") - % contract.name - ) - - @api.multi - def _convert_contract_lines(self, contract): - self.ensure_one() - new_lines = [] - for contract_line in contract.recurring_invoice_line_ids: - vals = contract_line._convert_to_write(contract_line.read()[0]) - # Remove template link field named as analytic account field - vals.pop('analytic_account_id', False) - new_lines.append((0, 0, vals)) - return new_lines - - @api.model - def get_relative_delta(self, recurring_rule_type, interval): - if recurring_rule_type == 'daily': - return relativedelta(days=interval) - elif recurring_rule_type == 'weekly': - return relativedelta(weeks=interval) - elif recurring_rule_type == 'monthly': - return relativedelta(months=interval) - elif recurring_rule_type == 'monthlylastday': - return relativedelta(months=interval, day=31) - else: - return relativedelta(years=interval) - - @api.model - def _insert_markers(self, line, date_format): - date_from = fields.Date.from_string(line.date_from) - date_to = fields.Date.from_string(line.date_to) - name = line.name - name = name.replace('#START#', date_from.strftime(date_format)) - name = name.replace('#END#', date_to.strftime(date_format)) - return name - - @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) - # Insert markers - contract = line.analytic_account_id - lang_obj = self.env['res.lang'] - lang = lang_obj.search( - [('code', '=', contract.partner_id.lang)]) - date_format = lang.date_format or '%m/%d/%Y' - name = self._insert_markers(line, date_format) - invoice_line_vals.update({ - 'name': name, - 'account_analytic_id': contract.id, - 'price_unit': line.price_unit, - }) - return invoice_line_vals - - @api.multi - def _prepare_invoice(self, journal=None): - self.ensure_one() - if not self.partner_id: - if self.contract_type == 'purchase': - raise ValidationError( - _("You must first select a Supplier for Contract %s!") % - self.name) - else: - raise ValidationError( - _("You must first select a Customer for Contract %s!") % - self.name) - if not journal: - journal = self.journal_id or self.env['account.journal'].search([ - ('type', '=', self.contract_type), - ('company_id', '=', self.company_id.id) - ], limit=1) - if not journal: - raise ValidationError( - _("Please define a %s journal for the company '%s'.") % - (self.contract_type, self.company_id.name or '') - ) - currency = ( - self.pricelist_id.currency_id or - self.partner_id.property_product_pricelist.currency_id or - self.company_id.currency_id - ) - invoice_type = 'out_invoice' - if self.contract_type == 'purchase': - invoice_type = 'in_invoice' - invoice = self.env['account.invoice'].new({ - 'reference': self.code, - 'type': invoice_type, - 'partner_id': self.partner_id.address_get( - ['invoice'])['invoice'], - 'currency_id': currency.id, - 'journal_id': journal.id, - 'date_invoice': self.recurring_next_date, - 'origin': self.name, - 'company_id': self.company_id.id, - 'contract_id': self.id, - 'user_id': self.partner_id.user_id.id, - }) - # Get other invoice values from partner onchange - invoice._onchange_partner_id() - return invoice._convert_to_write(invoice._cache) - - @api.multi - def _prepare_invoice_update(self, invoice): - vals = self._prepare_invoice() - update_vals = { - 'contract_id': self.id, - 'date_invoice': vals.get('date_invoice', False), - 'reference': ' '.join(filter(None, [ - invoice.reference, vals.get('reference')])), - 'origin': ' '.join(filter(None, [ - invoice.origin, vals.get('origin')])), - } - return update_vals - - @api.multi - def _create_invoice(self, invoice=False): - """ - :param invoice: If not False add lines to this invoice - :return: invoice created or updated - """ - self.ensure_one() - if invoice and invoice.state == 'draft': - invoice.update(self._prepare_invoice_update(invoice)) - else: - invoice = self.env['account.invoice'].create( - self._prepare_invoice()) - for line in self.recurring_invoice_line_ids: - invoice_line_vals = self._prepare_invoice_line(line, invoice.id) - if invoice_line_vals: - self.env['account.invoice.line'].create(invoice_line_vals) - invoice.compute_taxes() - return invoice - - @api.multi - def recurring_create_invoice(self): - """Create invoices from contracts - - :return: invoices created - """ - invoices = self.env['account.invoice'] - for contract in self: - ref_date = contract.recurring_next_date or fields.Date.today() - if (contract.date_start > ref_date or - contract.date_end and contract.date_end < ref_date): - if self.env.context.get('cron'): - continue # Don't fail on cron jobs - raise ValidationError( - _("You must review start and end dates!\n%s") % - contract.name - ) - old_date = fields.Date.from_string(ref_date) - new_date = old_date + self.get_relative_delta( - contract.recurring_rule_type, contract.recurring_interval) - ctx = self.env.context.copy() - ctx.update({ - 'old_date': old_date, - 'next_date': new_date, - # Force company for correct evaluation of domain access rules - 'force_company': contract.company_id.id, - }) - # Re-read contract with correct company - invoices |= contract.with_context(ctx)._create_invoice() - contract.write({ - 'recurring_next_date': fields.Date.to_string(new_date) - }) - return invoices - - @api.model - def cron_recurring_create_invoice(self): - today = fields.Date.today() - contracts = self.with_context(cron=True).search([ - ('recurring_invoices', '=', True), - ('recurring_next_date', '<=', today), - '|', - ('date_end', '=', False), - ('date_end', '>=', today), - ]) - return contracts.recurring_create_invoice() - - @api.multi - def action_contract_send(self): - self.ensure_one() - template = self.env.ref( - 'contract.email_contract_template', - False, - ) - compose_form = self.env.ref('mail.email_compose_message_wizard_form') - ctx = dict( - default_model='account.analytic.account', - default_res_id=self.id, - default_use_template=bool(template), - default_template_id=template and template.id or False, - default_composition_mode='comment', - ) - return { - 'name': _('Compose Email'), - 'type': 'ir.actions.act_window', - 'view_type': 'form', - 'view_mode': 'form', - 'res_model': 'mail.compose.message', - 'views': [(compose_form.id, 'form')], - 'view_id': compose_form.id, - 'target': 'new', - 'context': ctx, - } - - @api.multi - def button_show_recurring_invoices(self): - self.ensure_one() - action = self.env.ref( - 'contract.act_purchase_recurring_invoices') - if self.contract_type == 'sale': - action = self.env.ref( - 'contract.act_recurring_invoices') - return action.read()[0] diff --git a/contract/models/account_analytic_contract.py b/contract/models/account_analytic_contract.py deleted file mode 100644 index 356f90a0..00000000 --- a/contract/models/account_analytic_contract.py +++ /dev/null @@ -1,100 +0,0 @@ -# Copyright 2004-2010 OpenERP SA -# Copyright 2014 Angel Moya -# Copyright 2016 Carlos Dauden -# Copyright 2016-2017 LasLabs Inc. -# Copyright 2015-2017 Tecnativa - Pedro M. Baeza -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). - -from odoo import api, fields, models - - -class AccountAnalyticContract(models.Model): - _name = 'account.analytic.contract' - _description = "Account Analytic Contract" - - # These fields will not be synced to the contract - NO_SYNC = [ - 'name', - 'partner_id', - ] - - name = fields.Char( - required=True, - ) - # Needed for avoiding errors on several inherited behaviors - partner_id = fields.Many2one( - comodel_name="res.partner", - string="Partner (always False)", - ) - contract_type = fields.Selection( - selection=[ - ('sale', 'Customer'), - ('purchase', 'Supplier'), - ], default='sale', - ) - pricelist_id = fields.Many2one( - comodel_name='product.pricelist', - string='Pricelist', - ) - recurring_invoice_line_ids = fields.One2many( - comodel_name='account.analytic.contract.line', - inverse_name='analytic_account_id', - copy=True, - string='Invoice Lines', - ) - recurring_rule_type = fields.Selection( - [('daily', 'Day(s)'), - ('weekly', 'Week(s)'), - ('monthly', 'Month(s)'), - ('monthlylastday', 'Month(s) last day'), - ('yearly', 'Year(s)'), - ], - default='monthly', - string='Recurrence', - help="Specify Interval for automatic invoice generation.", - ) - recurring_invoicing_type = fields.Selection( - [('pre-paid', 'Pre-paid'), - ('post-paid', 'Post-paid'), - ], - default='pre-paid', - string='Invoicing type', - help="Specify if process date is 'from' or 'to' invoicing date", - ) - recurring_interval = fields.Integer( - default=1, - string='Repeat Every', - help="Repeat every (Days/Week/Month/Year)", - ) - journal_id = fields.Many2one( - 'account.journal', - string='Journal', - default=lambda s: s._default_journal(), - domain="[('type', '=', contract_type)," - "('company_id', '=', company_id)]", - ) - company_id = fields.Many2one( - 'res.company', - string='Company', - required=True, - default=lambda self: self.env.user.company_id, - ) - - @api.onchange('contract_type') - def _onchange_contract_type(self): - if self.contract_type == 'purchase': - self.recurring_invoice_line_ids.filtered('automatic_price').update( - {'automatic_price': False}) - self.journal_id = self.env['account.journal'].search([ - ('type', '=', self.contract_type), - ('company_id', '=', self.company_id.id) - ], limit=1) - - @api.model - def _default_journal(self): - company_id = self.env.context.get( - 'company_id', self.env.user.company_id.id) - domain = [ - ('type', '=', self.contract_type), - ('company_id', '=', company_id)] - return self.env['account.journal'].search(domain, limit=1) diff --git a/contract/models/account_analytic_contract_line.py b/contract/models/account_analytic_contract_line.py deleted file mode 100644 index 4eca3aa3..00000000 --- a/contract/models/account_analytic_contract_line.py +++ /dev/null @@ -1,221 +0,0 @@ -# Copyright 2004-2010 OpenERP SA -# Copyright 2014 Angel Moya -# Copyright 2016 Carlos Dauden -# Copyright 2016-2017 LasLabs Inc. -# Copyright 2015-2018 Tecnativa - Pedro M. Baeza -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). - -from dateutil.relativedelta import relativedelta - -from odoo import api, fields, models -from odoo.addons import decimal_precision as dp -from odoo.exceptions import ValidationError -from odoo.tools.translate import _ - - -class AccountAnalyticContractLine(models.Model): - _name = 'account.analytic.contract.line' - _description = 'Contract Lines' - _order = "sequence,id" - - product_id = fields.Many2one( - 'product.product', - string='Product', - required=True, - ) - analytic_account_id = fields.Many2one( - string='Contract', - comodel_name='account.analytic.contract', - required=True, - ondelete='cascade', - ) - name = fields.Text( - string='Description', - required=True, - ) - quantity = fields.Float( - default=1.0, - required=True, - ) - uom_id = fields.Many2one( - 'uom.uom', - string='Unit of Measure', - required=True, - ) - automatic_price = fields.Boolean( - string="Auto-price?", - help="If this is marked, the price will be obtained automatically " - "applying the pricelist to the product. If not, you will be " - "able to introduce a manual price", - ) - specific_price = fields.Float( - string='Specific Price', - ) - price_unit = fields.Float( - string='Unit Price', - compute="_compute_price_unit", - inverse="_inverse_price_unit", - ) - price_subtotal = fields.Float( - compute='_compute_price_subtotal', - digits=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', - ) - sequence = fields.Integer( - string="Sequence", - default=10, - help="Sequence of the contract line when displaying contracts", - ) - date_from = fields.Date( - string='Date From', - compute='_compute_date_from', - help='Date from invoiced period', - ) - date_to = fields.Date( - string='Date To', - compute='_compute_date_to', - help='Date to invoiced period', - ) - - @api.depends( - 'automatic_price', - 'specific_price', - 'product_id', - 'quantity', - 'analytic_account_id.pricelist_id', - 'analytic_account_id.partner_id', - ) - def _compute_price_unit(self): - """Get the specific price if no auto-price, and the price obtained - from the pricelist otherwise. - """ - for line in self: - if line.automatic_price: - product = line.product_id.with_context( - quantity=line.env.context.get( - 'contract_line_qty', line.quantity, - ), - pricelist=line.analytic_account_id.pricelist_id.id, - partner=line.analytic_account_id.partner_id.id, - date=line.env.context.get('old_date', fields.Date.today()), - ) - line.price_unit = product.price - else: - line.price_unit = line.specific_price - - # Tip in https://github.com/odoo/odoo/issues/23891#issuecomment-376910788 - @api.onchange('price_unit') - def _inverse_price_unit(self): - """Store the specific price in the no auto-price records.""" - for line in self.filtered(lambda x: not x.automatic_price): - line.specific_price = line.price_unit - - @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 - - def _compute_date_from(self): - # When call from template line.analytic_account_id comodel is - # 'account.analytic.contract', - if self._name != 'account.analytic.invoice.line': - return - for line in self: - contract = line.analytic_account_id - date_start = ( - self.env.context.get('old_date') or fields.Date.from_string( - contract.recurring_next_date or fields.Date.today()) - ) - if contract.recurring_invoicing_type == 'pre-paid': - date_from = date_start - else: - date_from = (date_start - contract.get_relative_delta( - contract.recurring_rule_type, - contract.recurring_interval) + relativedelta(days=1)) - line.date_from = fields.Date.to_string(date_from) - - def _compute_date_to(self): - # When call from template line.analytic_account_id comodel is - # 'account.analytic.contract', - if self._name != 'account.analytic.invoice.line': - return - for line in self: - contract = line.analytic_account_id - date_start = ( - self.env.context.get('old_date') or fields.Date.from_string( - contract.recurring_next_date or fields.Date.today()) - ) - next_date = ( - self.env.context.get('next_date') or - date_start + contract.get_relative_delta( - contract.recurring_rule_type, contract.recurring_interval) - ) - if contract.recurring_invoicing_type == 'pre-paid': - date_to = next_date - relativedelta(days=1) - else: - date_to = date_start - line.date_to = fields.Date.to_string(date_to) - - @api.multi - @api.constrains('discount') - def _check_discount(self): - for line in self: - if line.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 - - if self.analytic_account_id._name == 'account.analytic.account': - date = ( - self.analytic_account_id.recurring_next_date or - fields.Date.today() - ) - partner = self.analytic_account_id.partner_id - - else: - date = fields.Date.today() - partner = self.env.user.partner_id - - product = self.product_id.with_context( - lang=partner.lang, - partner=partner.id, - quantity=self.quantity, - date=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} diff --git a/contract/models/account_analytic_invoice_line.py b/contract/models/account_analytic_invoice_line.py deleted file mode 100644 index a82c8ee6..00000000 --- a/contract/models/account_analytic_invoice_line.py +++ /dev/null @@ -1,16 +0,0 @@ -# Copyright 2017 LasLabs Inc. -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). - -from odoo import fields, models - - -class AccountAnalyticInvoiceLine(models.Model): - _name = 'account.analytic.invoice.line' - _inherit = 'account.analytic.contract.line' - - analytic_account_id = fields.Many2one( - comodel_name='account.analytic.account', - string='Analytic Account', - required=True, - ondelete='cascade', - ) diff --git a/contract/models/account_invoice.py b/contract/models/account_invoice.py index b5ab13ee..b651ea2a 100644 --- a/contract/models/account_invoice.py +++ b/contract/models/account_invoice.py @@ -8,5 +8,5 @@ class AccountInvoice(models.Model): _inherit = 'account.invoice' contract_id = fields.Many2one( - 'account.analytic.account', - string='Contract') + 'account.analytic.account', string='Contract' + ) diff --git a/contract/models/contract.py b/contract/models/contract.py new file mode 100644 index 00000000..be422d25 --- /dev/null +++ b/contract/models/contract.py @@ -0,0 +1,223 @@ +# Copyright 2004-2010 OpenERP SA +# Copyright 2014 Angel Moya +# Copyright 2015 Pedro M. Baeza +# Copyright 2016-2018 Carlos Dauden +# Copyright 2016-2017 LasLabs Inc. +# Copyright 2018 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models +from odoo.exceptions import ValidationError +from odoo.tools.translate import _ + + +class AccountAnalyticAccount(models.Model): + _name = 'account.analytic.account' + _inherit = [ + 'account.analytic.account', + 'account.abstract.analytic.contract', + ] + + contract_template_id = fields.Many2one( + string='Contract Template', comodel_name='account.analytic.contract' + ) + recurring_invoice_line_ids = fields.One2many( + string='Invoice Lines', + comodel_name='account.analytic.invoice.line', + inverse_name='contract_id', + copy=True, + ) + recurring_invoices = fields.Boolean( + string='Generate recurring invoices automatically' + ) + user_id = fields.Many2one( + comodel_name='res.users', + string='Responsible', + index=True, + default=lambda self: self.env.user, + ) + create_invoice_visibility = fields.Boolean( + compute='_compute_create_invoice_visibility' + ) + recurring_next_date = fields.Date( + compute='_compute_recurring_next_date', + string='Date of Next Invoice', + store=True, + ) + date_end = fields.Date( + compute='_compute_date_end', string='Date End', store=True + ) + + @api.depends('recurring_invoice_line_ids.date_end') + def _compute_date_end(self): + for contract in self: + contract.date_end = False + date_end = contract.recurring_invoice_line_ids.mapped('date_end') + if date_end and all(date_end): + contract.date_end = max(date_end) + + @api.depends('recurring_invoice_line_ids.recurring_next_date') + def _compute_recurring_next_date(self): + for contract in self: + recurring_next_date = contract.recurring_invoice_line_ids.filtered( + 'create_invoice_visibility' + ).mapped('recurring_next_date') + if recurring_next_date: + contract.recurring_next_date = min(recurring_next_date) + + @api.depends('recurring_invoice_line_ids.create_invoice_visibility') + def _compute_create_invoice_visibility(self): + for contract in self: + contract.create_invoice_visibility = any( + contract.recurring_invoice_line_ids.mapped( + 'create_invoice_visibility' + ) + ) + + @api.onchange('contract_template_id') + def _onchange_contract_template_id(self): + """Update the contract fields with that of the template. + + Take special consideration with the `recurring_invoice_line_ids`, + which must be created using the data from the contract lines. Cascade + deletion ensures that any errant lines that are created are also + deleted. + """ + contract_template_id = self.contract_template_id + if not contract_template_id: + return + for field_name, field in contract_template_id._fields.items(): + if field.name == 'recurring_invoice_line_ids': + lines = self._convert_contract_lines(contract_template_id) + self.recurring_invoice_line_ids = lines + elif not any( + ( + field.compute, + field.related, + field.automatic, + field.readonly, + field.company_dependent, + field.name in self.NO_SYNC, + ) + ): + self[field_name] = self.contract_template_id[field_name] + + @api.onchange('partner_id') + def _onchange_partner_id(self): + self.pricelist_id = self.partner_id.property_product_pricelist.id + + @api.constrains('partner_id', 'recurring_invoices') + def _check_partner_id_recurring_invoices(self): + for contract in self.filtered('recurring_invoices'): + if not contract.partner_id: + raise ValidationError( + _("You must supply a customer for the contract '%s'") + % contract.name + ) + + @api.multi + def _convert_contract_lines(self, contract): + self.ensure_one() + new_lines = [] + for contract_line in contract.recurring_invoice_line_ids: + vals = contract_line._convert_to_write(contract_line.read()[0]) + # Remove template link field + vals.pop('contract_template_id', False) + vals['date_start'] = fields.Date.today() + vals['recurring_next_date'] = fields.Date.today() + self.recurring_invoice_line_ids._onchange_date_start() + new_lines.append((0, 0, vals)) + return new_lines + + @api.multi + def _prepare_invoice(self, date_invoice, journal=None): + self.ensure_one() + if not self.partner_id: + if self.contract_type == 'purchase': + raise ValidationError( + _("You must first select a Supplier for Contract %s!") + % self.name + ) + else: + raise ValidationError( + _("You must first select a Customer for Contract %s!") + % self.name + ) + if not journal: + journal = ( + self.journal_id + if self.journal_id.type == self.contract_type + else self.env['account.journal'].search( + [ + ('type', '=', self.contract_type), + ('company_id', '=', self.company_id.id), + ], + limit=1, + ) + ) + if not journal: + raise ValidationError( + _("Please define a %s journal for the company '%s'.") + % (self.contract_type, self.company_id.name or '') + ) + currency = ( + self.pricelist_id.currency_id + or self.partner_id.property_product_pricelist.currency_id + or self.company_id.currency_id + ) + invoice_type = 'out_invoice' + if self.contract_type == 'purchase': + invoice_type = 'in_invoice' + invoice = self.env['account.invoice'].new( + { + 'reference': self.code, + 'type': invoice_type, + 'partner_id': self.partner_id.address_get(['invoice'])[ + 'invoice' + ], + 'currency_id': currency.id, + 'date_invoice': date_invoice, + 'journal_id': journal.id, + 'origin': self.name, + 'company_id': self.company_id.id, + 'contract_id': self.id, + 'user_id': self.partner_id.user_id.id, + } + ) + # Get other invoice values from partner onchange + invoice._onchange_partner_id() + return invoice._convert_to_write(invoice._cache) + + @api.multi + def action_contract_send(self): + self.ensure_one() + template = self.env.ref('contract.email_contract_template', False) + compose_form = self.env.ref('mail.email_compose_message_wizard_form') + ctx = dict( + default_model='account.analytic.account', + default_res_id=self.id, + default_use_template=bool(template), + default_template_id=template and template.id or False, + default_composition_mode='comment', + ) + return { + 'name': _('Compose Email'), + 'type': 'ir.actions.act_window', + 'view_type': 'form', + 'view_mode': 'form', + 'res_model': 'mail.compose.message', + 'views': [(compose_form.id, 'form')], + 'view_id': compose_form.id, + 'target': 'new', + 'context': ctx, + } + + @api.multi + def recurring_create_invoice(self): + return self.env[ + 'account.analytic.invoice.line' + ].recurring_create_invoice(self) + + @api.model + def cron_recurring_create_invoice(self): + self.env['account.analytic.invoice.line'].recurring_create_invoice() diff --git a/contract/models/contract_line.py b/contract/models/contract_line.py new file mode 100644 index 00000000..2a8429b8 --- /dev/null +++ b/contract/models/contract_line.py @@ -0,0 +1,250 @@ +# Copyright 2017 LasLabs Inc. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from dateutil.relativedelta import relativedelta + +from odoo import api, fields, models, _ +from odoo.exceptions import ValidationError + + +class AccountAnalyticInvoiceLine(models.Model): + _name = 'account.analytic.invoice.line' + _inherit = 'account.abstract.analytic.contract.line' + + contract_id = fields.Many2one( + comodel_name='account.analytic.account', + string='Analytic Account', + required=True, + ondelete='cascade', + oldname='analytic_account_id', + ) + date_start = fields.Date(string='Date Start', default=fields.Date.today()) + date_end = fields.Date(string='Date End', index=True) + recurring_next_date = fields.Date( + copy=False, string='Date of Next Invoice' + ) + create_invoice_visibility = fields.Boolean( + compute='_compute_create_invoice_visibility' + ) + partner_id = fields.Many2one( + comodel_name="res.partner", + string="Partner (always False)", + related='contract_id.partner_id', + store=True, + readonly=True, + ) + pricelist_id = fields.Many2one( + comodel_name='product.pricelist', + string='Pricelist', + related='contract_id.pricelist_id', + store=True, + readonly=True, + ) + + @api.model + def _compute_first_recurring_next_date( + self, + date_start, + recurring_invoicing_type, + recurring_rule_type, + recurring_interval, + ): + if recurring_rule_type == 'monthlylastday': + return date_start + self.get_relative_delta( + recurring_rule_type, recurring_interval - 1 + ) + if recurring_invoicing_type == 'pre-paid': + return date_start + return date_start + self.get_relative_delta( + recurring_rule_type, recurring_interval + ) + + @api.onchange( + 'date_start', + 'recurring_invoicing_type', + 'recurring_rule_type', + 'recurring_interval', + ) + def _onchange_date_start(self): + for rec in self.filtered('date_start'): + rec.recurring_next_date = self._compute_first_recurring_next_date( + rec.date_start, + rec.recurring_invoicing_type, + rec.recurring_rule_type, + rec.recurring_interval, + ) + + @api.constrains('recurring_next_date', 'date_start') + def _check_recurring_next_date_start_date(self): + for line in self.filtered('recurring_next_date'): + if line.date_start and line.recurring_next_date: + if line.date_start > line.recurring_next_date: + raise ValidationError( + _( + "You can't have a next invoicing date before the " + "start of the contract '%s'" + ) + % line.contract_id.name + ) + + @api.constrains('recurring_next_date') + def _check_recurring_next_date_recurring_invoices(self): + for line in self.filtered('contract_id.recurring_invoices'): + if not line.recurring_next_date: + raise ValidationError( + _( + "You must supply a next invoicing date for contract " + "'%s'" + ) + % line.contract_id.name + ) + + @api.constrains('date_start') + def _check_date_start_recurring_invoices(self): + for line in self.filtered('contract_id.recurring_invoices'): + if not line.date_start: + raise ValidationError( + _("You must supply a start date for contract '%s'") + % line.contract_id.name + ) + + @api.constrains('date_start', 'date_end') + def _check_start_end_dates(self): + for line in self.filtered('date_end'): + if line.date_start and line.date_end: + if line.date_start > line.date_end: + raise ValidationError( + _( + "Contract '%s' start date can't be later than " + "end date" + ) + % line.contract_id.name + ) + + @api.depends('recurring_next_date', 'date_end') + def _compute_create_invoice_visibility(self): + for line in self: + line.create_invoice_visibility = not line.date_end or ( + line.recurring_next_date + and line.date_end + and line.recurring_next_date <= line.date_end + ) + + @api.model + def recurring_create_invoice(self, contract=False): + domain = [] + date_ref = fields.Date.today() + if contract: + contract.ensure_one() + date_ref = contract.recurring_next_date + domain.append(('contract_id', '=', contract.id)) + + domain.extend( + [ + ('contract_id.recurring_invoices', '=', True), + ('recurring_next_date', '<=', date_ref), + '|', + ('date_end', '=', False), + ('date_end', '>=', date_ref), + ] + ) + lines = self.search(domain).filtered('create_invoice_visibility') + if lines: + return lines._recurring_create_invoice() + return False + + @api.multi + def _recurring_create_invoice(self): + """Create invoices from contracts + + :return: invoices created + """ + invoices = self.env['account.invoice'] + for contract in self.mapped('contract_id'): + lines = self.filtered(lambda l: l.contract_id == contract) + invoices |= lines._create_invoice() + lines._update_recurring_next_date() + return invoices + + @api.multi + def _create_invoice(self): + """ + :param invoice: If not False add lines to this invoice + :return: invoice created or updated + """ + contract = self.mapped('contract_id') + date_invoice = min(self.mapped('recurring_next_date')) + invoice = self.env['account.invoice'].create( + contract._prepare_invoice(date_invoice) + ) + for line in self: + invoice_line_vals = line._prepare_invoice_line(invoice.id) + if invoice_line_vals: + self.env['account.invoice.line'].create(invoice_line_vals) + invoice.compute_taxes() + return invoice + + @api.multi + def _prepare_invoice_line(self, invoice_id): + self.ensure_one() + invoice_line = self.env['account.invoice.line'].new( + { + 'invoice_id': invoice_id, + 'product_id': self.product_id.id, + 'quantity': self.quantity, + 'uom_id': self.uom_id.id, + 'discount': self.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) + # Insert markers + contract = self.contract_id + lang_obj = self.env['res.lang'] + lang = lang_obj.search([('code', '=', contract.partner_id.lang)]) + date_format = lang.date_format or '%m/%d/%Y' + name = self._insert_markers(date_format) + invoice_line_vals.update( + { + 'name': name, + 'account_analytic_id': contract.id, + 'price_unit': self.price_unit, + } + ) + return invoice_line_vals + + @api.multi + def _insert_markers(self, date_format): + self.ensure_one() + date_from = fields.Date.from_string(self.recurring_next_date) + date_to = date_from + self.get_relative_delta( + self.recurring_rule_type, self.recurring_interval + ) + name = self.name + name = name.replace('#START#', date_from.strftime(date_format)) + name = name.replace('#END#', date_to.strftime(date_format)) + return name + + @api.multi + def _update_recurring_next_date(self): + for line in self: + ref_date = line.recurring_next_date or fields.Date.today() + old_date = fields.Date.from_string(ref_date) + new_date = old_date + self.get_relative_delta( + line.recurring_rule_type, line.recurring_interval + ) + line.recurring_next_date = new_date + + @api.model + def get_relative_delta(self, recurring_rule_type, interval): + if recurring_rule_type == 'daily': + return relativedelta(days=interval) + elif recurring_rule_type == 'weekly': + return relativedelta(weeks=interval) + elif recurring_rule_type == 'monthly': + return relativedelta(months=interval) + elif recurring_rule_type == 'monthlylastday': + return relativedelta(months=interval, day=31) + else: + return relativedelta(years=interval) diff --git a/contract/models/contract_template.py b/contract/models/contract_template.py new file mode 100644 index 00000000..48064f2c --- /dev/null +++ b/contract/models/contract_template.py @@ -0,0 +1,22 @@ +# Copyright 2004-2010 OpenERP SA +# Copyright 2014 Angel Moya +# Copyright 2015 Pedro M. Baeza +# Copyright 2016-2018 Carlos Dauden +# Copyright 2016-2017 LasLabs Inc. +# Copyright 2018 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class AccountAnalyticContract(models.Model): + _name = 'account.analytic.contract' + _inherit = 'account.abstract.analytic.contract' + _description = "Account Analytic Contract" + + recurring_invoice_line_ids = fields.One2many( + comodel_name='account.analytic.contract.line', + inverse_name='contract_template_id', + copy=True, + string='Invoice Lines', + ) diff --git a/contract/models/contract_template_line.py b/contract/models/contract_template_line.py new file mode 100644 index 00000000..abeca107 --- /dev/null +++ b/contract/models/contract_template_line.py @@ -0,0 +1,38 @@ +# Copyright 2004-2010 OpenERP SA +# Copyright 2014 Angel Moya +# Copyright 2015 Pedro M. Baeza +# Copyright 2016-2018 Carlos Dauden +# Copyright 2016-2017 LasLabs Inc. +# Copyright 2018 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class AccountAnalyticContractLine(models.Model): + _name = 'account.analytic.contract.line' + _inherit = 'account.abstract.analytic.contract.line' + _description = 'Contract Lines' + _order = "sequence,id" + + contract_template_id = fields.Many2one( + string='Contract', + comodel_name='account.analytic.contract', + required=True, + ondelete='cascade', + oldname='analytic_account_id', + ) + partner_id = fields.Many2one( + comodel_name="res.partner", + string="Partner (always False)", + related='contract_template_id.partner_id', + store=True, + readonly=True, + ) + pricelist_id = fields.Many2one( + comodel_name='product.pricelist', + string='Pricelist', + related='contract_template_id.pricelist_id', + store=True, + readonly=True, + ) diff --git a/contract/models/res_partner.py b/contract/models/res_partner.py index ead5e4c7..038ab3fc 100644 --- a/contract/models/res_partner.py +++ b/contract/models/res_partner.py @@ -8,35 +8,47 @@ class ResPartner(models.Model): _inherit = 'res.partner' sale_contract_count = fields.Integer( - string='Sale Contracts', - compute='_compute_contract_count', + string='Sale Contracts', compute='_compute_contract_count' ) purchase_contract_count = fields.Integer( - string='Purchase Contracts', - compute='_compute_contract_count', + string='Purchase Contracts', compute='_compute_contract_count' ) def _compute_contract_count(self): contract_model = self.env['account.analytic.account'] today = fields.Date.today() - fetch_data = contract_model.read_group([ - ('recurring_invoices', '=', True), - ('partner_id', 'child_of', self.ids), - '|', - ('date_end', '=', False), - ('date_end', '>=', today)], - ['partner_id', 'contract_type'], ['partner_id', 'contract_type'], - lazy=False) - result = [[data['partner_id'][0], data['contract_type'], - data['__count']] for data in fetch_data] + fetch_data = contract_model.read_group( + [ + ('recurring_invoices', '=', True), + ('partner_id', 'child_of', self.ids), + '|', + ('date_end', '=', False), + ('date_end', '>=', today), + ], + ['partner_id', 'contract_type'], + ['partner_id', 'contract_type'], + lazy=False, + ) + result = [ + [data['partner_id'][0], data['contract_type'], data['__count']] + for data in fetch_data + ] for partner in self: partner_child_ids = partner.child_ids.ids + partner.ids - partner.sale_contract_count = sum([ - r[2] for r in result - if r[0] in partner_child_ids and r[1] == 'sale']) - partner.purchase_contract_count = sum([ - r[2] for r in result - if r[0] in partner_child_ids and r[1] == 'purchase']) + partner.sale_contract_count = sum( + [ + r[2] + for r in result + if r[0] in partner_child_ids and r[1] == 'sale' + ] + ) + partner.purchase_contract_count = sum( + [ + r[2] + for r in result + if r[0] in partner_child_ids and r[1] == 'purchase' + ] + ) def act_show_contract(self): """ This opens contract view @@ -55,14 +67,16 @@ class ResPartner(models.Model): default_partner_id=self.id, default_recurring_invoices=True, default_pricelist_id=self.property_product_pricelist.id, - ), + ) ) return res def _get_act_window_contract_xml(self, contract_type): if contract_type == 'purchase': return self.env['ir.actions.act_window'].for_xml_id( - 'contract', 'action_account_analytic_purchase_overdue_all') + 'contract', 'action_account_analytic_purchase_overdue_all' + ) else: return self.env['ir.actions.act_window'].for_xml_id( - 'contract', 'action_account_analytic_sale_overdue_all') + 'contract', 'action_account_analytic_sale_overdue_all' + ) diff --git a/contract/report/report_contract.xml b/contract/report/report_contract.xml index 19e73aee..ac6c636b 100644 --- a/contract/report/report_contract.xml +++ b/contract/report/report_contract.xml @@ -9,34 +9,54 @@
-

Partner:

-
-

VAT:

+

+ Partner: +

+
+

VAT: + +

- Date Start:

- Responsible:

- Contract:

+ Responsible: +

+ Contract: +

-

Recurring Items

+

+ Recurring Items +

- - - - + + + + + - + @@ -44,12 +64,18 @@ - + +
DescriptionQuantityUnit PricePrice + Description + + Quantity + + Unit Price + + Price + + Date Start +
- + - + + +
@@ -57,9 +83,12 @@
- +
Total + Total + - +
diff --git a/contract/tests/test_contract.py b/contract/tests/test_contract.py index 4ce51aaa..87588b3f 100644 --- a/contract/tests/test_contract.py +++ b/contract/tests/test_contract.py @@ -7,6 +7,10 @@ from odoo.exceptions import ValidationError from odoo.tests import common +def to_date(date): + return fields.Date.to_date(date) + + class TestContractBase(common.SavepointCase): @classmethod def setUpClass(cls): @@ -14,51 +18,85 @@ class TestContractBase(common.SavepointCase): cls.partner = cls.env.ref('base.res_partner_2') cls.product = cls.env.ref('product.product_product_2') cls.product.taxes_id += cls.env['account.tax'].search( - [('type_tax_use', '=', 'sale')], limit=1) + [('type_tax_use', '=', 'sale')], limit=1 + ) cls.product.description_sale = 'Test description sale' - cls.template_vals = { + cls.line_template_vals = { + 'product_id': cls.product.id, + 'name': 'Services from #START# to #END#', + 'quantity': 1, + 'uom_id': cls.product.uom_id.id, + 'price_unit': 100, + 'discount': 50, 'recurring_rule_type': 'yearly', - 'recurring_interval': 12345, + 'recurring_interval': 1, + } + cls.template_vals = { 'name': 'Test Contract Template', + 'recurring_invoice_line_ids': [(0, 0, cls.line_template_vals)], } cls.template = cls.env['account.analytic.contract'].create( - cls.template_vals, + cls.template_vals ) # For being sure of the applied price - cls.env['product.pricelist.item'].create({ - 'pricelist_id': cls.partner.property_product_pricelist.id, - 'product_id': cls.product.id, - 'compute_price': 'formula', - 'base': 'list_price', - }) - cls.contract = cls.env['account.analytic.account'].create({ - 'name': 'Test Contract', - 'partner_id': cls.partner.id, - 'pricelist_id': cls.partner.property_product_pricelist.id, - 'recurring_invoices': True, - 'date_start': '2016-02-15', - 'recurring_next_date': '2016-02-29', - }) - cls.contract2 = cls.env['account.analytic.account'].create({ - 'name': 'Test Contract 2', - 'partner_id': cls.partner.id, - 'pricelist_id': cls.partner.property_product_pricelist.id, - 'recurring_invoices': True, - 'date_start': '2016-02-15', - 'recurring_next_date': '2016-02-29', - 'contract_type': 'purchase', - }) + cls.env['product.pricelist.item'].create( + { + 'pricelist_id': cls.partner.property_product_pricelist.id, + 'product_id': cls.product.id, + 'compute_price': 'formula', + 'base': 'list_price', + } + ) + cls.contract = cls.env['account.analytic.account'].create( + { + 'name': 'Test Contract', + 'partner_id': cls.partner.id, + 'pricelist_id': cls.partner.property_product_pricelist.id, + 'recurring_invoices': True, + } + ) + cls.contract2 = cls.env['account.analytic.account'].create( + { + 'name': 'Test Contract 2', + 'partner_id': cls.partner.id, + 'pricelist_id': cls.partner.property_product_pricelist.id, + 'recurring_invoices': True, + 'contract_type': 'purchase', + 'recurring_invoice_line_ids': [ + ( + 0, + 0, + { + 'product_id': cls.product.id, + 'name': 'Services from #START# to #END#', + 'quantity': 1, + 'uom_id': cls.product.uom_id.id, + 'price_unit': 100, + 'discount': 50, + 'recurring_rule_type': 'monthly', + 'recurring_interval': 1, + 'date_start': '2016-02-15', + 'recurring_next_date': '2016-02-29', + }, + ) + ], + } + ) cls.line_vals = { - 'analytic_account_id': cls.contract.id, + 'contract_id': cls.contract.id, 'product_id': cls.product.id, 'name': 'Services from #START# to #END#', 'quantity': 1, 'uom_id': cls.product.uom_id.id, 'price_unit': 100, 'discount': 50, + 'recurring_rule_type': 'monthly', + 'recurring_interval': 1, + 'date_start': '2016-02-15', + 'recurring_next_date': '2016-02-29', } cls.acct_line = cls.env['account.analytic.invoice.line'].create( - cls.line_vals, + cls.line_vals ) @@ -67,7 +105,9 @@ class TestContract(TestContractBase): if overrides is None: overrides = {} vals = self.line_vals.copy() - vals['analytic_account_id'] = self.template.id + del vals['contract_id'] + del vals['date_start'] + vals['contract_template_id'] = self.template.id vals.update(overrides) return self.env['account.analytic.contract.line'].create(vals) @@ -90,7 +130,7 @@ class TestContract(TestContractBase): self.assertEqual(self.acct_line.price_unit, 10) def test_contract(self): - recurring_next_date = fields.Date.to_date('2016-03-29') + recurring_next_date = to_date('2016-03-29') self.assertAlmostEqual(self.acct_line.price_subtotal, 50.0) res = self.acct_line._onchange_product_id() self.assertIn('uom_id', res['domain']) @@ -100,81 +140,96 @@ class TestContract(TestContractBase): 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)]) + [('contract_id', '=', self.contract.id)] + ) self.assertTrue(self.invoice_monthly) - self.assertEqual(self.contract.recurring_next_date, - recurring_next_date) + self.assertEqual( + self.acct_line.recurring_next_date, recurring_next_date + ) self.inv_line = self.invoice_monthly.invoice_line_ids[0] self.assertTrue(self.inv_line.invoice_line_tax_ids) self.assertAlmostEqual(self.inv_line.price_subtotal, 50.0) - self.assertEqual(self.contract.partner_id.user_id, - self.invoice_monthly.user_id) + self.assertEqual( + self.contract.partner_id.user_id, self.invoice_monthly.user_id + ) def test_contract_daily(self): - recurring_next_date = fields.Date.to_date('2016-03-01') - self.contract.recurring_next_date = '2016-02-29' - self.contract.recurring_rule_type = 'daily' + recurring_next_date = to_date('2016-03-01') + self.acct_line.recurring_next_date = '2016-02-29' + self.acct_line.recurring_rule_type = 'daily' self.contract.pricelist_id = False - self.contract.cron_recurring_create_invoice() + self.contract.recurring_create_invoice() invoice_daily = self.env['account.invoice'].search( - [('contract_id', '=', self.contract.id)]) + [('contract_id', '=', self.contract.id)] + ) self.assertTrue(invoice_daily) - self.assertEqual(self.contract.recurring_next_date, - recurring_next_date) + self.assertEqual( + self.acct_line.recurring_next_date, recurring_next_date + ) def test_contract_weekly(self): - recurring_next_date = fields.Date.to_date('2016-03-07') - self.contract.recurring_next_date = '2016-02-29' - self.contract.recurring_rule_type = 'weekly' - self.contract.recurring_invoicing_type = 'post-paid' + recurring_next_date = to_date('2016-03-07') + self.acct_line.recurring_next_date = '2016-02-29' + self.acct_line.recurring_rule_type = 'weekly' + self.acct_line.recurring_invoicing_type = 'post-paid' self.contract.recurring_create_invoice() invoices_weekly = self.env['account.invoice'].search( - [('contract_id', '=', self.contract.id)]) + [('contract_id', '=', self.contract.id)] + ) self.assertTrue(invoices_weekly) self.assertEqual( - self.contract.recurring_next_date, recurring_next_date) + self.acct_line.recurring_next_date, recurring_next_date + ) def test_contract_yearly(self): - recurring_next_date = fields.Date.to_date('2017-02-28') - self.contract.recurring_next_date = '2016-02-29' - self.contract.recurring_rule_type = 'yearly' + recurring_next_date = to_date('2017-02-28') + self.acct_line.recurring_next_date = '2016-02-29' + self.acct_line.recurring_rule_type = 'yearly' self.contract.recurring_create_invoice() invoices_weekly = self.env['account.invoice'].search( - [('contract_id', '=', self.contract.id)]) + [('contract_id', '=', self.contract.id)] + ) self.assertTrue(invoices_weekly) self.assertEqual( - self.contract.recurring_next_date, recurring_next_date) + self.acct_line.recurring_next_date, recurring_next_date + ) def test_contract_monthly_lastday(self): - recurring_next_date = fields.Date.to_date('2016-03-31') - self.contract.recurring_next_date = '2016-02-29' - self.contract.recurring_invoicing_type = 'post-paid' - self.contract.recurring_rule_type = 'monthlylastday' + recurring_next_date = to_date('2016-03-31') + self.acct_line.recurring_next_date = '2016-02-29' + self.acct_line.recurring_invoicing_type = 'post-paid' + self.acct_line.recurring_rule_type = 'monthlylastday' self.contract.recurring_create_invoice() invoices_monthly_lastday = self.env['account.invoice'].search( - [('contract_id', '=', self.contract.id)]) + [('contract_id', '=', self.contract.id)] + ) self.assertTrue(invoices_monthly_lastday) - self.assertEqual(self.contract.recurring_next_date, - recurring_next_date) + self.assertEqual( + self.acct_line.recurring_next_date, recurring_next_date + ) def test_onchange_partner_id(self): self.contract._onchange_partner_id() - self.assertEqual(self.contract.pricelist_id, - self.contract.partner_id.property_product_pricelist) + self.assertEqual( + self.contract.pricelist_id, + self.contract.partner_id.property_product_pricelist, + ) def test_onchange_date_start(self): - recurring_next_date = fields.Date.to_date('2016-01-01') - self.contract.date_start = recurring_next_date - self.contract._onchange_date_start() - self.assertEqual(self.contract.recurring_next_date, - recurring_next_date) + recurring_next_date = to_date('2016-01-01') + self.acct_line.date_start = recurring_next_date + self.acct_line._onchange_date_start() + self.assertEqual( + self.acct_line.recurring_next_date, recurring_next_date + ) def test_uom(self): uom_litre = self.env.ref('uom.product_uom_litre') self.acct_line.uom_id = uom_litre.id self.acct_line._onchange_product_id() - self.assertEqual(self.acct_line.uom_id, - self.acct_line.product_id.uom_id) + self.assertEqual( + self.acct_line.uom_id, self.acct_line.product_id.uom_id + ) def test_onchange_product_id(self): line = self.env['account.analytic.invoice.line'].new() @@ -187,45 +242,55 @@ class TestContract(TestContractBase): self.assertAlmostEqual(self.acct_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() + self.contract.recurring_create_invoice() def test_check_date_end(self): with self.assertRaises(ValidationError): - self.contract.date_end = '2015-12-31' + self.acct_line.date_end = '2015-12-31' def test_check_recurring_next_date_start_date(self): with self.assertRaises(ValidationError): - self.contract.write({ - 'date_start': '2017-01-01', - 'recurring_next_date': '2016-01-01', - }) + self.acct_line.write( + { + 'date_start': '2017-01-01', + 'recurring_next_date': '2016-01-01', + } + ) def test_check_recurring_next_date_recurring_invoices(self): with self.assertRaises(ValidationError): - self.contract.write({ - 'recurring_invoices': True, - 'recurring_next_date': False, - }) + self.contract.write({'recurring_invoices': True}) + self.acct_line.write({'recurring_next_date': False}) def test_check_date_start_recurring_invoices(self): with self.assertRaises(ValidationError): - self.contract.write({ - 'recurring_invoices': True, - 'date_start': False, - }) + self.contract.write({'recurring_invoices': True}) + self.acct_line.write({'date_start': False}) def test_onchange_contract_template_id(self): """It should change the contract values to match the template.""" self.contract.contract_template_id = self.template self.contract._onchange_contract_template_id() res = { - 'recurring_rule_type': self.contract.recurring_rule_type, - 'recurring_interval': self.contract.recurring_interval, + 'recurring_invoice_line_ids': [ + ( + 0, + 0, + { + '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, + 'recurring_rule_type': 'yearly', + 'recurring_interval': 1, + }, + ) + ] } del self.template_vals['name'] self.assertDictEqual(res, self.template_vals) @@ -234,19 +299,17 @@ class TestContract(TestContractBase): """It should create invoice lines for the contract lines.""" self.acct_line.unlink() - self.line_vals['analytic_account_id'] = self.template.id - self.env['account.analytic.contract.line'].create(self.line_vals) self.contract.contract_template_id = self.template - self.assertFalse(self.contract.recurring_invoice_line_ids, - 'Recurring lines were not removed.') - + self.assertFalse( + self.contract.recurring_invoice_line_ids, + 'Recurring lines were not removed.', + ) + self.contract.contract_template_id = self.template self.contract._onchange_contract_template_id() - del self.line_vals['analytic_account_id'] - self.assertEqual(len(self.contract.recurring_invoice_line_ids), 1) - for key, value in self.line_vals.items(): + for key, value in self.line_template_vals.items(): test_value = self.contract.recurring_invoice_line_ids[0][key] try: test_value = test_value.id @@ -262,7 +325,8 @@ class TestContract(TestContractBase): self.contract._onchange_contract_type() self.assertEqual(self.contract.journal_id.type, 'sale') self.assertEqual( - self.contract.journal_id.company_id, self.contract.company_id) + self.contract.journal_id.company_id, self.contract.company_id + ) def test_contract_onchange_product_id_domain_blank(self): """It should return a blank UoM domain when no product.""" @@ -286,18 +350,19 @@ class TestContract(TestContractBase): ) line.product_id.uom_id = self.env.ref('uom.product_uom_day').id line._onchange_product_id() - self.assertEqual(line.uom_id, - line.product_id.uom_id) + self.assertEqual(line.uom_id, line.product_id.uom_id) def test_contract_onchange_product_id_name(self): """It should update the name for the line.""" line = self._add_template_line() line.product_id.description_sale = 'Test' line._onchange_product_id() - self.assertEqual(line.name, - '\n'.join([line.product_id.name, - line.product_id.description_sale, - ])) + self.assertEqual( + line.name, + '\n'.join( + [line.product_id.name, line.product_id.description_sale] + ), + ) def test_contract_count(self): """It should return sale contract count.""" @@ -313,48 +378,48 @@ class TestContract(TestContractBase): def test_same_date_start_and_date_end(self): """It should create one invoice with same start and end date.""" account_invoice_model = self.env['account.invoice'] - self.contract.write({ - 'date_start': fields.Date.today(), - 'date_end': fields.Date.today(), - 'recurring_next_date': fields.Date.today(), - }) + self.acct_line.write( + { + 'date_start': fields.Date.today(), + 'date_end': fields.Date.today(), + 'recurring_next_date': fields.Date.today(), + } + ) + self.contract._compute_recurring_next_date() init_count = account_invoice_model.search_count( - [('contract_id', '=', self.contract.id)]) - self.contract.cron_recurring_create_invoice() + [('contract_id', '=', self.contract.id)] + ) + self.contract.recurring_create_invoice() last_count = account_invoice_model.search_count( - [('contract_id', '=', self.contract.id)]) + [('contract_id', '=', self.contract.id)] + ) + self.assertEqual(last_count, init_count + 1) + self.contract.recurring_create_invoice() + last_count = account_invoice_model.search_count( + [('contract_id', '=', self.contract.id)] + ) self.assertEqual(last_count, init_count + 1) - with self.assertRaises(ValidationError): - self.contract.recurring_create_invoice() def test_compute_create_invoice_visibility(self): - self.contract.write({ - 'recurring_next_date': '2017-01-01', - 'date_start': '2016-01-01', - 'date_end': False, - }) + self.acct_line.write( + { + 'recurring_next_date': '2017-01-01', + 'date_start': '2016-01-01', + 'date_end': False, + } + ) self.assertTrue(self.contract.create_invoice_visibility) - self.contract.date_end = '2017-01-01' + self.acct_line.date_end = '2017-01-01' + self.contract.refresh() self.assertTrue(self.contract.create_invoice_visibility) - self.contract.date_end = '2016-01-01' + self.acct_line.date_end = '2016-01-01' + self.contract.refresh() self.assertFalse(self.contract.create_invoice_visibility) - def test_extend_invoice(self): - account_invoice_model = self.env['account.invoice'] - self.contract.recurring_create_invoice() - invoice = account_invoice_model.search( - [('contract_id', '=', self.contract.id)]) - invoice.origin = 'Orig Invoice' - self.contract._create_invoice(invoice) - self.assertEqual(invoice.origin, 'Orig Invoice Test Contract') - invoice_count = account_invoice_model.search_count( - [('contract_id', '=', self.contract.id)]) - self.assertEqual(invoice_count, 1) - self.assertEqual(len(invoice.invoice_line_ids), 2) - def test_act_show_contract(self): - show_contract = self.partner.\ - with_context(contract_type='sale').act_show_contract() + show_contract = self.partner.with_context( + contract_type='sale' + ).act_show_contract() self.assertDictContainsSubset( { 'name': 'Customer Contracts', @@ -364,5 +429,104 @@ class TestContract(TestContractBase): 'xml_id': 'contract.action_account_analytic_sale_overdue_all', }, show_contract, - 'There was an error and the view couldn\'t be opened.' + 'There was an error and the view couldn\'t be opened.', + ) + + def test_compute_first_recurring_next_date(self): + """Test different combination to compute recurring_next_date + Combination format + { + 'recurring_next_date': ( # date + date_start, # date + recurring_invoicing_type, # ('pre-paid','post-paid',) + recurring_rule_type, # ('daily', 'weekly', 'monthly', + # 'monthlylastday', 'yearly'), + recurring_interval, # integer + ), + } + """ + + def error_message( + date_start, + recurring_invoicing_type, + recurring_rule_type, + recurring_interval, + ): + return "Error in %s every %d %s case, start with %s " % ( + recurring_invoicing_type, + recurring_interval, + recurring_rule_type, + date_start, + ) + + combinations = [ + ( + to_date('2018-01-01'), + (to_date('2018-01-01'), 'pre-paid', 'monthly', 1), + ), + ( + to_date('2018-01-01'), + (to_date('2018-01-01'), 'pre-paid', 'monthly', 2), + ), + ( + to_date('2018-02-01'), + (to_date('2018-01-01'), 'post-paid', 'monthly', 1), + ), + ( + to_date('2018-03-01'), + (to_date('2018-01-01'), 'post-paid', 'monthly', 2), + ), + ( + to_date('2018-01-31'), + (to_date('2018-01-05'), 'post-paid', 'monthlylastday', 1), + ), + ( + to_date('2018-01-31'), + (to_date('2018-01-06'), 'pre-paid', 'monthlylastday', 1), + ), + ( + to_date('2018-02-28'), + (to_date('2018-01-05'), 'pre-paid', 'monthlylastday', 2), + ), + ( + to_date('2018-01-05'), + (to_date('2018-01-05'), 'pre-paid', 'yearly', 1), + ), + ( + to_date('2019-01-05'), + (to_date('2018-01-05'), 'post-paid', 'yearly', 1), + ), + ] + contract_line_env = self.env['account.analytic.invoice.line'] + for recurring_next_date, combination in combinations: + self.assertEqual( + recurring_next_date, + contract_line_env._compute_first_recurring_next_date( + *combination + ), + error_message(*combination), + ) + + def test_recurring_next_date(self): + """recurring next date for a contract is the min for all lines""" + self.contract.recurring_create_invoice() + self.assertEqual( + self.contract.recurring_next_date, + min( + self.contract.recurring_invoice_line_ids.mapped( + 'recurring_next_date' + ) + ), + ) + + def test_date_end(self): + """recurring next date for a contract is the min for all lines""" + self.assertFalse(self.contract.date_end) + self.acct_line.date_end = '2018-01-01' + self.assertEqual( + self.contract.date_end, + max(self.contract.recurring_invoice_line_ids.mapped('date_end')), ) + self.acct_line.copy() + self.acct_line.date_end = False + self.assertFalse(self.contract.date_end) diff --git a/contract/views/abstract_contract_line.xml b/contract/views/abstract_contract_line.xml new file mode 100644 index 00000000..db67f768 --- /dev/null +++ b/contract/views/abstract_contract_line.xml @@ -0,0 +1,39 @@ + + + + + Account Abstract Analytic Contract Line Form View + account.abstract.analytic.contract.line + +
+ + + + + + + + + + + + + + + + + +
+
+
+ +
diff --git a/contract/views/account_analytic_account_view.xml b/contract/views/contract.xml similarity index 91% rename from contract/views/account_analytic_account_view.xml rename to contract/views/contract.xml index 07b640a7..48827084 100644 --- a/contract/views/account_analytic_account_view.xml +++ b/contract/views/contract.xml @@ -48,35 +48,16 @@ attrs="{'required': [('recurring_invoices', '=', True)]}" /> - -