new module sale_mandatory_customer_reference and small fix to delivery_carrier_partner_account
This commit is contained in:
parent
1fb6ad5a2c
commit
c30130a3d6
14 changed files with 427 additions and 3 deletions
|
|
@ -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"]
|
||||
|
|
|
|||
2
sale_mandatory_customer_reference/__init__.py
Normal file
2
sale_mandatory_customer_reference/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
from . import models
|
||||
from . import controllers
|
||||
38
sale_mandatory_customer_reference/__manifest__.py
Normal file
38
sale_mandatory_customer_reference/__manifest__.py
Normal 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,
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
from . import portal
|
||||
61
sale_mandatory_customer_reference/controllers/portal.py
Normal file
61
sale_mandatory_customer_reference/controllers/portal.py
Normal 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)}
|
||||
2
sale_mandatory_customer_reference/models/__init__.py
Normal file
2
sale_mandatory_customer_reference/models/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
from . import res_config_settings
|
||||
from . import sale_order
|
||||
|
|
@ -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')."
|
||||
)
|
||||
26
sale_mandatory_customer_reference/models/sale_order.py
Normal file
26
sale_mandatory_customer_reference/models/sale_order.py
Normal 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()
|
||||
|
|
@ -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;
|
||||
|
|
@ -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>
|
||||
1
sale_mandatory_customer_reference/tests/__init__.py
Normal file
1
sale_mandatory_customer_reference/tests/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from . import test_sale_order
|
||||
113
sale_mandatory_customer_reference/tests/test_sale_order.py
Normal file
113
sale_mandatory_customer_reference/tests/test_sale_order.py
Normal 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")
|
||||
29
sale_mandatory_customer_reference/views/portal_templates.xml
Normal file
29
sale_mandatory_customer_reference/views/portal_templates.xml
Normal 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>
|
||||
|
|
@ -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>
|
||||
Loading…
Reference in a new issue