from odoo import models, fields, api, _ from odoo.exceptions import UserError from datetime import datetime, timedelta from dateutil.relativedelta import relativedelta class SubscriptionTemplate(models.Model): _name = 'subscription.template' _description = 'Subscription Template' _order = 'sequence, name' name = fields.Char(string='Template Name', required=True) sequence = fields.Integer(string='Sequence', default=10) interval_number = fields.Integer(string='Invoice Every', default=1, required=True) interval_type = fields.Selection([ ('days', 'Days'), ('weeks', 'Weeks'), ('months', 'Months'), ('years', 'Years'), ], string='Interval Type', default='months', required=True) active = fields.Boolean(string='Active', default=True) product_id = fields.Many2one('product.product', string='Product', required=True) quantity = fields.Float(string='Quantity', default=1.0) # FIX: Changed to Float because list_price is Float price_unit = fields.Float(string='Price', related='product_id.list_price') currency_id = fields.Many2one(related='product_id.currency_id') description = fields.Text(string='Description') def name_get(self): result = [] for record in self: name = f"{record.name} ({record.interval_number} {record.interval_type})" result.append((record.id, name)) return result class Subscription(models.Model): _name = 'subscription.subscription' _description = 'Subscription' _inherit = ['mail.thread', 'mail.activity.mixin'] _order = 'create_date desc' name = fields.Char(string='Reference', required=True, copy=False, readonly=True, default='New') template_id = fields.Many2one('subscription.template', string='Subscription Template', required=True) partner_id = fields.Many2one('res.partner', string='Customer', required=True) company_id = fields.Many2one('res.company', string='Company', required=True, default=lambda self: self.env.company) # Dates start_date = fields.Date(string='Start Date', required=True, default=fields.Date.context_today) next_invoice_date = fields.Date(string='Next Invoice Date', required=True, copy=False) end_date = fields.Date(string='End Date') last_invoice_date = fields.Date(string='Last Invoice Date', copy=False) # Status state = fields.Selection([ ('draft', 'Draft'), ('active', 'Active'), ('closed', 'Closed'), ('cancelled', 'Cancelled'), ], string='Status', default='draft', tracking=True, copy=False) # Pricing currency_id = fields.Many2one(related='template_id.currency_id') # FIX: Changed to Float to match template price_unit = fields.Float(related='template_id.price_unit') # FIX: Changed to Float quantity = fields.Float(related='template_id.quantity') recurring_total = fields.Float(string='Recurring Price', compute='_compute_recurring_total', store=True) # Invoicing invoice_count = fields.Integer(string='Invoice Count', compute='_compute_invoice_count') invoice_ids = fields.One2many('account.move', 'subscription_id', string='Invoices') # Additional info note = fields.Text(string='Notes') active = fields.Boolean(string='Active', default=True) @api.model def create(self, vals): if vals.get('name', 'New') == 'New': vals['name'] = self.env['ir.sequence'].next_by_code('subscription.subscription') or 'New' return super().create(vals) def _compute_recurring_total(self): for sub in self: sub.recurring_total = sub.price_unit * sub.quantity def _compute_invoice_count(self): for sub in self: sub.invoice_count = len(sub.invoice_ids) def action_start_subscription(self): self.write({ 'state': 'active', 'next_invoice_date': self.start_date }) return True def action_close_subscription(self): self.write({'state': 'closed'}) return True def action_cancel_subscription(self): self.write({'state': 'cancelled'}) return True def action_draft_subscription(self): self.write({'state': 'draft'}) return True def action_view_invoices(self): self.ensure_one() return { 'name': _('Invoices'), 'type': 'ir.actions.act_window', 'res_model': 'account.move', 'view_mode': 'tree,form', 'domain': [('subscription_id', '=', self.id)], 'context': {'default_subscription_id': self.id} } def generate_invoice(self): """Generate invoice for active subscriptions""" for sub in self: if sub.state != 'active': continue if sub.end_date and sub.next_invoice_date > sub.end_date: sub.write({'state': 'closed'}) continue # Create invoice invoice_vals = { 'move_type': 'out_invoice', 'partner_id': sub.partner_id.id, 'company_id': sub.company_id.id, 'currency_id': sub.currency_id.id, 'subscription_id': sub.id, 'invoice_date': sub.next_invoice_date, 'invoice_origin': f"Subscription: {sub.name}", 'invoice_line_ids': [(0, 0, { 'product_id': sub.template_id.product_id.id, 'name': sub.template_id.description or sub.template_id.product_id.name, 'quantity': sub.quantity, 'price_unit': sub.price_unit, 'account_id': sub.template_id.product_id.property_account_income_id.id or sub.template_id.product_id.categ_id.property_account_income_categ_id.id, })] } invoice = self.env['account.move'].create(invoice_vals) # Update subscription next_date = sub.next_invoice_date if sub.template_id.interval_type == 'days': next_date += timedelta(days=sub.template_id.interval_number) elif sub.template_id.interval_type == 'weeks': next_date += timedelta(weeks=sub.template_id.interval_number) elif sub.template_id.interval_type == 'months': next_date += relativedelta(months=sub.template_id.interval_number) elif sub.template_id.interval_type == 'years': next_date += relativedelta(years=sub.template_id.interval_number) sub.write({ 'last_invoice_date': sub.next_invoice_date, 'next_invoice_date': next_date, }) return True class AccountMove(models.Model): _inherit = 'account.move' subscription_id = fields.Many2one('subscription.subscription', string='Subscription', ondelete='set null', copy=False)