diff --git a/odex25_donation/ensan_donation_request/__manifest__.py b/odex25_donation/ensan_donation_request/__manifest__.py index 4c8407814..bcd078211 100644 --- a/odex25_donation/ensan_donation_request/__manifest__.py +++ b/odex25_donation/ensan_donation_request/__manifest__.py @@ -12,6 +12,7 @@ 'data/sequence.xml', 'data/donation_stage_data.xml', 'data/donation_priority_data.xml', + 'data/donation_recurring_cron.xml', 'views/donation_request_views.xml', 'views/donation_stage_views.xml', 'views/donation_priority_views.xml', @@ -19,6 +20,7 @@ 'views/res_config_settings_views.xml', 'report/campaign_report.xml', 'views/product_public_category.xml', + 'views/donation_recurring_views.xml', 'views/menus.xml', ], 'application': True, diff --git a/odex25_donation/ensan_donation_request/data/donation_recurring_cron.xml b/odex25_donation/ensan_donation_request/data/donation_recurring_cron.xml new file mode 100644 index 000000000..fd20a45cf --- /dev/null +++ b/odex25_donation/ensan_donation_request/data/donation_recurring_cron.xml @@ -0,0 +1,15 @@ + + + cron_recurring_create_invoice() + + Process Recurring Donations + + code + model.cron_recurring_create_donations() + + 1 + days + -1 + + + diff --git a/odex25_donation/ensan_donation_request/data/sequence.xml b/odex25_donation/ensan_donation_request/data/sequence.xml index 115ab9d84..f364cf083 100644 --- a/odex25_donation/ensan_donation_request/data/sequence.xml +++ b/odex25_donation/ensan_donation_request/data/sequence.xml @@ -7,4 +7,11 @@ 5 + + Donation Recurring Sequence + donation.recurring + DR/ + 5 + + diff --git a/odex25_donation/ensan_donation_request/models/__init__.py b/odex25_donation/ensan_donation_request/models/__init__.py index 751ef34c6..0b9fcf24f 100644 --- a/odex25_donation/ensan_donation_request/models/__init__.py +++ b/odex25_donation/ensan_donation_request/models/__init__.py @@ -1,4 +1,6 @@ from . import donation_request +from . import donation_recurring +from . import sale_order from . import donation_stage from . import donation_priority from . import product_template diff --git a/odex25_donation/ensan_donation_request/models/donation_recurring.py b/odex25_donation/ensan_donation_request/models/donation_recurring.py new file mode 100644 index 000000000..4d79ff1dc --- /dev/null +++ b/odex25_donation/ensan_donation_request/models/donation_recurring.py @@ -0,0 +1,366 @@ +from odoo import models, fields, api, _ +from odoo.exceptions import ValidationError,UserError +from dateutil.relativedelta import relativedelta + + +class DonationRecurring(models.Model): + _name = "donation.recurring" + _description = "Donation Recurring" + _rec_name = "name" + _inherit = ['mail.thread', 'mail.activity.mixin'] + + name = fields.Char(string="Name", required=True, tracking=True, copy=False, default=_('New')) + partner_id = fields.Many2one(comodel_name="res.partner", required=True, tracking=True) + partner_mobile = fields.Char(string="Mobile Number", related='partner_id.mobile') + currency_id = fields.Many2one('res.currency', related='partner_id.currency_id', readonly=True) + total_amount = fields.Monetary(string="Total Amount", compute='_compute_total_amount', store=True, + currency_field='currency_id', tracking=True, + help="Total amount") + date_start = fields.Date(string="Date Start", default=lambda self: fields.Date.context_today(self), tracking=True) + date_end = fields.Date(string="Date End", tracking=True) + recurring_next_date = fields.Date( + string="Next Donation Date", tracking=True, + help="The date when the next donation will be processed.", default=lambda self: fields.Date.context_today(self) + ) + recurring_interval = fields.Integer( + string="Repeat Every", + default=1, + required=True, tracking=True + ) + frequency = fields.Selection( + [('daily', 'Day(s)'), + ('weekly', 'Week(s)'), + ('monthly', 'Month(s)'), + ], + string='Recurrence Frequency', + default='monthly', + required=True, + help='How often the donation should recur.', tracking=True + ) + active = fields.Boolean(default=True) + state = fields.Selection([ + ('active', 'Active'), + ('paused', 'Paused'), + ('cancel', 'Cancelled'), + ], default='active', string='Status', tracking=True) + recurring_line_ids = fields.One2many( + 'donation.recurring.line', + 'recurring_id', + string="Donation Lines" + ) + sale_order_ids = fields.One2many( + 'sale.order', 'donation_recurring_id', + string="Related Sale Orders" + ) + sale_order_count = fields.Integer(string="Sale Orders", compute="_compute_sale_order_count", store=True) + invoice_count = fields.Integer(string="Invoices", compute="_compute_invoice_count", store=True) + + @api.depends('sale_order_ids') + def _compute_sale_order_count(self): + for rec in self: + rec.sale_order_count = len(rec.sale_order_ids) + + @api.depends('sale_order_ids.invoice_ids') + def _compute_invoice_count(self): + for rec in self: + invoices = rec.sale_order_ids.mapped('invoice_ids') + rec.invoice_count = len(invoices) + + def action_view_sale_orders(self): + self.ensure_one() + sale_orders = self.env['sale.order'].search([('donation_recurring_id', '=', self.id)]) + return { + 'type': 'ir.actions.act_window', + 'name': _('Sales Orders'), + 'view_mode': 'tree,form', + 'res_model': 'sale.order', + 'domain': [('id', 'in', sale_orders.ids)], + 'context': {'default_partner_id': self.partner_id.id, 'create': False, + 'edit': False, }, + } + + def action_view_invoices(self): + self.ensure_one() + invoices = self.sale_order_ids.mapped('invoice_ids') + return { + 'type': 'ir.actions.act_window', + 'name': _('Invoices'), + 'view_mode': 'tree,form', + 'res_model': 'account.move', + 'domain': [('id', 'in', invoices.ids)], + 'context': { + 'default_move_type': 'out_invoice', + 'create': False, + 'edit': False, + }, + } + + @api.depends('recurring_line_ids', 'recurring_line_ids.total_amount') + def _compute_total_amount(self): + for rec in self: + rec.total_amount = sum(line.total_amount for line in rec.recurring_line_ids) + + @api.onchange('recurring_interval', 'frequency') + def _onchange_frequency_or_interval(self): + for rec in self: + last_line = rec.recurring_line_ids.sorted('date')[-1] if rec.recurring_line_ids else None + if last_line: + interval = rec.recurring_interval or 1 + if rec.frequency == 'daily': + rec.recurring_next_date = last_line.date + relativedelta(days=interval) + elif rec.frequency == 'weekly': + rec.recurring_next_date = last_line.date + relativedelta(weeks=interval) + elif rec.frequency == 'monthly': + rec.recurring_next_date = last_line.date + relativedelta(months=interval) + + def _create_donation_line(self): + for rec in self: + last_line = rec.recurring_line_ids.sorted('date')[-1] if rec.recurring_line_ids else None + if not last_line: + continue + + new_line = self.env['donation.recurring.line'].create({ + 'recurring_id': rec.id, + 'product_id': last_line.product_id.id, + 'quantity': last_line.quantity, + 'price_unit': last_line.price_unit, + 'date': fields.Date.context_today(self), + }) + return new_line + + def _create_sale_order(self, line): + self.ensure_one() + + if not self.partner_id or not line or line.sale_order_id: + return + + # 1. Prepare Sale Order vals + sale_vals = { + 'partner_id': self.partner_id.id, + 'currency_id': self.currency_id.id, + 'date_order': line.date, + 'donation_recurring_id': self.id, + } + + # 2. Simulate sale.order with .new() and trigger onchanges + sale_draft = self.env['sale.order'].new(sale_vals) + sale_draft.onchange_partner_id() + + # 3. Convert to create dict + sale_final_vals = sale_draft._convert_to_write(sale_draft._cache) + sale_order = self.env['sale.order'].sudo().create(sale_final_vals) + + # 4. Prepare Sale Order Line vals + line_vals = { + 'order_id': sale_order.id, + 'product_id': line.product_id.id, + 'product_uom_qty': line.quantity, + 'qty_invoiced': line.quantity, + 'qty_delivered': line.quantity, + 'price_unit': line.price_unit, + 'name': line.product_id.name or 'Donation Item', + } + + # 5. Simulate sale.order.line with .new() and trigger onchanges + line_draft = self.env['sale.order.line'].new(line_vals) + + # 6. Convert to create dict + line_final_vals = line_draft._convert_to_write(line_draft._cache) + self.env['sale.order.line'].sudo().create(line_final_vals) + + # 7. Confirm the order + sale_order.action_confirm() + + # 8. Link the recurring line + line.sale_order_id = sale_order.id + + self.message_post( + body=_("โœ… Sale Order %s created for donation dated %s.") % (sale_order.name, line.date) + ) + + return sale_order + + # def process_delivery_from_order(self, order, quantity_done=None): + # picking = order.picking_ids.filtered(lambda p: p.state not in ('done', 'cancel')) + # if not picking: + # return False + # + # for move in picking.move_lines: + # move.quantity_done = quantity_done or move.product_uom_qty + # + # result = picking.button_validate() + # if isinstance(result, dict) and result.get('res_model'): + # wizard = self.env[result['res_model']].browse(result['res_id']) + # wizard.process() + # + # self.message_post(body=_("๐Ÿ“ฆ Delivery validated for Sale Order %s.") % order.name) + # return picking + + def create_invoice_from_order(self, order): + invoice = order._create_invoices() + invoice.action_post() + self.message_post(body=_("๐Ÿงพ Invoice %s posted for Sale Order %s.") % (invoice.name, order.name)) + return invoice + + def action_pause(self): + for record in self: + record.state = 'paused' + template = self.env.company.donation_recurring_paused_sms_template_id + if not template: + raise ValidationError( + _("โš ๏ธ SMS template for 'Paused Recurring Donation' is not configured in Company settings.")) + record._message_sms_with_template( + template=template, + partner_ids=record.partner_id.ids, + put_in_queue=True + ) + + def action_resume(self): + today = fields.Date.context_today(self) + for record in self: + if record.state != 'active': + old_date = record.recurring_next_date + record.state = 'active' + + if old_date and old_date < today: + record.recurring_next_date = today + record.message_post( + body=_( + "๐Ÿ” Recurring profile resumed. " + "Next donation date was in the past (%s) and has been reset to today (%s)." + ) % (old_date, today) + ) + else: + record.message_post( + body=_("๐Ÿ” Recurring profile resumed. Next donation scheduled on %s.") % old_date + ) + template = self.env.company.donation_recurring_resumed_sms_template_id + if not template: + raise ValidationError( + _("โš ๏ธ SMS template for 'Resumed Recurring Donation' is not configured in Company settings.") + ) + record._message_sms_with_template( + template=template, + partner_ids=record.partner_id.ids, + put_in_queue=True + ) + + def action_cancel(self): + for record in self: + record.state = 'cancel' + record.active = False + + def action_reset_to_active(self): + for record in self: + record.state = 'active' + + @api.model + def cron_recurring_create_donations(self, date_ref=None): + return self._cron_recurring_create(date_ref) + + @api.model + def _cron_recurring_create(self, date_ref=None): + if not date_ref: + date_ref = fields.Date.context_today(self) + domain = self._get_donations_to_process_domain(date_ref) + + records = self.search(domain) + for rec in records: + try: + unprocessed_lines = rec.recurring_line_ids.filtered(lambda l: not l.sale_order_id) + if unprocessed_lines: + for line in unprocessed_lines: + order = rec._create_sale_order(line) + if order: + rec.create_invoice_from_order(order) + new_line = rec._create_donation_line() + if not new_line: + continue + order = rec._create_sale_order(new_line) + if order: + rec.create_invoice_from_order(order) + rec._advance_next_date() + except Exception as e: + rec.message_post(body=_("โ›” Unexpected error:
%s
") % str(e)) + continue + + return True + + @api.model + def _get_donations_to_process_domain(self, date_ref=None): + if not date_ref: + date_ref = fields.Date.context_today(self) + return [ + ('recurring_next_date', '<=', date_ref), + ('state', '=', 'active'), + ('active', '=', True), + ] + + def _advance_next_date(self): + for rec in self: + interval = rec.recurring_interval or 1 + if rec.frequency == 'daily': + rec.recurring_next_date += relativedelta(days=interval) + elif rec.frequency == 'weekly': + rec.recurring_next_date += relativedelta(weeks=interval) + elif rec.frequency == 'monthly': + rec.recurring_next_date += relativedelta(months=interval) + + @api.model + def create(self, vals): + if vals.get('name', _('New')) == _('New'): + vals['name'] = self.env['ir.sequence'].next_by_code('donation.recurring') or _('New') + res = super(DonationRecurring, self).create(vals) + for line in res.recurring_line_ids: + order = res._create_sale_order(line) + if not order: + continue + res.sudo().create_invoice_from_order(order) + res._advance_next_date() + template = self.env.company.donation_recurring_created_sms_template_id + if not template: + raise ValidationError(_("โš ๏ธ SMS template for 'Send When Created' is not configured in Company settings.")) + + res._message_sms_with_template( + template=template, + partner_ids=res.partner_id.ids, + put_in_queue=True + ) + return res + + def unlink(self): + for rec in self: + if rec.state == 'active': + raise UserError( + _("โŒ Cannot delete a profile while it is in 'Active' state. Please cancel or pause it first.")) + + if rec.sale_order_ids: + raise UserError(_("โŒ Cannot delete a profile that has related Sale Orders. Archive it instead.")) + template = self.env.company.donation_recurring_deleted_sms_template_id + if template: + rec._message_sms_with_template( + template=template, + partner_ids=rec.partner_id.ids, + put_in_queue=True + ) + return super(DonationRecurring, self).unlink() + + +class DonationRecurringLine(models.Model): + _name = 'donation.recurring.line' + _description = 'Recurring Donation Line' + + recurring_id = fields.Many2one('donation.recurring', string="Recurring Profile") + product_id = fields.Many2one('product.product', string="Product", domain=[('is_recurring_donation', '=', True)], + required=True) + quantity = fields.Float(string="Quantity", default=1.0, digits='Product Unit of Measure') + price_unit = fields.Float(string="Unit Price", digits='Product Price') + total_amount = fields.Monetary(string="Total Amount", compute="_compute_total_amount", store=True) + currency_id = fields.Many2one('res.currency', related='recurring_id.partner_id.currency_id', readonly=True) + date = fields.Date(string="Donation Date", default=fields.Date.context_today) + sale_order_id = fields.Many2one('sale.order', string="Related Sale Order", readonly=True) + + @api.depends('quantity', 'price_unit') + def _compute_total_amount(self): + for line in self: + line.total_amount = line.quantity * line.price_unit diff --git a/odex25_donation/ensan_donation_request/models/donation_request.py b/odex25_donation/ensan_donation_request/models/donation_request.py index e857a86fc..81481540e 100644 --- a/odex25_donation/ensan_donation_request/models/donation_request.py +++ b/odex25_donation/ensan_donation_request/models/donation_request.py @@ -57,7 +57,7 @@ class DonationRequest(models.Model): @api.depends('remaining_amount') def _compute_stage_id(self): for rec in self: - if rec.remaining_amount <= 0: + if rec.product_id and rec.remaining_amount <= 0: new_stage = self.env.ref('ensan_donation_request.stage_done') else: new_stage = rec.stage_id diff --git a/odex25_donation/ensan_donation_request/models/product_template.py b/odex25_donation/ensan_donation_request/models/product_template.py index 4f537cb22..9ce171f58 100644 --- a/odex25_donation/ensan_donation_request/models/product_template.py +++ b/odex25_donation/ensan_donation_request/models/product_template.py @@ -5,3 +5,7 @@ class ProductTemplate(models.Model): donation_request_id = fields.Many2one('donation.request', ondelete='restrict') hide_from_shop_front = fields.Boolean() + is_recurring_donation = fields.Boolean( + string='Is Recurring Donation Product', + help='Enable this if the product can be used in recurring donations.' + ) diff --git a/odex25_donation/ensan_donation_request/models/res_config_settings.py b/odex25_donation/ensan_donation_request/models/res_config_settings.py index 45f889f20..5d98182c8 100644 --- a/odex25_donation/ensan_donation_request/models/res_config_settings.py +++ b/odex25_donation/ensan_donation_request/models/res_config_settings.py @@ -1,12 +1,39 @@ from odoo import models, fields + class ResConfigSettings(models.TransientModel): _inherit = 'res.config.settings' - donation_request_confirmation_sms_template_id = fields.Many2one('sms.template', required=True, related='company_id.donation_request_confirmation_sms_template_id', readonly=False) + donation_request_confirmation_sms_template_id = fields.Many2one('sms.template', required=True, + related='company_id.donation_request_confirmation_sms_template_id', + readonly=False) + donation_recurring_created_sms_template_id = fields.Many2one( + 'sms.template', related='company_id.donation_recurring_created_sms_template_id', readonly=False + ) + donation_recurring_paused_sms_template_id = fields.Many2one( + 'sms.template', related='company_id.donation_recurring_paused_sms_template_id', readonly=False + ) + donation_recurring_resumed_sms_template_id = fields.Many2one( + 'sms.template', related='company_id.donation_recurring_resumed_sms_template_id', readonly=False + ) + donation_recurring_cancelled_sms_template_id = fields.Many2one( + 'sms.template', related='company_id.donation_recurring_cancelled_sms_template_id', readonly=False + ) + donation_recurring_charged_sms_template_id = fields.Many2one( + 'sms.template', related='company_id.donation_recurring_charged_sms_template_id', readonly=False + ) + donation_recurring_deleted_sms_template_id = fields.Many2one( + 'sms.template', related='company_id.donation_recurring_deleted_sms_template_id', readonly=False + ) class ResCompany(models.Model): _inherit = 'res.company' - donation_request_confirmation_sms_template_id = fields.Many2one('sms.template', required=True) \ No newline at end of file + donation_request_confirmation_sms_template_id = fields.Many2one('sms.template', required=True) + donation_recurring_created_sms_template_id = fields.Many2one('sms.template') + donation_recurring_paused_sms_template_id = fields.Many2one('sms.template') + donation_recurring_resumed_sms_template_id = fields.Many2one('sms.template') + donation_recurring_cancelled_sms_template_id = fields.Many2one('sms.template') + donation_recurring_charged_sms_template_id = fields.Many2one('sms.template') + donation_recurring_deleted_sms_template_id = fields.Many2one('sms.template') diff --git a/odex25_donation/ensan_donation_request/models/sale_order.py b/odex25_donation/ensan_donation_request/models/sale_order.py new file mode 100644 index 000000000..cf1a8c5c0 --- /dev/null +++ b/odex25_donation/ensan_donation_request/models/sale_order.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +from odoo import models, fields, api,_ + +class SaleOrder(models.Model): + _inherit = 'sale.order' + + donation_recurring_id = fields.Many2one('donation.recurring', string="Recurring Donation") \ No newline at end of file diff --git a/odex25_donation/ensan_donation_request/security/ir.model.access.csv b/odex25_donation/ensan_donation_request/security/ir.model.access.csv index 7444341e0..049f04f1a 100644 --- a/odex25_donation/ensan_donation_request/security/ir.model.access.csv +++ b/odex25_donation/ensan_donation_request/security/ir.model.access.csv @@ -1,5 +1,7 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_donation_request,access_donation_request,model_donation_request,base.group_user,1,1,1,1 +access_donation_recurring,access_donation_recurring,model_donation_recurring,base.group_user,1,1,1,1 +access_donation_recurring_line,access_donation_recurring_line,model_donation_recurring_line,base.group_user,1,1,1,1 access_donation_request_public,access_donation_request_public,model_donation_request,base.group_public,1,0,0,0 access_donation_request_portal,access_donation_request_portal,model_donation_request,base.group_portal,1,0,0,0 access_donation_stage,access_donation_stage,model_donation_stage,base.group_user,1,1,1,1 diff --git a/odex25_donation/ensan_donation_request/views/donation_recurring_views.xml b/odex25_donation/ensan_donation_request/views/donation_recurring_views.xml new file mode 100644 index 000000000..1798b5bf4 --- /dev/null +++ b/odex25_donation/ensan_donation_request/views/donation_recurring_views.xml @@ -0,0 +1,193 @@ + + + + donation.recurring.form + donation.recurring + +
+ +
+
+ +
+ + + +
+ +
+

+ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + +
+ + +
+
+ + + donation.recurring.kanban + donation.recurring + + + + + + + + + + + + +
+
+ + + +
+ +
+ + +
+ Amount: + +
+
+ Remaining: + +
+
+
+
+
+
+
+
+ + + donation.recurring.tree + donation.recurring + + + + + + + + + + + + + + + Recurring Donations + donation.recurring + tree,kanban,form + +
\ No newline at end of file diff --git a/odex25_donation/ensan_donation_request/views/menus.xml b/odex25_donation/ensan_donation_request/views/menus.xml index 61b2fad2b..5e24a1de4 100644 --- a/odex25_donation/ensan_donation_request/views/menus.xml +++ b/odex25_donation/ensan_donation_request/views/menus.xml @@ -1,31 +1,36 @@ + name="Donation Requests" + web_icon="ensan_donation_request,static/description/icon.png" + groups="ensan_donation_request.group_donations_user" + sequence="10"> + id="menu_donation_request" + name="Requests" + action="action_donation_request" + sequence="10"/> + id="menu_donation_recurring" + name="Recurring Donations" + action="action_donation_recurring" + sequence="20"/> + + action="action_donation_request_config" sequence="0"/> + action="action_donation_stage" sequence="10"/> + action="action_donation_priority" sequence="30"/> + id="public_category_donation_menu" + name="Categories" + action="product_public_category_donation_action" + sequence="40"/> diff --git a/odex25_donation/ensan_donation_request/views/product_template_views.xml b/odex25_donation/ensan_donation_request/views/product_template_views.xml index 6db24224c..b6f62a7cf 100644 --- a/odex25_donation/ensan_donation_request/views/product_template_views.xml +++ b/odex25_donation/ensan_donation_request/views/product_template_views.xml @@ -7,13 +7,16 @@ +
+ +
- +
-
diff --git a/odex25_donation/ensan_donation_request/views/res_config_settings_views.xml b/odex25_donation/ensan_donation_request/views/res_config_settings_views.xml index 4d3b0a15d..4f441f1a2 100644 --- a/odex25_donation/ensan_donation_request/views/res_config_settings_views.xml +++ b/odex25_donation/ensan_donation_request/views/res_config_settings_views.xml @@ -4,38 +4,90 @@ res.config.settings.view.form.inherit.donation res.config.settings - - + + - +
+ data-key="d_requests" groups="ensan_donation_request.group_donations_manager">

