diff --git a/.gitignore b/.gitignore index f7f8a408..5e48466d 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,7 @@ var/ *.egg-info/ .installed.cfg *.egg +*.eggs # Installer logs pip-log.txt diff --git a/contract/__init__.py b/contract/__init__.py index 0650744f..aee8895e 100644 --- a/contract/__init__.py +++ b/contract/__init__.py @@ -1 +1,2 @@ from . import models +from . import wizards diff --git a/contract/__manifest__.py b/contract/__manifest__.py index 7bdc150c..51cff28a 100644 --- a/contract/__manifest__.py +++ b/contract/__manifest__.py @@ -4,11 +4,12 @@ # 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.4.0.0', 'category': 'Contract Management', 'license': 'AGPL-3', 'author': "OpenERP SA, " @@ -16,17 +17,22 @@ "LasLabs, " "Odoo Community Association (OCA)", 'website': 'https://github.com/oca/contract', - 'depends': ['base', 'account', 'analytic'], + 'depends': ['base', 'account', 'product'], + "external_dependencies": {"python": ["dateutil"]}, 'data': [ 'security/ir.model.access.csv', 'security/contract_security.xml', 'report/report_contract.xml', 'report/contract_views.xml', 'data/contract_cron.xml', + 'data/contract_renew_cron.xml', 'data/mail_template.xml', - 'views/account_analytic_account_view.xml', - 'views/account_analytic_contract_view.xml', - 'views/account_invoice_view.xml', + 'wizards/contract_line_wizard.xml', + 'views/abstract_contract_line.xml', + 'views/contract.xml', + 'views/contract_line.xml', + 'views/contract_template.xml', + 'views/contract_template_line.xml', 'views/res_partner_view.xml', ], 'installable': True, diff --git a/contract/data/contract_cron.xml b/contract/data/contract_cron.xml index c864374d..754efa29 100644 --- a/contract/data/contract_cron.xml +++ b/contract/data/contract_cron.xml @@ -1,9 +1,9 @@ - + Generate Recurring Invoices from Contracts - + code model.cron_recurring_create_invoice() diff --git a/contract/data/contract_renew_cron.xml b/contract/data/contract_renew_cron.xml new file mode 100644 index 00000000..3365fd21 --- /dev/null +++ b/contract/data/contract_renew_cron.xml @@ -0,0 +1,16 @@ + + + + + Renew Contract lines + + code + model.cron_renew_contract_line() + + 1 + days + -1 + + + + diff --git a/contract/data/mail_template.xml b/contract/data/mail_template.xml index 9d3728b0..78368c9a 100644 --- a/contract/data/mail_template.xml +++ b/contract/data/mail_template.xml @@ -6,7 +6,7 @@ ${(object.user_id.email and '%s <%s>' % (object.user_id.name, object.user_id.email) or '')|safe} ${object.company_id.name} Contract (Ref ${object.name or 'n/a'}) ${object.partner_id.id} - + Contract diff --git a/contract/migrations/12.0.2.0.0/pre-migration.py b/contract/migrations/12.0.2.0.0/pre-migration.py new file mode 100644 index 00000000..87de6704 --- /dev/null +++ b/contract/migrations/12.0.2.0.0/pre-migration.py @@ -0,0 +1,94 @@ +# Copyright 2018 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging + +from openupgradelib import openupgrade + +_logger = logging.getLogger(__name__) + + +def _set_finished_contract(cr): + _logger.info("set recurring_next_date to false for finished contract") + openupgrade.logged_query( + cr, + """ + UPDATE account_analytic_account + SET recurring_next_date=NULL + WHERE recurring_next_date > date_end + """, + ) + + +def _move_contract_recurrence_info_to_contract_line(cr): + _logger.info("Move contract data to line level") + openupgrade.logged_query( + cr, + """ + ALTER TABLE account_analytic_invoice_line + ADD COLUMN IF NOT EXISTS recurring_rule_type VARCHAR(255), + ADD COLUMN IF NOT EXISTS recurring_invoicing_type VARCHAR(255), + ADD COLUMN IF NOT EXISTS recurring_interval INTEGER, + ADD COLUMN IF NOT EXISTS recurring_next_date DATE, + ADD COLUMN IF NOT EXISTS date_start DATE, + ADD COLUMN IF NOT EXISTS date_end DATE + """, + ) + + openupgrade.logged_query( + cr, + """ + 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.analytic_account_id + """, + ) + + +def _move_contract_template_recurrence_info_to_contract_template_line(cr): + _logger.info("Move contract template data to line level") + openupgrade.logged_query( + cr, + """ + ALTER TABLE account_analytic_contract_line + ADD COLUMN IF NOT EXISTS recurring_rule_type VARCHAR(255), + ADD COLUMN IF NOT EXISTS recurring_invoicing_type VARCHAR(255), + ADD COLUMN IF NOT EXISTS recurring_interval INTEGER + """, + ) + + openupgrade.logged_query( + cr, + """ + UPDATE account_analytic_contract_line AS contract_template_line + SET + recurring_rule_type=contract_template.recurring_rule_type, + recurring_invoicing_type=contract_template.recurring_invoicing_type, + recurring_interval=contract_template.recurring_interval + FROM + account_analytic_contract AS contract_template + WHERE + contract_template.id=contract_template_line.analytic_account_id + """, + ) + + +@openupgrade.migrate() +def migrate(env, version): + """ + set recurring_next_date to false for finished contract + """ + _logger.info(">> Pre-Migration 12.0.2.0.0") + cr = env.cr + _set_finished_contract(cr) + _move_contract_recurrence_info_to_contract_line(cr) + _move_contract_template_recurrence_info_to_contract_template_line(cr) diff --git a/contract/migrations/12.0.4.0.0/post-migration.py b/contract/migrations/12.0.4.0.0/post-migration.py new file mode 100644 index 00000000..aeea18c7 --- /dev/null +++ b/contract/migrations/12.0.4.0.0/post-migration.py @@ -0,0 +1,47 @@ +# Copyright 2019 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging + +from openupgradelib import openupgrade +from odoo.tools import parse_version + +_logger = logging.getLogger(__name__) + + +def _update_no_update_ir_cron(env): + # Update ir.cron + env.ref('contract.contract_cron_for_invoice').model_id = env.ref( + 'contract.model_contract_contract' + ) + env.ref('contract.contract_line_cron_for_renew').model_id = env.ref( + 'contract.model_contract_line' + ) + env.ref('contract.email_contract_template').model_id = env.ref( + 'contract.model_contract_contract' + ) + + +def _init_last_date_invoiced_on_contract_lines(env): + _logger.info("init last_date_invoiced field for contract lines") + contract_lines = env["contract.line"].search( + [("recurring_next_date", "!=", False)] + ) + contract_lines._init_last_date_invoiced() + + +def _init_invoicing_partner_id_on_contracts(env): + _logger.info("Populate invoicing partner field on contracts") + contracts = env["contract.contract"].search([]) + contracts._inverse_partner_id() + + +@openupgrade.migrate() +def migrate(env, version): + _update_no_update_ir_cron(env) + if parse_version(version) < parse_version('12.0.2.0.0'): + # We check the version here as this post-migration script was in + # 12.0.2.0.0 and already done for those who used the module when + # it was a PR + _init_last_date_invoiced_on_contract_lines(env) + _init_invoicing_partner_id_on_contracts(env) diff --git a/contract/migrations/12.0.4.0.0/pre-migration.py b/contract/migrations/12.0.4.0.0/pre-migration.py new file mode 100644 index 00000000..ea8e54ec --- /dev/null +++ b/contract/migrations/12.0.4.0.0/pre-migration.py @@ -0,0 +1,113 @@ +# Copyright 2019 ACSONE SA/NV +# Copyright 2019 Tecnativa 2019 - Pedro M. Baeza +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging + +from openupgradelib import openupgrade +from psycopg2 import sql + +_logger = logging.getLogger(__name__) + +models_to_rename = [ + # Contract Line Wizard + ('account.analytic.invoice.line.wizard', 'contract.line.wizard'), + # Abstract Contract + ('account.abstract.analytic.contract', 'contract.abstract.contract'), + # Abstract Contract Line + ( + 'account.abstract.analytic.contract.line', + 'contract.abstract.contract.line', + ), + # Contract Line + ('account.analytic.invoice.line', 'contract.line'), + # Contract Template + ('account.analytic.contract', 'contract.template'), + # Contract Template Line + ('account.analytic.contract.line', 'contract.template.line'), +] +tables_to_rename = [ + # Contract Line + ('account_analytic_invoice_line', 'contract_line'), + # Contract Template + ('account_analytic_contract', 'contract_template'), + # Contract Template Line + ('account_analytic_contract_line', 'contract_template_line'), +] +columns_to_copy = { + 'contract_line': [ + ('analytic_account_id', 'contract_id', None), + ], +} +xmlids_to_rename = [ + ( + 'contract.account_analytic_cron_for_invoice', + 'contract.contract_cron_for_invoice', + ), + ( + 'contract.account_analytic_contract_manager', + 'contract.contract_template_manager', + ), + ( + 'contract.account_analytic_contract_user', + 'contract.contract_template_user', + ), + ( + 'contract.account_analytic_invoice_line_manager', + 'contract.contract_line_manager', + ), + ( + 'contract.account_analytic_invoice_line_user', + 'contract.contract_line_user', + ), + ( + 'contract.account_analytic_contract_line_manager', + 'contract.contract_template_line_manager', + ), + ( + 'contract.account_analytic_contract_line_user', + 'contract.contract_template_line_user', + ), +] + + +def _get_contract_field_name(cr): + """ + Contract field changed the name from analytic_account_id to contract_id + in 12.0.2.0.0. This method used to get the contract field name in + account_analytic_invoice_line""" + return ( + 'contract_id' + if openupgrade.column_exists( + cr, 'account_analytic_invoice_line', 'contract_id' + ) + else 'analytic_account_id' + ) + + +def create_contract_records(cr): + contract_field_name = _get_contract_field_name(cr) + openupgrade.logged_query( + cr, """ + CREATE TABLE contract_contract + (LIKE account_analytic_account INCLUDING ALL)""", + ) + openupgrade.logged_query( + cr, sql.SQL(""" + INSERT INTO contract_contract + SELECT * FROM account_analytic_account + WHERE id IN (SELECT DISTINCT {} FROM contract_line) + """).format( + sql.Identifier(contract_field_name), + ), + ) + + +@openupgrade.migrate() +def migrate(env, version): + cr = env.cr + openupgrade.rename_models(cr, models_to_rename) + openupgrade.rename_tables(cr, tables_to_rename) + openupgrade.rename_xmlids(cr, xmlids_to_rename) + openupgrade.copy_columns(cr, columns_to_copy) + create_contract_records(cr) diff --git a/contract/models/__init__.py b/contract/models/__init__.py index 406cc389..28dc9922 100644 --- a/contract/models/__init__.py +++ b/contract/models/__init__.py @@ -1,8 +1,11 @@ # 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 account_invoice_line from . import res_partner diff --git a/contract/models/abstract_contract.py b/contract/models/abstract_contract.py new file mode 100644 index 00000000..75dd9030 --- /dev/null +++ b/contract/models/abstract_contract.py @@ -0,0 +1,73 @@ +# 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 ContractAbstractContract(models.AbstractModel): + _name = 'contract.abstract.contract' + _description = 'Abstract Recurring 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)", index=True + ) + pricelist_id = fields.Many2one( + comodel_name='product.pricelist', string='Pricelist' + ) + contract_type = fields.Selection( + selection=[('sale', 'Customer'), ('purchase', 'Supplier')], + default='sale', + index=True, + ) + + journal_id = fields.Many2one( + 'account.journal', + string='Journal', + default=lambda s: s._default_journal(), + domain="[('type', '=', contract_type)," + "('company_id', '=', company_id)]", + index=True, + ) + company_id = fields.Many2one( + 'res.company', + string='Company', + required=True, + default=lambda self: self.env['res.company']._company_default_get( + self._name + ), + ) + + @api.onchange('contract_type') + def _onchange_contract_type(self): + if self.contract_type == 'purchase': + self.contract_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..c1320208 --- /dev/null +++ b/contract/models/abstract_contract_line.py @@ -0,0 +1,206 @@ +# 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 ContractAbstractContractLine(models.AbstractModel): + _name = 'contract.abstract.contract.line' + _description = 'Abstract Recurring 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='Invoice Every', + help="Invoice every (Days/Week/Month/Year)", + required=True, + ) + date_start = fields.Date(string='Date Start') + recurring_next_date = fields.Date(string='Date of Next Invoice') + last_date_invoiced = fields.Date(string='Last Date Invoiced') + is_canceled = fields.Boolean(string="Canceled", default=False) + is_auto_renew = fields.Boolean(string="Auto Renew", default=False) + auto_renew_interval = fields.Integer( + default=1, + string='Renew Every', + help="Renew every (Days/Week/Month/Year)", + ) + auto_renew_rule_type = fields.Selection( + [ + ('daily', 'Day(s)'), + ('weekly', 'Week(s)'), + ('monthly', 'Month(s)'), + ('yearly', 'Year(s)'), + ], + default='yearly', + string='Renewal type', + help="Specify Interval for automatic renewal.", + ) + termination_notice_interval = fields.Integer( + default=1, string='Termination Notice Before' + ) + termination_notice_rule_type = fields.Selection( + [('daily', 'Day(s)'), ('weekly', 'Week(s)'), ('monthly', 'Month(s)')], + default='monthly', + string='Termination Notice type', + ) + contract_id = fields.Many2one( + string='Contract', + comodel_name='contract.abstract.contract', + required=True, + ondelete='cascade', + ) + + @api.depends( + 'automatic_price', + 'specific_price', + 'product_id', + 'quantity', + 'contract_id.pricelist_id', + 'contract_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.contract_id.pricelist_id.id, + partner=line.contract_id.partner_id.id, + date=line.env.context.get( + 'old_date', fields.Date.context_today(line) + ), + ) + 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.contract_id.pricelist_id: + cur = line.contract_id.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.context_today(self) + partner = self.contract_id.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.contract_id.pricelist_id.id, + uom=self.uom_id.id, + ) + vals['name'] = self.product_id.get_product_multiline_description_sale() + 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..f7608020 100644 --- a/contract/models/account_invoice.py +++ b/contract/models/account_invoice.py @@ -7,6 +7,5 @@ from odoo import fields, models class AccountInvoice(models.Model): _inherit = 'account.invoice' - contract_id = fields.Many2one( - 'account.analytic.account', - string='Contract') + # We keep this field for migration purpose + old_contract_id = fields.Many2one('contract.contract') diff --git a/contract/models/account_invoice_line.py b/contract/models/account_invoice_line.py new file mode 100644 index 00000000..31454e41 --- /dev/null +++ b/contract/models/account_invoice_line.py @@ -0,0 +1,12 @@ +# Copyright 2018 ACSONE SA/NV. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class AccountInvoiceLine(models.Model): + _inherit = 'account.invoice.line' + + contract_line_id = fields.Many2one( + 'contract.line', string='Contract Line', index=True + ) diff --git a/contract/models/contract.py b/contract/models/contract.py new file mode 100644 index 00000000..10c630f6 --- /dev/null +++ b/contract/models/contract.py @@ -0,0 +1,457 @@ +# 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 ContractContract(models.Model): + _name = 'contract.contract' + _description = "Contract" + _order = 'code, name asc' + _inherit = [ + 'mail.thread', + 'mail.activity.mixin', + 'contract.abstract.contract', + ] + + active = fields.Boolean( + default=True, + ) + code = fields.Char( + string="Reference", + ) + group_id = fields.Many2one( + string="Group", + comodel_name='account.analytic.account', + ondelete='restrict', + ) + currency_id = fields.Many2one( + related="company_id.currency_id", + string="Currency", + readonly=True, + ) + contract_template_id = fields.Many2one( + string='Contract Template', comodel_name='contract.template' + ) + contract_line_ids = fields.One2many( + string='Contract lines', + comodel_name='contract.line', + inverse_name='contract_id', + copy=True, + ) + + 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 + ) + payment_term_id = fields.Many2one( + comodel_name='account.payment.term', string='Payment Terms', index=True + ) + invoice_count = fields.Integer(compute="_compute_invoice_count") + fiscal_position_id = fields.Many2one( + comodel_name='account.fiscal.position', + string='Fiscal Position', + ondelete='restrict', + ) + invoice_partner_id = fields.Many2one( + string="Invoicing contact", + comodel_name='res.partner', + ondelete='restrict', + ) + partner_id = fields.Many2one( + comodel_name='res.partner', + inverse='_inverse_partner_id', + required=True + ) + + @api.multi + def _inverse_partner_id(self): + for rec in self: + if not rec.invoice_partner_id: + rec.invoice_partner_id = rec.partner_id.address_get( + ['invoice'] + )['invoice'] + + @api.multi + def _get_related_invoices(self): + self.ensure_one() + + invoices = ( + self.env['account.invoice.line'] + .search( + [ + ( + 'contract_line_id', + 'in', + self.contract_line_ids.ids, + ) + ] + ) + .mapped('invoice_id') + ) + invoices |= self.env['account.invoice'].search( + [('old_contract_id', '=', self.id)] + ) + return invoices + + @api.multi + def _compute_invoice_count(self): + for rec in self: + rec.invoice_count = len(rec._get_related_invoices()) + + @api.multi + def action_show_invoices(self): + self.ensure_one() + tree_view_ref = ( + 'account.invoice_supplier_tree' + if self.contract_type == 'purchase' + else 'account.invoice_tree_with_onboarding' + ) + form_view_ref = ( + 'account.invoice_supplier_form' + if self.contract_type == 'purchase' + else 'account.invoice_form' + ) + tree_view = self.env.ref(tree_view_ref, raise_if_not_found=False) + form_view = self.env.ref(form_view_ref, raise_if_not_found=False) + action = { + 'type': 'ir.actions.act_window', + 'name': 'Invoices', + 'res_model': 'account.invoice', + 'view_type': 'form', + 'view_mode': 'tree,kanban,form,calendar,pivot,graph,activity', + 'domain': [('id', 'in', self._get_related_invoices().ids)], + } + if tree_view and form_view: + action['views'] = [(tree_view.id, 'tree'), (form_view.id, 'form')] + return action + + @api.depends('contract_line_ids.date_end') + def _compute_date_end(self): + for contract in self: + contract.date_end = False + date_end = contract.contract_line_ids.mapped('date_end') + if date_end and all(date_end): + contract.date_end = max(date_end) + + @api.depends( + 'contract_line_ids.recurring_next_date', + 'contract_line_ids.is_canceled', + ) + def _compute_recurring_next_date(self): + for contract in self: + recurring_next_date = contract.contract_line_ids.filtered( + lambda l: l.recurring_next_date and not l.is_canceled + ).mapped('recurring_next_date') + if recurring_next_date: + contract.recurring_next_date = min(recurring_next_date) + + @api.depends('contract_line_ids.create_invoice_visibility') + def _compute_create_invoice_visibility(self): + for contract in self: + contract.create_invoice_visibility = any( + contract.contract_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 `contract_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 == 'contract_line_ids': + lines = self._convert_contract_lines(contract_template_id) + self.contract_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 + self.fiscal_position_id = self.partner_id.property_account_position_id + if self.contract_type == 'purchase': + self.payment_term_id = \ + self.partner_id.property_supplier_payment_term_id + else: + self.payment_term_id = \ + self.partner_id.property_payment_term_id + self.invoice_partner_id = self.partner_id.address_get(['invoice'])[ + 'invoice' + ] + return { + 'domain': { + 'invoice_partner_id': [ + '|', + ('id', 'parent_of', self.partner_id.id), + ('id', 'child_of', self.partner_id.id), + ] + } + } + + @api.multi + def _convert_contract_lines(self, contract): + self.ensure_one() + new_lines = self.env['contract.line'] + contract_line_model = self.env['contract.line'] + for contract_line in contract.contract_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.context_today(contract_line) + vals['recurring_next_date'] = fields.Date.context_today( + contract_line + ) + new_lines += contract_line_model.new(vals) + new_lines._onchange_date_start() + new_lines._onchange_is_auto_renew() + return new_lines + + @api.multi + def _prepare_invoice(self, date_invoice, journal=None): + self.ensure_one() + 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' + return { + 'name': self.code, + 'type': invoice_type, + 'partner_id': self.invoice_partner_id.id, + 'currency_id': currency.id, + 'date_invoice': date_invoice, + 'journal_id': journal.id, + 'origin': self.name, + 'company_id': self.company_id.id, + 'user_id': self.user_id.id, + 'payment_term_id': self.payment_term_id.id, + 'fiscal_position_id': self.fiscal_position_id.id, + } + + @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='contract.contract', + 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.model + def _finalize_invoice_values(self, invoice_values): + """ + This method adds the missing values in the invoice lines dictionaries. + + If no account on the product, the invoice lines account is + taken from the invoice's journal in _onchange_product_id + This code is not in finalize_creation_from_contract because it's + not possible to create an invoice line with no account + + :param invoice_values: dictionary (invoice values) + :return: updated dictionary (invoice values) + """ + # If no account on the product, the invoice lines account is + # taken from the invoice's journal in _onchange_product_id + # This code is not in finalize_creation_from_contract because it's + # not possible to create an invoice line with no account + new_invoice = self.env['account.invoice'].new(invoice_values) + for invoice_line in new_invoice.invoice_line_ids: + name = invoice_line.name + account_analytic_id = invoice_line.account_analytic_id + price_unit = invoice_line.price_unit + invoice_line.invoice_id = new_invoice + invoice_line._onchange_product_id() + invoice_line.update( + { + 'name': name, + 'account_analytic_id': account_analytic_id, + 'price_unit': price_unit, + } + ) + return new_invoice._convert_to_write(new_invoice._cache) + + @api.model + def _finalize_invoice_creation(self, invoices): + for invoice in invoices: + payment_term = invoice.payment_term_id + fiscal_position = invoice.fiscal_position_id + invoice._onchange_partner_id() + invoice.payment_term_id = payment_term + invoice.fiscal_position_id = fiscal_position + invoices.compute_taxes() + + @api.model + def _finalize_and_create_invoices(self, invoices_values): + """ + This method: + - finalizes the invoices values (onchange's...) + - creates the invoices + - finalizes the created invoices (onchange's, tax computation...) + :param invoices_values: list of dictionaries (invoices values) + :return: created invoices (account.invoice) + """ + if isinstance(invoices_values, dict): + invoices_values = [invoices_values] + final_invoices_values = [] + for invoice_values in invoices_values: + final_invoices_values.append( + self._finalize_invoice_values(invoice_values) + ) + invoices = self.env['account.invoice'].create(final_invoices_values) + self._finalize_invoice_creation(invoices) + return invoices + + @api.model + def _get_contracts_to_invoice_domain(self, date_ref=None): + """ + This method builds the domain to use to find all + contracts (contract.contract) to invoice. + :param date_ref: optional reference date to use instead of today + :return: list (domain) usable on contract.contract + """ + domain = [] + if not date_ref: + date_ref = fields.Date.context_today(self) + domain.extend([('recurring_next_date', '<=', date_ref)]) + return domain + + @api.multi + def _get_lines_to_invoice(self, date_ref): + """ + This method fetches and returns the lines to invoice on the contract + (self), based on the given date. + :param date_ref: date used as reference date to find lines to invoice + :return: contract lines (contract.line recordset) + """ + self.ensure_one() + return self.contract_line_ids.filtered( + lambda l: not l.is_canceled + and l.recurring_next_date + and l.recurring_next_date <= date_ref + ) + + @api.multi + def _prepare_recurring_invoices_values(self, date_ref=False): + """ + This method builds the list of invoices values to create, based on + the lines to invoice of the contracts in self. + !!! The date of next invoice (recurring_next_date) is updated here !!! + :return: list of dictionaries (invoices values) + """ + invoices_values = [] + for contract in self: + if not date_ref: + date_ref = contract.recurring_next_date + if not date_ref: + # this use case is possible when recurring_create_invoice is + # called for a finished contract + continue + contract_lines = contract._get_lines_to_invoice(date_ref) + if not contract_lines: + continue + invoice_values = contract._prepare_invoice(date_ref) + for line in contract_lines: + invoice_values.setdefault('invoice_line_ids', []) + invoice_line_values = line._prepare_invoice_line( + invoice_id=False + ) + if invoice_line_values: + invoice_values['invoice_line_ids'].append( + (0, 0, invoice_line_values) + ) + invoices_values.append(invoice_values) + contract_lines._update_recurring_next_date() + return invoices_values + + @api.multi + def recurring_create_invoice(self): + """ + This method triggers the creation of the next invoices of the contracts + even if their next invoicing date is in the future. + """ + return self._recurring_create_invoice() + + @api.multi + def _recurring_create_invoice(self, date_ref=False): + invoices_values = self._prepare_recurring_invoices_values(date_ref) + return self._finalize_and_create_invoices(invoices_values) + + @api.model + def cron_recurring_create_invoice(self): + domain = self._get_contracts_to_invoice_domain() + contracts_to_invoice = self.search(domain) + date_ref = fields.Date.context_today(contracts_to_invoice) + contracts_to_invoice._recurring_create_invoice(date_ref) diff --git a/contract/models/contract_line.py b/contract/models/contract_line.py new file mode 100644 index 00000000..40c4d657 --- /dev/null +++ b/contract/models/contract_line.py @@ -0,0 +1,1107 @@ +# Copyright 2017 LasLabs Inc. +# Copyright 2018 ACSONE SA/NV. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from datetime import timedelta +from dateutil.relativedelta import relativedelta + +from odoo import api, fields, models, _ +from odoo.exceptions import ValidationError + +from .contract_line_constraints import get_allowed + + +class ContractLine(models.Model): + _name = 'contract.line' + _description = "Contract Line" + _inherit = 'contract.abstract.contract.line' + + sequence = fields.Integer( + string="Sequence", + ) + contract_id = fields.Many2one( + comodel_name='contract.contract', + string='Contract', + required=True, + index=True, + ondelete='cascade', + ) + analytic_account_id = fields.Many2one( + string="Analytic account", + comodel_name='account.analytic.account', + ) + date_start = fields.Date( + string='Date Start', + required=True, + default=lambda self: fields.Date.context_today(self), + ) + date_end = fields.Date(string='Date End', index=True) + recurring_next_date = fields.Date(string='Date of Next Invoice') + last_date_invoiced = fields.Date( + string='Last Date Invoiced', readonly=True, copy=False + ) + termination_notice_date = fields.Date( + string='Termination notice date', + compute="_compute_termination_notice_date", + store=True, + copy=False, + ) + create_invoice_visibility = fields.Boolean( + compute='_compute_create_invoice_visibility' + ) + successor_contract_line_id = fields.Many2one( + comodel_name='contract.line', + string="Successor Contract Line", + required=False, + readonly=True, + index=True, + copy=False, + help="In case of restart after suspension, this field contain the new " + "contract line created.", + ) + predecessor_contract_line_id = fields.Many2one( + comodel_name='contract.line', + string="Predecessor Contract Line", + required=False, + readonly=True, + index=True, + copy=False, + help="Contract Line origin of this one.", + ) + manual_renew_needed = fields.Boolean( + string="Manual renew needed", + default=False, + help="This flag is used to make a difference between a definitive stop" + "and temporary one for which a user is not able to plan a" + "successor in advance", + ) + is_plan_successor_allowed = fields.Boolean( + string="Plan successor allowed?", compute='_compute_allowed' + ) + is_stop_plan_successor_allowed = fields.Boolean( + string="Stop/Plan successor allowed?", compute='_compute_allowed' + ) + is_stop_allowed = fields.Boolean( + string="Stop allowed?", compute='_compute_allowed' + ) + is_cancel_allowed = fields.Boolean( + string="Cancel allowed?", compute='_compute_allowed' + ) + is_un_cancel_allowed = fields.Boolean( + string="Un-Cancel allowed?", compute='_compute_allowed' + ) + state = fields.Selection( + string="State", + selection=[ + ('upcoming', 'Upcoming'), + ('in-progress', 'In-progress'), + ('to-renew', 'To renew'), + ('upcoming-close', 'Upcoming Close'), + ('closed', 'Closed'), + ('canceled', 'Canceled'), + ], + compute="_compute_state", + search='_search_state', + ) + active = fields.Boolean( + string="Active", + related="contract_id.active", + store=True, + readonly=True, + default=True, + ) + + @api.multi + @api.depends( + 'date_end', + 'termination_notice_rule_type', + 'termination_notice_interval', + ) + def _compute_termination_notice_date(self): + for rec in self: + if rec.date_end: + rec.termination_notice_date = ( + rec.date_end + - self.get_relative_delta( + rec.termination_notice_rule_type, + rec.termination_notice_interval, + ) + ) + + @api.multi + @api.depends('is_canceled', 'date_start', 'date_end', 'is_auto_renew') + def _compute_state(self): + today = fields.Date.context_today(self) + for rec in self: + if rec.is_canceled: + rec.state = 'canceled' + continue + + if rec.date_start and rec.date_start > today: + # Before period + rec.state = 'upcoming' + continue + if ( + rec.date_start + and rec.date_start <= today + and (not rec.date_end or rec.date_end >= today) + ): + # In period + if ( + rec.termination_notice_date + and rec.termination_notice_date < today + and not rec.is_auto_renew + and not rec.manual_renew_needed + ): + rec.state = 'upcoming-close' + else: + rec.state = 'in-progress' + continue + if rec.date_end and rec.date_end < today: + # After + if ( + rec.manual_renew_needed + and not rec.successor_contract_line_id + or rec.is_auto_renew + ): + rec.state = 'to-renew' + else: + rec.state = 'closed' + + @api.model + def _get_state_domain(self, state): + today = fields.Date.context_today(self) + if state == 'upcoming': + return [ + "&", + ('date_start', '>', today), + ('is_canceled', '=', False), + ] + if state == 'in-progress': + return [ + "&", + "&", + "&", + ('date_start', '<=', today), + ('is_canceled', '=', False), + "|", + ('date_end', '>=', today), + ('date_end', '=', False), + "|", + "&", + ('is_auto_renew', '=', True), + ('is_auto_renew', '=', False), + ('termination_notice_date', '>', today), + ] + if state == 'to-renew': + return [ + "&", + "&", + ('is_canceled', '=', False), + ('date_end', '<', today), + "|", + "&", + ('manual_renew_needed', '=', True), + ('successor_contract_line_id', '=', False), + ('is_auto_renew', '=', True), + ] + if state == 'upcoming-close': + return [ + "&", + "&", + "&", + "&", + "&", + ('date_start', '<=', today), + ('is_auto_renew', '=', False), + ('manual_renew_needed', '=', False), + ('is_canceled', '=', False), + ('termination_notice_date', '<', today), + ('date_end', '>=', today), + ] + if state == 'closed': + return [ + "&", + "&", + "&", + ('is_canceled', '=', False), + ('date_end', '<', today), + ('is_auto_renew', '=', False), + "|", + "&", + ('manual_renew_needed', '=', True), + ('successor_contract_line_id', '!=', False), + ('manual_renew_needed', '=', False), + ] + if state == 'canceled': + return [('is_canceled', '=', True)] + + @api.model + def _search_state(self, operator, value): + states = [ + 'upcoming', + 'in-progress', + 'to-renew', + 'upcoming-close', + 'closed', + 'canceled', + ] + if operator == '!=' and not value: + return [] + if operator == '=' and not value: + return [('id', '=', False)] + if operator == '=': + return self._get_state_domain(value) + if operator == '!=': + domain = [] + for state in states: + if state != value: + if domain: + domain.insert(0, '|') + domain.extend(self._get_state_domain(state)) + return domain + if operator == 'in': + domain = [] + if not value: + return [('id', '=', False)] + for state in value: + if domain: + domain.insert(0, '|') + domain.extend(self._get_state_domain(state)) + return domain + + if operator == 'not in': + return self._search_state( + 'in', [state for state in states if state not in value] + ) + + @api.depends( + 'date_start', + 'date_end', + 'last_date_invoiced', + 'is_auto_renew', + 'successor_contract_line_id', + 'predecessor_contract_line_id', + 'is_canceled', + ) + def _compute_allowed(self): + for rec in self: + if rec.date_start: + allowed = get_allowed( + rec.date_start, + rec.date_end, + rec.last_date_invoiced, + rec.is_auto_renew, + rec.successor_contract_line_id, + rec.predecessor_contract_line_id, + rec.is_canceled, + ) + if allowed: + rec.is_plan_successor_allowed = allowed.plan_successor + rec.is_stop_plan_successor_allowed = ( + allowed.stop_plan_successor + ) + rec.is_stop_allowed = allowed.stop + rec.is_cancel_allowed = allowed.cancel + rec.is_un_cancel_allowed = allowed.uncancel + + @api.constrains('is_auto_renew', 'successor_contract_line_id', 'date_end') + def _check_allowed(self): + """ + logical impossible combination: + * a line with is_auto_renew True should have date_end and + couldn't have successor_contract_line_id + * a line without date_end can't have successor_contract_line_id + + """ + for rec in self: + if rec.is_auto_renew: + if rec.successor_contract_line_id: + raise ValidationError( + _( + "A contract line with a successor " + "can't be set to auto-renew" + ) + ) + if not rec.date_end: + raise ValidationError( + _("An auto-renew line must have a end date") + ) + else: + if not rec.date_end and rec.successor_contract_line_id: + raise ValidationError( + _( + "A contract line with a successor " + "must have a end date" + ) + ) + + @api.constrains('successor_contract_line_id', 'date_end') + def _check_overlap_successor(self): + for rec in self: + if rec.date_end and rec.successor_contract_line_id: + if rec.date_end >= rec.successor_contract_line_id.date_start: + raise ValidationError( + _("Contract line and its successor overlapped") + ) + + @api.constrains('predecessor_contract_line_id', 'date_start') + def _check_overlap_predecessor(self): + for rec in self: + if rec.predecessor_contract_line_id: + if rec.date_start <= rec.predecessor_contract_line_id.date_end: + raise ValidationError( + _("Contract line and its predecessor overlapped") + ) + + @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.model + def compute_first_date_end( + self, date_start, auto_renew_rule_type, auto_renew_interval + ): + return ( + date_start + + self.get_relative_delta( + auto_renew_rule_type, auto_renew_interval + ) + - relativedelta(days=1) + ) + + @api.onchange( + 'date_start', + 'is_auto_renew', + 'auto_renew_rule_type', + 'auto_renew_interval', + ) + def _onchange_is_auto_renew(self): + """Date end should be auto-computed if a contract line is set to + auto_renew""" + for rec in self.filtered('is_auto_renew'): + if rec.date_start: + rec.date_end = self.compute_first_date_end( + rec.date_start, + rec.auto_renew_rule_type, + rec.auto_renew_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('is_canceled', 'is_auto_renew') + def _check_auto_renew_canceled_lines(self): + for rec in self: + if rec.is_canceled and rec.is_auto_renew: + raise ValidationError( + _("A canceled contract line can't be set to auto-renew") + ) + + @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 date of next invoice anterior " + "to the start of the contract line '%s'" + ) + % line.name + ) + + @api.constrains('date_start', 'date_end', 'last_date_invoiced') + def _check_last_date_invoiced(self): + for rec in self.filtered('last_date_invoiced'): + if rec.date_start and rec.date_start > rec.last_date_invoiced: + raise ValidationError( + _( + "You can't have the start date after the date of last " + "invoice for the contract line '%s'" + ) + % rec.name + ) + if rec.date_end and rec.date_end < rec.last_date_invoiced: + raise ValidationError( + _( + "You can't have the end date before the date of last " + "invoice for the contract line '%s'" + ) + % rec.name + ) + + @api.constrains('recurring_next_date') + def _check_recurring_next_date_recurring_invoices(self): + for rec in self: + if not rec.recurring_next_date and ( + not rec.date_end + or not rec.last_date_invoiced + or rec.last_date_invoiced < rec.date_end + ): + raise ValidationError( + _( + "You must supply a date of next invoice for contract " + "line '%s'" + ) + % rec.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 line '%s' start date can't be later than" + " end date" + ) + % line.name + ) + + @api.depends('recurring_next_date', 'date_start', 'date_end') + def _compute_create_invoice_visibility(self): + today = fields.Date.context_today(self) + for rec in self: + if rec.date_start: + if today < rec.date_start: + rec.create_invoice_visibility = False + else: + rec.create_invoice_visibility = bool( + rec.recurring_next_date + ) + + @api.multi + def _prepare_invoice_line(self, invoice_id=False): + self.ensure_one() + dates = self._get_period_to_invoice( + self.last_date_invoiced, self.recurring_next_date + ) + invoice_line_vals = { + 'product_id': self.product_id.id, + 'quantity': self._get_quantity_to_invoice(*dates), + 'uom_id': self.uom_id.id, + 'discount': self.discount, + 'contract_line_id': self.id, + } + if invoice_id: + invoice_line_vals['invoice_id'] = invoice_id.id + invoice_line = self.env['account.invoice.line'].new(invoice_line_vals) + # 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 + name = self._insert_markers(dates[0], dates[1]) + invoice_line_vals.update( + { + 'name': name, + 'account_analytic_id': self.analytic_account_id.id, + 'price_unit': self.price_unit, + } + ) + return invoice_line_vals + + @api.multi + def _get_period_to_invoice( + self, last_date_invoiced, recurring_next_date, stop_at_date_end=True + ): + self.ensure_one() + first_date_invoiced = False + if not recurring_next_date: + return first_date_invoiced, last_date_invoiced, recurring_next_date + first_date_invoiced = ( + last_date_invoiced + relativedelta(days=1) + if last_date_invoiced + else self.date_start + ) + if self.recurring_rule_type == 'monthlylastday': + last_date_invoiced = recurring_next_date + else: + if self.recurring_invoicing_type == 'pre-paid': + last_date_invoiced = ( + recurring_next_date + + self.get_relative_delta( + self.recurring_rule_type, self.recurring_interval + ) + - relativedelta(days=1) + ) + else: + last_date_invoiced = recurring_next_date - relativedelta( + days=1 + ) + if stop_at_date_end: + if self.date_end and self.date_end < last_date_invoiced: + last_date_invoiced = self.date_end + return first_date_invoiced, last_date_invoiced, recurring_next_date + + @api.multi + def _insert_markers(self, first_date_invoiced, last_date_invoiced): + self.ensure_one() + lang_obj = self.env['res.lang'] + lang = lang_obj.search( + [('code', '=', self.contract_id.partner_id.lang)] + ) + date_format = lang.date_format or '%m/%d/%Y' + name = self.name + name = name.replace( + '#START#', first_date_invoiced.strftime(date_format) + ) + name = name.replace('#END#', last_date_invoiced.strftime(date_format)) + return name + + @api.multi + def _update_recurring_next_date(self): + for rec in self: + old_date = rec.recurring_next_date + new_date = old_date + self.get_relative_delta( + rec.recurring_rule_type, rec.recurring_interval + ) + if rec.recurring_rule_type == 'monthlylastday': + last_date_invoiced = old_date + elif rec.recurring_invoicing_type == 'post-paid': + last_date_invoiced = old_date - relativedelta(days=1) + elif rec.recurring_invoicing_type == 'pre-paid': + last_date_invoiced = new_date - relativedelta(days=1) + + if rec.date_end and last_date_invoiced >= rec.date_end: + rec.last_date_invoiced = rec.date_end + rec.recurring_next_date = False + else: + rec.last_date_invoiced = last_date_invoiced + rec.recurring_next_date = new_date + + @api.multi + def _init_last_date_invoiced(self): + """Used to init last_date_invoiced for migration purpose""" + for rec in self: + last_date_invoiced = rec.recurring_next_date - relativedelta( + days=1 + ) + if rec.recurring_rule_type == 'monthlylastday': + last_date_invoiced = ( + rec.recurring_next_date + - self.get_relative_delta( + rec.recurring_rule_type, rec.recurring_interval + ) + ) + elif rec.recurring_invoicing_type == 'post-paid': + last_date_invoiced = ( + rec.recurring_next_date + - self.get_relative_delta( + rec.recurring_rule_type, rec.recurring_interval + ) + ) - relativedelta(days=1) + if last_date_invoiced > rec.date_start: + rec.last_date_invoiced = last_date_invoiced + + @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.multi + def _delay(self, delay_delta): + """ + Delay a contract line + :param delay_delta: delay relative delta + :return: delayed contract line + """ + for rec in self: + if rec.last_date_invoiced: + raise ValidationError( + _( + "You can't delay a contract line " + "invoiced at least one time." + ) + ) + new_date_start = rec.date_start + delay_delta + rec.recurring_next_date = self._compute_first_recurring_next_date( + new_date_start, + rec.recurring_invoicing_type, + rec.recurring_rule_type, + rec.recurring_interval, + ) + if rec.date_end: + rec.date_end += delay_delta + rec.date_start = new_date_start + + @api.multi + def stop(self, date_end, manual_renew_needed=False, post_message=True): + """ + Put date_end on contract line + We don't consider contract lines that end's before the new end date + :param date_end: new date end for contract line + :return: True + """ + if not all(self.mapped('is_stop_allowed')): + raise ValidationError(_('Stop not allowed for this line')) + for rec in self: + if date_end < rec.date_start: + rec.cancel() + else: + if not rec.date_end or rec.date_end > date_end: + old_date_end = rec.date_end + values = { + 'date_end': date_end, + 'is_auto_renew': False, + 'manual_renew_needed': manual_renew_needed, + } + if rec.last_date_invoiced == date_end: + values['recurring_next_date'] = False + rec.write(values) + if post_message: + msg = _( + """Contract line for {product} + stopped:
+ - End: {old_end} -- {new_end} + """.format( + product=rec.name, + old_end=old_date_end, + new_end=rec.date_end, + ) + ) + rec.contract_id.message_post(body=msg) + else: + rec.write( + { + 'is_auto_renew': False, + "manual_renew_needed": manual_renew_needed, + } + ) + return True + + @api.multi + def _prepare_value_for_plan_successor( + self, date_start, date_end, is_auto_renew, recurring_next_date=False + ): + self.ensure_one() + if not recurring_next_date: + recurring_next_date = self._compute_first_recurring_next_date( + date_start, + self.recurring_invoicing_type, + self.recurring_rule_type, + self.recurring_interval, + ) + new_vals = self.read()[0] + new_vals.pop("id", None) + new_vals.pop("last_date_invoiced", None) + values = self._convert_to_write(new_vals) + values['date_start'] = date_start + values['date_end'] = date_end + values['recurring_next_date'] = recurring_next_date + values['is_auto_renew'] = is_auto_renew + values['predecessor_contract_line_id'] = self.id + return values + + @api.multi + def plan_successor( + self, + date_start, + date_end, + is_auto_renew, + recurring_next_date=False, + post_message=True, + ): + """ + Create a copy of a contract line in a new interval + :param date_start: date_start for the successor_contract_line + :param date_end: date_end for the successor_contract_line + :param is_auto_renew: is_auto_renew option for successor_contract_line + :param recurring_next_date: recurring_next_date for the + successor_contract_line + :return: successor_contract_line + """ + contract_line = self.env['contract.line'] + for rec in self: + if not rec.is_plan_successor_allowed: + raise ValidationError( + _('Plan successor not allowed for this line') + ) + rec.is_auto_renew = False + new_line = self.create( + rec._prepare_value_for_plan_successor( + date_start, date_end, is_auto_renew, recurring_next_date + ) + ) + rec.successor_contract_line_id = new_line + contract_line |= new_line + if post_message: + msg = _( + """Contract line for {product} + planned a successor:
+ - Start: {new_date_start} +
+ - End: {new_date_end} + """.format( + product=rec.name, + new_date_start=new_line.date_start, + new_date_end=new_line.date_end, + ) + ) + rec.contract_id.message_post(body=msg) + return contract_line + + @api.multi + def stop_plan_successor(self, date_start, date_end, is_auto_renew): + """ + Stop a contract line for a defined period and start it later + Cases to consider: + * contract line end's before the suspension period: + -> apply stop + * contract line start before the suspension period and end in it + -> apply stop at suspension start date + -> apply plan successor: + - date_start: suspension.date_end + - date_end: date_end + (contract_line.date_end + - suspension.date_start) + * contract line start before the suspension period and end after it + -> apply stop at suspension start date + -> apply plan successor: + - date_start: suspension.date_end + - date_end: date_end + (suspension.date_end + - suspension.date_start) + * contract line start and end's in the suspension period + -> apply delay + - delay: suspension.date_end - contract_line.date_start + * contract line start in the suspension period and end after it + -> apply delay + - delay: suspension.date_end - contract_line.date_start + * contract line start and end after the suspension period + -> apply delay + - delay: suspension.date_end - suspension.start_date + :param date_start: suspension start date + :param date_end: suspension end date + :param is_auto_renew: is the new line is set to auto_renew + :return: created contract line + """ + if not all(self.mapped('is_stop_plan_successor_allowed')): + raise ValidationError( + _('Stop/Plan successor not allowed for this line') + ) + contract_line = self.env['contract.line'] + for rec in self: + if rec.date_start >= date_start: + if rec.date_start < date_end: + delay = (date_end - rec.date_start) + timedelta(days=1) + else: + delay = (date_end - date_start) + timedelta(days=1) + rec._delay(delay) + contract_line |= rec + else: + if rec.date_end and rec.date_end < date_start: + rec.stop(date_start, post_message=False) + elif ( + rec.date_end + and rec.date_end > date_start + and rec.date_end < date_end + ): + new_date_start = date_end + relativedelta(days=1) + new_date_end = ( + date_end + + (rec.date_end - date_start) + + relativedelta(days=1) + ) + rec.stop( + date_start - relativedelta(days=1), + manual_renew_needed=True, + post_message=False, + ) + contract_line |= rec.plan_successor( + new_date_start, + new_date_end, + is_auto_renew, + post_message=False, + ) + else: + new_date_start = date_end + relativedelta(days=1) + if rec.date_end: + new_date_end = ( + rec.date_end + + (date_end - date_start) + + relativedelta(days=1) + ) + else: + new_date_end = rec.date_end + + rec.stop( + date_start - relativedelta(days=1), + manual_renew_needed=True, + post_message=False, + ) + contract_line |= rec.plan_successor( + new_date_start, + new_date_end, + is_auto_renew, + post_message=False, + ) + msg = _( + """Contract line for {product} + suspended:
+ - Suspension Start: {new_date_start} +
+ - Suspension End: {new_date_end} + """.format( + product=rec.name, + new_date_start=date_start, + new_date_end=date_end, + ) + ) + rec.contract_id.message_post(body=msg) + return contract_line + + @api.multi + def cancel(self): + if not all(self.mapped('is_cancel_allowed')): + raise ValidationError(_('Cancel not allowed for this line')) + for contract in self.mapped('contract_id'): + lines = self.filtered(lambda l, c=contract: l.contract_id == c) + msg = _( + """Contract line canceled: %s""" + % "
- ".join( + [ + "%s" % name + for name in lines.mapped('name') + ] + ) + ) + contract.message_post(body=msg) + self.mapped('predecessor_contract_line_id').write( + {'successor_contract_line_id': False} + ) + return self.write({'is_canceled': True, 'is_auto_renew': False}) + + @api.multi + def uncancel(self, recurring_next_date): + if not all(self.mapped('is_un_cancel_allowed')): + raise ValidationError(_('Un-cancel not allowed for this line')) + for contract in self.mapped('contract_id'): + lines = self.filtered(lambda l, c=contract: l.contract_id == c) + msg = _( + """Contract line Un-canceled: %s""" + % "
- ".join( + [ + "%s" % name + for name in lines.mapped('name') + ] + ) + ) + contract.message_post(body=msg) + for rec in self: + if rec.predecessor_contract_line_id: + predecessor_contract_line = rec.predecessor_contract_line_id + assert not predecessor_contract_line.successor_contract_line_id + predecessor_contract_line.successor_contract_line_id = rec + rec.is_canceled = False + rec.recurring_next_date = recurring_next_date + return True + + @api.multi + def action_uncancel(self): + self.ensure_one() + context = { + 'default_contract_line_id': self.id, + 'default_recurring_next_date': fields.Date.context_today(self), + } + context.update(self.env.context) + view_id = self.env.ref( + 'contract.contract_line_wizard_uncancel_form_view' + ).id + return { + 'type': 'ir.actions.act_window', + 'name': 'Un-Cancel Contract Line', + 'res_model': 'contract.line.wizard', + 'view_type': 'form', + 'view_mode': 'form', + 'views': [(view_id, 'form')], + 'target': 'new', + 'context': context, + } + + @api.multi + def action_plan_successor(self): + self.ensure_one() + context = { + 'default_contract_line_id': self.id, + 'default_is_auto_renew': self.is_auto_renew, + } + context.update(self.env.context) + view_id = self.env.ref( + 'contract.contract_line_wizard_plan_successor_form_view' + ).id + return { + 'type': 'ir.actions.act_window', + 'name': 'Plan contract line successor', + 'res_model': 'contract.line.wizard', + 'view_type': 'form', + 'view_mode': 'form', + 'views': [(view_id, 'form')], + 'target': 'new', + 'context': context, + } + + @api.multi + def action_stop(self): + self.ensure_one() + context = { + 'default_contract_line_id': self.id, + 'default_date_end': self.date_end, + } + context.update(self.env.context) + view_id = self.env.ref( + 'contract.contract_line_wizard_stop_form_view' + ).id + return { + 'type': 'ir.actions.act_window', + 'name': 'Resiliate contract line', + 'res_model': 'contract.line.wizard', + 'view_type': 'form', + 'view_mode': 'form', + 'views': [(view_id, 'form')], + 'target': 'new', + 'context': context, + } + + @api.multi + def action_stop_plan_successor(self): + self.ensure_one() + context = { + 'default_contract_line_id': self.id, + 'default_is_auto_renew': self.is_auto_renew, + } + context.update(self.env.context) + view_id = self.env.ref( + 'contract.contract_line_wizard_stop_plan_successor_form_view' + ).id + return { + 'type': 'ir.actions.act_window', + 'name': 'Suspend contract line', + 'res_model': 'contract.line.wizard', + 'view_type': 'form', + 'view_mode': 'form', + 'views': [(view_id, 'form')], + 'target': 'new', + 'context': context, + } + + @api.multi + def _get_renewal_dates(self): + self.ensure_one() + date_start = self.date_end + relativedelta(days=1) + date_end = self.compute_first_date_end( + date_start, self.auto_renew_rule_type, self.auto_renew_interval + ) + return date_start, date_end + + @api.multi + def renew(self): + res = self.env['contract.line'] + for rec in self: + is_auto_renew = rec.is_auto_renew + rec.stop(rec.date_end, post_message=False) + date_start, date_end = rec._get_renewal_dates() + new_line = rec.plan_successor( + date_start, date_end, is_auto_renew, post_message=False + ) + new_line._onchange_date_start() + res |= new_line + msg = _( + """Contract line for {product} + renewed:
+ - Start: {new_date_start} +
+ - End: {new_date_end} + """.format( + product=rec.name, + new_date_start=date_start, + new_date_end=date_end, + ) + ) + rec.contract_id.message_post(body=msg) + return res + + @api.model + def _contract_line_to_renew_domain(self): + return [ + ('is_auto_renew', '=', True), + ('is_canceled', '=', False), + ('termination_notice_date', '<=', fields.Date.context_today(self)), + ] + + @api.model + def cron_renew_contract_line(self): + domain = self._contract_line_to_renew_domain() + to_renew = self.search(domain) + to_renew.renew() + + @api.model + def fields_view_get( + self, view_id=None, view_type='form', toolbar=False, submenu=False + ): + default_contract_type = self.env.context.get('default_contract_type') + if view_type == 'tree' and default_contract_type == 'purchase': + view_id = self.env.ref( + 'contract.contract_line_supplier_tree_view' + ).id + if view_type == 'form': + if default_contract_type == 'purchase': + view_id = self.env.ref( + 'contract.contract_line_supplier_form_view' + ).id + elif default_contract_type == 'sale': + view_id = self.env.ref( + 'contract.contract_line_customer_form_view' + ).id + return super().fields_view_get( + view_id, view_type, toolbar, submenu + ) + + @api.multi + def unlink(self): + """stop unlink uncnacled lines""" + if not all(self.mapped('is_canceled')): + raise ValidationError( + _("Contract line must be canceled before delete") + ) + return super().unlink() + + @api.multi + def _get_quantity_to_invoice( + self, period_first_date, period_last_date, invoice_date + ): + self.ensure_one() + return self.quantity diff --git a/contract/models/contract_line_constraints.py b/contract/models/contract_line_constraints.py new file mode 100644 index 00000000..b1fa3798 --- /dev/null +++ b/contract/models/contract_line_constraints.py @@ -0,0 +1,428 @@ +# Copyright 2018 ACSONE SA/NV. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import itertools +from collections import namedtuple +from odoo.fields import Date + +Criteria = namedtuple( + 'Criteria', + [ + 'when', # Contract line relatively to today (BEFORE, IN, AFTER) + 'has_date_end', # Is date_end set on contract line (bool) + 'has_last_date_invoiced', # Is last_date_invoiced set on contract line + 'is_auto_renew', # Is is_auto_renew set on contract line (bool) + 'has_successor', # Is contract line has_successor (bool) + 'predecessor_has_successor', + # Is contract line predecessor has successor (bool) + # In almost of the cases + # contract_line.predecessor.successor == contract_line + # But at cancel action, + # contract_line.predecessor.successor == False + # This is to permit plan_successor on predecessor + # If contract_line.predecessor.successor != False + # and contract_line is canceled, we don't allow uncancel + # else we re-link contract_line and its predecessor + 'canceled', # Is contract line canceled (bool) + ], +) +Allowed = namedtuple( + 'Allowed', + ['plan_successor', 'stop_plan_successor', 'stop', 'cancel', 'uncancel'], +) + + +def _expand_none(criteria): + variations = [] + for attribute, value in criteria._asdict().items(): + if value is None: + if attribute == 'when': + variations.append(['BEFORE', 'IN', 'AFTER']) + else: + variations.append([True, False]) + else: + variations.append([value]) + return itertools.product(*variations) + + +def _add(matrix, criteria, allowed): + """ Expand None values to True/False combination """ + for c in _expand_none(criteria): + matrix[c] = allowed + + +CRITERIA_ALLOWED_DICT = { + Criteria( + when='BEFORE', + has_date_end=True, + has_last_date_invoiced=False, + is_auto_renew=True, + has_successor=False, + predecessor_has_successor=None, + canceled=False, + ): Allowed( + plan_successor=False, + stop_plan_successor=True, + stop=True, + cancel=True, + uncancel=False, + ), + Criteria( + when='BEFORE', + has_date_end=True, + has_last_date_invoiced=False, + is_auto_renew=False, + has_successor=True, + predecessor_has_successor=None, + canceled=False, + ): Allowed( + plan_successor=False, + stop_plan_successor=False, + stop=True, + cancel=True, + uncancel=False, + ), + Criteria( + when='BEFORE', + has_date_end=True, + has_last_date_invoiced=False, + is_auto_renew=False, + has_successor=False, + predecessor_has_successor=None, + canceled=False, + ): Allowed( + plan_successor=True, + stop_plan_successor=True, + stop=True, + cancel=True, + uncancel=False, + ), + Criteria( + when='BEFORE', + has_date_end=False, + has_last_date_invoiced=False, + is_auto_renew=False, + has_successor=False, + predecessor_has_successor=None, + canceled=False, + ): Allowed( + plan_successor=False, + stop_plan_successor=True, + stop=True, + cancel=True, + uncancel=False, + ), + Criteria( + when='IN', + has_date_end=True, + has_last_date_invoiced=False, + is_auto_renew=True, + has_successor=False, + predecessor_has_successor=None, + canceled=False, + ): Allowed( + plan_successor=False, + stop_plan_successor=True, + stop=True, + cancel=True, + uncancel=False, + ), + Criteria( + when='IN', + has_date_end=True, + has_last_date_invoiced=False, + is_auto_renew=False, + has_successor=True, + predecessor_has_successor=None, + canceled=False, + ): Allowed( + plan_successor=False, + stop_plan_successor=False, + stop=True, + cancel=True, + uncancel=False, + ), + Criteria( + when='IN', + has_date_end=True, + has_last_date_invoiced=False, + is_auto_renew=False, + has_successor=False, + predecessor_has_successor=None, + canceled=False, + ): Allowed( + plan_successor=True, + stop_plan_successor=True, + stop=True, + cancel=True, + uncancel=False, + ), + Criteria( + when='IN', + has_date_end=False, + has_last_date_invoiced=False, + is_auto_renew=False, + has_successor=False, + predecessor_has_successor=None, + canceled=False, + ): Allowed( + plan_successor=False, + stop_plan_successor=True, + stop=True, + cancel=True, + uncancel=False, + ), + Criteria( + when='BEFORE', + has_date_end=True, + has_last_date_invoiced=True, + is_auto_renew=True, + has_successor=False, + predecessor_has_successor=None, + canceled=False, + ): Allowed( + plan_successor=False, + stop_plan_successor=True, + stop=True, + cancel=False, + uncancel=False, + ), + Criteria( + when='BEFORE', + has_date_end=True, + has_last_date_invoiced=True, + is_auto_renew=False, + has_successor=True, + predecessor_has_successor=None, + canceled=False, + ): Allowed( + plan_successor=False, + stop_plan_successor=False, + stop=True, + cancel=False, + uncancel=False, + ), + Criteria( + when='BEFORE', + has_date_end=True, + has_last_date_invoiced=True, + is_auto_renew=False, + has_successor=False, + predecessor_has_successor=None, + canceled=False, + ): Allowed( + plan_successor=True, + stop_plan_successor=True, + stop=True, + cancel=False, + uncancel=False, + ), + Criteria( + when='BEFORE', + has_date_end=False, + has_last_date_invoiced=True, + is_auto_renew=False, + has_successor=False, + predecessor_has_successor=None, + canceled=False, + ): Allowed( + plan_successor=False, + stop_plan_successor=True, + stop=True, + cancel=False, + uncancel=False, + ), + Criteria( + when='IN', + has_date_end=True, + has_last_date_invoiced=True, + is_auto_renew=True, + has_successor=False, + predecessor_has_successor=None, + canceled=False, + ): Allowed( + plan_successor=False, + stop_plan_successor=True, + stop=True, + cancel=False, + uncancel=False, + ), + Criteria( + when='IN', + has_date_end=True, + has_last_date_invoiced=True, + is_auto_renew=False, + has_successor=True, + predecessor_has_successor=None, + canceled=False, + ): Allowed( + plan_successor=False, + stop_plan_successor=False, + stop=True, + cancel=False, + uncancel=False, + ), + Criteria( + when='IN', + has_date_end=True, + has_last_date_invoiced=True, + is_auto_renew=False, + has_successor=False, + predecessor_has_successor=None, + canceled=False, + ): Allowed( + plan_successor=True, + stop_plan_successor=True, + stop=True, + cancel=False, + uncancel=False, + ), + Criteria( + when='IN', + has_date_end=False, + has_last_date_invoiced=True, + is_auto_renew=False, + has_successor=False, + predecessor_has_successor=None, + canceled=False, + ): Allowed( + plan_successor=False, + stop_plan_successor=True, + stop=True, + cancel=False, + uncancel=False, + ), + Criteria( + when='AFTER', + has_date_end=True, + has_last_date_invoiced=None, + is_auto_renew=True, + has_successor=False, + predecessor_has_successor=None, + canceled=False, + ): Allowed( + plan_successor=False, + stop_plan_successor=False, + stop=True, + cancel=False, + uncancel=False, + ), + Criteria( + when='AFTER', + has_date_end=True, + has_last_date_invoiced=None, + is_auto_renew=False, + has_successor=True, + predecessor_has_successor=None, + canceled=False, + ): Allowed( + plan_successor=False, + stop_plan_successor=False, + stop=False, + cancel=False, + uncancel=False, + ), + Criteria( + when='AFTER', + has_date_end=True, + has_last_date_invoiced=None, + is_auto_renew=False, + has_successor=False, + predecessor_has_successor=None, + canceled=False, + ): Allowed( + plan_successor=True, + stop_plan_successor=False, + stop=True, + cancel=False, + uncancel=False, + ), + Criteria( + when=None, + has_date_end=None, + has_last_date_invoiced=None, + is_auto_renew=None, + has_successor=None, + predecessor_has_successor=False, + canceled=True, + ): Allowed( + plan_successor=False, + stop_plan_successor=False, + stop=False, + cancel=False, + uncancel=True, + ), + Criteria( + when=None, + has_date_end=None, + has_last_date_invoiced=None, + is_auto_renew=None, + has_successor=None, + predecessor_has_successor=True, + canceled=True, + ): Allowed( + plan_successor=False, + stop_plan_successor=False, + stop=False, + cancel=False, + uncancel=False, + ), +} +criteria_allowed_dict = {} + +for c in CRITERIA_ALLOWED_DICT: + _add(criteria_allowed_dict, c, CRITERIA_ALLOWED_DICT[c]) + + +def compute_when(date_start, date_end): + today = Date.today() + if today < date_start: + return 'BEFORE' + if date_end and today > date_end: + return 'AFTER' + return 'IN' + + +def compute_criteria( + date_start, + date_end, + has_last_date_invoiced, + is_auto_renew, + successor_contract_line_id, + predecessor_contract_line_id, + is_canceled, +): + return Criteria( + when=compute_when(date_start, date_end), + has_date_end=bool(date_end), + has_last_date_invoiced=bool(has_last_date_invoiced), + is_auto_renew=is_auto_renew, + has_successor=bool(successor_contract_line_id), + predecessor_has_successor=bool( + predecessor_contract_line_id.successor_contract_line_id + ), + canceled=is_canceled, + ) + + +def get_allowed( + date_start, + date_end, + has_last_date_invoiced, + is_auto_renew, + successor_contract_line_id, + predecessor_contract_line_id, + is_canceled, +): + criteria = compute_criteria( + date_start, + date_end, + has_last_date_invoiced, + is_auto_renew, + successor_contract_line_id, + predecessor_contract_line_id, + is_canceled, + ) + if criteria in criteria_allowed_dict: + return criteria_allowed_dict[criteria] + return False diff --git a/contract/models/contract_template.py b/contract/models/contract_template.py new file mode 100644 index 00000000..66915c98 --- /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 ContractTemplate(models.Model): + _name = 'contract.template' + _inherit = 'contract.abstract.contract' + _description = "Contract Template" + + contract_line_ids = fields.One2many( + comodel_name='contract.template.line', + inverse_name='contract_id', + copy=True, + string='Contract template lines', + ) diff --git a/contract/models/contract_template_line.py b/contract/models/contract_template_line.py new file mode 100644 index 00000000..25f0cd52 --- /dev/null +++ b/contract/models/contract_template_line.py @@ -0,0 +1,24 @@ +# 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 ContractTemplateLine(models.Model): + _name = 'contract.template.line' + _inherit = 'contract.abstract.contract.line' + _description = "Contract Template Line" + _order = "sequence,id" + + contract_id = fields.Many2one( + string='Contract', + comodel_name='contract.template', + required=True, + ondelete='cascade', + oldname='analytic_account_id', + ) diff --git a/contract/models/res_partner.py b/contract/models/res_partner.py index ead5e4c7..0e4e7149 100644 --- a/contract/models/res_partner.py +++ b/contract/models/res_partner.py @@ -15,16 +15,16 @@ class ResPartner(models.Model): string='Purchase Contracts', compute='_compute_contract_count', ) + contract_ids = fields.One2many( + comodel_name='contract.contract', + inverse='partner_id', + string="Contracts", + ) def _compute_contract_count(self): - contract_model = self.env['account.analytic.account'] - today = fields.Date.today() + contract_model = self.env['contract.contract'] fetch_data = contract_model.read_group([ - ('recurring_invoices', '=', True), - ('partner_id', 'child_of', self.ids), - '|', - ('date_end', '=', False), - ('date_end', '>=', today)], + ('partner_id', 'child_of', self.ids)], ['partner_id', 'contract_type'], ['partner_id', 'contract_type'], lazy=False) result = [[data['partner_id'][0], data['contract_type'], @@ -49,11 +49,8 @@ class ResPartner(models.Model): res.update( context=dict( self.env.context, - search_default_recurring_invoices=True, - search_default_not_finished=True, search_default_partner_id=self.id, default_partner_id=self.id, - default_recurring_invoices=True, default_pricelist_id=self.property_product_pricelist.id, ), ) @@ -62,7 +59,9 @@ class ResPartner(models.Model): 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_supplier_contract' + ) else: return self.env['ir.actions.act_window'].for_xml_id( - 'contract', 'action_account_analytic_sale_overdue_all') + 'contract', 'action_customer_contract' + ) diff --git a/contract/readme/CONTRIBUTORS.rst b/contract/readme/CONTRIBUTORS.rst index 820de990..054e1a8c 100644 --- a/contract/readme/CONTRIBUTORS.rst +++ b/contract/readme/CONTRIBUTORS.rst @@ -5,3 +5,6 @@ * Vicent Cubells * Miquel Raïch * Souheil Bejaoui +* Thomas Binsfeld +* Rafael Blasco +* Guillaume Vandamme diff --git a/contract/readme/USAGE.rst b/contract/readme/USAGE.rst index 55474837..0fecae98 100644 --- a/contract/readme/USAGE.rst +++ b/contract/readme/USAGE.rst @@ -1,27 +1,25 @@ -To use this module, you need to: +#. Contracts are in Invoicing -> Customers -> Customer and Invoicing -> Vendors -> Supplier Contracts +#. When creating a contract, fill fields for selecting the invoicing parameters: -#. Go to Accounting -> Contracts and select or create a new contract. -#. Check *Generate recurring invoices automatically*. -#. Fill fields for selecting the recurrency and invoice parameters: + * a journal + * a price list (optional) - * Journal - * Pricelist - * Period. It can be any interval of days, weeks, months, months last day or - years. - * Start date and next invoice date. - * Invoicing type: pre-paid or post-paid. +#. And add the lines to be invoiced with: -#. Add the lines to be invoiced with the product, description, quantity and - price. -#. You can mark Auto-price? for having a price automatically obtained applying - the pricelist to the product price. -#. You have the possibility to use the markers #START# or #END# in the - description field to show the start and end date of the invoiced period. -#. Choosing between pre-paid and post-paid, you modify the dates that are shown - with the markers. -#. A cron is created with daily interval, but if you are in debug mode, you can - click on *Create invoices* to force this action. -#. Click *Show recurring invoices* link to show all invoices created by the + * the product with a description, a quantity and a price + * the recurrence parameters: interval (days, weeks, months, months last day or years), + start date, date of next invoice (automatically computed, can be modified) and end date (optional) + * auto-price, for having a price automatically obtained from the price list + * #START# or #END# in the description field to display the start/end date of + the invoiced period in the invoice line description + * pre-paid (invoice at period start) or post-paid (invoice at start of next period) + +#. The "Generate Recurring Invoices from Contracts" cron runs daily to generate the invoices. + If you are in debug mode, you can click on the invoice creation button. +#. The *Show recurring invoices* shortcut on contracts shows all invoices created from the contract. -#. Click on *Print > Contract* menu to print contract report. -#. Click on *Send by Email* button to send contract by email. +#. The contract report can be printed from the Print menu +#. The contract can be sent by email with the *Send by Email* button +#. Contract templates can be created from the Configuration -> Contracts -> Contract Templates menu. + They allow to define default journal, price list and lines when creating a contract. + To use it, just select the template on the contract and fields will be filled automatically. diff --git a/contract/report/contract_views.xml b/contract/report/contract_views.xml index bcbb1459..81030664 100644 --- a/contract/report/contract_views.xml +++ b/contract/report/contract_views.xml @@ -3,7 +3,7 @@
- Date Start:

Responsible:

Contract:

@@ -33,6 +32,7 @@ Quantity Unit Price Price + Date Start @@ -49,6 +49,9 @@ + + + diff --git a/contract/security/contract_security.xml b/contract/security/contract_security.xml index 3884e6ef..08b0f9b9 100644 --- a/contract/security/contract_security.xml +++ b/contract/security/contract_security.xml @@ -2,7 +2,7 @@ Contract template multi-company - + ['|', ('company_id', '=', False), ('company_id', 'child_of', [user.company_id.id])] diff --git a/contract/security/ir.model.access.csv b/contract/security/ir.model.access.csv index 549a2d7a..4b3e42de 100644 --- a/contract/security/ir.model.access.csv +++ b/contract/security/ir.model.access.csv @@ -1,7 +1,9 @@ "id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink" -"account_analytic_contract_manager","Recurring manager","model_account_analytic_contract","account.group_account_manager",1,1,1,1 -"account_analytic_contract_user","Recurring user","model_account_analytic_contract","account.group_account_invoice",1,0,0,0 -"account_analytic_invoice_line_manager","Recurring manager","model_account_analytic_invoice_line","account.group_account_manager",1,1,1,1 -"account_analytic_invoice_line_user","Recurring user","model_account_analytic_invoice_line","account.group_account_invoice",1,0,0,0 -"account_analytic_contract_line_manager","Recurring manager","model_account_analytic_contract_line","account.group_account_manager",1,1,1,1 -"account_analytic_contract_line_user","Recurring user","model_account_analytic_contract_line","account.group_account_invoice",1,0,0,0 +"contract_template_manager","Recurring manager","model_contract_template","account.group_account_manager",1,1,1,1 +"contract_template_user","Recurring user","model_contract_template","account.group_account_invoice",1,0,0,0 +"contract_manager","Recurring manager","model_contract_contract","account.group_account_manager",1,1,1,1 +"contract_user","Recurring user","model_contract_contract","account.group_account_invoice",1,0,0,0 +"contract_line_manager","Recurring manager","model_contract_line","account.group_account_manager",1,1,1,1 +"contract_line_user","Recurring user","model_contract_line","account.group_account_invoice",1,0,0,0 +"contract_template_line_manager","Recurring manager","model_contract_template_line","account.group_account_manager",1,1,1,1 +"contract_template_line_user","Recurring user","model_contract_template_line","account.group_account_invoice",1,0,0,0 diff --git a/contract/tests/test_contract.py b/contract/tests/test_contract.py index 4ce51aaa..f8667537 100644 --- a/contract/tests/test_contract.py +++ b/contract/tests/test_contract.py @@ -1,65 +1,107 @@ -# Copyright 2016 Tecnativa - Carlos Dauden -# Copyright 2017 Tecnativa - Pedro M. Baeza +# Copyright 2018 Tecnativa - Carlos Dauden +# Copyright 2018 Tecnativa - Pedro M. Baeza # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from datetime import timedelta +from dateutil.relativedelta import relativedelta from odoo import fields 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): - super(TestContractBase, cls).setUpClass() + super().setUpClass() + cls.today = fields.Date.today() 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) - cls.product.description_sale = 'Test description sale' - cls.template_vals = { + cls.product_1 = cls.env.ref('product.product_product_1') + cls.product_2 = cls.env.ref('product.product_product_2') + cls.product_1.taxes_id += cls.env['account.tax'].search( + [('type_tax_use', '=', 'sale')], limit=1 + ) + cls.product_1.description_sale = 'Test description sale' + cls.line_template_vals = { + 'product_id': cls.product_1.id, + 'name': 'Services from #START# to #END#', + 'quantity': 1, + 'uom_id': cls.product_1.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', + 'contract_line_ids': [(0, 0, cls.line_template_vals)], } - cls.template = cls.env['account.analytic.contract'].create( - cls.template_vals, + cls.template = cls.env['contract.template'].create( + 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_1.id, + 'compute_price': 'formula', + 'base': 'list_price', + } + ) + cls.contract = cls.env['contract.contract'].create( + { + 'name': 'Test Contract', + 'partner_id': cls.partner.id, + 'pricelist_id': cls.partner.property_product_pricelist.id, + } + ) + cls.contract2 = cls.env['contract.contract'].create( + { + 'name': 'Test Contract 2', + 'partner_id': cls.partner.id, + 'pricelist_id': cls.partner.property_product_pricelist.id, + 'contract_type': 'purchase', + 'contract_line_ids': [ + ( + 0, + 0, + { + 'product_id': cls.product_1.id, + 'name': 'Services from #START# to #END#', + 'quantity': 1, + 'uom_id': cls.product_1.uom_id.id, + 'price_unit': 100, + 'discount': 50, + 'recurring_rule_type': 'monthly', + 'recurring_interval': 1, + 'date_start': '2018-02-15', + 'recurring_next_date': '2018-02-22', + }, + ) + ], + } + ) cls.line_vals = { - 'analytic_account_id': cls.contract.id, - 'product_id': cls.product.id, + 'contract_id': cls.contract.id, + 'product_id': cls.product_1.id, 'name': 'Services from #START# to #END#', 'quantity': 1, - 'uom_id': cls.product.uom_id.id, + 'uom_id': cls.product_1.uom_id.id, 'price_unit': 100, 'discount': 50, + 'recurring_rule_type': 'monthly', + 'recurring_interval': 1, + 'date_start': '2018-01-01', + 'recurring_next_date': '2018-01-15', + 'is_auto_renew': False, } - cls.acct_line = cls.env['account.analytic.invoice.line'].create( - cls.line_vals, + cls.acct_line = cls.env['contract.line'].create( + cls.line_vals ) + cls.acct_line.product_id.is_auto_renew = True class TestContract(TestContractBase): @@ -67,9 +109,11 @@ 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_id'] = self.template.id vals.update(overrides) - return self.env['account.analytic.contract.line'].create(vals) + return self.env['contract.template.line'].create(vals) def test_check_discount(self): with self.assertRaises(ValidationError): @@ -77,7 +121,7 @@ class TestContract(TestContractBase): def test_automatic_price(self): self.acct_line.automatic_price = True - self.product.list_price = 1100 + self.product_1.list_price = 1100 self.assertEqual(self.acct_line.price_unit, 1100) # Try to write other price self.acct_line.price_unit = 10 @@ -90,94 +134,229 @@ 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('2018-02-15') self.assertAlmostEqual(self.acct_line.price_subtotal, 50.0) res = self.acct_line._onchange_product_id() self.assertIn('uom_id', res['domain']) self.acct_line.price_unit = 100.0 - with self.assertRaises(ValidationError): - self.contract.partner_id = False 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.invoice_monthly = self.contract._get_related_invoices() 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.user_id, self.invoice_monthly.user_id) + + def test_contract_recurring_next_date(self): + recurring_next_date = to_date('2018-01-15') + self.assertEqual( + self.contract.recurring_next_date, recurring_next_date + ) + contract_line = self.acct_line.copy( + {'recurring_next_date': '2018-01-14'} + ) + recurring_next_date = to_date('2018-01-14') + self.assertEqual( + self.contract.recurring_next_date, recurring_next_date + ) + contract_line.cancel() + recurring_next_date = to_date('2018-01-15') + self.assertEqual( + self.contract.recurring_next_date, recurring_next_date + ) 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('2018-02-23') + last_date_invoiced = to_date('2018-02-22') + self.acct_line.recurring_next_date = '2018-02-22' + self.acct_line.recurring_rule_type = 'daily' self.contract.pricelist_id = False - self.contract.cron_recurring_create_invoice() - invoice_daily = self.env['account.invoice'].search( - [('contract_id', '=', self.contract.id)]) + self.contract.recurring_create_invoice() + invoice_daily = self.contract._get_related_invoices() 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 + ) + self.assertEqual(self.acct_line.last_date_invoiced, last_date_invoiced) + + def test_contract_weekly_post_paid(self): + recurring_next_date = to_date('2018-03-01') + last_date_invoiced = to_date('2018-02-21') + self.acct_line.recurring_next_date = '2018-02-22' + self.acct_line.recurring_rule_type = 'weekly' + self.acct_line.recurring_invoicing_type = 'post-paid' + self.contract.recurring_create_invoice() + invoices_weekly = self.contract._get_related_invoices() + self.assertTrue(invoices_weekly) + self.assertEqual( + self.acct_line.recurring_next_date, recurring_next_date + ) + self.assertEqual(self.acct_line.last_date_invoiced, last_date_invoiced) - 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' + def test_contract_weekly_pre_paid(self): + recurring_next_date = to_date('2018-03-01') + last_date_invoiced = to_date('2018-02-28') + self.acct_line.recurring_next_date = '2018-02-22' + self.acct_line.recurring_rule_type = 'weekly' + self.acct_line.recurring_invoicing_type = 'pre-paid' self.contract.recurring_create_invoice() - invoices_weekly = self.env['account.invoice'].search( - [('contract_id', '=', self.contract.id)]) + invoices_weekly = self.contract._get_related_invoices() self.assertTrue(invoices_weekly) self.assertEqual( - self.contract.recurring_next_date, recurring_next_date) + self.acct_line.recurring_next_date, recurring_next_date + ) + self.assertEqual(self.acct_line.last_date_invoiced, last_date_invoiced) - 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' + def test_contract_yearly_post_paid(self): + recurring_next_date = to_date('2019-02-22') + last_date_invoiced = to_date('2018-02-21') + self.acct_line.recurring_next_date = '2018-02-22' + self.acct_line.recurring_rule_type = 'yearly' + 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)]) + invoices_weekly = self.contract._get_related_invoices() self.assertTrue(invoices_weekly) self.assertEqual( - self.contract.recurring_next_date, recurring_next_date) + self.acct_line.recurring_next_date, recurring_next_date + ) + self.assertEqual(self.acct_line.last_date_invoiced, last_date_invoiced) + + def test_contract_yearly_pre_paid(self): + recurring_next_date = to_date('2019-02-22') + last_date_invoiced = to_date('2019-02-21') + self.acct_line.date_end = '2020-02-22' + self.acct_line.recurring_next_date = '2018-02-22' + self.acct_line.recurring_rule_type = 'yearly' + self.acct_line.recurring_invoicing_type = 'pre-paid' + self.contract.recurring_create_invoice() + invoices_weekly = self.contract._get_related_invoices() + self.assertTrue(invoices_weekly) + self.assertEqual( + self.acct_line.recurring_next_date, recurring_next_date + ) + self.assertEqual(self.acct_line.last_date_invoiced, last_date_invoiced) 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('2018-03-31') + last_date_invoiced = to_date('2018-02-22') + self.acct_line.recurring_next_date = '2018-02-22' + 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)]) + invoices_monthly_lastday = self.contract._get_related_invoices() 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 + ) + self.assertEqual(self.acct_line.last_date_invoiced, last_date_invoiced) + + def test_last_invoice_post_paid(self): + self.acct_line.date_start = '2018-01-01' + self.acct_line.recurring_invoicing_type = 'post-paid' + self.acct_line.date_end = '2018-03-15' + self.acct_line._onchange_date_start() + self.assertTrue(self.acct_line.create_invoice_visibility) + self.assertEqual( + self.acct_line.recurring_next_date, to_date('2018-02-01') + ) + self.assertFalse(self.acct_line.last_date_invoiced) + self.contract.recurring_create_invoice() + self.assertEqual( + self.acct_line.recurring_next_date, to_date('2018-03-01') + ) + self.assertEqual( + self.acct_line.last_date_invoiced, to_date('2018-01-31') + ) + self.contract.recurring_create_invoice() + self.assertEqual( + self.acct_line.recurring_next_date, to_date('2018-04-01') + ) + self.assertEqual( + self.acct_line.last_date_invoiced, to_date('2018-02-28') + ) + self.contract.recurring_create_invoice() + self.assertEqual( + self.acct_line.last_date_invoiced, to_date('2018-03-15') + ) + self.assertFalse(self.acct_line.recurring_next_date) + self.assertFalse(self.acct_line.create_invoice_visibility) + invoices = self.contract._get_related_invoices() + self.contract.recurring_create_invoice() + new_invoices = self.contract._get_related_invoices() + self.assertEqual( + invoices, + new_invoices, + "Should not create a new invoice after the last one", + ) + + def test_last_invoice_pre_paid(self): + self.acct_line.date_start = '2018-01-01' + self.acct_line.recurring_invoicing_type = 'pre-paid' + self.acct_line.date_end = '2018-03-15' + self.acct_line._onchange_date_start() + self.assertTrue(self.acct_line.create_invoice_visibility) + self.assertEqual( + self.acct_line.recurring_next_date, to_date('2018-01-01') + ) + self.assertFalse(self.acct_line.last_date_invoiced) + self.contract.recurring_create_invoice() + self.assertEqual( + self.acct_line.recurring_next_date, to_date('2018-02-01') + ) + self.assertEqual( + self.acct_line.last_date_invoiced, to_date('2018-01-31') + ) + self.contract.recurring_create_invoice() + self.assertEqual( + self.acct_line.last_date_invoiced, to_date('2018-02-28') + ) + self.assertEqual( + self.acct_line.last_date_invoiced, to_date('2018-02-28') + ) + self.contract.recurring_create_invoice() + self.assertEqual( + self.acct_line.last_date_invoiced, to_date('2018-03-15') + ) + self.assertFalse(self.acct_line.recurring_next_date) + self.assertFalse(self.acct_line.create_invoice_visibility) + invoices = self.contract._get_related_invoices() + self.contract.recurring_create_invoice() + new_invoices = self.contract._get_related_invoices() + self.assertEqual( + invoices, + new_invoices, + "Should not create a new invoice after the last one", + ) 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('2018-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() + line = self.env['contract.line'].new() res = line._onchange_product_id() self.assertFalse(res['domain']['uom_id']) @@ -187,67 +366,67 @@ 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', - }) - - def test_check_recurring_next_date_recurring_invoices(self): - with self.assertRaises(ValidationError): - self.contract.write({ - 'recurring_invoices': True, - '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.acct_line.write( + { + 'date_start': '2018-01-01', + 'recurring_next_date': '2017-01-01', + } + ) def test_onchange_contract_template_id(self): """It should change the contract values to match the template.""" + self.contract.contract_template_id = False + self.contract._onchange_contract_template_id() 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, + 'contract_line_ids': [ + ( + 0, + 0, + { + 'product_id': self.product_1.id, + 'name': 'Services from #START# to #END#', + 'quantity': 1, + 'uom_id': self.product_1.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) def test_onchange_contract_template_id_lines(self): """It should create invoice lines for the contract lines.""" - + self.acct_line.cancel() 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.contract_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) + self.assertEqual(len(self.contract.contract_line_ids), 1) - for key, value in self.line_vals.items(): - test_value = self.contract.recurring_invoice_line_ids[0][key] + for key, value in self.line_template_vals.items(): + test_value = self.contract.contract_line_ids[0][key] try: test_value = test_value.id except AttributeError: @@ -262,11 +441,21 @@ 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 + ) + self.contract.type = 'purchase' + self.contract._onchange_contract_type() + self.assertFalse( + any( + self.contract.contract_line_ids.mapped( + 'automatic_price' + ) + ) + ) def test_contract_onchange_product_id_domain_blank(self): """It should return a blank UoM domain when no product.""" - line = self.env['account.analytic.contract.line'].new() + line = self.env['contract.template.line'].new() res = line._onchange_product_id() self.assertFalse(res['domain']['uom_id']) @@ -276,7 +465,7 @@ class TestContract(TestContractBase): res = line._onchange_product_id() self.assertEqual( res['domain']['uom_id'][0], - ('category_id', '=', self.product.uom_id.category_id.id), + ('category_id', '=', self.product_1.uom_id.category_id.id), ) def test_contract_onchange_product_id_uom(self): @@ -286,18 +475,16 @@ 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, line.product_id.get_product_multiline_description_sale() + ) def test_contract_count(self): """It should return sale contract count.""" @@ -312,57 +499,1337 @@ 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(), - }) - init_count = account_invoice_model.search_count( - [('contract_id', '=', self.contract.id)]) - self.contract.cron_recurring_create_invoice() - last_count = account_invoice_model.search_count( - [('contract_id', '=', self.contract.id)]) + self.acct_line.write( + { + 'date_start': self.today, + 'date_end': self.today, + 'recurring_next_date': self.today, + } + ) + self.contract._compute_recurring_next_date() + init_count = len(self.contract._get_related_invoices()) + self.contract.recurring_create_invoice() + last_count = len(self.contract._get_related_invoices()) 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.assertTrue(self.contract.create_invoice_visibility) - self.contract.date_end = '2017-01-01' - self.assertTrue(self.contract.create_invoice_visibility) - self.contract.date_end = '2016-01-01' - 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) + last_count = len(self.contract._get_related_invoices()) + self.assertEqual(last_count, init_count + 1) 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', 'type': 'ir.actions.act_window', 'view_type': 'form', - 'res_model': 'account.analytic.account', - 'xml_id': 'contract.action_account_analytic_sale_overdue_all', + 'res_model': 'contract.contract', + 'xml_id': 'contract.action_customer_contract', }, 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['contract.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.contract_line_ids.mapped( + 'recurring_next_date' + ) + ), + ) + + def test_date_end(self): + """recurring next date for a contract is the min for all lines""" + self.acct_line.date_end = '2018-01-01' + self.acct_line.copy() + self.acct_line.write({'date_end': False, 'is_auto_renew': False}) + self.assertFalse(self.contract.date_end) + + def test_stop_contract_line(self): + """It should raise a validation error""" + self.acct_line.cancel() + with self.assertRaises(ValidationError): + self.acct_line.stop(self.today) + + def test_stop_contract_line(self): + """It should put end to the contract line""" + self.acct_line.write( + { + 'date_start': self.today, + 'recurring_next_date': self.today, + 'date_end': self.today + relativedelta(months=7), + 'is_auto_renew': True, + } + ) + self.acct_line.stop(self.today + relativedelta(months=5)) + self.assertEqual( + self.acct_line.date_end, self.today + relativedelta(months=5) + ) + + def test_stop_upcoming_contract_line(self): + """It should put end to the contract line""" + self.acct_line.write( + { + 'date_start': self.today + relativedelta(months=3), + 'recurring_next_date': self.today + relativedelta(months=3), + 'date_end': self.today + relativedelta(months=7), + 'is_auto_renew': True, + } + ) + self.acct_line.stop(self.today) + self.assertEqual( + self.acct_line.date_end, self.today + relativedelta(months=7) + ) + self.assertTrue(self.acct_line.is_canceled) + + def test_stop_past_contract_line(self): + """Past contract line are ignored on stop""" + self.acct_line.write( + { + 'date_end': self.today + relativedelta(months=5), + 'is_auto_renew': True, + } + ) + self.acct_line.stop(self.today + relativedelta(months=7)) + self.assertEqual( + self.acct_line.date_end, self.today + relativedelta(months=5) + ) + + def test_stop_contract_line_without_date_end(self): + """Past contract line are ignored on stop""" + self.acct_line.write({'date_end': False, 'is_auto_renew': False}) + self.acct_line.stop(self.today + relativedelta(months=7)) + self.assertEqual( + self.acct_line.date_end, self.today + relativedelta(months=7) + ) + + def test_stop_wizard(self): + self.acct_line.write( + { + 'date_start': self.today, + 'recurring_next_date': self.today, + 'date_end': self.today + relativedelta(months=5), + 'is_auto_renew': True, + } + ) + wizard = self.env['contract.line.wizard'].create( + { + 'date_end': self.today + relativedelta(months=3), + 'contract_line_id': self.acct_line.id, + } + ) + wizard.stop() + self.assertEqual( + self.acct_line.date_end, self.today + relativedelta(months=3) + ) + self.assertFalse(self.acct_line.is_auto_renew) + + def test_stop_plan_successor_contract_line_0(self): + successor_contract_line = self.acct_line.copy( + { + 'date_start': self.today + relativedelta(months=5), + 'recurring_next_date': self.today + relativedelta(months=5), + } + ) + self.acct_line.write( + { + 'successor_contract_line_id': successor_contract_line.id, + 'is_auto_renew': False, + 'date_end': self.today, + } + ) + suspension_start = self.today + relativedelta(months=5) + suspension_end = self.today + relativedelta(months=6) + with self.assertRaises(ValidationError): + self.acct_line.stop_plan_successor( + suspension_start, suspension_end, True + ) + + def test_stop_plan_successor_contract_line_1(self): + """ + * contract line end's before the suspension period: + -> apply stop + """ + suspension_start = self.today + relativedelta(months=5) + suspension_end = self.today + relativedelta(months=6) + start_date = self.today + end_date = self.today + relativedelta(months=4) + self.acct_line.write( + { + 'date_start': start_date, + 'recurring_next_date': start_date, + 'date_end': end_date, + } + ) + self.acct_line.stop_plan_successor( + suspension_start, suspension_end, True + ) + self.assertEqual(self.acct_line.date_end, end_date) + new_line = self.env['contract.line'].search( + [('predecessor_contract_line_id', '=', self.acct_line.id)] + ) + self.assertFalse(new_line) + + def test_stop_plan_successor_contract_line_2(self): + """ + * contract line start before the suspension period and end in it + -> apply stop at suspension start date + -> apply plan successor: + - date_start: suspension.date_end + - date_end: suspension.date_end + (contract_line.date_end + - suspension.date_start) + """ + suspension_start = self.today + relativedelta(months=3) + suspension_end = self.today + relativedelta(months=5) + start_date = self.today + end_date = self.today + relativedelta(months=4) + self.acct_line.write( + { + 'date_start': start_date, + 'recurring_next_date': start_date, + 'date_end': end_date, + } + ) + self.acct_line.stop_plan_successor( + suspension_start, suspension_end, True + ) + self.assertEqual( + self.acct_line.date_end, suspension_start - relativedelta(days=1) + ) + new_line = self.env['contract.line'].search( + [('predecessor_contract_line_id', '=', self.acct_line.id)] + ) + self.assertTrue(new_line) + new_date_end = ( + suspension_end + + (end_date - suspension_start) + + relativedelta(days=1) + ) + self.assertEqual( + new_line.date_start, suspension_end + relativedelta(days=1) + ) + self.assertEqual(new_line.date_end, new_date_end) + self.assertTrue(self.acct_line.manual_renew_needed) + + def test_stop_plan_successor_contract_line_3(self): + """ + * contract line start before the suspension period and end after it + -> apply stop at suspension start date + -> apply plan successor: + - date_start: suspension.date_end + - date_end: suspension.date_end + (suspension.date_end + - suspension.date_start) + """ + suspension_start = self.today + relativedelta(months=3) + suspension_end = self.today + relativedelta(months=5) + start_date = self.today + end_date = self.today + relativedelta(months=6) + self.acct_line.write( + { + 'date_start': start_date, + 'recurring_next_date': start_date, + 'date_end': end_date, + } + ) + self.acct_line.stop_plan_successor( + suspension_start, suspension_end, True + ) + self.assertEqual( + self.acct_line.date_end, suspension_start - relativedelta(days=1) + ) + new_line = self.env['contract.line'].search( + [('predecessor_contract_line_id', '=', self.acct_line.id)] + ) + self.assertTrue(new_line) + new_date_end = ( + end_date + + (suspension_end - suspension_start) + + relativedelta(days=1) + ) + self.assertEqual( + new_line.date_start, suspension_end + relativedelta(days=1) + ) + self.assertEqual(new_line.date_end, new_date_end) + self.assertTrue(self.acct_line.manual_renew_needed) + + def test_stop_plan_successor_contract_line_3_without_end_date(self): + """ + * contract line start before the suspension period and end after it + -> apply stop at suspension start date + -> apply plan successor: + - date_start: suspension.date_end + - date_end: suspension.date_end + (suspension.date_end + - suspension.date_start) + """ + suspension_start = self.today + relativedelta(months=3) + suspension_end = self.today + relativedelta(months=5) + start_date = self.today + end_date = False + self.acct_line.write( + { + 'date_start': start_date, + 'recurring_next_date': start_date, + 'date_end': end_date, + 'is_auto_renew': False, + } + ) + self.acct_line.stop_plan_successor( + suspension_start, suspension_end, False + ) + self.assertEqual( + self.acct_line.date_end, suspension_start - relativedelta(days=1) + ) + new_line = self.env['contract.line'].search( + [('predecessor_contract_line_id', '=', self.acct_line.id)] + ) + self.assertTrue(new_line) + self.assertEqual( + new_line.date_start, suspension_end + relativedelta(days=1) + ) + self.assertFalse(new_line.date_end) + self.assertTrue(self.acct_line.manual_renew_needed) + + def test_stop_plan_successor_contract_line_4(self): + """ + * contract line start and end's in the suspension period + -> apply delay + - delay: suspension.date_end - contract_line.end_date + """ + suspension_start = self.today + relativedelta(months=2) + suspension_end = self.today + relativedelta(months=5) + start_date = self.today + relativedelta(months=3) + end_date = self.today + relativedelta(months=4) + self.acct_line.write( + { + 'date_start': start_date, + 'recurring_next_date': start_date, + 'date_end': end_date, + } + ) + self.acct_line.stop_plan_successor( + suspension_start, suspension_end, True + ) + self.assertEqual( + self.acct_line.date_start, + start_date + (suspension_end - start_date) + timedelta(days=1), + ) + self.assertEqual( + self.acct_line.date_end, + end_date + (suspension_end - start_date) + timedelta(days=1), + ) + new_line = self.env['contract.line'].search( + [('predecessor_contract_line_id', '=', self.acct_line.id)] + ) + self.assertFalse(new_line) + + def test_stop_plan_successor_contract_line_5(self): + """ + * contract line start in the suspension period and end after it + -> apply delay + - delay: suspension.date_end - contract_line.date_start + """ + suspension_start = self.today + relativedelta(months=2) + suspension_end = self.today + relativedelta(months=5) + start_date = self.today + relativedelta(months=3) + end_date = self.today + relativedelta(months=6) + self.acct_line.write( + { + 'date_start': start_date, + 'recurring_next_date': start_date, + 'date_end': end_date, + } + ) + self.acct_line.stop_plan_successor( + suspension_start, suspension_end, True + ) + self.assertEqual( + self.acct_line.date_start, + start_date + (suspension_end - start_date) + timedelta(days=1), + ) + self.assertEqual( + self.acct_line.date_end, + end_date + (suspension_end - start_date) + timedelta(days=1), + ) + new_line = self.env['contract.line'].search( + [('predecessor_contract_line_id', '=', self.acct_line.id)] + ) + self.assertFalse(new_line) + + def test_stop_plan_successor_contract_line_5_without_date_end(self): + """ + * contract line start in the suspension period and end after it + -> apply delay + - delay: suspension.date_end - contract_line.date_start + """ + suspension_start = self.today + relativedelta(months=2) + suspension_end = self.today + relativedelta(months=5) + start_date = self.today + relativedelta(months=3) + end_date = False + self.acct_line.write( + { + 'date_start': start_date, + 'recurring_next_date': start_date, + 'date_end': end_date, + 'is_auto_renew': False, + } + ) + self.acct_line.stop_plan_successor( + suspension_start, suspension_end, True + ) + self.assertEqual( + self.acct_line.date_start, + start_date + (suspension_end - start_date) + timedelta(days=1), + ) + self.assertFalse(self.acct_line.date_end) + new_line = self.env['contract.line'].search( + [('predecessor_contract_line_id', '=', self.acct_line.id)] + ) + self.assertFalse(new_line) + + def test_stop_plan_successor_contract_line_6(self): + """ + * contract line start and end after the suspension period + -> apply delay + - delay: suspension.date_end - suspension.start_date + """ + suspension_start = self.today + relativedelta(months=2) + suspension_end = self.today + relativedelta(months=3) + start_date = self.today + relativedelta(months=4) + end_date = self.today + relativedelta(months=6) + self.acct_line.write( + { + 'date_start': start_date, + 'recurring_next_date': start_date, + 'date_end': end_date, + } + ) + self.acct_line.stop_plan_successor( + suspension_start, suspension_end, True + ) + self.assertEqual( + self.acct_line.date_start, + start_date + + (suspension_end - suspension_start) + + timedelta(days=1), + ) + self.assertEqual( + self.acct_line.date_end, + end_date + (suspension_end - suspension_start) + timedelta(days=1), + ) + new_line = self.env['contract.line'].search( + [('predecessor_contract_line_id', '=', self.acct_line.id)] + ) + self.assertFalse(new_line) + + def test_stop_plan_successor_contract_line_6_without_date_end(self): + """ + * contract line start and end after the suspension period + -> apply delay + - delay: suspension.date_end - suspension.start_date + """ + suspension_start = self.today + relativedelta(months=2) + suspension_end = self.today + relativedelta(months=3) + start_date = self.today + relativedelta(months=4) + end_date = False + self.acct_line.write( + { + 'date_start': start_date, + 'recurring_next_date': start_date, + 'date_end': end_date, + 'is_auto_renew': False, + } + ) + self.acct_line.stop_plan_successor( + suspension_start, suspension_end, True + ) + self.assertEqual( + self.acct_line.date_start, + start_date + + (suspension_end - suspension_start) + + timedelta(days=1), + ) + self.assertFalse(self.acct_line.date_end) + new_line = self.env['contract.line'].search( + [('predecessor_contract_line_id', '=', self.acct_line.id)] + ) + self.assertFalse(new_line) + + def test_stop_plan_successor_wizard(self): + suspension_start = self.today + relativedelta(months=2) + suspension_end = self.today + relativedelta(months=3) + start_date = self.today + relativedelta(months=4) + end_date = self.today + relativedelta(months=6) + self.acct_line.write( + { + 'date_start': start_date, + 'recurring_next_date': start_date, + 'date_end': end_date, + } + ) + wizard = self.env['contract.line.wizard'].create( + { + 'date_start': suspension_start, + 'date_end': suspension_end, + 'is_auto_renew': False, + 'contract_line_id': self.acct_line.id, + } + ) + wizard.stop_plan_successor() + self.assertEqual( + self.acct_line.date_start, + start_date + + (suspension_end - suspension_start) + + timedelta(days=1), + ) + self.assertEqual( + self.acct_line.date_end, + end_date + (suspension_end - suspension_start) + timedelta(days=1), + ) + new_line = self.env['contract.line'].search( + [('predecessor_contract_line_id', '=', self.acct_line.id)] + ) + self.assertFalse(new_line) + + def test_plan_successor_contract_line(self): + self.acct_line.write( + { + 'date_start': self.today, + 'recurring_next_date': self.today, + 'date_end': self.today + relativedelta(months=3), + 'is_auto_renew': False, + } ) + self.acct_line.plan_successor( + self.today + relativedelta(months=5), + self.today + relativedelta(months=7), + True, + ) + new_line = self.env['contract.line'].search( + [('predecessor_contract_line_id', '=', self.acct_line.id)] + ) + self.assertFalse(self.acct_line.is_auto_renew) + self.assertTrue(new_line.is_auto_renew) + self.assertTrue(new_line, "should create a new contract line") + self.assertEqual( + new_line.date_start, self.today + relativedelta(months=5) + ) + self.assertEqual( + new_line.date_end, self.today + relativedelta(months=7) + ) + + def test_overlap(self): + self.acct_line.write( + { + 'date_start': self.today, + 'recurring_next_date': self.today, + 'date_end': self.today + relativedelta(months=3), + 'is_auto_renew': False, + } + ) + self.acct_line.plan_successor( + self.today + relativedelta(months=5), + self.today + relativedelta(months=7), + True, + ) + new_line = self.env['contract.line'].search( + [('predecessor_contract_line_id', '=', self.acct_line.id)] + ) + with self.assertRaises(ValidationError): + new_line.date_start = self.today + relativedelta(months=2) + with self.assertRaises(ValidationError): + self.acct_line.date_end = self.today + relativedelta(months=6) + + def test_plan_successor_wizard(self): + self.acct_line.write( + { + 'date_start': self.today, + 'recurring_next_date': self.today, + 'date_end': self.today + relativedelta(months=2), + 'is_auto_renew': False, + } + ) + wizard = self.env['contract.line.wizard'].create( + { + 'date_start': self.today + relativedelta(months=3), + 'date_end': self.today + relativedelta(months=5), + 'is_auto_renew': True, + 'contract_line_id': self.acct_line.id, + } + ) + wizard.plan_successor() + new_line = self.env['contract.line'].search( + [('predecessor_contract_line_id', '=', self.acct_line.id)] + ) + self.assertFalse(self.acct_line.is_auto_renew) + self.assertTrue(new_line.is_auto_renew) + self.assertTrue(new_line, "should create a new contract line") + self.assertEqual( + new_line.date_start, self.today + relativedelta(months=3) + ) + self.assertEqual( + new_line.date_end, self.today + relativedelta(months=5) + ) + + def test_cancel(self): + self.acct_line.write( + { + 'date_end': self.today + relativedelta(months=5), + 'is_auto_renew': True, + } + ) + self.acct_line.cancel() + self.assertTrue(self.acct_line.is_canceled) + self.assertFalse(self.acct_line.is_auto_renew) + with self.assertRaises(ValidationError): + self.acct_line.is_auto_renew = True + self.acct_line.uncancel(self.today) + self.assertFalse(self.acct_line.is_canceled) + + def test_uncancel_wizard(self): + self.acct_line.cancel() + self.assertTrue(self.acct_line.is_canceled) + wizard = self.env['contract.line.wizard'].create( + { + 'recurring_next_date': self.today, + 'contract_line_id': self.acct_line.id, + } + ) + wizard.uncancel() + self.assertFalse(self.acct_line.is_canceled) + + def test_cancel_uncancel_with_predecessor(self): + suspension_start = self.today + relativedelta(months=3) + suspension_end = self.today + relativedelta(months=5) + start_date = self.today + end_date = self.today + relativedelta(months=4) + self.acct_line.write( + { + 'date_start': start_date, + 'recurring_next_date': start_date, + 'date_end': end_date, + } + ) + self.acct_line.stop_plan_successor( + suspension_start, suspension_end, True + ) + self.assertEqual( + self.acct_line.date_end, suspension_start - relativedelta(days=1) + ) + new_line = self.env['contract.line'].search( + [('predecessor_contract_line_id', '=', self.acct_line.id)] + ) + self.assertEqual(self.acct_line.successor_contract_line_id, new_line) + new_line.cancel() + self.assertTrue(new_line.is_canceled) + self.assertFalse(self.acct_line.successor_contract_line_id) + self.assertEqual(new_line.predecessor_contract_line_id, self.acct_line) + new_line.uncancel(suspension_end + relativedelta(days=1)) + self.assertFalse(new_line.is_canceled) + self.assertEqual(self.acct_line.successor_contract_line_id, new_line) + self.assertEqual( + new_line.recurring_next_date, + suspension_end + relativedelta(days=1), + ) + + def test_cancel_uncancel_with_predecessor_has_successor(self): + suspension_start = self.today + relativedelta(months=6) + suspension_end = self.today + relativedelta(months=7) + start_date = self.today + end_date = self.today + relativedelta(months=8) + self.acct_line.write( + { + 'date_start': start_date, + 'recurring_next_date': start_date, + 'date_end': end_date, + } + ) + self.acct_line.stop_plan_successor( + suspension_start, suspension_end, True + ) + new_line = self.env['contract.line'].search( + [('predecessor_contract_line_id', '=', self.acct_line.id)] + ) + new_line.cancel() + suspension_start = self.today + relativedelta(months=4) + suspension_end = self.today + relativedelta(months=5) + self.acct_line.stop_plan_successor( + suspension_start, suspension_end, True + ) + with self.assertRaises(ValidationError): + new_line.uncancel(suspension_end) + + def test_check_has_not_date_end_has_successor(self): + self.acct_line.write({'date_end': False, 'is_auto_renew': False}) + with self.assertRaises(ValidationError): + self.acct_line.plan_successor( + to_date('2016-03-01'), to_date('2016-09-01'), False + ) + + def test_check_has_not_date_end_is_auto_renew(self): + with self.assertRaises(ValidationError): + self.acct_line.write({'date_end': False, 'is_auto_renew': True}) + + def test_check_has_successor_is_auto_renew(self): + with self.assertRaises(ValidationError): + self.acct_line.plan_successor( + to_date('2016-03-01'), to_date('2018-09-01'), False + ) + + def test_search_contract_line_to_renew(self): + self.acct_line.write({'date_end': self.today, 'is_auto_renew': True}) + line_1 = self.acct_line.copy( + {'date_end': self.today + relativedelta(months=1)} + ) + line_2 = self.acct_line.copy( + {'date_end': self.today - relativedelta(months=1)} + ) + line_3 = self.acct_line.copy( + {'date_end': self.today - relativedelta(months=2)} + ) + line_4 = self.acct_line.copy( + {'date_end': self.today + relativedelta(months=2)} + ) + to_renew = self.acct_line.search( + self.acct_line._contract_line_to_renew_domain() + ) + self.assertEqual( + set(to_renew), set((self.acct_line, line_1, line_2, line_3)) + ) + self.acct_line.cron_renew_contract_line() + self.assertTrue(self.acct_line.successor_contract_line_id) + self.assertTrue(line_1.successor_contract_line_id) + self.assertTrue(line_2.successor_contract_line_id) + self.assertTrue(line_3.successor_contract_line_id) + self.assertFalse(line_4.successor_contract_line_id) + + def test_renew(self): + date_start = self.today - relativedelta(months=9) + date_end = ( + date_start + relativedelta(months=12) - relativedelta(days=1) + ) + self.acct_line.write( + { + 'is_auto_renew': True, + 'date_start': date_start, + 'recurring_next_date': date_start, + 'date_end': self.today, + } + ) + self.acct_line._onchange_is_auto_renew() + self.assertEqual(self.acct_line.date_end, date_end) + new_line = self.acct_line.renew() + self.assertFalse(self.acct_line.is_auto_renew) + self.assertTrue(new_line.is_auto_renew) + self.assertEqual( + new_line.date_start, date_start + relativedelta(months=12) + ) + self.assertEqual( + new_line.date_end, date_end + relativedelta(months=12) + ) + + def test_cron_recurring_create_invoice(self): + self.acct_line.date_start = '2018-01-01' + self.acct_line.recurring_invoicing_type = 'post-paid' + self.acct_line.date_end = '2018-03-15' + self.acct_line._onchange_date_start() + contracts = self.contract2 + for i in range(10): + contracts |= self.contract.copy() + self.env['contract.contract'].cron_recurring_create_invoice() + invoice_lines = self.env['account.invoice.line'].search( + [('contract_line_id', 'in', + contracts.mapped('contract_line_ids').ids)] + ) + self.assertEqual( + len(contracts.mapped('contract_line_ids')), + len(invoice_lines), + ) + + def test_get_period_to_invoice_monthlylastday(self): + self.acct_line.date_start = '2018-01-05' + self.acct_line.recurring_invoicing_type = 'post-paid' + self.acct_line.recurring_rule_type = 'monthlylastday' + self.acct_line.date_end = '2018-03-15' + self.acct_line._onchange_date_start() + first, last, recurring_next_date = \ + self.acct_line._get_period_to_invoice( + self.acct_line.last_date_invoiced, + self.acct_line.recurring_next_date, + ) + self.assertEqual(first, to_date('2018-01-05')) + self.assertEqual(last, to_date('2018-01-31')) + self.contract.recurring_create_invoice() + first, last, recurring_next_date = \ + self.acct_line._get_period_to_invoice( + self.acct_line.last_date_invoiced, + self.acct_line.recurring_next_date, + ) + self.assertEqual(first, to_date('2018-02-01')) + self.assertEqual(last, to_date('2018-02-28')) + self.contract.recurring_create_invoice() + first, last, recurring_next_date = \ + self.acct_line._get_period_to_invoice( + self.acct_line.last_date_invoiced, + self.acct_line.recurring_next_date, + ) + self.assertEqual(first, to_date('2018-03-01')) + self.assertEqual(last, to_date('2018-03-15')) + self.acct_line.manual_renew_needed = True + + def test_get_period_to_invoice_monthly_pre_paid_2(self): + self.acct_line.date_start = '2018-01-05' + self.acct_line.recurring_invoicing_type = 'pre-paid' + self.acct_line.recurring_rule_type = 'monthly' + self.acct_line.date_end = '2018-08-15' + self.acct_line._onchange_date_start() + self.contract.recurring_create_invoice() + first, last, recurring_next_date = \ + self.acct_line._get_period_to_invoice( + self.acct_line.last_date_invoiced, + self.acct_line.recurring_next_date, + ) + self.assertEqual(first, to_date('2018-02-05')) + self.assertEqual(last, to_date('2018-03-04')) + self.acct_line.recurring_next_date = '2018-06-05' + first, last, recurring_next_date = \ + self.acct_line._get_period_to_invoice( + self.acct_line.last_date_invoiced, + self.acct_line.recurring_next_date, + ) + self.assertEqual(first, to_date('2018-02-05')) + self.assertEqual(last, to_date('2018-07-04')) + + def test_get_period_to_invoice_monthly_post_paid_2(self): + self.acct_line.date_start = '2018-01-05' + self.acct_line.recurring_invoicing_type = 'post-paid' + self.acct_line.recurring_rule_type = 'monthly' + self.acct_line.date_end = '2018-08-15' + self.acct_line._onchange_date_start() + self.contract.recurring_create_invoice() + first, last, recurring_next_date = \ + self.acct_line._get_period_to_invoice( + self.acct_line.last_date_invoiced, + self.acct_line.recurring_next_date, + ) + self.assertEqual(first, to_date('2018-02-05')) + self.assertEqual(last, to_date('2018-03-04')) + self.acct_line.recurring_next_date = '2018-06-05' + first, last, recurring_next_date = \ + self.acct_line._get_period_to_invoice( + self.acct_line.last_date_invoiced, + self.acct_line.recurring_next_date, + ) + self.assertEqual(first, to_date('2018-02-05')) + self.assertEqual(last, to_date('2018-06-04')) + + def test_get_period_to_invoice_monthly_post_paid(self): + self.acct_line.date_start = '2018-01-05' + self.acct_line.recurring_invoicing_type = 'post-paid' + self.acct_line.recurring_rule_type = 'monthly' + self.acct_line.date_end = '2018-03-15' + self.acct_line._onchange_date_start() + first, last, recurring_next_date = \ + self.acct_line._get_period_to_invoice( + self.acct_line.last_date_invoiced, + self.acct_line.recurring_next_date, + ) + self.assertEqual(first, to_date('2018-01-05')) + self.assertEqual(last, to_date('2018-02-04')) + self.contract.recurring_create_invoice() + first, last, recurring_next_date = \ + self.acct_line._get_period_to_invoice( + self.acct_line.last_date_invoiced, + self.acct_line.recurring_next_date, + ) + self.assertEqual(first, to_date('2018-02-05')) + self.assertEqual(last, to_date('2018-03-04')) + self.contract.recurring_create_invoice() + first, last, recurring_next_date = \ + self.acct_line._get_period_to_invoice( + self.acct_line.last_date_invoiced, + self.acct_line.recurring_next_date, + ) + self.assertEqual(first, to_date('2018-03-05')) + self.assertEqual(last, to_date('2018-03-15')) + + def test_get_period_to_invoice_monthly_pre_paid(self): + self.acct_line.date_start = '2018-01-05' + self.acct_line.recurring_invoicing_type = 'pre-paid' + self.acct_line.recurring_rule_type = 'monthly' + self.acct_line.date_end = '2018-03-15' + self.acct_line._onchange_date_start() + first, last, recurring_next_date = \ + self.acct_line._get_period_to_invoice( + self.acct_line.last_date_invoiced, + self.acct_line.recurring_next_date, + ) + self.assertEqual(first, to_date('2018-01-05')) + self.assertEqual(last, to_date('2018-02-04')) + self.contract.recurring_create_invoice() + first, last, recurring_next_date = \ + self.acct_line._get_period_to_invoice( + self.acct_line.last_date_invoiced, + self.acct_line.recurring_next_date, + ) + self.assertEqual(first, to_date('2018-02-05')) + self.assertEqual(last, to_date('2018-03-04')) + self.contract.recurring_create_invoice() + first, last, recurring_next_date = \ + self.acct_line._get_period_to_invoice( + self.acct_line.last_date_invoiced, + self.acct_line.recurring_next_date, + ) + self.assertEqual(first, to_date('2018-03-05')) + self.assertEqual(last, to_date('2018-03-15')) + + def test_get_period_to_invoice_yearly_post_paid(self): + self.acct_line.date_start = '2018-01-05' + self.acct_line.recurring_invoicing_type = 'post-paid' + self.acct_line.recurring_rule_type = 'yearly' + self.acct_line.date_end = '2020-03-15' + self.acct_line._onchange_date_start() + first, last, recurring_next_date = \ + self.acct_line._get_period_to_invoice( + self.acct_line.last_date_invoiced, + self.acct_line.recurring_next_date, + ) + self.assertEqual(first, to_date('2018-01-05')) + self.assertEqual(last, to_date('2019-01-04')) + self.contract.recurring_create_invoice() + first, last, recurring_next_date = \ + self.acct_line._get_period_to_invoice( + self.acct_line.last_date_invoiced, + self.acct_line.recurring_next_date, + ) + self.assertEqual(first, to_date('2019-01-05')) + self.assertEqual(last, to_date('2020-01-04')) + self.contract.recurring_create_invoice() + first, last, recurring_next_date = \ + self.acct_line._get_period_to_invoice( + self.acct_line.last_date_invoiced, + self.acct_line.recurring_next_date, + ) + self.assertEqual(first, to_date('2020-01-05')) + self.assertEqual(last, to_date('2020-03-15')) + + def test_get_period_to_invoice_yearly_pre_paid(self): + self.acct_line.date_start = '2018-01-05' + self.acct_line.recurring_invoicing_type = 'pre-paid' + self.acct_line.recurring_rule_type = 'yearly' + self.acct_line.date_end = '2020-03-15' + self.acct_line._onchange_date_start() + first, last, recurring_next_date = \ + self.acct_line._get_period_to_invoice( + self.acct_line.last_date_invoiced, + self.acct_line.recurring_next_date, + ) + self.assertEqual(first, to_date('2018-01-05')) + self.assertEqual(last, to_date('2019-01-04')) + self.contract.recurring_create_invoice() + first, last, recurring_next_date = \ + self.acct_line._get_period_to_invoice( + self.acct_line.last_date_invoiced, + self.acct_line.recurring_next_date, + ) + self.assertEqual(first, to_date('2019-01-05')) + self.assertEqual(last, to_date('2020-01-04')) + self.contract.recurring_create_invoice() + first, last, recurring_next_date = \ + self.acct_line._get_period_to_invoice( + self.acct_line.last_date_invoiced, + self.acct_line.recurring_next_date, + ) + self.assertEqual(first, to_date('2020-01-05')) + self.assertEqual(last, to_date('2020-03-15')) + + def test_unlink(self): + with self.assertRaises(ValidationError): + self.acct_line.unlink() + + def test_contract_line_state(self): + lines = self.env['contract.line'] + # upcoming + lines |= self.acct_line.copy( + { + 'date_start': self.today + relativedelta(months=3), + 'recurring_next_date': self.today + relativedelta(months=3), + 'date_end': self.today + relativedelta(months=5), + } + ) + # in-progress + lines |= self.acct_line.copy( + { + 'date_start': self.today, + 'recurring_next_date': self.today, + 'date_end': self.today + relativedelta(months=5), + } + ) + # in-progress + lines |= self.acct_line.copy( + { + 'date_start': self.today, + 'recurring_next_date': self.today, + 'date_end': self.today + relativedelta(months=5), + 'manual_renew_needed': True, + } + ) + # to-renew + lines |= self.acct_line.copy( + { + 'date_start': self.today - relativedelta(months=5), + 'recurring_next_date': self.today - relativedelta(months=5), + 'date_end': self.today - relativedelta(months=2), + 'manual_renew_needed': True, + } + ) + # upcoming-close + lines |= self.acct_line.copy( + { + 'date_start': self.today - relativedelta(months=5), + 'recurring_next_date': self.today - relativedelta(months=5), + 'date_end': self.today + relativedelta(days=20), + 'is_auto_renew': False, + } + ) + # closed + lines |= self.acct_line.copy( + { + 'date_start': self.today - relativedelta(months=5), + 'recurring_next_date': self.today - relativedelta(months=5), + 'date_end': self.today - relativedelta(months=2), + 'is_auto_renew': False, + } + ) + # canceled + lines |= self.acct_line.copy( + { + 'date_start': self.today - relativedelta(months=5), + 'recurring_next_date': self.today - relativedelta(months=5), + 'date_end': self.today - relativedelta(months=2), + 'is_canceled': True, + } + ) + states = [ + 'upcoming', + 'in-progress', + 'to-renew', + 'upcoming-close', + 'closed', + 'canceled', + ] + self.assertEqual(set(lines.mapped('state')), set(states)) + for state in states: + lines = self.env['contract.line'].search( + [('state', '=', state)] + ) + self.assertEqual(len(set(lines.mapped('state'))), 1, state) + self.assertEqual(lines.mapped('state')[0], state, state) + + for state in states: + lines = self.env['contract.line'].search( + [('state', '!=', state)] + ) + self.assertFalse(state in lines.mapped('state')) + + lines = self.env['contract.line'].search( + [('state', 'in', states)] + ) + self.assertEqual(set(lines.mapped('state')), set(states)) + lines = self.env['contract.line'].search( + [('state', 'in', [])] + ) + self.assertFalse(lines.mapped('state')) + with self.assertRaises(TypeError): + self.env['contract.line'].search( + [('state', 'in', 'upcoming')] + ) + lines = self.env['contract.line'].search( + [('state', 'not in', [])] + ) + self.assertEqual(set(lines.mapped('state')), set(states)) + lines = self.env['contract.line'].search( + [('state', 'not in', states)] + ) + self.assertFalse(lines.mapped('state')) + lines = self.env['contract.line'].search( + [('state', 'not in', ['upcoming', 'in-progress'])] + ) + self.assertEqual( + set(lines.mapped('state')), + set(['to-renew', 'upcoming-close', 'closed', 'canceled']), + ) + + def test_check_auto_renew_contract_line_with_successor(self): + """ + A contract line with a successor can't be set to auto-renew + """ + successor_contract_line = self.acct_line.copy() + with self.assertRaises(ValidationError): + self.acct_line.write( + { + 'is_auto_renew': True, + 'successor_contract_line_id': successor_contract_line.id, + } + ) + + def test_check_no_date_end_contract_line_with_successor(self): + """ + A contract line with a successor must have a end date + """ + successor_contract_line = self.acct_line.copy() + with self.assertRaises(ValidationError): + self.acct_line.write( + { + 'date_end': False, + 'successor_contract_line_id': successor_contract_line.id, + } + ) + + def test_check_last_date_invoiced_1(self): + """ + start end can't be before the date of last invoice + """ + with self.assertRaises(ValidationError): + self.acct_line.write( + { + 'last_date_invoiced': self.acct_line.date_start + - relativedelta(days=1) + } + ) + + def test_check_last_date_invoiced_2(self): + """ + start date can't be after the date of last invoice + """ + self.acct_line.write({'date_end': self.today}) + with self.assertRaises(ValidationError): + self.acct_line.write( + { + 'last_date_invoiced': self.acct_line.date_end + + relativedelta(days=1) + } + ) + + def test_init_last_date_invoiced(self): + self.acct_line.write( + {'date_start': '2019-01-01', 'recurring_next_date': '2019-03-01'} + ) + line_monthlylastday = self.acct_line.copy( + { + 'recurring_rule_type': 'monthlylastday', + 'recurring_next_date': '2019-03-31', + } + ) + line_prepaid = self.acct_line.copy( + { + 'recurring_invoicing_type': 'pre-paid', + 'recurring_rule_type': 'monthly', + } + ) + line_postpaid = self.acct_line.copy( + { + 'recurring_invoicing_type': 'post-paid', + 'recurring_rule_type': 'monthly', + } + ) + lines = line_monthlylastday | line_prepaid | line_postpaid + lines.write({'last_date_invoiced': False}) + self.assertFalse(any(lines.mapped('last_date_invoiced'))) + lines._init_last_date_invoiced() + self.assertEqual( + line_monthlylastday.last_date_invoiced, to_date("2019-02-28") + ) + self.assertEqual( + line_prepaid.last_date_invoiced, to_date("2019-02-28") + ) + self.assertEqual( + line_postpaid.last_date_invoiced, to_date("2019-01-31") + ) + + def test_delay_invoiced_contract_line(self): + self.acct_line.write( + { + 'last_date_invoiced': self.acct_line.date_start + + relativedelta(days=1) + } + ) + with self.assertRaises(ValidationError): + self.acct_line._delay(relativedelta(months=1)) + + def test_cancel_invoiced_contract_line(self): + self.acct_line.write( + { + 'last_date_invoiced': self.acct_line.date_start + + relativedelta(days=1) + } + ) + with self.assertRaises(ValidationError): + self.acct_line.cancel() + + def test_action_uncancel(self): + action = self.acct_line.action_uncancel() + self.assertEqual( + action['context']['default_contract_line_id'], self.acct_line.id + ) + + def test_action_plan_successor(self): + action = self.acct_line.action_plan_successor() + self.assertEqual( + action['context']['default_contract_line_id'], self.acct_line.id + ) + + def test_action_stop(self): + action = self.acct_line.action_stop() + self.assertEqual( + action['context']['default_contract_line_id'], self.acct_line.id + ) + + def test_action_stop_plan_successor(self): + action = self.acct_line.action_stop_plan_successor() + self.assertEqual( + action['context']['default_contract_line_id'], self.acct_line.id + ) + + def test_purchase_fields_view_get(self): + purchase_tree_view = self.env.ref( + 'contract.contract_line_supplier_tree_view' + ) + purchase_form_view = self.env.ref( + 'contract.contract_line_supplier_form_view' + ) + view = self.acct_line.with_context( + default_contract_type='purchase' + ).fields_view_get(view_type='tree') + self.assertEqual(view['view_id'], purchase_tree_view.id) + view = self.acct_line.with_context( + default_contract_type='purchase' + ).fields_view_get(view_type='form') + self.assertEqual(view['view_id'], purchase_form_view.id) + + def test_sale_fields_view_get(self): + sale_form_view = self.env.ref( + 'contract.contract_line_customer_form_view' + ) + view = self.acct_line.with_context( + default_contract_type='sale' + ).fields_view_get(view_type='form') + self.assertEqual(view['view_id'], sale_form_view.id) + + def test_contract_count_invoice(self): + self.contract.recurring_create_invoice() + self.contract.recurring_create_invoice() + self.contract.recurring_create_invoice() + self.contract._compute_invoice_count() + self.assertEqual(self.contract.invoice_count, 3) + + def test_contract_count_invoice(self): + invoices = self.env['account.invoice'] + invoices |= self.contract.recurring_create_invoice() + invoices |= self.contract.recurring_create_invoice() + invoices |= self.contract.recurring_create_invoice() + action = self.contract.action_show_invoices() + self.assertEqual(set(action['domain'][0][2]), set(invoices.ids)) + + def test_compute_create_invoice_visibility(self): + self.assertTrue(self.contract.create_invoice_visibility) + self.acct_line.write( + { + 'date_start': '2018-01-01', + 'date_end': '2018-12-31', + 'last_date_invoiced': '2018-12-31', + 'recurring_next_date': False, + } + ) + self.assertFalse(self.acct_line.create_invoice_visibility) + self.assertFalse(self.contract.create_invoice_visibility) + + def test_invoice_contract_without_lines(self): + self.contract.contract_line_ids.cancel() + self.contract.contract_line_ids.unlink() + self.assertFalse(self.contract.recurring_create_invoice()) + + def test_stop_at_last_date_invoiced(self): + self.contract.recurring_create_invoice() + self.assertTrue(self.acct_line.recurring_next_date) + self.acct_line.stop(self.acct_line.last_date_invoiced) + self.assertFalse(self.acct_line.recurring_next_date) diff --git a/contract/views/abstract_contract_line.xml b/contract/views/abstract_contract_line.xml new file mode 100644 index 00000000..b66077ee --- /dev/null +++ b/contract/views/abstract_contract_line.xml @@ -0,0 +1,71 @@ + + + + + + contract.abstract.contract.line form view (in contract) + contract.abstract.contract.line + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/contract/views/account_analytic_account_view.xml b/contract/views/account_analytic_account_view.xml deleted file mode 100644 index 07b640a7..00000000 --- a/contract/views/account_analytic_account_view.xml +++ /dev/null @@ -1,279 +0,0 @@ - - - - - Contract form - account.analytic.account - - primary - - - - {'required': [('recurring_invoices', '=', True)]} - - -
-
-
- - - - - -
- - -
- - - - - - -
-
-
- - - account.analytic.account.sale.form - account.analytic.account - - primary - - - - Customer - [('customer', '=', True)] - {'default_customer': True, 'default_supplier': False} - - - [('type', '=', 'sale'),('company_id', '=', company_id)] - - - [('sale_ok', '=', True)] - - - - - - account.analytic.account.purchase.form - account.analytic.account - - primary - - - - Supplier - [('supplier', '=', True)] - {'default_customer': False, 'default_supplier': True} - - - [('type', '=', 'purchase'),('company_id', '=', company_id)] - - - [('purchase_ok', '=', True)] - - - True - - - - - - - Contract list - account.analytic.account - - primary - - - - - - - - - - - - - - Contract search - account.analytic.account - - - - - - - - - - - - - - - - - - - - - - - - Customer Contracts - account.analytic.account - form - tree,form - [('contract_type', '=', 'sale')] - {'is_contract':1, 'search_default_not_finished':1, 'search_default_recurring_invoices':1, 'default_recurring_invoices': 1, 'default_contract_type': 'sale'} - - -

