new module sale_mandatory_customer_reference and small fix to delivery_carrier_partner_account

This commit is contained in:
Marc Durepos 2025-02-12 12:10:09 -05:00
parent 1fb6ad5a2c
commit c30130a3d6
14 changed files with 427 additions and 3 deletions

View file

@ -24,6 +24,9 @@ class Partner(models.Model):
res = super().write(vals)
if update_default_carrier and self.carrier_account_ids:
self.default_carrier_account_id = self.carrier_account_ids[0]
self.property_delivery_carrier_id = (
self.default_carrier_account_id.delivery_carrier_id
)
return res
@api.model_create_multi
@ -41,9 +44,11 @@ class Partner(models.Model):
)
if own_accounts:
return own_accounts[0]
commercial_patner_accounts = self.commercial_partner_id.carrier_account_ids.filtered(
lambda account: account.delivery_carrier_id == carrier
commercial_patner_accounts = (
self.commercial_partner_id.carrier_account_ids.filtered(
lambda account: account.delivery_carrier_id == carrier
)
)
if commercial_patner_accounts:
return commercial_patner_accounts[0]
return self.env["delivery.carrier.account"]
return self.env["delivery.carrier.account"]

View file

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

View file

@ -0,0 +1,38 @@
{
"name": "Mandatory Customer Reference on Sales Orders",
"version": "18.0.1.0.0",
"category": "Sales",
"summary": "Enforce customer reference on sales orders with portal integration",
"description": """
This module enforces the requirement of a customer reference (purchase order number) on sales orders before confirmation.
It provides the following features:
* Prevents confirmation of sales orders without a customer reference
* Allows customers to set their reference number through the portal
* Automatically sets reference to "Credit Card" for online payments
* Integrates with both backend and portal interfaces
* Compatible with e-commerce flows
""",
"author": "Bemade Inc",
"website": "https://bemade.org",
"depends": [
"sale", # For portal templates and base functionality
"portal", # For portal features
"payment", # For payment integration
"website", # For frontend assets and portal templates
"website_sale", # For sale portal features
],
"data": [
"views/res_config_settings_views.xml",
"views/portal_templates.xml",
],
"assets": {
"web.assets_frontend": [
"sale_mandatory_customer_reference/static/src/js/portal_sale.js",
],
},
"license": "LGPL-3",
"installable": True,
"application": False,
"auto_install": False,
}

View file

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

View file

@ -0,0 +1,61 @@
from odoo import http
from odoo.http import request
from odoo.addons.sale.controllers.portal import CustomerPortal
from odoo.addons.portal.controllers.portal import pager as portal_pager
class CustomerPortalInherit(CustomerPortal):
def _prepare_quotations_domain(self, partner):
domain = super()._prepare_quotations_domain(partner)
return domain
def _prepare_sale_portal_rendering_values(self, order, **kwargs):
values = super()._prepare_sale_portal_rendering_values(order, **kwargs)
values["enforce_customer_reference"] = (
request.env["ir.config_parameter"]
.sudo()
.get_param(
"sale_mandatory_customer_reference.enforce_customer_reference", False
)
)
return values
@http.route(
["/my/orders/<int:order_id>/update_reference"],
type="json",
auth="public",
website=True,
)
def portal_update_sale_reference(
self, order_id, reference, access_token=None, **kw
):
try:
order = request.env["sale.order"].browse(order_id)
if not order.exists():
return {"error": "Order not found"}
# Try user access first
try:
# These will raise if access denied
order.check_access_rights("write")
order.check_access_rule("write")
except Exception:
# If user access fails, try token access
if access_token:
order = request.env["sale.order"].sudo().browse(order_id)
try:
order.check_access_token(access_token)
except Exception:
return {"error": "Access Denied"}
else:
return {"error": "Access Denied"}
if order.state not in ("draft", "sent"):
return {"error": "Order cannot be modified in its current state"}
order.write({"client_order_ref": reference})
return {"success": True}
except Exception as e:
return {"error": str(e)}

View file

@ -0,0 +1,2 @@
from . import res_config_settings
from . import sale_order

View file

@ -0,0 +1,12 @@
from odoo import fields, models
class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
enforce_customer_reference = fields.Boolean(
string="Require Customer Reference on Sales Orders",
config_parameter='sale_mandatory_customer_reference.enforce_customer_reference',
help="When enabled, sales orders cannot be confirmed without a customer reference "
"(except for online payments where it will be set to 'Credit Card')."
)

View file

@ -0,0 +1,26 @@
from odoo import models, _
from odoo.exceptions import ValidationError
class SaleOrder(models.Model):
_inherit = 'sale.order'
def _get_enforce_customer_reference(self):
"""Get the configuration parameter for customer reference enforcement."""
return self.env['ir.config_parameter'].sudo().get_param(
'sale_mandatory_customer_reference.enforce_customer_reference', False)
def action_confirm(self):
"""Override to check for customer reference before confirmation."""
for order in self:
if order._get_enforce_customer_reference():
# For online payments with successful transactions, set reference to "Credit Card" if not set
successful_tx = order.transaction_ids.filtered(lambda tx: tx.state == 'done')
if successful_tx and not order.client_order_ref:
order.client_order_ref = "Credit Card"
elif not order.client_order_ref:
raise ValidationError(_(
"Customer reference (PO Number) is required before confirming this order. "
"Please set the customer reference field."
))
return super().action_confirm()

View file

@ -0,0 +1,99 @@
/** @odoo-module **/
import publicWidget from "@web/legacy/js/public/public_widget";
import { _t } from "@web/core/l10n/translation";
import { rpc } from "@web/core/network/rpc";
publicWidget.registry.SalePortalReference = publicWidget.Widget.extend({
selector: '.o_portal_sale_reference',
events: {
'change input[name="client_order_ref"]': '_onReferenceChange',
},
/**
* @override
*/
start: function () {
this.orderId = this.el.dataset.orderId;
this.accessToken = this.el.dataset.accessToken;
return this._super.apply(this, arguments);
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Show a notification message
* @private
*/
_showNotification(message, type = 'success') {
const alertClass = type === 'success' ? 'alert-success' : 'alert-danger';
const $notification = $('<div>', {
class: `alert ${alertClass} alert-dismissible fade show`,
role: 'alert',
text: message
}).append($('<button>', {
type: 'button',
class: 'btn-close',
'data-bs-dismiss': 'alert',
'aria-label': 'Close'
}));
// Remove any existing alerts
this.$('.alert').remove();
// Add the new alert before the input
this.$('input[name="client_order_ref"]').before($notification);
// Auto-hide after 5 seconds
setTimeout(() => {
$notification.alert('close');
}, 5000);
},
/**
* Save the reference when it changes
* @private
*/
async _saveReference(reference) {
try {
const params = { reference };
if (this.accessToken) {
params.access_token = this.accessToken;
}
const result = await rpc('/my/orders/' + this.orderId + '/update_reference', params);
if (result.error) {
this._showNotification(_t(result.error), 'danger');
return false;
}
this._showNotification(_t("Reference updated successfully"));
return true;
} catch (error) {
console.error('RPC error:', error);
this._showNotification(_t("Failed to save your reference. Please try again."), 'danger');
return false;
}
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* @private
*/
async _onReferenceChange(ev) {
const reference = ev.target.value;
if (!this.orderId) {
this._showNotification(_t("Could not determine the order ID"), 'danger');
return;
}
await this._saveReference(reference);
},
});
export default publicWidget.registry.SalePortalReference;

View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="sale_mandatory_customer_reference.CustomerReferenceInput" owl="1">
<div class="customer-reference-input">
<input
type="text"
class="form-control"
name="client_order_ref"
t-att-value="state.reference"
t-on-input="onInput"
placeholder="Your Reference / Purchase Order Number"/>
<div t-if="state.showSuccess" class="alert alert-success mt-2" role="alert">
Reference updated successfully
</div>
<div t-if="state.showError" class="alert alert-danger mt-2" role="alert">
Failed to update reference
</div>
</div>
</t>
</templates>

View file

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

View file

@ -0,0 +1,113 @@
from odoo.tests import tagged
from odoo.exceptions import ValidationError
from odoo.addons.payment.tests.common import PaymentCommon
@tagged("post_install", "-at_install")
class TestSaleOrder(PaymentCommon):
def setUp(self):
super().setUp()
# Create test payment provider
self.payment_provider = self._prepare_provider()
# Create test payment method
self.payment_method = self.env["payment.method"].create(
{
"name": "Test Card",
"code": "card",
}
)
self.partner = self.env["res.partner"].create(
{
"name": "Test Customer",
"email": "test@example.com",
}
)
self.product = self.env["product.product"].create(
{
"name": "Test Product",
"type": "consu",
"invoice_policy": "order",
}
)
self.sale_order = self.env["sale.order"].create(
{
"partner_id": self.partner.id,
"order_line": [
(
0,
0,
{
"product_id": self.product.id,
"product_uom_qty": 1,
},
)
],
}
)
def test_confirm_without_reference_disabled(self):
"""Test that order can be confirmed without reference when setting is disabled."""
# Ensure setting is disabled
self.env["ir.config_parameter"].sudo().set_param(
"sale_mandatory_customer_reference.enforce_customer_reference", False
)
# Should confirm without error
self.sale_order.action_confirm()
self.assertEqual(self.sale_order.state, "sale")
def test_confirm_without_reference_enabled(self):
"""Test that order cannot be confirmed without reference when setting is enabled."""
# Enable the setting
self.env["ir.config_parameter"].sudo().set_param(
"sale_mandatory_customer_reference.enforce_customer_reference", True
)
# Should raise ValidationError
with self.assertRaises(ValidationError):
self.sale_order.action_confirm()
def test_confirm_with_reference_enabled(self):
"""Test that order can be confirmed with reference when setting is enabled."""
# Enable the setting
self.env["ir.config_parameter"].sudo().set_param(
"sale_mandatory_customer_reference.enforce_customer_reference", True
)
# Set customer reference
self.sale_order.client_order_ref = "PO001"
# Should confirm without error
self.sale_order.action_confirm()
self.assertEqual(self.sale_order.state, "sale")
def test_confirm_online_payment_without_reference(self):
"""Test that online payments auto-set reference to 'Credit Card'."""
# Enable the setting
self.env["ir.config_parameter"].sudo().set_param(
"sale_mandatory_customer_reference.enforce_customer_reference", True
)
# Create a mock transaction
transaction = self.env["payment.transaction"].create(
{
"reference": "Test Transaction",
"partner_id": self.partner.id,
"amount": 100,
"currency_id": self.env.company.currency_id.id,
"provider_id": self.payment_provider.id,
"payment_method_id": self.payment_method.id,
"state": "done",
"partner_name": "Test Customer",
"partner_email": "test@example.com",
}
)
self.sale_order.transaction_ids = [(4, transaction.id)]
# Should confirm without error and set reference
self.sale_order.action_confirm()
self.assertEqual(self.sale_order.state, "sale")
self.assertEqual(self.sale_order.client_order_ref, "Credit Card")

View file

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="sale_order_portal_content_inherit_sale_mandatory_reference" inherit_id="sale.sale_order_portal_content" priority="30">
<xpath expr="//div[@id='informations']" position="inside">
<div t-if="sale_order.state in ('draft', 'sent')"
class="col-12 col-lg-6 mb-4 o_portal_sale_reference"
t-att-data-order-id="sale_order.id"
t-att-data-access-token="access_token">
<h5 class="mb-1">Your Reference</h5>
<hr class="mt-1 mb-2"/>
<input type="text"
class="form-control"
name="client_order_ref"
t-att-value="sale_order.client_order_ref or ''"
placeholder="Enter your purchase order number"
t-att-required="enforce_customer_reference"/>
<small t-if="enforce_customer_reference" class="text-muted">
A reference (PO number) is required before confirming this order.
</small>
</div>
</xpath>
</template>
<template id="sale_order_portal_template_inherit_sale_mandatory_reference" inherit_id="sale.sale_order_portal_template" priority="30">
<xpath expr="//div[@id='quote_content']" position="attributes">
<attribute name="t-att-data-reference-required">enforce_customer_reference</attribute>
</xpath>
</template>
</odoo>

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="res_config_settings_view_form" model="ir.ui.view">
<field name="name">res.config.settings.view.form.inherit.sale.mandatory.reference</field>
<field name="model">res.config.settings</field>
<field name="inherit_id" ref="sale.res_config_settings_view_form"/>
<field name="arch" type="xml">
<xpath expr="//block[@id='pricing_setting_container']//div[last()]" position="after">
<setting id="sale_mandatory_reference" help="Require customer reference (PO number) before sales order confirmation">
<field name="enforce_customer_reference"/>
</setting>
</xpath>
</field>
</record>
</odoo>