price_update_notifications module added

This commit is contained in:
Marc Durepos 2024-12-05 13:16:27 -05:00
parent c8bbeac919
commit a968b64e80
11 changed files with 474 additions and 0 deletions

View file

@ -0,0 +1,2 @@
from . import models
from . import wizard

View file

@ -0,0 +1,39 @@
#
# Bemade Inc.
#
# Copyright (C) 2023-June Bemade Inc. (<https://www.bemade.org>).
# 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,
}

View file

@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="mail_template_price_update" model="mail.template">
<field name="name">Product Price Update</field>
<field name="model_id"
ref="price_update_notifications.model_price_notice_mailing"/>
<field name="subject">
{{ object.partner_id.company_id.name }} Price Update
Notification -
Effective {{ object.effective_date.strftime("%Y-%m-%d") }}
</field>
<field name="description">Sent using the send price update
letter action on partners.
</field>
<field name="email_from">
{{ object.partner_id.company_id.email}}
</field>
<field name="email_to">{{ object.email }}</field>
<field name="lang">{{ object.partner_id.lang }}</field>
<field name="body_html" type="html">
<div style="margin: 0px; padding: 0px" id="intro">
<p>Dear <span t-field="object.partner_id.name"/>,</p>
<p>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
<span t-out="object.effective_date.strftime('%Y-%m-%d')"/>.
</p>
</div>
<t t-set="company"
t-value="object.partner_id.company_id or object.env.company"/>
<div id="table">
<table class="table" style="width: 100%; border-collapse: collapse; font-family: Arial, sans-serif;">
<thead style="background-color: #f2f2f2; text-align: left;">
<tr>
<th style="padding: 8px; border-bottom: 2px solid #ddd;">Product</th>
<th style="padding: 8px; border-bottom: 2px solid #ddd;">Unit</th>
<th style="padding: 8px; border-bottom: 2px solid #ddd; text-align: right;">Unit Price</th>
<th style="padding: 8px; border-bottom: 2px solid #ddd;">Currency</th>
</tr>
</thead>
<tbody>
<tr t-foreach="object.line_ids" t-as="line" t-attf-style="background-color: {{ line_index % 2 == 0 and '#f9f9f9' or '#ffffff' }};">
<td style="padding: 8px; border-bottom: 1px solid #ddd;">
<span t-field="line.product_id.display_name"/>
</td>
<td style="padding: 8px; border-bottom: 1px solid #ddd;">
<span t-field="line.product_id.uom_id.display_name"/>
</td>
<td style="padding: 8px; border-bottom: 1px solid #ddd; text-align: right;">
<span t-field="line.price" t-options="{'widget': 'monetary', 'display_currency': line.currency_id}"/>
</td>
<td style="padding: 8px; border-bottom: 1px solid #ddd;">
<span t-field="line.currency_id.name"/>
</td>
</tr>
</tbody>
</table>
<p>Best regards,</p>
<p>The <span t-out="company.name"/> team.</p>
</div>
</field>
</record>
</odoo>

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="send_pricing_update_mails" model="ir.cron">
<field name="name">Price Update Notifications: Send
Notifications</field>
<field name="model_id"
ref="price_update_notifications.model_price_notice_mailing"/>
<field name="state">code</field>
<field name="code">model.action_send_all()</field>
<field name="active" eval="True"/>
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field name="numbercall">-1</field>
<field name="doall" eval="False"/>
<field name="nextcall"
eval="(DateTime.now() + timedelta(days=1))"/>
</record>
</odoo>

View file

@ -0,0 +1 @@
from . import price_notice_mailing

View file

@ -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)

View file