- Click to create a new contract. -

-
-
- - - - tree - - - - - - - form - - - - - - - - - Supplier Contracts - account.analytic.account - form - tree,form - [('contract_type', '=', 'purchase')] - {'is_contract':1, 'search_default_not_finished':1, 'search_default_recurring_invoices':1, 'default_recurring_invoices': 1, 'default_contract_type': 'purchase'} - - -

- Click to create a new contract. -

-
-
- - - - tree - - - - - - - form - - - - - - -
diff --git a/contract/views/account_invoice_view.xml b/contract/views/account_invoice_view.xml deleted file mode 100644 index e618fbe2..00000000 --- a/contract/views/account_invoice_view.xml +++ /dev/null @@ -1,44 +0,0 @@ - - - - - account.invoice.select.contract - account.invoice - - - - - - - - - - - Invoices - account.invoice - - { - 'search_default_contract_id': [active_id], - 'default_contract_id': active_id} - - [('type','in', ['out_invoice', 'out_refund'])] - - - - Vendor Bills - account.invoice - - { - 'search_default_contract_id': [active_id], - 'default_contract_id': active_id} - - [('type','in', ['in_invoice', 'in_refund'])] - - - diff --git a/contract/views/contract.xml b/contract/views/contract.xml new file mode 100644 index 00000000..ee810e87 --- /dev/null +++ b/contract/views/contract.xml @@ -0,0 +1,293 @@ + + + + + + contract.contract form view (in contract) + contract.contract + +
+
+
+ +
+ + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