SMS

+ name="donation_sms_settings">
-
+
Confirmation SMS Template + title="Values set here are company-specific." + aria-label="Values set here are company-specific." + groups="base.group_multi_company" role="img"/>
Template used to send confirmation sms to donation request submitter.
+
+ +

Recurring Donation SMS

+
+
+
+
+ SMS Templates +
Templates used for SMS notifications.
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
diff --git a/odex25_donation/ensan_sale_management/models/product.py b/odex25_donation/ensan_sale_management/models/product.py index f54bc8d42..624c7da1b 100644 --- a/odex25_donation/ensan_sale_management/models/product.py +++ b/odex25_donation/ensan_sale_management/models/product.py @@ -68,7 +68,7 @@ class ProductTemplate_Inherit(models.Model): # logger.info("Done Percentage Change %s",self.done_percentage) # if self.done_percentage and self.done_percentage == 100: # self.sale_ok = False - + @api.depends('end_donation_date') def _get_date_remaining(self): for rec in self: @@ -91,13 +91,15 @@ class ProductTemplate_Inherit(models.Model): @api.depends('target_amount', 'donated_amount') def _get_remaining_amount(self): for rec in self: - rec.remaining_amount = rec.target_amount - rec.donated_amount + remaining = rec.target_amount - rec.donated_amount + rec.remaining_amount = remaining if remaining > 0 else 0.0 @api.depends('target_amount', 'donated_amount') def get_done_percentage(self): for rec in self: if rec.target_amount: - rec.done_percentage = (rec.donated_amount / rec.target_amount) * 100 + raw_percentage = (rec.donated_amount / rec.target_amount) * 100 + rec.done_percentage = min(raw_percentage, 100) # When the product is 100% complete the product is deleted if rec.done_percentage >= 100 and rec.target_amount != 1: rec.sale_ok = False @@ -182,12 +184,15 @@ class ProductTemplate_Inherit(models.Model): for product in products: donated_amount = donated_amounts.get(product.id, 0) - new_remaining_amount = product.target_amount - donated_amount + # new_remaining_amount = product.target_amount - donated_amount + new_remaining_amount = max(product.target_amount - donated_amount, 0.0) product.remaining_amount = new_remaining_amount product.sale_ok = not (new_remaining_amount <= 0 and product.target_amount != 0) - product.done_percentage = (donated_amount / product.target_amount) * 100 if product.target_amount else 0 - + # product.done_percentage = (donated_amount / product.target_amount) * 100 if product.target_amount else 0 + product.done_percentage = min((donated_amount / product.target_amount) * 100, + 100) if product.target_amount else 0 + class ProductProduct_Inherit(models.Model): _inherit = 'product.product'