@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 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
3 access_price_notice_mailing_manager access_price_notice_mailing_manager model_price_notice_mailing sales_team.group_sale_manager 1 1 1 1
4 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
5 access_wizard_price_update_notice_user access_wizard_price_update_notice_user model_wizard_price_update_notice base.group_user 1 0 0 0
6 access_price_notice_mailing_user access_price_notice_mailing_user model_price_notice_mailing base.group_user 1 0 0 0
7 access_price_notice_mailing_line_user access_price_notice_mailing_line_user model_price_notice_mailing_line base.group_user 1 0 0 0

View file

@ -0,0 +1,97 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="price_notice_mailing_view_form" model="ir.ui.view">
<field name="name">price.notice.mailing.view.form</field>
<field name="model">price.notice.mailing</field>
<field name="arch" type="xml">
<form>
<sheet>
<group colspan="2">
<field name="partner_id"/>
<field name="send_date"/>
<field name="effective_date"/>
<field name="sent"/>
</group>
<notebook>
<page string="Notification Lines">
<field name="line_ids">
<tree>
<field name="product_id"/>
<field name="price"/>
<field name="currency_id"/>
</tree>
</field>
</page>
<page string="Applicable Products">
<field name="product_template_ids">
<tree>
<field name="name"/>
</tree>
</field>
</page>
</notebook>
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids" widget="mail_followers"/>
<field name="message_ids" widget="mail_thread"/>
</div>
</form>
</field>
</record>
<record id="price_notice_mailing_view_list" model="ir.ui.view">
<field name="name">price.notice.mailing.view.list</field>
<field name="model">price.notice.mailing</field>
<field name="arch" type="xml">
<tree>
<field name="send_date"/>
<field name="partner_id"/>
<field name="effective_date"/>
<field name="send_date"/>
<field name="sent"/>
</tree>
</field>
</record>
<record id="action_view_price_notice_mailings" model="ir.actions.act_window">
<field name="name">Price Notice Mailings</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">price.notice.mailing</field>
<field name="view_mode">tree,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No mailings yet. Create one!
</p>
</field>
</record>
<record id="action_send_price_update_notice_wizard" model="ir.actions.act_window">
<field name="name">Send Price Update Notice</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">wizard.price.update.notice</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
<record id="partner_action_send_price_update_notice" model="ir.actions.server">
<field name="name">Send Price Update Notice(s)</field>
<field name="model_id" ref="base.model_res_partner"/>
<field name="binding_model_id" ref="base.model_res_partner"/>
<field name="binding_type">action</field>
<field name="state">code</field>
<field name="code">
action = {
'type': 'ir.actions.act_window',
'name': 'Send Price Update Notice',
'res_model': 'wizard.price.update.notice',
'view_mode': 'form',
'target': 'new',
}
</field>
</record>
<menuitem id="sale_menu_price_notice_mailing"
parent="sale.sale_menu_root"
action="action_view_price_notice_mailings"
sequence="20"
name="Price Update Notices"/>
</odoo>

View file

@ -0,0 +1 @@
from . import send_price_update_notice

View file

@ -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 = _("<p>The following partners do not have emails configured:</p>")
msg += "<ul>"
for partner in partner_ids:
msg += f"<li>{partner.display_name}</li>"
msg += "</ul>"
return msg

View file

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="send_price_update_notice_view_form" model="ir.ui.view">
<field name="name">send.price.update.notice.view.form</field>
<field name="model">wizard.price.update.notice</field>
<field name="arch" type="xml">
<form>
<sheet>
<div invisible="not warning_msg" class="alert alert-warning"
role="alert">
<field name="warning_msg" readonly="True"/>
</div>
<group>
<group>
<field name="partner_ids" invisible="True"/>
<field name="pricing_date"/>
<field name="send_date"/>
</group>
</group>
<notebook>
<page string="Applicable Products">
<field name="product_template_ids"/>
</page>
</notebook>
<footer>
<button special="cancel" string="Cancel"
class="btn btn-secondary"/>
<button type="object" name="action_create"
string="Create Notifications"
class="btn btn-primary"/>
</footer>
</sheet>
</form>
</field>
</record>
</odoo>