#START#: Start + date + of the + invoiced period +

+

#END#: End date + of + the + invoiced period +

+
+
+
+
+
+ + + +
+
+ +
+
+ + + + contract.contract customer form view (in contract) + contract.contract + + primary + + + + Customer + [('customer', '=', True)] + {'default_customer': True, 'default_supplier': False} + + + [('type', '=', 'sale'),('company_id', '=', company_id)] + + + + + + + contract.contract supplier form view (in contract) + contract.contract + + primary + + + + Supplier + [('supplier', '=', True)] + {'default_customer': False, 'default_supplier': True} + + + [('type', '=', 'purchase'),('company_id', '=', company_id)] + + + + + + + contract.contract tree view (in contract) + contract.contract + + + + + + + + + + + + + + + contract.contract search view (in contract) + contract.contract + + + + + + + + + + + + + + + + + + + + + + + Customer Contracts + contract.contract + form + tree,form + [('contract_type', '=', 'sale')] + {'is_contract':1, + 'search_default_not_finished':1, + 'default_contract_type': 'sale'} + + + +

+ Click to create a new contract. +

+
+
+ + + + tree + + + + + + + form + + + + + + + + + Supplier Contracts + contract.contract + form + tree,form + [('contract_type', '=', 'purchase')] + {'is_contract':1, + 'search_default_not_finished':1, + 'default_contract_type': 'purchase'} + + + +

