diff --git a/contract/README.rst b/contract/README.rst index d2386eebe..be8ff73b7 100644 --- a/contract/README.rst +++ b/contract/README.rst @@ -74,7 +74,8 @@ Known issues / Roadmap ====================== * Recover states and others functional fields in Contracts. - +* Depending on the contract lines, some sections of contract + line have no meaning that they are propagated to certain invoices Bug Tracker =========== diff --git a/contract/__manifest__.py b/contract/__manifest__.py index 41ebe2524..e039db3ca 100644 --- a/contract/__manifest__.py +++ b/contract/__manifest__.py @@ -35,6 +35,7 @@ 'wizards/contract_manually_create_invoice.xml', 'wizards/contract_contract_terminate.xml', 'views/contract_tag.xml', + 'views/assets.xml', 'views/abstract_contract_line.xml', 'views/contract.xml', 'views/contract_line.xml', diff --git a/contract/models/abstract_contract_line.py b/contract/models/abstract_contract_line.py index 99892d82f..6624f3371 100644 --- a/contract/models/abstract_contract_line.py +++ b/contract/models/abstract_contract_line.py @@ -17,13 +17,13 @@ class ContractAbstractContractLine(models.AbstractModel): _description = 'Abstract Recurring Contract Line' product_id = fields.Many2one( - 'product.product', string='Product', required=True + 'product.product', string='Product' ) 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 + 'uom.uom', string='Unit of Measure' ) automatic_price = fields.Boolean( string="Auto-price?", @@ -125,6 +125,9 @@ class ContractAbstractContractLine(models.AbstractModel): required=True, ondelete='cascade', ) + display_type = fields.Selection([ + ('line_section', "Section"), + ('line_note', "Note")], default=False, help="Technical field for UX purpose.") @api.model def _get_default_recurring_invoicing_offset( diff --git a/contract/models/contract_line.py b/contract/models/contract_line.py index 0c4de8667..011cdc4bd 100644 --- a/contract/models/contract_line.py +++ b/contract/models/contract_line.py @@ -146,6 +146,9 @@ class ContractLine(models.Model): def _compute_state(self): today = fields.Date.context_today(self) for rec in self: + if rec.display_type: + rec.state = False + continue if rec.is_canceled: rec.state = 'canceled' continue @@ -248,6 +251,8 @@ class ContractLine(models.Model): ] if state == 'canceled': return [('is_canceled', '=', True)] + if not state: + return [('display_type', '!=', False)] @api.model def _search_state(self, operator, value): @@ -258,11 +263,8 @@ class ContractLine(models.Model): 'upcoming-close', 'closed', 'canceled', + False, ] - if operator == '!=' and not value: - return [] - if operator == '=' and not value: - return [('id', '=', False)] if operator == '=': return self._get_state_domain(value) if operator == '!=': @@ -275,8 +277,6 @@ class ContractLine(models.Model): return domain if operator == 'in': domain = [] - if not value: - return [('id', '=', False)] for state in value: if domain: domain.insert(0, '|') @@ -284,6 +284,8 @@ class ContractLine(models.Model): return domain if operator == 'not in': + if set(value) == set(states): + return [('id', '=', False)] return self._search_state( 'in', [state for state in states if state not in value] ) @@ -648,15 +650,15 @@ class ContractLine(models.Model): @api.depends('recurring_next_date', 'date_start', 'date_end') def _compute_create_invoice_visibility(self): + # TODO: depending on the lines, and their order, some sections + # have no meaning in certain invoices 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 - ) + if (not rec.display_type and + rec.date_start and today >= rec.date_start): + rec.create_invoice_visibility = bool(rec.recurring_next_date) + else: + rec.create_invoice_visibility = False @api.multi def _prepare_invoice_line(self, invoice_id=False, invoice_values=False): @@ -665,6 +667,7 @@ class ContractLine(models.Model): self.last_date_invoiced, self.recurring_next_date ) invoice_line_vals = { + 'display_type': self.display_type, 'product_id': self.product_id.id, 'quantity': self._get_quantity_to_invoice(*dates), 'uom_id': self.uom_id.id, @@ -1299,10 +1302,11 @@ class ContractLine(models.Model): @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") - ) + for record in self: + if not (record.is_canceled or record.display_type): + raise ValidationError( + _("Contract line must be canceled before delete") + ) return super().unlink() @api.multi diff --git a/contract/readme/ROADMAP.rst b/contract/readme/ROADMAP.rst index c8a2afdb5..4b0fa567e 100644 --- a/contract/readme/ROADMAP.rst +++ b/contract/readme/ROADMAP.rst @@ -1,2 +1,3 @@ * Recover states and others functional fields in Contracts. - +* Depending on the contract lines, some sections of contract + line have no meaning that they are propagated to certain invoices diff --git a/contract/report/report_contract.xml b/contract/report/report_contract.xml index c9d1095bc..c66f538ef 100644 --- a/contract/report/report_contract.xml +++ b/contract/report/report_contract.xml @@ -5,27 +5,27 @@ + + +

Partner:

+
+

VAT:

+
-
-
-

Partner:

-
-

VAT:

-
-
-
+
Responsible:

Contract:

-
+

Recurring Items

- +
@@ -36,30 +36,61 @@ - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + +
Description
- - - - - - - - - -
+ + + + + + + + + + + + + +
+ Subtotal + +
-
- - + +
+
+
+
Total @@ -68,10 +99,13 @@
-
- Notes: -

-

+

+
+
Notes:
+
+
+

+

