From a968b64e8028f4b17f86dac885773983dd0b9172 Mon Sep 17 00:00:00 2001 From: Marc Durepos Date: Thu, 5 Dec 2024 13:16:27 -0500 Subject: [PATCH] price_update_notifications module added --- price_update_notifications/__init__.py | 2 + price_update_notifications/__manifest__.py | 39 ++++++ .../data/email_templates.xml | 63 +++++++++ .../data/ir_cron_data.xml | 18 +++ price_update_notifications/models/__init__.py | 1 + .../models/price_notice_mailing.py | 132 ++++++++++++++++++ .../security/ir.model.access.csv | 7 + .../views/price_notice_mailing_views.xml | 97 +++++++++++++ price_update_notifications/wizard/__init__.py | 1 + .../wizard/send_price_update_notice.py | 78 +++++++++++ .../wizard/send_price_update_notice_views.xml | 36 +++++ 11 files changed, 474 insertions(+) create mode 100644 price_update_notifications/__init__.py create mode 100644 price_update_notifications/__manifest__.py create mode 100644 price_update_notifications/data/email_templates.xml create mode 100644 price_update_notifications/data/ir_cron_data.xml create mode 100644 price_update_notifications/models/__init__.py create mode 100644 price_update_notifications/models/price_notice_mailing.py create mode 100644 price_update_notifications/security/ir.model.access.csv create mode 100644 price_update_notifications/views/price_notice_mailing_views.xml create mode 100644 price_update_notifications/wizard/__init__.py create mode 100644 price_update_notifications/wizard/send_price_update_notice.py create mode 100644 price_update_notifications/wizard/send_price_update_notice_views.xml diff --git a/price_update_notifications/__init__.py b/price_update_notifications/__init__.py new file mode 100644 index 0000000..9b42961 --- /dev/null +++ b/price_update_notifications/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import wizard diff --git a/price_update_notifications/__manifest__.py b/price_update_notifications/__manifest__.py new file mode 100644 index 0000000..84b7f04 --- /dev/null +++ b/price_update_notifications/__manifest__.py @@ -0,0 +1,39 @@ +# +# Bemade Inc. +# +# Copyright (C) 2023-June Bemade Inc. (). +# Author: Marc Durepos (Contact : marc@bemade.org) +# +# This program is under the terms of the GNU Lesser General Public License, +# version 3. +# +# For full license details, see https://www.gnu.org/licenses/lgpl-3.0.en.html. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. +# +{ + "name": "Price Update Notifications", + "version": "17.0.1.0.0", + "summary": "Send a price update notification to clients", + "category": "Sale", + "author": "Bemade Inc.", + "website": "http://www.bemade.org", + "license": "LGPL-3", + "depends": ["sale_management", "sales_team", "mail"], + "data": [ + "security/ir.model.access.csv", + "data/ir_cron_data.xml", + "data/email_templates.xml", + "views/price_notice_mailing_views.xml", + "wizard/send_price_update_notice_views.xml", + ], + "assets": {}, + "installable": True, + "auto_install": False, +} diff --git a/price_update_notifications/data/email_templates.xml b/price_update_notifications/data/email_templates.xml new file mode 100644 index 0000000..d9cc0b6 --- /dev/null +++ b/price_update_notifications/data/email_templates.xml @@ -0,0 +1,63 @@ + + + + Product Price Update + + + {{ object.partner_id.company_id.name }} Price Update + Notification - + Effective {{ object.effective_date.strftime("%Y-%m-%d") }} + + Sent using the send price update + letter action on partners. + + + {{ object.partner_id.company_id.email}} + + {{ object.email }} + {{ object.partner_id.lang }} + +
+

Dear ,

+

We have recently updated the pricing for the following + products you have purchased or requested pricing for in the past + year. Please take note that these prices will be effective as of + . +

+
+ +
+ + + + + + + + + + + + + + + + + +
ProductUnitUnit PriceCurrency
+ + + + + + + +
+

Best regards,

+

The team.