+ Click to create a new contract. +

+
+
+ + + + tree + + + + + + + form + + + + + + +
diff --git a/contract/views/contract_line.xml b/contract/views/contract_line.xml new file mode 100644 index 00000000..2d14a81f --- /dev/null +++ b/contract/views/contract_line.xml @@ -0,0 +1,169 @@ + + + + + + contract.line form view (in contract) + contract.line + + primary + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + contract.line customer form view (in contract) + contract.line + + primary + + + + [('sale_ok', '=', True)] + + + + + + + contract.line supplier form view (in contract) + contract.line + + primary + + + + [('purchase_ok', '=', True)] + + + True + + + + + + + contract.line tree view (in contract) + contract.line + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + + + + res.partner + + + + + + +
diff --git a/contract/wizards/__init__.py b/contract/wizards/__init__.py new file mode 100644 index 00000000..146dcc66 --- /dev/null +++ b/contract/wizards/__init__.py @@ -0,0 +1 @@ +from . import contract_line_wizard diff --git a/contract/wizards/contract_line_wizard.py b/contract/wizards/contract_line_wizard.py new file mode 100644 index 00000000..f136d36d --- /dev/null +++ b/contract/wizards/contract_line_wizard.py @@ -0,0 +1,58 @@ +# Copyright 2018 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models + + +class ContractLineWizard(models.TransientModel): + + _name = 'contract.line.wizard' + _description = 'Contract Line Wizard' + + date_start = fields.Date(string='Date Start') + date_end = fields.Date(string='Date End') + recurring_next_date = fields.Date(string='Next Invoice Date') + is_auto_renew = fields.Boolean(string="Auto Renew", default=False) + manual_renew_needed = fields.Boolean( + string="Manual renew needed", + default=False, + help="This flag is used to make a difference between a definitive stop" + "and temporary one for which a user is not able to plan a" + "successor in advance", + ) + contract_line_id = fields.Many2one( + comodel_name="contract.line", + string="Contract Line", + required=True, + index=True, + ) + + @api.multi + def stop(self): + for wizard in self: + wizard.contract_line_id.stop( + wizard.date_end, manual_renew_needed=wizard.manual_renew_needed + ) + return True + + @api.multi + def plan_successor(self): + for wizard in self: + wizard.contract_line_id.plan_successor( + wizard.date_start, wizard.date_end, wizard.is_auto_renew + ) + return True + + @api.multi + def stop_plan_successor(self): + for wizard in self: + wizard.contract_line_id.stop_plan_successor( + wizard.date_start, wizard.date_end, wizard.is_auto_renew + ) + return True + + @api.multi + def uncancel(self): + for wizard in self: + wizard.contract_line_id.uncancel(wizard.recurring_next_date) + return True diff --git a/contract/wizards/contract_line_wizard.xml b/contract/wizards/contract_line_wizard.xml new file mode 100644 index 00000000..faf0cfe5 --- /dev/null +++ b/contract/wizards/contract_line_wizard.xml @@ -0,0 +1,100 @@ + + + + + + + contract.line.stop.wizard.form (in contract) + contract.line.wizard + +
+ + + + + +
+
+
+
+
+ + + contract.line.plan_successor.wizard.form (in contract) + contract.line.wizard + +
+ + + + + + +
+
+
+
+
+ + + contract.line.stop_plan_successor.wizard.form (in contract) + contract.line.wizard + +
+ + + + + + +
+
+
+
+
+ + + contract.line.stop_plan_successor.wizard.form (in contract) + contract.line.wizard + +
+ + + + +
+
+
+
+
+ +
diff --git a/contract_sale/__manifest__.py b/contract_sale/__manifest__.py index 230d83d9..25155532 100644 --- a/contract_sale/__manifest__.py +++ b/contract_sale/__manifest__.py @@ -3,7 +3,7 @@ { 'name': 'Contract from Sale', - 'version': '12.0.1.0.0', + 'version': '12.0.2.0.0', 'category': 'Sales', 'author': 'Tecnativa, ' 'Odoo Community Association (OCA)', @@ -14,9 +14,11 @@ ], 'data': [ 'security/ir.model.access.csv', - 'security/account_analytic_account_security.xml', - 'views/account_analytic_account_view.xml', - 'views/account_analytic_contract_view.xml', + 'security/contract_security.xml', + 'views/abstract_contract_line.xml', + 'views/contract.xml', + 'views/contract_line.xml', + 'views/contract_template.xml', ], 'license': 'AGPL-3', 'installable': True, diff --git a/contract_sale/i18n/contract_sale.pot b/contract_sale/i18n/contract_sale.pot index 3ddf1e28..715a4dfb 100644 --- a/contract_sale/i18n/contract_sale.pot +++ b/contract_sale/i18n/contract_sale.pot @@ -6,6 +6,8 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 12.0\n" "Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2019-05-29 10:00+0000\n" +"PO-Revision-Date: 2019-05-29 10:00+0000\n" "Last-Translator: <>\n" "Language-Team: \n" "MIME-Version: 1.0\n" diff --git a/contract_sale/i18n/gl.po b/contract_sale/i18n/gl.po index c148bf6d..139fe485 100644 --- a/contract_sale/i18n/gl.po +++ b/contract_sale/i18n/gl.po @@ -1,11 +1,12 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: -# * contract_sale +# * contract_sale # msgid "" msgstr "" "Project-Id-Version: Odoo Server 12.0\n" "Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2019-05-29 10:00+0000\n" "PO-Revision-Date: 2019-02-04 18:50+0000\n" "Last-Translator: Marta Vázquez Rodríguez \n" "Language-Team: none\n" diff --git a/contract_sale/migrations/12.0.2.0.0/pre-migration.py b/contract_sale/migrations/12.0.2.0.0/pre-migration.py new file mode 100644 index 00000000..69665469 --- /dev/null +++ b/contract_sale/migrations/12.0.2.0.0/pre-migration.py @@ -0,0 +1,32 @@ +# Copyright 2019 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging + +from openupgradelib import openupgrade + +_logger = logging.getLogger(__name__) + + +def migrate(cr, version): + xmlids_to_rename = [ + ('contract_sale.account_analytic_account_own_salesman', + 'contract_sale.contract_contract_own_salesman'), + ('contract_sale.account_analytic_account_see_all', + 'contract_sale.contract_contract_see_all'), + ('contract_sale.account_analytic_contract_salesman', + 'contract_sale.contract_template_salesman'), + ('contract_sale.account_analytic_contract_sale_manager', + 'contract_sale.contract_template_sale_manager'), + ('contract_sale.account_analytic_invoice_line_saleman', + 'contract_sale.contract_line_saleman'), + ('contract_sale.account_analytic_invoice_line_manager', + 'contract_sale.contract_line_manager'), + ('contract_sale.account_analytic_contract_line_salesman', + 'contract_sale.contract_template_line_salesman'), + ('contract_sale.account_analytic_contract_line_manager', + 'contract_sale.contract_template_line_manager'), + ('contract_sale.account_analytic_account_contract_salesman', + 'contract_sale.contract_contract_salesman'), + ] + openupgrade.rename_xmlids(cr, xmlids_to_rename) diff --git a/contract_sale/security/account_analytic_account_security.xml b/contract_sale/security/contract_security.xml similarity index 66% rename from contract_sale/security/account_analytic_account_security.xml rename to contract_sale/security/contract_security.xml index 1833a1ce..6d1c6e64 100644 --- a/contract_sale/security/account_analytic_account_security.xml +++ b/contract_sale/security/contract_security.xml @@ -1,15 +1,15 @@ - + See Own Contracts - + ['|', ('user_id','=',user.id), ('user_id','=',False)] - + See All Contracts - + [(1,'=',1)] + + + + + contract.abstract.contract.line form view (in contract_sale) + contract.abstract.contract.line + + + + sale.group_discount_per_so_line + + + + + diff --git a/contract_sale/views/account_analytic_account_view.xml b/contract_sale/views/account_analytic_account_view.xml deleted file mode 100644 index 5b08a3ad..00000000 --- a/contract_sale/views/account_analytic_account_view.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - Contract form (in contract_sale) - account.analytic.account - - - - - sale.group_discount_per_so_line - - - - - - - - - diff --git a/contract_sale/views/account_analytic_contract_view.xml b/contract_sale/views/account_analytic_contract_view.xml deleted file mode 100644 index adfc650a..00000000 --- a/contract_sale/views/account_analytic_contract_view.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - Account Analytic Contract Form View (in - sale_contract) - - account.analytic.contract - - - - - sale.group_discount_per_so_line - - - - - - - diff --git a/contract_sale/views/contract.xml b/contract_sale/views/contract.xml new file mode 100644 index 00000000..1bf893b9 --- /dev/null +++ b/contract_sale/views/contract.xml @@ -0,0 +1,12 @@ + + + + + + diff --git a/contract_sale/views/contract_line.xml b/contract_sale/views/contract_line.xml new file mode 100644 index 00000000..90c499a3 --- /dev/null +++ b/contract_sale/views/contract_line.xml @@ -0,0 +1,16 @@ + + + + + + contract.template.line tree view (in contract_sale) + contract.line + + + + sale.group_discount_per_so_line + + + + + diff --git a/contract_sale/views/contract_template.xml b/contract_sale/views/contract_template.xml new file mode 100644 index 00000000..1fd7de4d --- /dev/null +++ b/contract_sale/views/contract_template.xml @@ -0,0 +1,17 @@ + + + + + + contract.template form view (in contract_sale) + contract.template + + + + sale.group_discount_per_so_line + + + + + + diff --git a/contract_sale_invoicing/i18n/ca.po b/contract_sale_invoicing/i18n/ca.po index bf6b105d..32feee3f 100644 --- a/contract_sale_invoicing/i18n/ca.po +++ b/contract_sale_invoicing/i18n/ca.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 11.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-04-17 02:41+0000\n" +"POT-Creation-Date: 2019-05-29 14:59+0000\n" "PO-Revision-Date: 2018-04-17 02:41+0000\n" "Last-Translator: OCA Transbot , 2018\n" "Language-Team: Catalan (https://www.transifex.com/oca/teams/23907/ca/)\n" @@ -19,18 +19,21 @@ msgstr "" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. module: contract_sale_invoicing -#: model:ir.model,name:contract_sale_invoicing.model_account_analytic_account -msgid "Analytic Account" -msgstr "Compte analític" - -#. module: contract_sale_invoicing -#: model:ir.model.fields,help:contract_sale_invoicing.field_account_analytic_account__invoicing_sales +#: model:ir.model.fields,help:contract_sale_invoicing.field_contract_contract__invoicing_sales msgid "" "If checked include sales with same analytic account to invoice in contract " "invoice creation." msgstr "" #. module: contract_sale_invoicing -#: model:ir.model.fields,field_description:contract_sale_invoicing.field_account_analytic_account__invoicing_sales +#: model:ir.model.fields,field_description:contract_sale_invoicing.field_contract_contract__invoicing_sales msgid "Invoice Pending Sales Orders" msgstr "" + +#. module: contract_sale_invoicing +#: model:ir.model,name:contract_sale_invoicing.model_contract_contract +msgid "contract.contract" +msgstr "" + +#~ msgid "Analytic Account" +#~ msgstr "Compte analític" diff --git a/contract_sale_invoicing/i18n/contract_sale_invoicing.pot b/contract_sale_invoicing/i18n/contract_sale_invoicing.pot index c1be6111..086d5256 100644 --- a/contract_sale_invoicing/i18n/contract_sale_invoicing.pot +++ b/contract_sale_invoicing/i18n/contract_sale_invoicing.pot @@ -6,6 +6,8 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 12.0\n" "Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2019-05-29 14:59+0000\n" +"PO-Revision-Date: 2019-05-29 14:59+0000\n" "Last-Translator: <>\n" "Language-Team: \n" "MIME-Version: 1.0\n" @@ -14,17 +16,17 @@ msgstr "" "Plural-Forms: \n" #. module: contract_sale_invoicing -#: model:ir.model,name:contract_sale_invoicing.model_account_analytic_account -msgid "Analytic Account" +#: model:ir.model.fields,help:contract_sale_invoicing.field_contract_contract__invoicing_sales +msgid "If checked include sales with same analytic account to invoice in contract invoice creation." msgstr "" #. module: contract_sale_invoicing -#: model:ir.model.fields,help:contract_sale_invoicing.field_account_analytic_account__invoicing_sales -msgid "If checked include sales with same analytic account to invoice in contract invoice creation." +#: model:ir.model.fields,field_description:contract_sale_invoicing.field_contract_contract__invoicing_sales +msgid "Invoice Pending Sales Orders" msgstr "" #. module: contract_sale_invoicing -#: model:ir.model.fields,field_description:contract_sale_invoicing.field_account_analytic_account__invoicing_sales -msgid "Invoice Pending Sales Orders" +#: model:ir.model,name:contract_sale_invoicing.model_contract_contract +msgid "contract.contract" msgstr "" diff --git a/contract_sale_invoicing/i18n/de.po b/contract_sale_invoicing/i18n/de.po index 13b2f59c..d82f72db 100644 --- a/contract_sale_invoicing/i18n/de.po +++ b/contract_sale_invoicing/i18n/de.po @@ -8,33 +8,32 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 11.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-04-17 02:41+0000\n" -"PO-Revision-Date: 2019-06-28 13:42+0000\n" -"Last-Translator: Maria Sparenberg \n" +"POT-Creation-Date: 2019-05-29 14:59+0000\n" +"PO-Revision-Date: 2018-04-17 02:41+0000\n" +"Last-Translator: OCA Transbot , 2018\n" "Language-Team: German (https://www.transifex.com/oca/teams/23907/de/)\n" "Language: de\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 3.6.1\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" #. module: contract_sale_invoicing -#: model:ir.model,name:contract_sale_invoicing.model_account_analytic_account -msgid "Analytic Account" -msgstr "Kostenstelle" - -#. module: contract_sale_invoicing -#: model:ir.model.fields,help:contract_sale_invoicing.field_account_analytic_account__invoicing_sales +#: model:ir.model.fields,help:contract_sale_invoicing.field_contract_contract__invoicing_sales msgid "" "If checked include sales with same analytic account to invoice in contract " "invoice creation." msgstr "" -"Wenn der Haken gesetzt ist, dann werden alle noch abzurechnenden " -"Verkaufsaufträge mit derselben Kostenstelle bei der Rechnungserstellung für " -"den Vertrag einbezogen." #. module: contract_sale_invoicing -#: model:ir.model.fields,field_description:contract_sale_invoicing.field_account_analytic_account__invoicing_sales +#: model:ir.model.fields,field_description:contract_sale_invoicing.field_contract_contract__invoicing_sales msgid "Invoice Pending Sales Orders" -msgstr "Ausstehende Verkaufsaufträge abrechnen" +msgstr "" + +#. module: contract_sale_invoicing +#: model:ir.model,name:contract_sale_invoicing.model_contract_contract +msgid "contract.contract" +msgstr "" + +#~ msgid "Analytic Account" +#~ msgstr "Kostenstelle" diff --git a/contract_sale_invoicing/i18n/el_GR.po b/contract_sale_invoicing/i18n/el_GR.po index 3a987be8..3370617e 100644 --- a/contract_sale_invoicing/i18n/el_GR.po +++ b/contract_sale_invoicing/i18n/el_GR.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 11.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-04-17 02:41+0000\n" +"POT-Creation-Date: 2019-05-29 14:59+0000\n" "PO-Revision-Date: 2018-04-17 02:41+0000\n" "Last-Translator: OCA Transbot , 2018\n" "Language-Team: Greek (Greece) (https://www.transifex.com/oca/teams/23907/" @@ -20,18 +20,21 @@ msgstr "" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. module: contract_sale_invoicing -#: model:ir.model,name:contract_sale_invoicing.model_account_analytic_account -msgid "Analytic Account" -msgstr "Αναλυτικός Λογαριασμός" - -#. module: contract_sale_invoicing -#: model:ir.model.fields,help:contract_sale_invoicing.field_account_analytic_account__invoicing_sales +#: model:ir.model.fields,help:contract_sale_invoicing.field_contract_contract__invoicing_sales msgid "" "If checked include sales with same analytic account to invoice in contract " "invoice creation." msgstr "" #. module: contract_sale_invoicing -#: model:ir.model.fields,field_description:contract_sale_invoicing.field_account_analytic_account__invoicing_sales +#: model:ir.model.fields,field_description:contract_sale_invoicing.field_contract_contract__invoicing_sales msgid "Invoice Pending Sales Orders" msgstr "" + +#. module: contract_sale_invoicing +#: model:ir.model,name:contract_sale_invoicing.model_contract_contract +msgid "contract.contract" +msgstr "" + +#~ msgid "Analytic Account" +#~ msgstr "Αναλυτικός Λογαριασμός" diff --git a/contract_sale_invoicing/i18n/es.po b/contract_sale_invoicing/i18n/es.po index ca60162c..4392586b 100644 --- a/contract_sale_invoicing/i18n/es.po +++ b/contract_sale_invoicing/i18n/es.po @@ -6,7 +6,7 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 11.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-04-06 17:36+0200\n" +"POT-Creation-Date: 2019-05-29 14:59+0000\n" "PO-Revision-Date: 2018-04-06 17:38+0200\n" "Last-Translator: Carlos Dauden \n" "Language-Team: \n" @@ -18,12 +18,7 @@ msgstr "" "X-Generator: Poedit 1.8.7.1\n" #. module: contract_sale_invoicing -#: model:ir.model,name:contract_sale_invoicing.model_account_analytic_account -msgid "Analytic Account" -msgstr "Cuenta analítica" - -#. module: contract_sale_invoicing -#: model:ir.model.fields,help:contract_sale_invoicing.field_account_analytic_account__invoicing_sales +#: model:ir.model.fields,help:contract_sale_invoicing.field_contract_contract__invoicing_sales msgid "" "If checked include sales with same analytic account to invoice in contract " "invoice creation." @@ -32,6 +27,14 @@ msgstr "" "cuenta analítica en la creación de la factura del contrato" #. module: contract_sale_invoicing -#: model:ir.model.fields,field_description:contract_sale_invoicing.field_account_analytic_account__invoicing_sales +#: model:ir.model.fields,field_description:contract_sale_invoicing.field_contract_contract__invoicing_sales msgid "Invoice Pending Sales Orders" msgstr "Facturar pedidos pendientes" + +#. module: contract_sale_invoicing +#: model:ir.model,name:contract_sale_invoicing.model_contract_contract +msgid "contract.contract" +msgstr "" + +#~ msgid "Analytic Account" +#~ msgstr "Cuenta analítica" diff --git a/contract_sale_invoicing/i18n/es_MX.po b/contract_sale_invoicing/i18n/es_MX.po index a9e3d7a1..ce1b68e9 100644 --- a/contract_sale_invoicing/i18n/es_MX.po +++ b/contract_sale_invoicing/i18n/es_MX.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 11.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-04-17 02:41+0000\n" +"POT-Creation-Date: 2019-05-29 14:59+0000\n" "PO-Revision-Date: 2018-04-17 02:41+0000\n" "Last-Translator: OCA Transbot , 2018\n" "Language-Team: Spanish (Mexico) (https://www.transifex.com/oca/teams/23907/" @@ -20,18 +20,21 @@ msgstr "" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. module: contract_sale_invoicing -#: model:ir.model,name:contract_sale_invoicing.model_account_analytic_account -msgid "Analytic Account" -msgstr "Cuenta analítica" - -#. module: contract_sale_invoicing -#: model:ir.model.fields,help:contract_sale_invoicing.field_account_analytic_account__invoicing_sales +#: model:ir.model.fields,help:contract_sale_invoicing.field_contract_contract__invoicing_sales msgid "" "If checked include sales with same analytic account to invoice in contract " "invoice creation." msgstr "" #. module: contract_sale_invoicing -#: model:ir.model.fields,field_description:contract_sale_invoicing.field_account_analytic_account__invoicing_sales +#: model:ir.model.fields,field_description:contract_sale_invoicing.field_contract_contract__invoicing_sales msgid "Invoice Pending Sales Orders" msgstr "" + +#. module: contract_sale_invoicing +#: model:ir.model,name:contract_sale_invoicing.model_contract_contract +msgid "contract.contract" +msgstr "" + +#~ msgid "Analytic Account" +#~ msgstr "Cuenta analítica" diff --git a/contract_sale_invoicing/i18n/fi.po b/contract_sale_invoicing/i18n/fi.po index 7f4698db..72c3e60d 100644 --- a/contract_sale_invoicing/i18n/fi.po +++ b/contract_sale_invoicing/i18n/fi.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 11.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-04-17 02:41+0000\n" +"POT-Creation-Date: 2019-05-29 14:59+0000\n" "PO-Revision-Date: 2018-04-17 02:41+0000\n" "Last-Translator: OCA Transbot , 2018\n" "Language-Team: Finnish (https://www.transifex.com/oca/teams/23907/fi/)\n" @@ -19,18 +19,21 @@ msgstr "" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. module: contract_sale_invoicing -#: model:ir.model,name:contract_sale_invoicing.model_account_analytic_account -msgid "Analytic Account" -msgstr "Analyyttinen tili" - -#. module: contract_sale_invoicing -#: model:ir.model.fields,help:contract_sale_invoicing.field_account_analytic_account__invoicing_sales +#: model:ir.model.fields,help:contract_sale_invoicing.field_contract_contract__invoicing_sales msgid "" "If checked include sales with same analytic account to invoice in contract " "invoice creation." msgstr "" #. module: contract_sale_invoicing -#: model:ir.model.fields,field_description:contract_sale_invoicing.field_account_analytic_account__invoicing_sales +#: model:ir.model.fields,field_description:contract_sale_invoicing.field_contract_contract__invoicing_sales msgid "Invoice Pending Sales Orders" msgstr "" + +#. module: contract_sale_invoicing +#: model:ir.model,name:contract_sale_invoicing.model_contract_contract +msgid "contract.contract" +msgstr "" + +#~ msgid "Analytic Account" +#~ msgstr "Analyyttinen tili" diff --git a/contract_sale_invoicing/i18n/fr.po b/contract_sale_invoicing/i18n/fr.po index 07004e15..419e28f5 100644 --- a/contract_sale_invoicing/i18n/fr.po +++ b/contract_sale_invoicing/i18n/fr.po @@ -9,7 +9,7 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 11.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-05-05 02:28+0000\n" +"POT-Creation-Date: 2019-05-29 14:59+0000\n" "PO-Revision-Date: 2018-05-05 02:28+0000\n" "Last-Translator: Quentin THEURET , 2018\n" "Language-Team: French (https://www.transifex.com/oca/teams/23907/fr/)\n" @@ -20,12 +20,7 @@ msgstr "" "Plural-Forms: nplurals=2; plural=(n > 1);\n" #. module: contract_sale_invoicing -#: model:ir.model,name:contract_sale_invoicing.model_account_analytic_account -msgid "Analytic Account" -msgstr "Compte analytique" - -#. module: contract_sale_invoicing -#: model:ir.model.fields,help:contract_sale_invoicing.field_account_analytic_account__invoicing_sales +#: model:ir.model.fields,help:contract_sale_invoicing.field_contract_contract__invoicing_sales msgid "" "If checked include sales with same analytic account to invoice in contract " "invoice creation." @@ -34,6 +29,14 @@ msgstr "" "de la création de facture du contrat." #. module: contract_sale_invoicing -#: model:ir.model.fields,field_description:contract_sale_invoicing.field_account_analytic_account__invoicing_sales +#: model:ir.model.fields,field_description:contract_sale_invoicing.field_contract_contract__invoicing_sales msgid "Invoice Pending Sales Orders" msgstr "Commandes avec une facture en attente" + +#. module: contract_sale_invoicing +#: model:ir.model,name:contract_sale_invoicing.model_contract_contract +msgid "contract.contract" +msgstr "" + +#~ msgid "Analytic Account" +#~ msgstr "Compte analytique" diff --git a/contract_sale_invoicing/i18n/gl.po b/contract_sale_invoicing/i18n/gl.po index e73dcb74..f6538a1e 100644 --- a/contract_sale_invoicing/i18n/gl.po +++ b/contract_sale_invoicing/i18n/gl.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 11.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-04-17 02:41+0000\n" +"POT-Creation-Date: 2019-05-29 14:59+0000\n" "PO-Revision-Date: 2018-04-17 02:41+0000\n" "Last-Translator: OCA Transbot , 2018\n" "Language-Team: Galician (https://www.transifex.com/oca/teams/23907/gl/)\n" @@ -19,18 +19,21 @@ msgstr "" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. module: contract_sale_invoicing -#: model:ir.model,name:contract_sale_invoicing.model_account_analytic_account -msgid "Analytic Account" -msgstr "Conta analítica" - -#. module: contract_sale_invoicing -#: model:ir.model.fields,help:contract_sale_invoicing.field_account_analytic_account__invoicing_sales +#: model:ir.model.fields,help:contract_sale_invoicing.field_contract_contract__invoicing_sales msgid "" "If checked include sales with same analytic account to invoice in contract " "invoice creation." msgstr "" #. module: contract_sale_invoicing -#: model:ir.model.fields,field_description:contract_sale_invoicing.field_account_analytic_account__invoicing_sales +#: model:ir.model.fields,field_description:contract_sale_invoicing.field_contract_contract__invoicing_sales msgid "Invoice Pending Sales Orders" msgstr "" + +#. module: contract_sale_invoicing +#: model:ir.model,name:contract_sale_invoicing.model_contract_contract +msgid "contract.contract" +msgstr "" + +#~ msgid "Analytic Account" +#~ msgstr "Conta analítica" diff --git a/contract_sale_invoicing/i18n/hi_IN.po b/contract_sale_invoicing/i18n/hi_IN.po index a7c04c9c..0b13b1df 100644 --- a/contract_sale_invoicing/i18n/hi_IN.po +++ b/contract_sale_invoicing/i18n/hi_IN.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 11.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-04-17 02:41+0000\n" +"POT-Creation-Date: 2019-05-29 14:59+0000\n" "PO-Revision-Date: 2018-04-17 02:41+0000\n" "Last-Translator: OCA Transbot , 2018\n" "Language-Team: Hindi (India) (https://www.transifex.com/oca/teams/23907/" @@ -20,18 +20,21 @@ msgstr "" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. module: contract_sale_invoicing -#: model:ir.model,name:contract_sale_invoicing.model_account_analytic_account -msgid "Analytic Account" -msgstr "विश्लेषणात्मक खाता" - -#. module: contract_sale_invoicing -#: model:ir.model.fields,help:contract_sale_invoicing.field_account_analytic_account__invoicing_sales +#: model:ir.model.fields,help:contract_sale_invoicing.field_contract_contract__invoicing_sales msgid "" "If checked include sales with same analytic account to invoice in contract " "invoice creation." msgstr "" #. module: contract_sale_invoicing -#: model:ir.model.fields,field_description:contract_sale_invoicing.field_account_analytic_account__invoicing_sales +#: model:ir.model.fields,field_description:contract_sale_invoicing.field_contract_contract__invoicing_sales msgid "Invoice Pending Sales Orders" msgstr "" + +#. module: contract_sale_invoicing +#: model:ir.model,name:contract_sale_invoicing.model_contract_contract +msgid "contract.contract" +msgstr "" + +#~ msgid "Analytic Account" +#~ msgstr "विश्लेषणात्मक खाता" diff --git a/contract_sale_invoicing/i18n/hr.po b/contract_sale_invoicing/i18n/hr.po index 6d2312a8..c3f37388 100644 --- a/contract_sale_invoicing/i18n/hr.po +++ b/contract_sale_invoicing/i18n/hr.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 11.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-04-17 02:41+0000\n" +"POT-Creation-Date: 2019-05-29 14:59+0000\n" "PO-Revision-Date: 2018-04-17 02:41+0000\n" "Last-Translator: OCA Transbot , 2018\n" "Language-Team: Croatian (https://www.transifex.com/oca/teams/23907/hr/)\n" @@ -20,18 +20,21 @@ msgstr "" "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" #. module: contract_sale_invoicing -#: model:ir.model,name:contract_sale_invoicing.model_account_analytic_account -msgid "Analytic Account" -msgstr "Analitički konto" - -#. module: contract_sale_invoicing -#: model:ir.model.fields,help:contract_sale_invoicing.field_account_analytic_account__invoicing_sales +#: model:ir.model.fields,help:contract_sale_invoicing.field_contract_contract__invoicing_sales msgid "" "If checked include sales with same analytic account to invoice in contract " "invoice creation." msgstr "" #. module: contract_sale_invoicing -#: model:ir.model.fields,field_description:contract_sale_invoicing.field_account_analytic_account__invoicing_sales +#: model:ir.model.fields,field_description:contract_sale_invoicing.field_contract_contract__invoicing_sales msgid "Invoice Pending Sales Orders" msgstr "" + +#. module: contract_sale_invoicing +#: model:ir.model,name:contract_sale_invoicing.model_contract_contract +msgid "contract.contract" +msgstr "" + +#~ msgid "Analytic Account" +#~ msgstr "Analitički konto" diff --git a/contract_sale_invoicing/i18n/hr_HR.po b/contract_sale_invoicing/i18n/hr_HR.po index fc6d32ac..4495d6bf 100644 --- a/contract_sale_invoicing/i18n/hr_HR.po +++ b/contract_sale_invoicing/i18n/hr_HR.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 11.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-04-17 02:41+0000\n" +"POT-Creation-Date: 2019-05-29 14:59+0000\n" "PO-Revision-Date: 2018-04-17 02:41+0000\n" "Last-Translator: OCA Transbot , 2018\n" "Language-Team: Croatian (Croatia) (https://www.transifex.com/oca/teams/23907/" @@ -21,18 +21,21 @@ msgstr "" "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" #. module: contract_sale_invoicing -#: model:ir.model,name:contract_sale_invoicing.model_account_analytic_account -msgid "Analytic Account" -msgstr "Konto analitike" - -#. module: contract_sale_invoicing -#: model:ir.model.fields,help:contract_sale_invoicing.field_account_analytic_account__invoicing_sales +#: model:ir.model.fields,help:contract_sale_invoicing.field_contract_contract__invoicing_sales msgid "" "If checked include sales with same analytic account to invoice in contract " "invoice creation." msgstr "" #. module: contract_sale_invoicing -#: model:ir.model.fields,field_description:contract_sale_invoicing.field_account_analytic_account__invoicing_sales +#: model:ir.model.fields,field_description:contract_sale_invoicing.field_contract_contract__invoicing_sales msgid "Invoice Pending Sales Orders" msgstr "" + +#. module: contract_sale_invoicing +#: model:ir.model,name:contract_sale_invoicing.model_contract_contract +msgid "contract.contract" +msgstr "" + +#~ msgid "Analytic Account" +#~ msgstr "Konto analitike" diff --git a/contract_sale_invoicing/i18n/hu.po b/contract_sale_invoicing/i18n/hu.po index 8c385ce2..753db616 100644 --- a/contract_sale_invoicing/i18n/hu.po +++ b/contract_sale_invoicing/i18n/hu.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 11.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-04-17 02:41+0000\n" +"POT-Creation-Date: 2019-05-29 14:59+0000\n" "PO-Revision-Date: 2018-04-17 02:41+0000\n" "Last-Translator: OCA Transbot , 2018\n" "Language-Team: Hungarian (https://www.transifex.com/oca/teams/23907/hu/)\n" @@ -19,18 +19,21 @@ msgstr "" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. module: contract_sale_invoicing -#: model:ir.model,name:contract_sale_invoicing.model_account_analytic_account -msgid "Analytic Account" -msgstr "Analitikus gyűjtőkód könyvelés" - -#. module: contract_sale_invoicing -#: model:ir.model.fields,help:contract_sale_invoicing.field_account_analytic_account__invoicing_sales +#: model:ir.model.fields,help:contract_sale_invoicing.field_contract_contract__invoicing_sales msgid "" "If checked include sales with same analytic account to invoice in contract " "invoice creation." msgstr "" #. module: contract_sale_invoicing -#: model:ir.model.fields,field_description:contract_sale_invoicing.field_account_analytic_account__invoicing_sales +#: model:ir.model.fields,field_description:contract_sale_invoicing.field_contract_contract__invoicing_sales msgid "Invoice Pending Sales Orders" msgstr "" + +#. module: contract_sale_invoicing +#: model:ir.model,name:contract_sale_invoicing.model_contract_contract +msgid "contract.contract" +msgstr "" + +#~ msgid "Analytic Account" +#~ msgstr "Analitikus gyűjtőkód könyvelés" diff --git a/contract_sale_invoicing/i18n/it.po b/contract_sale_invoicing/i18n/it.po index 5594490b..abad20ca 100644 --- a/contract_sale_invoicing/i18n/it.po +++ b/contract_sale_invoicing/i18n/it.po @@ -8,32 +8,32 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 11.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-04-17 02:41+0000\n" -"PO-Revision-Date: 2019-06-26 15:42+0000\n" -"Last-Translator: Sergio Zanchetta \n" +"POT-Creation-Date: 2019-05-29 14:59+0000\n" +"PO-Revision-Date: 2018-04-17 02:41+0000\n" +"Last-Translator: OCA Transbot , 2018\n" "Language-Team: Italian (https://www.transifex.com/oca/teams/23907/it/)\n" "Language: it\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Plural-Forms: nplurals=2; plural=n != 1;\n" -"X-Generator: Weblate 3.6.1\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" #. module: contract_sale_invoicing -#: model:ir.model,name:contract_sale_invoicing.model_account_analytic_account -msgid "Analytic Account" -msgstr "Conto analitico" - -#. module: contract_sale_invoicing -#: model:ir.model.fields,help:contract_sale_invoicing.field_account_analytic_account__invoicing_sales +#: model:ir.model.fields,help:contract_sale_invoicing.field_contract_contract__invoicing_sales msgid "" "If checked include sales with same analytic account to invoice in contract " "invoice creation." msgstr "" -"Se selezionato, in fase di creazione del contratto aggiunge alla fattura le " -"vendite con lo stesso conto analitico." #. module: contract_sale_invoicing -#: model:ir.model.fields,field_description:contract_sale_invoicing.field_account_analytic_account__invoicing_sales +#: model:ir.model.fields,field_description:contract_sale_invoicing.field_contract_contract__invoicing_sales msgid "Invoice Pending Sales Orders" -msgstr "Ordini di vendita in sospeso della fattura" +msgstr "" + +#. module: contract_sale_invoicing +#: model:ir.model,name:contract_sale_invoicing.model_contract_contract +msgid "contract.contract" +msgstr "" + +#~ msgid "Analytic Account" +#~ msgstr "Conto Analitico" diff --git a/contract_sale_invoicing/i18n/nl.po b/contract_sale_invoicing/i18n/nl.po index f15145d2..20f81450 100644 --- a/contract_sale_invoicing/i18n/nl.po +++ b/contract_sale_invoicing/i18n/nl.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 11.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-04-17 02:41+0000\n" +"POT-Creation-Date: 2019-05-29 14:59+0000\n" "PO-Revision-Date: 2018-04-17 02:41+0000\n" "Last-Translator: OCA Transbot , 2018\n" "Language-Team: Dutch (https://www.transifex.com/oca/teams/23907/nl/)\n" @@ -19,18 +19,21 @@ msgstr "" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. module: contract_sale_invoicing -#: model:ir.model,name:contract_sale_invoicing.model_account_analytic_account -msgid "Analytic Account" -msgstr "Kostenplaats" - -#. module: contract_sale_invoicing -#: model:ir.model.fields,help:contract_sale_invoicing.field_account_analytic_account__invoicing_sales +#: model:ir.model.fields,help:contract_sale_invoicing.field_contract_contract__invoicing_sales msgid "" "If checked include sales with same analytic account to invoice in contract " "invoice creation." msgstr "" #. module: contract_sale_invoicing -#: model:ir.model.fields,field_description:contract_sale_invoicing.field_account_analytic_account__invoicing_sales +#: model:ir.model.fields,field_description:contract_sale_invoicing.field_contract_contract__invoicing_sales msgid "Invoice Pending Sales Orders" msgstr "" + +#. module: contract_sale_invoicing +#: model:ir.model,name:contract_sale_invoicing.model_contract_contract +msgid "contract.contract" +msgstr "" + +#~ msgid "Analytic Account" +#~ msgstr "Kostenplaats" diff --git a/contract_sale_invoicing/i18n/pt.po b/contract_sale_invoicing/i18n/pt.po index e5fe4fd2..414554a7 100644 --- a/contract_sale_invoicing/i18n/pt.po +++ b/contract_sale_invoicing/i18n/pt.po @@ -8,9 +8,9 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 11.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-04-17 02:41+0000\n" -"PO-Revision-Date: 2019-08-14 13:44+0000\n" -"Last-Translator: Pedro Castro Silva \n" +"POT-Creation-Date: 2019-05-29 14:59+0000\n" +"PO-Revision-Date: 2018-04-17 02:41+0000\n" +"Last-Translator: OCA Transbot , 2018\n" "Language-Team: Portuguese (https://www.transifex.com/oca/teams/23907/pt/)\n" "Language: pt\n" "MIME-Version: 1.0\n" @@ -20,12 +20,7 @@ msgstr "" "X-Generator: Weblate 3.7.1\n" #. module: contract_sale_invoicing -#: model:ir.model,name:contract_sale_invoicing.model_account_analytic_account -msgid "Analytic Account" -msgstr "Conta Analítica" - -#. module: contract_sale_invoicing -#: model:ir.model.fields,help:contract_sale_invoicing.field_account_analytic_account__invoicing_sales +#: model:ir.model.fields,help:contract_sale_invoicing.field_contract_contract__invoicing_sales msgid "" "If checked include sales with same analytic account to invoice in contract " "invoice creation." @@ -34,6 +29,6 @@ msgstr "" "faturas do contrato." #. module: contract_sale_invoicing -#: model:ir.model.fields,field_description:contract_sale_invoicing.field_account_analytic_account__invoicing_sales +#: model:ir.model.fields,field_description:contract_sale_invoicing.field_contract_contract__invoicing_sales msgid "Invoice Pending Sales Orders" msgstr "Faturar Encomendas de Venda Pendentes" diff --git a/contract_sale_invoicing/i18n/pt_PT.po b/contract_sale_invoicing/i18n/pt_PT.po index 5121f7f2..31730f50 100644 --- a/contract_sale_invoicing/i18n/pt_PT.po +++ b/contract_sale_invoicing/i18n/pt_PT.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 11.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-04-17 02:41+0000\n" +"POT-Creation-Date: 2019-05-29 14:59+0000\n" "PO-Revision-Date: 2018-04-17 02:41+0000\n" "Last-Translator: OCA Transbot , 2018\n" "Language-Team: Portuguese (Portugal) (https://www.transifex.com/oca/" @@ -20,18 +20,21 @@ msgstr "" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. module: contract_sale_invoicing -#: model:ir.model,name:contract_sale_invoicing.model_account_analytic_account -msgid "Analytic Account" -msgstr "Conta Analítica" - -#. module: contract_sale_invoicing -#: model:ir.model.fields,help:contract_sale_invoicing.field_account_analytic_account__invoicing_sales +#: model:ir.model.fields,help:contract_sale_invoicing.field_contract_contract__invoicing_sales msgid "" "If checked include sales with same analytic account to invoice in contract " "invoice creation." msgstr "" #. module: contract_sale_invoicing -#: model:ir.model.fields,field_description:contract_sale_invoicing.field_account_analytic_account__invoicing_sales +#: model:ir.model.fields,field_description:contract_sale_invoicing.field_contract_contract__invoicing_sales msgid "Invoice Pending Sales Orders" msgstr "" + +#. module: contract_sale_invoicing +#: model:ir.model,name:contract_sale_invoicing.model_contract_contract +msgid "contract.contract" +msgstr "" + +#~ msgid "Analytic Account" +#~ msgstr "Conta Analítica" diff --git a/contract_sale_invoicing/i18n/ro.po b/contract_sale_invoicing/i18n/ro.po index 5b474832..7862c73b 100644 --- a/contract_sale_invoicing/i18n/ro.po +++ b/contract_sale_invoicing/i18n/ro.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 11.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-04-17 02:41+0000\n" +"POT-Creation-Date: 2019-05-29 14:59+0000\n" "PO-Revision-Date: 2018-04-17 02:41+0000\n" "Last-Translator: OCA Transbot , 2018\n" "Language-Team: Romanian (https://www.transifex.com/oca/teams/23907/ro/)\n" @@ -20,18 +20,21 @@ msgstr "" "2:1));\n" #. module: contract_sale_invoicing -#: model:ir.model,name:contract_sale_invoicing.model_account_analytic_account -msgid "Analytic Account" -msgstr "Cont analitic" - -#. module: contract_sale_invoicing -#: model:ir.model.fields,help:contract_sale_invoicing.field_account_analytic_account__invoicing_sales +#: model:ir.model.fields,help:contract_sale_invoicing.field_contract_contract__invoicing_sales msgid "" "If checked include sales with same analytic account to invoice in contract " "invoice creation." msgstr "" #. module: contract_sale_invoicing -#: model:ir.model.fields,field_description:contract_sale_invoicing.field_account_analytic_account__invoicing_sales +#: model:ir.model.fields,field_description:contract_sale_invoicing.field_contract_contract__invoicing_sales msgid "Invoice Pending Sales Orders" msgstr "" + +#. module: contract_sale_invoicing +#: model:ir.model,name:contract_sale_invoicing.model_contract_contract +msgid "contract.contract" +msgstr "" + +#~ msgid "Analytic Account" +#~ msgstr "Cont analitic" diff --git a/contract_sale_invoicing/i18n/sk_SK.po b/contract_sale_invoicing/i18n/sk_SK.po index f47a072a..f0b96f94 100644 --- a/contract_sale_invoicing/i18n/sk_SK.po +++ b/contract_sale_invoicing/i18n/sk_SK.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 11.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-04-17 02:41+0000\n" +"POT-Creation-Date: 2019-05-29 14:59+0000\n" "PO-Revision-Date: 2018-04-17 02:41+0000\n" "Last-Translator: OCA Transbot , 2018\n" "Language-Team: Slovak (Slovakia) (https://www.transifex.com/oca/teams/23907/" @@ -20,18 +20,21 @@ msgstr "" "Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" #. module: contract_sale_invoicing -#: model:ir.model,name:contract_sale_invoicing.model_account_analytic_account -msgid "Analytic Account" -msgstr "Analytický účet" - -#. module: contract_sale_invoicing -#: model:ir.model.fields,help:contract_sale_invoicing.field_account_analytic_account__invoicing_sales +#: model:ir.model.fields,help:contract_sale_invoicing.field_contract_contract__invoicing_sales msgid "" "If checked include sales with same analytic account to invoice in contract " "invoice creation." msgstr "" #. module: contract_sale_invoicing -#: model:ir.model.fields,field_description:contract_sale_invoicing.field_account_analytic_account__invoicing_sales +#: model:ir.model.fields,field_description:contract_sale_invoicing.field_contract_contract__invoicing_sales msgid "Invoice Pending Sales Orders" msgstr "" + +#. module: contract_sale_invoicing +#: model:ir.model,name:contract_sale_invoicing.model_contract_contract +msgid "contract.contract" +msgstr "" + +#~ msgid "Analytic Account" +#~ msgstr "Analytický účet" diff --git a/contract_sale_invoicing/i18n/sl.po b/contract_sale_invoicing/i18n/sl.po index b9684e57..987587d9 100644 --- a/contract_sale_invoicing/i18n/sl.po +++ b/contract_sale_invoicing/i18n/sl.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 11.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-04-17 02:41+0000\n" +"POT-Creation-Date: 2019-05-29 14:59+0000\n" "PO-Revision-Date: 2018-04-17 02:41+0000\n" "Last-Translator: OCA Transbot , 2018\n" "Language-Team: Slovenian (https://www.transifex.com/oca/teams/23907/sl/)\n" @@ -20,18 +20,21 @@ msgstr "" "%100==4 ? 2 : 3);\n" #. module: contract_sale_invoicing -#: model:ir.model,name:contract_sale_invoicing.model_account_analytic_account -msgid "Analytic Account" -msgstr "Analitični konto" - -#. module: contract_sale_invoicing -#: model:ir.model.fields,help:contract_sale_invoicing.field_account_analytic_account__invoicing_sales +#: model:ir.model.fields,help:contract_sale_invoicing.field_contract_contract__invoicing_sales msgid "" "If checked include sales with same analytic account to invoice in contract " "invoice creation." msgstr "" #. module: contract_sale_invoicing -#: model:ir.model.fields,field_description:contract_sale_invoicing.field_account_analytic_account__invoicing_sales +#: model:ir.model.fields,field_description:contract_sale_invoicing.field_contract_contract__invoicing_sales msgid "Invoice Pending Sales Orders" msgstr "" + +#. module: contract_sale_invoicing +#: model:ir.model,name:contract_sale_invoicing.model_contract_contract +msgid "contract.contract" +msgstr "" + +#~ msgid "Analytic Account" +#~ msgstr "Analitični konto" diff --git a/contract_sale_invoicing/i18n/tr.po b/contract_sale_invoicing/i18n/tr.po index d6cd144d..22044663 100644 --- a/contract_sale_invoicing/i18n/tr.po +++ b/contract_sale_invoicing/i18n/tr.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 11.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-04-17 02:41+0000\n" +"POT-Creation-Date: 2019-05-29 14:59+0000\n" "PO-Revision-Date: 2018-04-17 02:41+0000\n" "Last-Translator: OCA Transbot , 2018\n" "Language-Team: Turkish (https://www.transifex.com/oca/teams/23907/tr/)\n" @@ -19,18 +19,21 @@ msgstr "" "Plural-Forms: nplurals=2; plural=(n > 1);\n" #. module: contract_sale_invoicing -#: model:ir.model,name:contract_sale_invoicing.model_account_analytic_account -msgid "Analytic Account" -msgstr "Analitik Hesap" - -#. module: contract_sale_invoicing -#: model:ir.model.fields,help:contract_sale_invoicing.field_account_analytic_account__invoicing_sales +#: model:ir.model.fields,help:contract_sale_invoicing.field_contract_contract__invoicing_sales msgid "" "If checked include sales with same analytic account to invoice in contract " "invoice creation." msgstr "" #. module: contract_sale_invoicing -#: model:ir.model.fields,field_description:contract_sale_invoicing.field_account_analytic_account__invoicing_sales +#: model:ir.model.fields,field_description:contract_sale_invoicing.field_contract_contract__invoicing_sales msgid "Invoice Pending Sales Orders" msgstr "" + +#. module: contract_sale_invoicing +#: model:ir.model,name:contract_sale_invoicing.model_contract_contract +msgid "contract.contract" +msgstr "" + +#~ msgid "Analytic Account" +#~ msgstr "Analitik Hesap" diff --git a/contract_sale_invoicing/i18n/tr_TR.po b/contract_sale_invoicing/i18n/tr_TR.po index 4467a5d1..772f1d5e 100644 --- a/contract_sale_invoicing/i18n/tr_TR.po +++ b/contract_sale_invoicing/i18n/tr_TR.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 11.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-04-17 02:41+0000\n" +"POT-Creation-Date: 2019-05-29 14:59+0000\n" "PO-Revision-Date: 2018-04-17 02:41+0000\n" "Last-Translator: OCA Transbot , 2018\n" "Language-Team: Turkish (Turkey) (https://www.transifex.com/oca/teams/23907/" @@ -20,18 +20,21 @@ msgstr "" "Plural-Forms: nplurals=2; plural=(n > 1);\n" #. module: contract_sale_invoicing -#: model:ir.model,name:contract_sale_invoicing.model_account_analytic_account -msgid "Analytic Account" -msgstr "Analitik Hesap" - -#. module: contract_sale_invoicing -#: model:ir.model.fields,help:contract_sale_invoicing.field_account_analytic_account__invoicing_sales +#: model:ir.model.fields,help:contract_sale_invoicing.field_contract_contract__invoicing_sales msgid "" "If checked include sales with same analytic account to invoice in contract " "invoice creation." msgstr "" #. module: contract_sale_invoicing -#: model:ir.model.fields,field_description:contract_sale_invoicing.field_account_analytic_account__invoicing_sales +#: model:ir.model.fields,field_description:contract_sale_invoicing.field_contract_contract__invoicing_sales msgid "Invoice Pending Sales Orders" msgstr "" + +#. module: contract_sale_invoicing +#: model:ir.model,name:contract_sale_invoicing.model_contract_contract +msgid "contract.contract" +msgstr "" + +#~ msgid "Analytic Account" +#~ msgstr "Analitik Hesap" diff --git a/contract_sale_invoicing/i18n/zh.po b/contract_sale_invoicing/i18n/zh.po index 3e456ec4..feb0561c 100644 --- a/contract_sale_invoicing/i18n/zh.po +++ b/contract_sale_invoicing/i18n/zh.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: Odoo Server 11.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-04-27 01:12+0000\n" +"POT-Creation-Date: 2019-05-29 14:59+0000\n" "PO-Revision-Date: 2018-04-27 01:12+0000\n" "Last-Translator: DIT INTL , 2018\n" "Language-Team: Chinese (https://www.transifex.com/oca/teams/23907/zh/)\n" @@ -19,18 +19,21 @@ msgstr "" "Plural-Forms: nplurals=1; plural=0;\n" #. module: contract_sale_invoicing -#: model:ir.model,name:contract_sale_invoicing.model_account_analytic_account -msgid "Analytic Account" -msgstr "分析会计" - -#. module: contract_sale_invoicing -#: model:ir.model.fields,help:contract_sale_invoicing.field_account_analytic_account__invoicing_sales +#: model:ir.model.fields,help:contract_sale_invoicing.field_contract_contract__invoicing_sales msgid "" "If checked include sales with same analytic account to invoice in contract " "invoice creation." msgstr "如果选中,则在销售发票中包含分析帐户。" #. module: contract_sale_invoicing -#: model:ir.model.fields,field_description:contract_sale_invoicing.field_account_analytic_account__invoicing_sales +#: model:ir.model.fields,field_description:contract_sale_invoicing.field_contract_contract__invoicing_sales msgid "Invoice Pending Sales Orders" msgstr "发票挂起的销售订单" + +#. module: contract_sale_invoicing +#: model:ir.model,name:contract_sale_invoicing.model_contract_contract +msgid "contract.contract" +msgstr "" + +#~ msgid "Analytic Account" +#~ msgstr "分析会计" diff --git a/contract_sale_invoicing/models/contract.py b/contract_sale_invoicing/models/contract.py index 9ae245bb..092c1911 100644 --- a/contract_sale_invoicing/models/contract.py +++ b/contract_sale_invoicing/models/contract.py @@ -4,8 +4,8 @@ from odoo import api, fields, models -class AccountAnalyticAccount(models.Model): - _inherit = 'account.analytic.account' +class ContractContract(models.Model): + _inherit = 'contract.contract' invoicing_sales = fields.Boolean( string='Invoice Pending Sales Orders', @@ -14,11 +14,12 @@ class AccountAnalyticAccount(models.Model): ) @api.multi - def _create_invoice(self, invoice=False): + def _recurring_create_invoice(self, date_ref=False): + invoices = super()._recurring_create_invoice(date_ref) if not self.invoicing_sales: - return super(AccountAnalyticAccount, self)._create_invoice() + return invoices sales = self.env['sale.order'].search([ - ('analytic_account_id', '=', self.id), + ('analytic_account_id', '=', self.analytic_account_id.id), ('partner_invoice_id', 'child_of', self.partner_id.commercial_partner_id.ids), ('invoice_status', '=', 'to invoice'), @@ -27,6 +28,4 @@ class AccountAnalyticAccount(models.Model): ]) if sales: invoice_ids = sales.action_invoice_create() - invoice = self.env['account.invoice'].browse(invoice_ids)[:1] - return super(AccountAnalyticAccount, self)._create_invoice( - invoice=invoice) + invoices |= self.env['account.invoice'].browse(invoice_ids)[:1] diff --git a/contract_sale_invoicing/tests/test_contract_sale_invoicing.py b/contract_sale_invoicing/tests/test_contract_sale_invoicing.py index e655849b..9722add7 100644 --- a/contract_sale_invoicing/tests/test_contract_sale_invoicing.py +++ b/contract_sale_invoicing/tests/test_contract_sale_invoicing.py @@ -8,6 +8,8 @@ class TestContractSaleInvoicing(TestContractBase): @classmethod def setUpClass(cls): super(TestContractSaleInvoicing, cls).setUpClass() + cls.contract.analytic_account_id = \ + cls.env['account.analytic.account'].search([], limit=1) cls.product_so = cls.env.ref( 'product.product_product_1') cls.product_so.invoice_policy = 'order' @@ -21,7 +23,7 @@ class TestContractSaleInvoicing(TestContractBase): 'product_uom': cls.product_so.uom_id.id, 'price_unit': cls.product_so.list_price})], 'pricelist_id': cls.partner.property_product_pricelist.id, - 'analytic_account_id': cls.contract.id, + 'analytic_account_id': cls.contract.analytic_account_id.id, 'date_order': '2016-02-15', }) diff --git a/contract_sale_invoicing/views/contract_view.xml b/contract_sale_invoicing/views/contract_view.xml index b492211f..46ec28c2 100644 --- a/contract_sale_invoicing/views/contract_view.xml +++ b/contract_sale_invoicing/views/contract_view.xml @@ -4,14 +4,14 @@ - - account.analytic.account - - - - + + contract.contract + + + + + - - +