diff --git a/contract/static/description/index.html b/contract/static/description/index.html index ea92b8ee2..778a518a7 100644 --- a/contract/static/description/index.html +++ b/contract/static/description/index.html @@ -425,6 +425,8 @@ To use it, just select the template on the contract and fields will be filled au

Known issues / Roadmap

  • Recover states and others functional fields in Contracts.
  • +
  • Depending on the contract lines, some sections of contract +line have no meaning that they are propagated to certain invoices
diff --git a/contract/static/src/js/section_and_note_fields_backend.js b/contract/static/src/js/section_and_note_fields_backend.js new file mode 100644 index 000000000..ff63ecf74 --- /dev/null +++ b/contract/static/src/js/section_and_note_fields_backend.js @@ -0,0 +1,38 @@ +/* Copyright 2020 Tecnativa - Ernesto Tejeda + * License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + */ +/* + If in the sub-tree view where the sections and notes are to be used +there are fields that have defined in the XML attrs = {'invisible': ....} +and this condition is met, then an extra space appears in the rows +corresponding to the sections and lines. + This js was written to deal with that problem, but a solution based on +this can be applied directly to Odoo*/ +odoo.define('contract.section_and_note_backend', function (require) { + "use strict"; + + var fieldRegistry = require('web.field_registry'); + var section_and_note_one2many = fieldRegistry.get('section_and_note_one2many'); + + section_and_note_one2many.include({ + _getRenderer: function () { + var result = this._super.apply(this, arguments); + if (this.view.arch.tag === 'tree') { + result.include({ + _renderBodyCell: function (record, node, index, options) { + var $cell = this._super.apply(this, arguments); + + var isSection = record.data.display_type === 'line_section'; + var isNote = record.data.display_type === 'line_note'; + + if (isSection || isNote) { + $cell.removeClass('o_invisible_modifier'); + } + return $cell; + } + }) + } + return result + }, + }); +}); diff --git a/contract/tests/test_contract.py b/contract/tests/test_contract.py index 53daf7180..e4b9c7d77 100644 --- a/contract/tests/test_contract.py +++ b/contract/tests/test_contract.py @@ -42,9 +42,16 @@ class TestContractBase(common.SavepointCase): 'recurring_rule_type': 'yearly', 'recurring_interval': 1, } + cls.section_template_vals = { + 'display_type': 'line_section', + 'name': 'Test section', + } cls.template_vals = { 'name': 'Test Contract Template', - 'contract_line_ids': [(0, 0, cls.line_template_vals)], + 'contract_line_ids': [ + (0, 0, cls.section_template_vals), + (0, 0, cls.line_template_vals), + ], } cls.template = cls.env['contract.template'].create( cls.template_vals @@ -403,6 +410,14 @@ class TestContract(TestContractBase): self.contract._onchange_contract_template_id() res = { 'contract_line_ids': [ + ( + 0, + 0, + { + 'display_type': 'line_section', + 'name': 'Test section', + } + ), ( 0, 0, @@ -434,15 +449,19 @@ class TestContract(TestContractBase): ) self.contract.contract_template_id = self.template self.contract._onchange_contract_template_id() - self.assertEqual(len(self.contract.contract_line_ids), 1) - - 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: - pass - self.assertEqual(test_value, value) + self.assertEqual(len(self.contract.contract_line_ids), 2) + for index, vals in [ + (0, self.section_template_vals), + (1, self.line_template_vals) + ]: + contract_line = self.contract.contract_line_ids[index] + for key, value in vals.items(): + test_value = contract_line[key] + try: + test_value = test_value.id + except AttributeError: + pass + self.assertEqual(test_value, value) def test_send_mail_contract(self): result = self.contract.action_contract_send() @@ -2098,6 +2117,12 @@ class TestContract(TestContractBase): 'is_canceled': True, } ) + # section + lines |= self.env['contract.line'].create({ + "contract_id": self.contract.id, + "display_type": "line_section", + "name": "Test section", + }) states = [ 'upcoming', 'in-progress', @@ -2105,48 +2130,29 @@ class TestContract(TestContractBase): 'upcoming-close', 'closed', 'canceled', + False, ] self.assertEqual(set(lines.mapped('state')), set(states)) + # Test search method 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)] - ) + lines = self.env['contract.line'].search([('state', '=', state)]) + self.assertTrue(lines, state) + self.assertTrue(state in lines.mapped('state'), state) + lines = self.env['contract.line'].search([('state', '!=', state)]) + self.assertFalse(state in lines.mapped('state'), 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', [])] - ) + 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.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)] - ) + 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']), - ) + state2 = ['upcoming', 'in-progress'] + lines = self.env['contract.line'].search([('state', 'not in', state2)]) + self.assertEqual(set(lines.mapped('state')), set(states) - set(state2)) def test_check_auto_renew_contract_line_with_successor(self): """ @@ -2331,6 +2337,12 @@ class TestContract(TestContractBase): ) self.assertFalse(self.acct_line.create_invoice_visibility) self.assertFalse(self.contract.create_invoice_visibility) + section = self.env['contract.line'].create({ + "contract_id": self.contract.id, + "display_type": "line_section", + "name": "Test section", + }) + self.assertFalse(section.create_invoice_visibility) def test_invoice_contract_without_lines(self): self.contract.contract_line_ids.cancel() diff --git a/contract/views/abstract_contract_line.xml b/contract/views/abstract_contract_line.xml index 288511304..d4c7d3d02 100644 --- a/contract/views/abstract_contract_line.xml +++ b/contract/views/abstract_contract_line.xml @@ -7,24 +7,38 @@ contract.abstract.contract.line
-
+ +
- - - - - + + + + - + +