+
+
+
+
\ No newline at end of file diff --git a/price_update_notifications/data/ir_cron_data.xml b/price_update_notifications/data/ir_cron_data.xml new file mode 100644 index 0000000..38b94ab --- /dev/null +++ b/price_update_notifications/data/ir_cron_data.xml @@ -0,0 +1,18 @@ + + + + Price Update Notifications: Send + Notifications + + code + model.action_send_all() + + 1 + days + -1 + + + + \ No newline at end of file diff --git a/price_update_notifications/models/__init__.py b/price_update_notifications/models/__init__.py new file mode 100644 index 0000000..0c775fd --- /dev/null +++ b/price_update_notifications/models/__init__.py @@ -0,0 +1 @@ +from . import price_notice_mailing diff --git a/price_update_notifications/models/price_notice_mailing.py b/price_update_notifications/models/price_notice_mailing.py new file mode 100644 index 0000000..f953302 --- /dev/null +++ b/price_update_notifications/models/price_notice_mailing.py @@ -0,0 +1,132 @@ +from odoo import models, fields, api +from odoo.exceptions import ValidationError +from datetime import timedelta + + +class PriceNoticeMailing(models.Model): + _name = "price.notice.mailing" + _description = "Price Notice Mailing" + _inherit = ["mail.thread"] + + partner_id = fields.Many2one("res.partner", required=True, readonly=True) + email = fields.Char(readonly=True) + product_template_ids = fields.Many2many("product.template") + line_ids = fields.One2many( + comodel_name="price.notice.mailing.line", + inverse_name="mailing_id", + ) + effective_date = fields.Date( + required=True, + string="Effective Pricing Date", + ) + send_date = fields.Date(required=True) + sent = fields.Boolean(default=False, readonly=True) + active = fields.Boolean(default=True) + + def _compute_display_name(self): + for rec in self: + rec.display_name = f"{rec.send_date} - {rec.partner_id.name}" + + def create(self, vals_list): + res = super().create(vals_list) + for mailing in res: + email = mailing.partner_id.email + if not email: + raise ValidationError( + f"Partner email not found for price " + f"notice " + "mailing to {partner.display_name}." + ) + mailing.email = mailing.partner_id.email + mailing._create_lines() + if not mailing.line_ids: + mailing.active = False + return res + + def _create_lines(self): + line_vals = [] + for mailing in self: + products = mailing._get_purchased_products() + for product in products: + pricelist = mailing.partner_id.property_product_pricelist + price = pricelist._get_product_price( + product, + quantity=1.0, + date=mailing.effective_date, + ) + currency = ( + pricelist.currency_id + or mailing.partner_id.company_id.currency_id + or self.env.company.currency_id + ) + line_vals.append( + { + "mailing_id": mailing.id, + "product_id": product.id, + "currency_id": currency.id, + "price": price, + } + ) + self.env["price.notice.mailing.line"].create(line_vals) + + def _get_purchased_products(self): + self.ensure_one() + # Retrieve partner purchase history + so_domain = [ + ("date_order", ">=", fields.Date.today() - timedelta(days=365)), + "|", + "|", + ("partner_id", "=", self.partner_id.id), + ("partner_id", "=", self.partner_id.commercial_partner_id.id), + ( + "partner_id.commercial_partner_id", + "=", + self.partner_id.commercial_partner_id.id, + ), + ] + if self.product_template_ids: + so_domain = [ + "order_line.product_tmpl_id", + "in", + self.product_template_ids.ids, + ] + so_domain + sale_ids = self.env["sale.order"].search(so_domain) + + # Retrieve purchased products and limit to the selected product + # templates + product_domain = [ + ("id", "in", sale_ids.order_line.mapped("product_id").ids), + ] + if self.product_template_ids: + product_domain = product_domain + [ + ("product_tmpl_id", "in", self.product_template_ids.ids) + ] + product_ids = self.env["product.product"].search(product_domain) + return product_ids + + @api.model + def action_send_all(self): + due_mailings = self.search( + [("sent", "=", False), ("send_date", "<=", fields.Date.today())] + ) + due_mailings._action_send() + + def _action_send(self): + template = self.env.ref("price_update_notifications.mail_template_price_update") + for mailing in self: + template.send_mail(mailing.id) + mailing.sent = True + + +class PriceNoticeMailingLine(models.Model): + _name = "price.notice.mailing.line" + _description = "Price Notice Mailing Line" + + mailing_id = fields.Many2one( + "price.notice.mailing", + required=True, + ondelete="cascade", + ) + product_id = fields.Many2one("product.product", required=True) + currency_id = fields.Many2one("res.currency", required=True) + price = fields.Monetary(required=True) diff --git a/price_update_notifications/security/ir.model.access.csv b/price_update_notifications/security/ir.model.access.csv new file mode 100644 index 0000000..2f5226c --- /dev/null +++ b/price_update_notifications/security/ir.model.access.csv @@ -0,0 +1,7 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_wizard_price_update_notice_manager,access_wizard_price_update_notice_manager,model_wizard_price_update_notice,sales_team.group_sale_manager,1,1,1,1 +access_price_notice_mailing_manager,access_price_notice_mailing_manager,model_price_notice_mailing,sales_team.group_sale_manager,1,1,1,1 +access_price_notice_mailing_line_manager,access_price_notice_mailing_line_manager,model_price_notice_mailing_line,sales_team.group_sale_manager,1,1,1,1 +access_wizard_price_update_notice_user,access_wizard_price_update_notice_user,model_wizard_price_update_notice,base.group_user,1,0,0,0 +access_price_notice_mailing_user,access_price_notice_mailing_user,model_price_notice_mailing,base.group_user,1,0,0,0 +access_price_notice_mailing_line_user,access_price_notice_mailing_line_user,model_price_notice_mailing_line,base.group_user,1,0,0,0 \ No newline at end of file diff --git a/price_update_notifications/views/price_notice_mailing_views.xml b/price_update_notifications/views/price_notice_mailing_views.xml new file mode 100644 index 0000000..aaa97dd --- /dev/null +++ b/price_update_notifications/views/price_notice_mailing_views.xml @@ -0,0 +1,97 @@ + + + + price.notice.mailing.view.form + price.notice.mailing + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+
+
+ + price.notice.mailing.view.list + price.notice.mailing + + + + + + + + + + + + Price Notice Mailings + ir.actions.act_window + price.notice.mailing + tree,form + +

