# 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 ContractAbstractContractLine(models.AbstractModel): _inherit = "contract.recurrency.basic.mixin" _name = "contract.abstract.contract.line" _description = "Abstract Recurring Contract Line" product_id = fields.Many2one("product.product", string="Product") name = fields.Text(string="Description", required=True) quantity = fields.Float(default=1.0, required=True) product_uom_category_id = fields.Many2one( # Used for domain of field uom_id comodel_name="uom.category", related="product_id.uom_id.category_id", ) uom_id = fields.Many2one( comodel_name="uom.uom", string="Unit of Measure", domain="[('category_id', '=', product_uom_category_id)]", ) 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() price_unit = fields.Float( string="Unit Price", compute="_compute_price_unit", inverse="_inverse_price_unit", ) price_subtotal = fields.Float( compute="_compute_price_subtotal", digits="Account", string="Sub Total", ) discount = fields.Float( string="Discount (%)", digits="Discount", help="Discount that is applied in generated invoices." " It should be less or equal to 100", ) sequence = fields.Integer( default=10, help="Sequence of the contract line when displaying contracts", ) recurring_rule_type = fields.Selection( compute="_compute_recurring_rule_type", store=True, readonly=False, required=True, copy=True, ) recurring_invoicing_type = fields.Selection( compute="_compute_recurring_invoicing_type", store=True, readonly=False, required=True, copy=True, ) recurring_interval = fields.Integer( compute="_compute_recurring_interval", store=True, readonly=False, required=True, copy=True, ) date_start = fields.Date( compute="_compute_date_start", store=True, readonly=False, copy=True, ) last_date_invoiced = fields.Date() 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", ) display_type = fields.Selection( selection=[("line_section", "Section"), ("line_note", "Note")], default=False, help="Technical field for UX purpose.", ) note_invoicing_mode = fields.Selection( selection=[ ("with_previous_line", "With previous line"), ("with_next_line", "With next line"), ("custom", "Custom"), ], default="with_previous_line", help="Defines when the Note is invoiced:\n" "- With previous line: If the previous line can be invoiced.\n" "- With next line: If the next line can be invoiced.\n" "- Custom: Depending on the recurrence to be define.", ) is_recurring_note = fields.Boolean(compute="_compute_is_recurring_note") company_id = fields.Many2one(related="contract_id.company_id", store=True) def _set_recurrence_field(self, field): """Helper method for computed methods that gets the equivalent field in the header. We need to re-assign the original value for avoiding a missing error. """ for record in self: if record.contract_id.line_recurrence: record[field] = record[field] else: record[field] = record.contract_id[field] @api.depends("contract_id.recurring_rule_type", "contract_id.line_recurrence") def _compute_recurring_rule_type(self): self._set_recurrence_field("recurring_rule_type") @api.depends("contract_id.recurring_invoicing_type", "contract_id.line_recurrence") def _compute_recurring_invoicing_type(self): self._set_recurrence_field("recurring_invoicing_type") @api.depends("contract_id.recurring_interval", "contract_id.line_recurrence") def _compute_recurring_interval(self): self._set_recurrence_field("recurring_interval") @api.depends("contract_id.date_start", "contract_id.line_recurrence") def _compute_date_start(self): self._set_recurrence_field("date_start") # pylint: disable=missing-return @api.depends("contract_id.recurring_next_date", "contract_id.line_recurrence") def _compute_recurring_next_date(self): super()._compute_recurring_next_date() self._set_recurrence_field("recurring_next_date") @api.depends("display_type", "note_invoicing_mode") def _compute_is_recurring_note(self): for record in self: record.is_recurring_note = ( record.display_type == "line_note" and record.note_invoicing_mode == "custom" ) @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: pricelist = ( line.contract_id.pricelist_id or line.contract_id.partner_id.with_company( line.contract_id.company_id ).property_product_pricelist ) product = line.product_id.with_context( quantity=line.env.context.get( "contract_line_qty", line.quantity, ), pricelist=pricelist.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.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.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.onchange("product_id") def _onchange_product_id(self): vals = {} 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)