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)
|
res = super().write(vals)
|
||||||
if update_default_carrier and self.carrier_account_ids:
|
if update_default_carrier and self.carrier_account_ids:
|
||||||
self.default_carrier_account_id = self.carrier_account_ids[0]
|
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
|
return res
|
||||||
|
|
||||||
@api.model_create_multi
|
@api.model_create_multi
|
||||||
|
|
@ -41,8 +44,10 @@ class Partner(models.Model):
|
||||||
)
|
)
|
||||||
if own_accounts:
|
if own_accounts:
|
||||||
return own_accounts[0]
|
return own_accounts[0]
|
||||||
commercial_patner_accounts = self.commercial_partner_id.carrier_account_ids.filtered(
|
commercial_patner_accounts = (
|
||||||
lambda account: account.delivery_carrier_id == carrier
|
self.commercial_partner_id.carrier_account_ids.filtered(
|
||||||
|
lambda account: account.delivery_carrier_id == carrier
|
||||||
|
)
|
||||||
)
|
)
|
||||||
if commercial_patner_accounts:
|
if commercial_patner_accounts:
|
||||||
return commercial_patner_accounts[0]
|
return commercial_patner_accounts[0]
|
||||||
|
|
|
||||||
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