+ No mailings yet. Create one! +

+
+
+ + + Send Price Update Notice + ir.actions.act_window + wizard.price.update.notice + form + new + + + + Send Price Update Notice(s) + + + action + code + + action = { + 'type': 'ir.actions.act_window', + 'name': 'Send Price Update Notice', + 'res_model': 'wizard.price.update.notice', + 'view_mode': 'form', + 'target': 'new', + } + + + + +
\ No newline at end of file diff --git a/price_update_notifications/wizard/__init__.py b/price_update_notifications/wizard/__init__.py new file mode 100644 index 0000000..5448d5c --- /dev/null +++ b/price_update_notifications/wizard/__init__.py @@ -0,0 +1 @@ +from . import send_price_update_notice diff --git a/price_update_notifications/wizard/send_price_update_notice.py b/price_update_notifications/wizard/send_price_update_notice.py new file mode 100644 index 0000000..233b229 --- /dev/null +++ b/price_update_notifications/wizard/send_price_update_notice.py @@ -0,0 +1,78 @@ +""" Send a price update notice to selected partners """ + +from odoo import models, fields, api, _ +from odoo.exceptions import UserError +from datetime import timedelta + + +class SendPriceUpdateNotice(models.TransientModel): + _name = "wizard.price.update.notice" + _description = "Price Update Notice Wizard" + + partner_ids = fields.Many2many(comodel_name="res.partner") + product_template_ids = fields.Many2many( + string="Applicable Products", + comodel_name="product.template", + ) + pricing_date = fields.Date( + string="Effective Pricing Date", + help="The date at which the prices are valid.", + ) + send_date = fields.Date( + default=fields.Date.today() + timedelta(days=1), + ) + warning_msg = fields.Html() + + @api.depends_context("active_ids") + def default_get(self, fields): + vals = {} + if "partner_ids" in fields: + partners = self.env["res.partner"].search( + [("id", "in", self.env.context.get("active_ids"))] + ) + if no_email_partners := partners.filtered(lambda p: not p.email): + warning_msg = self._get_warning_message(no_email_partners) + else: + warning_msg = None + valid_partners = partners.filtered("email") + if not valid_partners: + raise UserError(_("No valid partners were selected.")) + vals.update( + partner_ids=partners.filtered("email").ids, + warning_msg=warning_msg, + ) + if "product_template_ids" in fields: + vals.update(product_template_ids=[]) + return vals + + def action_create(self): + self.ensure_one() + vals_list = [] + for partner in self.partner_ids: + vals_list.append( + { + "partner_id": partner.id, + "product_template_ids": self.product_template_ids.ids, + "effective_date": self.pricing_date, + "send_date": self.send_date, + } + ) + mailings = self.env["price.notice.mailing"].create(vals_list) + return { + "name": "Price Notice Mailings", + "type": "ir.actions.act_window", + "view_mode": "tree,form", + "res_model": "price.notice.mailing", + "res_id": False, + "domain": [["id", "in", mailings.ids]], + "target": "current", + } + + @api.model + def _get_warning_message(self, partner_ids): + msg = _("

The following partners do not have emails configured:

") + msg += "" + return msg diff --git a/price_update_notifications/wizard/send_price_update_notice_views.xml b/price_update_notifications/wizard/send_price_update_notice_views.xml new file mode 100644 index 0000000..04cbe41 --- /dev/null +++ b/price_update_notifications/wizard/send_price_update_notice_views.xml @@ -0,0 +1,36 @@ + + + + send.price.update.notice.view.form + wizard.price.update.notice + +
+ + + + + + + + + + + + + + + + +
+
+
+
\ No newline at end of file