product_supplierinfo_tracking

This commit is contained in:
xtremxpert 2025-08-29 09:43:24 -04:00
parent e5e90d6c7e
commit 583e25e092
21 changed files with 764 additions and 188 deletions

View file

@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
{
"name": "Product Supplier Info Chatter (Legacy)",
"version": "18.0.4.0.0",
"license": "AGPL-3",
"author": "Bemade",
"category": "Inventory/Purchase",
"depends": [
"product",
"mail",
],
"description": """
Legacy module - use product_supplierinfo_chatter instead.
This module adds chatter functionality to supplier information,
providing tracking of price changes and communication history.
""",
"demo": [],
'data': [
'views/product_view.xml',
'views/supplierinfo_pricelist.xml',
],
'test': [],
'installable': True,
'active': False
}
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View file

@ -5,37 +5,18 @@ from markupsafe import Markup
class ProductSupplierInfo(models.Model):
_name = "product.supplierinfo"
_inherit = [
"product.supplierinfo",
"mail.thread",
"mail.activity.mixin"
]
# Commented out because breaking basic Odoo tests. See if this is really needed.
# _sql_constraints = [("supplierinfo_product_tmpl_id_no_null",
# "CHECK((product_tmpl_id IS NOT NULL))",
# "Supplier pricelist need product template"),
# ]
# This while avoid the database to save those record with lost product_tmpl_id
@api.depends("supplier_list_price", "supplier_discount_percent")
def _compute_price(self):
for rec in self:
rec.price = rec.supplier_list_price - (
rec.supplier_list_price * rec.supplier_discount_percent / 100
)
@api.depends("supplier_discount_percent")
def _inverse_price(self):
for rec in self:
discount = (
rec.supplier_discount_percent if rec.supplier_discount_percent else 0
)
rec.supplier_list_price = (100 * rec.price) / (100 - discount)
partner_id = fields.Many2one(tracking=True, domain=[("is_company", "=", True)])
# We add tracking to the existing fields for chatter functionality
partner_id = fields.Many2one(
tracking=True,
domain=[("is_company", "=", True)]
)
product_name = fields.Char(tracking=True)
product_code = fields.Char(tracking=True)
product_uom = fields.Many2one(tracking=True)
@ -47,35 +28,9 @@ class ProductSupplierInfo(models.Model):
product_tmpl_id = fields.Many2one(tracking=True)
delay = fields.Integer(tracking=True)
date_updated = fields.Date(
string="Last updated",
help="Date at which the supplier " + " list price was last updated.",
)
purchasing_notes = fields.Text(tracking=True, string="Purchasing Notes")
supplier_list_price = fields.Float(
string="Supplier List Price",
digits="Product Price",
tracking=True,
help="""
This the supplier list price to which supplier discounts are applied, if
any, and the net price if no supplier discounts are to be applied
""",
)
supplier_discount_percent = fields.Float(
string="Supplier discount (%)", digits="Product Price", tracking=True, default=0
)
price = fields.Float(
compute="_compute_price",
inverse="_inverse_price",
string="Supplier Price",
digits="Product Price",
help="This price will be considered as a price for the supplier UoM if any or "
"the default Unit of Measure of the product otherwise",
store=True,
purchasing_notes = fields.Text(
tracking=True,
string="Purchasing Notes"
)
def _generate_chatter(self, vals, operation):

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

View file

@ -1,18 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Vue de liste personnalisée pour supplier info sans édition en ligne -->
<record id="product_supplierinfo_tree_view_no_edit" model="ir.ui.view">
<field name="name">product.supplierinfo.tree.no.edit</field>
<field name="model">product.supplierinfo</field>
<field name="inherit_id" ref="purchase.product_supplierinfo_tree_view2" />
<field name="arch" type="xml">
<xpath expr="//list" position="attributes">
<!-- Supprimer l'attribut editable pour forcer l'ouverture du formulaire -->
<attribute name="editable"></attribute>
</xpath>
</field>
</record>
<!-- Modification de la vue produit pour utiliser notre vue personnalisée -->
<record id="product_template_only_form_view_landed" model="ir.ui.view">
<field name="name">product.template.form.view.landed</field>

View file

@ -1,74 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<!-- Vue de liste personnalisée pour supplier info sans édition en ligne -->
<record id="product_supplierinfo_tree_view_no_edit" model="ir.ui.view">
<field name="name">product.supplierinfo.tree.no.edit</field>
<field name="model">product.supplierinfo</field>
<field name="inherit_id" ref="purchase.product_supplierinfo_tree_view2" />
<field name="arch" type="xml">
<xpath expr="//list" position="attributes">
<attribute name="editable"></attribute>
</xpath>
</field>
</record>
<record id="product_supplierinfo_form_view_chatter" model="ir.ui.view">
<field name="name">supplierinfo.pricelist.chatter</field>
<field name="model">product.supplierinfo</field>
<field name="inherit_id" ref="product.product_supplierinfo_form_view" />
<field name="arch" type="xml">
<form string="Vendor Information">
<sheet>
<group>
<group name="vendor" string="Vendor">
<field name="product_variant_count" invisible="1" />
<field name="partner_id"
context="{'res_partner_search_mode': 'supplier'}" />
<field name="product_name" />
<field name="product_code" />
<label for="delay" />
<div>
<field name="delay" class="oe_inline" /> days </div>
</group>
<group string="Pricelist">
<field name="product_tmpl_id" string="Product"
invisible="context.get('visible_product_tmpl_id', True)" />
<field name="product_id" groups="product.group_product_variant"
options="{'no_create': True}" />
<label for="min_qty" />
<div class="o_row">
<field name="min_qty" />
<field name="product_uom" groups="uom.group_uom"
options="{'no_open': True}" />
</div>
<label for="price" string="Unit Price" />
<div class="o_row">
<field name="price" class="oe_inline" widget="monetary"
options="{'currency_field': 'currency_id'}" />
<field name="currency_id" groups="base.group_multi_currency"
options="{'no_open': True}" />
</div>
<label for="date_start" string="Validity" />
<div class="o_row">
<field name="date_start" class="oe_inline" /> to <field
name="date_end" class="oe_inline" />
</div>
<field name="discount" />
<field name="company_id" options="{'no_create': True}" />
</group>
</group>
<!-- Section personnalisée pour les informations fournisseur -->
<group>
<group string="Supplier Pricing" col="2">
<field name="supplier_list_price" widget="monetary"
options="{'currency_field': 'currency_id'}" />
<field name="supplier_discount_percent" widget="percentage" />
</group>
<group string="Notes" col="1">
<field name="purchasing_notes" nolabel="1"
placeholder="Add purchasing notes, terms, conditions, or special instructions..." />
</group>
</group>
</sheet>
<!-- Chatter pour la traçabilité -->
<div class="oe_chatter">
<field name="message_follower_ids" groups="base.group_user" />
<field name="activity_ids" />
<field name="message_ids" />
</div>
</form>
<xpath expr="//sheet" position="after">
<chatter />
</xpath>
</field>
</record>
</data>

View file

@ -0,0 +1,91 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="product_supplierinfo_form_view_chatter" model="ir.ui.view">
<field name="name">supplierinfo.pricelist.chatter</field>
<field name="model">product.supplierinfo</field>
<field name="arch" type="xml">
<form string="Vendor Information">
<sheet>
<group>
<group name="vendor" string="Vendor">
<field name="product_variant_count" invisible="1" />
<field name="partner_id"
context="{'res_partner_search_mode': 'supplier'}" />
<field name="product_name" />
<field name="product_code" />
<label for="delay" />
<div>
<field name="delay" class="oe_inline" /> days </div>
</group>
<group string="Pricelist">
<field name="product_tmpl_id" string="Product"
invisible="context.get('visible_product_tmpl_id', True)" />
<field name="product_id" groups="product.group_product_variant"
options="{'no_create': True}" />
<label for="min_qty" />
<div class="o_row">
<field name="min_qty" />
<field name="product_uom" groups="uom.group_uom"
options="{'no_open': True}" />
</div>
<label for="price" string="Unit Price" />
<div class="o_row">
<field name="price" class="oe_inline" widget="monetary"
options="{'currency_field': 'currency_id'}" />
<field name="currency_id" groups="base.group_multi_currency"
options="{'no_open': True}" />
</div>
<label for="date_start" string="Validity" />
<div class="o_row">
<field name="date_start" class="oe_inline" /> to <field
name="date_end" class="oe_inline" />
</div>
<field name="discount" />
<field name="company_id" options="{'no_create': True}"
groups="base.group_multi_company" />
</group>
</group>
<!-- Section personnalisée pour les informations fournisseur -->
<group>
<group string="Supplier Pricing" col="2">
<field name="supplier_list_price" widget="monetary"
options="{'currency_field': 'currency_id'}" />
<field name="supplier_discount_percent" widget="percentage" />
</group>
<group string="Notes" col="1">
<field name="purchasing_notes" nolabel="1"
placeholder="Add purchasing notes, terms, conditions, or special instructions..." />
</group>
</group>
</sheet>
<!-- Chatter pour la traçabilité -->
<div class="oe_chatter">
<field name="message_follower_ids" groups="base.group_user" />
<field name="activity_ids" />
<field name="message_ids" />
</div>
</form>
</field>
</record>
<!-- Surcharge simple pour ajouter groups au champ company_id de la vue standard -->
<record id="product_supplierinfo_company_groups" model="ir.ui.view">
<field name="name">product.supplierinfo.company.groups</field>
<field name="model">product.supplierinfo</field>
<field name="inherit_id" ref="product.product_supplierinfo_form_view" />
<field name="arch" type="xml">
<field name="company_id" position="attributes">
<attribute name="groups">base.group_multi_company</attribute>
</field>
</field>
</record>
</data>
</odoo>

View file

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
from . import models

View file

@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
{
"name": "Product Supplier Info Pricing",
"version": "18.0.1.0.0",
"license": "AGPL-3",
"author": "Bemade",
"category": "Inventory/Purchase",
"depends": [
"product",
],
"description": """
This module adds additional pricing fields to supplier information,
including supplier list price, discount percentage, and calculated price.
""",
"demo": [],
'data': [
'views/product_supplierinfo_pricing_views.xml',
],
'test': [],
'installable': True,
'active': False
}
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View file

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
from . import product_supplierinfo

View file

@ -0,0 +1,57 @@
# -*- coding: utf-8 -*-
from odoo import models, fields, api
class ProductSupplierInfo(models.Model):
_inherit = "product.supplierinfo"
date_updated = fields.Date(
string="Last updated",
help="Date at which the supplier list price was last updated.",
)
purchasing_notes = fields.Text(
string="Purchasing Notes"
)
supplier_list_price = fields.Float(
string="Supplier List Price",
digits="Product Price",
help="""This the supplier list price to which supplier discounts are applied, if
any, and the net price if no supplier discounts are to be applied""",
)
supplier_discount_percent = fields.Float(
string="Supplier discount (%)",
digits="Product Price",
default=0
)
price = fields.Float(
compute="_compute_price",
inverse="_inverse_price",
string="Supplier Price",
digits="Product Price",
help="This price will be considered as a price for the supplier UoM if any or "
"the default Unit of Measure of the product otherwise",
store=True,
)
@api.depends("supplier_list_price", "supplier_discount_percent")
def _compute_price(self):
for rec in self:
rec.price = rec.supplier_list_price - (
rec.supplier_list_price * rec.supplier_discount_percent / 100
)
@api.depends("supplier_discount_percent")
def _inverse_price(self):
for rec in self:
discount = (
rec.supplier_discount_percent if rec.supplier_discount_percent else 0
)
if discount >= 100:
rec.supplier_list_price = 0
else:
rec.supplier_list_price = (100 * rec.price) / (100 - discount)

View file

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="product_supplierinfo_form_view_pricing" model="ir.ui.view">
<field name="name">supplierinfo.pricing.form</field>
<field name="model">product.supplierinfo</field>
<field name="inherit_id" ref="product.product_supplierinfo_form_view" />
<field name="arch" type="xml">
<xpath expr="//sheet//group[last()]" position="after">
<group>
<group string="Supplier Pricing" col="2">
<field name="supplier_list_price" widget="monetary"
options="{'currency_field': 'currency_id'}" />
<field name="supplier_discount_percent" widget="percentage" />
<field name="price" widget="monetary"
options="{'currency_field': 'currency_id'}" />
</group>
<group string="Notes" col="1">
<field name="purchasing_notes" nolabel="1"
placeholder="Add purchasing notes, terms, conditions, or special instructions..." />
</group>
<group string="Tracking" col="2">
<field name="date_updated" />
</group>
</group>
</xpath>
</field>
</record>
</data>
</odoo>

View file

@ -1,31 +1,11 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
{
"name": "Product Supplierinfo Tracking",
"version": "18.0.2.0.5",
"version": "18.0.3.0.0",
"license": "AGPL-3",
"author": "Bemade",
"category": "Purchase",
"category": "Tools",
"depends": [
"base",
"product",
@ -35,14 +15,13 @@
"mrp",
],
"description": """
This module extends basic inventory and pricelist management in
OpenERP to include support for supplierinfo tracking.
This module extends basic inventory support for supplierinfo chatter making price
updates visible in the chatter.
""",
"demo": [],
'data': [
'views/product_view.xml',
'views/supplierinfo_pricelist.xml',
'views/supplierinfo.xml',
],
'test': [],
'installable': True,

View file

@ -1 +1 @@
from . import product_supplierinfo
from . import product_supplier_info

View file

@ -0,0 +1,88 @@
# -*- coding: utf-8 -*-
from odoo import models, fields, api
from markupsafe import Markup
class ProductSupplierInfo(models.Model):
_name = "product.supplierinfo"
_inherit = ["product.supplierinfo", "mail.thread", "mail.activity.mixin"]
# Commented out because breaking basic Odoo tests. See if this is really needed.
# _sql_constraints = [("supplierinfo_product_tmpl_id_no_null",
# "CHECK((product_tmpl_id IS NOT NULL))",
# "Supplier pricelist need product template"),
# ]
# We add tracking to the fields that are displayed in the chatter
partner_id = fields.Many2one(
tracking=True,
domain=[("is_company", "=", True)]
)
product_name = fields.Char(tracking=True)
product_code = fields.Char(tracking=True)
product_uom = fields.Many2one(tracking=True)
min_qty = fields.Float(tracking=True)
currency_id = fields.Many2one(tracking=True)
date_start = fields.Date(tracking=True)
date_end = fields.Date(tracking=True)
product_id = fields.Many2one(tracking=True)
product_tmpl_id = fields.Many2one(tracking=True)
delay = fields.Integer(tracking=True)
def _generate_chatter(self, vals, operation):
if len(self) == 1 and self.product_tmpl_id:
msg = ""
headmsg = (
f"<a href='#' data-oe-model='product.supplierinfo' data-oe-id='{self.id}'>"
f"Price {operation} for {self.product_id.name} : <br /></a>"
)
if self.min_qty > 0:
msg += f"<li>Minimum qty : {self.min_qty}</li>"
if self.date_start:
msg += f"<li>Starting : {self.date_start}</li>"
if self.date_end:
msg += f"<li>Ending : {self.date_end}</li>"
if msg:
msg = headmsg + "<ul>" + msg + "</ul>"
else:
msg = headmsg
for change in vals:
msg += f"{change} --> {vals[change]}<br />"
# Should always be there
if self.product_tmpl_id:
self.product_tmpl_id.message_post(
body=Markup(msg),
)
# only for variant
if self.product_id:
self.product_id.message_post(body=Markup(msg))
def write(self, vals):
res = super(ProductSupplierInfo, self).write(vals)
self._generate_chatter(vals, "modify")
return res
def supplierinfo_show_details(self):
"""
Action to open product.supplierinfo (pricelist) in its own windows and not in a javascript
popup to avoid loosing product_tmpl_id. We rely on both context and domain to pass the
information
"""
return {
"name": "Supplier price",
"view_type": "form",
"view_mode": "form",
"res_id": self.id,
"context": self.env.context,
"res_model": "product.supplierinfo",
"domain": [
("product_tmpl_id", "=", self.product_tmpl_id.id),
("product_id", "=", self.product_id.id if self.product_id else False),
],
"type": "ir.actions.act_window",
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

View file

@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
from . import test_product_supplierinfo_tracking

View file

@ -0,0 +1,266 @@
# -*- coding: utf-8 -*-
from odoo.tests.common import TransactionCase
from odoo.exceptions import ValidationError
from markupsafe import Markup
class TestProductSupplierinfoTracking(TransactionCase):
def setUp(self):
super().setUp()
# Use existing data to avoid database constraint issues
self.partner_supplier = self.env['res.partner'].create({
'name': 'Test Supplier',
'is_company': True,
'supplier_rank': 1,
})
# Use first available product template and variant
self.product_template = self.env['product.template'].search([], limit=1)
if not self.product_template:
self.skipTest("No product template available for testing")
self.product_variant = self.product_template.product_variant_ids[0] if self.product_template.product_variant_ids else None
self.currency_usd = self.env.ref('base.USD')
self.uom_unit = self.env.ref('uom.product_uom_unit')
def test_supplierinfo_tracking_fields(self):
"""Test that tracking is enabled on all expected fields"""
supplierinfo = self.env['product.supplierinfo'].create({
'partner_id': self.partner_supplier.id,
'product_tmpl_id': self.product_template.id,
'min_qty': 10.0,
'price': 100.0,
'currency_id': self.currency_usd.id,
'product_name': 'Supplier Product Name',
'product_code': 'SUP001',
'delay': 5,
})
# Check that the model inherits from mail.thread
self.assertTrue(hasattr(supplierinfo, 'message_ids'))
self.assertTrue(hasattr(supplierinfo, 'activity_ids'))
# Verify tracking fields are properly set
tracked_fields = [
'partner_id', 'product_name', 'product_code', 'product_uom',
'min_qty', 'currency_id', 'date_start', 'date_end',
'product_id', 'product_tmpl_id', 'delay'
]
for field_name in tracked_fields:
field = supplierinfo._fields.get(field_name)
self.assertTrue(field.tracking, f"Field {field_name} should have tracking enabled")
def test_chatter_message_generation_on_write(self):
"""Test that chatter messages are generated when supplierinfo is updated"""
# Create a supplierinfo record
supplierinfo = self.env['product.supplierinfo'].create({
'partner_id': self.partner_supplier.id,
'product_tmpl_id': self.product_template.id,
'product_id': self.product_variant.id if self.product_variant else False,
'min_qty': 1.0,
'price': 10.0,
})
# Clear any existing messages
if self.product_template:
self.product_template.message_ids.unlink()
if self.product_variant:
self.product_variant.message_ids.unlink()
# Update the supplierinfo to trigger chatter
supplierinfo.write({
'price': 15.0,
'min_qty': 2.0,
})
# Check that messages were posted to product template
if self.product_template:
template_messages = self.product_template.message_ids
self.assertTrue(len(template_messages) > 0, "Should have posted message to product template")
# Check that messages were posted to product variant if it exists
if self.product_variant:
variant_messages = self.product_variant.message_ids
self.assertTrue(len(variant_messages) > 0, "Should have posted message to product variant")
# Verify message content contains the changes
message_body = template_messages[0].body
self.assertIn('Price modify', message_body)
self.assertIn('min_qty', message_body)
def test_chatter_message_content_structure(self):
"""Test the structure and content of generated chatter messages"""
supplierinfo = self.env['product.supplierinfo'].create({
'partner_id': self.partner_supplier.id,
'product_tmpl_id': self.product_template.id,
'product_id': self.product_variant.id if self.product_variant else False,
'min_qty': 5.0,
'price': 75.0,
'date_start': '2024-01-01',
'date_end': '2024-12-31',
})
# Clear existing messages
if self.product_template:
self.product_template.message_ids.unlink()
if self.product_variant:
self.product_variant.message_ids.unlink()
# Update to trigger chatter message
supplierinfo.write({'price': 80.0, 'min_qty': 10.0})
# Check that messages were created
if self.product_template:
template_messages = self.product_template.message_ids
self.assertTrue(len(template_messages) > 0, "Should have posted message to product template")
# Verify message content structure
message_body = template_messages[0].body
self.assertIn('Price modify', message_body)
self.assertIn('price --&gt; 80.0', message_body)
self.assertIn('min_qty --&gt; 10.0', message_body)
def test_chatter_message_without_variant(self):
"""Test chatter message generation when no product variant is specified"""
# Create supplierinfo without variant
supplierinfo = self.env['product.supplierinfo'].create({
'partner_id': self.partner_supplier.id,
'product_tmpl_id': self.product_template.id,
'min_qty': 1.0,
'price': 50.0,
})
# Clear existing messages
if self.product_template:
self.product_template.message_ids.unlink()
# Update to trigger chatter message
supplierinfo.write({'price': 60.0})
# Verify message was posted to template
if self.product_template:
template_messages = self.product_template.message_ids
self.assertTrue(len(template_messages) > 0, "Should post message to template")
# Verify message content
message_body = template_messages[0].body
self.assertIn('Price modify', message_body)
self.assertIn('price --&gt; 60.0', message_body)
def test_supplierinfo_show_details_action(self):
"""Test the supplierinfo_show_details action method"""
supplierinfo = self.env['product.supplierinfo'].create({
'partner_id': self.partner_supplier.id,
'product_tmpl_id': self.product_template.id,
'product_id': self.product_variant.id,
'min_qty': 1.0,
'price': 25.0,
})
action = supplierinfo.supplierinfo_show_details()
# Verify action structure
self.assertEqual(action['name'], 'Supplier price')
self.assertEqual(action['view_mode'], 'form')
self.assertEqual(action['res_model'], 'product.supplierinfo')
self.assertEqual(action['res_id'], supplierinfo.id)
self.assertEqual(action['type'], 'ir.actions.act_window')
# Verify domain
expected_domain = [
('product_tmpl_id', '=', self.product_template.id),
('product_id', '=', self.product_variant.id if self.product_variant else False),
]
self.assertEqual(action['domain'], expected_domain)
def test_partner_domain_constraint(self):
"""Test that partner_id domain restricts to companies only"""
# Create a non-company partner
individual_partner = self.env['res.partner'].create({
'name': 'Individual Partner',
'is_company': False,
})
# The domain constraint is applied at the view level, not model level
# So we test that the field has the correct domain definition
partner_field = self.env['product.supplierinfo']._fields['partner_id']
self.assertEqual(partner_field.domain, [("is_company", "=", True)])
def test_multiple_supplierinfo_tracking(self):
"""Test tracking with multiple supplierinfo records"""
supplierinfo1 = self.env['product.supplierinfo'].create({
'partner_id': self.partner_supplier.id,
'product_tmpl_id': self.product_template.id,
'min_qty': 1.0,
'price': 10.0,
})
supplier2 = self.env['res.partner'].create({
'name': 'Second Supplier',
'is_company': True,
'supplier_rank': 1,
})
supplierinfo2 = self.env['product.supplierinfo'].create({
'partner_id': supplier2.id,
'product_tmpl_id': self.product_template.id,
'min_qty': 5.0,
'price': 8.0,
})
# Clear existing messages
self.product_template.message_ids.unlink()
# Modify both
supplierinfo1.write({'price': 12.0})
supplierinfo2.write({'price': 9.0})
# Should have messages for both modifications
messages = self.product_template.message_ids
self.assertEqual(len(messages), 2, "Should have messages for both supplierinfo modifications")
def test_chatter_message_with_minimal_data(self):
"""Test chatter message generation with minimal supplierinfo data"""
# Create minimal supplierinfo
supplierinfo = self.env['product.supplierinfo'].create({
'partner_id': self.partner_supplier.id,
'product_tmpl_id': self.product_template.id,
'price': 25.0,
})
# Clear existing messages
if self.product_template:
self.product_template.message_ids.unlink()
# Update to trigger chatter message
supplierinfo.write({'price': 30.0})
# Verify basic message structure
if self.product_template:
template_messages = self.product_template.message_ids
self.assertTrue(len(template_messages) > 0, "Should have posted message")
message_body = template_messages[0].body
self.assertIn('Price modify', message_body)
self.assertIn('price --&gt; 30.0', message_body)
# Should not contain qty, dates since they weren't set
self.assertNotIn('Minimum qty', message_body)
self.assertNotIn('Starting', message_body)
self.assertNotIn('Ending', message_body)
def test_inheritance_structure(self):
"""Test that the model properly inherits required mixins"""
supplierinfo = self.env['product.supplierinfo']
# Check inheritance
self.assertIn('mail.thread', supplierinfo._inherit)
self.assertIn('mail.activity.mixin', supplierinfo._inherit)
self.assertIn('product.supplierinfo', supplierinfo._inherit)
# Check that it's extending the existing model, not creating new one
self.assertEqual(supplierinfo._name, 'product.supplierinfo')

View file

@ -1,13 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="product_supplierinfo_tree_view_tracking" model="ir.ui.view">
<field name="name">product.supplierinfo.list.view2</field>
<field name="model">product.supplierinfo</field>
<field name="inherit_id" ref="purchase.product_supplierinfo_tree_view2"/>
<!-- Modification de la vue produit pour utiliser notre vue personnalisée -->
<record id="product_template_only_form_view_landed" model="ir.ui.view">
<field name="name">product.template.form.view.landed</field>
<field name="inherit_id" ref="purchase.view_product_supplier_inherit" />
<field name="model">product.template</field>
<field name="arch" type="xml">
<xpath expr="//list" position="attributes">
<attribute name="open_form_view">1</attribute>
</xpath>
<!-- Modifier le contexte pour utiliser notre vue sans édition -->
<field name="seller_ids" position="attributes">
<attribute name="context">
{'default_product_tmpl_id': id,
'product_template_invisible_variant': True,
'list_view_ref':'product_supplierinfo_tracking.product_supplierinfo_tree_view_no_edit'}</attribute>
</field>
<field name="variant_seller_ids" position="attributes">
<attribute name="context">
{'model': 'product.template', 'active_id': id,
'list_view_ref':'product_supplierinfo_tracking.product_supplierinfo_tree_view_no_edit'}</attribute>
</field>
</field>
</record>
</odoo>
</odoo>

View file

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<!-- Vue de liste personnalisée pour supplier info sans édition en ligne -->
<record id="product_supplierinfo_tree_view_no_edit" model="ir.ui.view">
<field name="name">product.supplierinfo.tree.no.edit</field>
<field name="model">product.supplierinfo</field>
<field name="inherit_id" ref="purchase.product_supplierinfo_tree_view2" />
<field name="arch" type="xml">
<xpath expr="//list" position="attributes">
<attribute name="editable"></attribute>
</xpath>
</field>
</record>
<record id="product_supplierinfo_form_view_chatter" model="ir.ui.view">
<field name="name">supplierinfo.pricelist.chatter</field>
<field name="model">product.supplierinfo</field>
<field name="inherit_id" ref="product.product_supplierinfo_form_view" />
<field name="arch" type="xml">
<xpath expr="//sheet" position="after">
<chatter />
</xpath>
</field>
</record>
</data>
</odoo>

View file

@ -1,22 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="product_supplierinfo_form_view_chatter" model="ir.ui.view">
<field name="name">supplierinfo.pricelist.chatter</field>
<field name="model">product.supplierinfo</field>
<field name="inherit_id" ref="product.product_supplierinfo_form_view" />
<field name="arch" type="xml">
<xpath expr="//form/sheet" position="after">
<chatter/>
</xpath>
<label for="price" position="before">
<field name="supplier_list_price"/>
<field name="supplier_discount_percent"/>
</label>
<field name="company_id" position="before">
<field name="purchasing_notes"/>
</field>
</field>
</record>
</data>
</odoo>

View file

@ -0,0 +1,91 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="product_supplierinfo_form_view_chatter" model="ir.ui.view">
<field name="name">supplierinfo.pricelist.chatter</field>
<field name="model">product.supplierinfo</field>
<field name="arch" type="xml">
<form string="Vendor Information">
<sheet>
<group>
<group name="vendor" string="Vendor">
<field name="product_variant_count" invisible="1" />
<field name="partner_id"
context="{'res_partner_search_mode': 'supplier'}" />
<field name="product_name" />
<field name="product_code" />
<label for="delay" />
<div>
<field name="delay" class="oe_inline" /> days </div>
</group>
<group string="Pricelist">
<field name="product_tmpl_id" string="Product"
invisible="context.get('visible_product_tmpl_id', True)" />
<field name="product_id" groups="product.group_product_variant"
options="{'no_create': True}" />
<label for="min_qty" />
<div class="o_row">
<field name="min_qty" />
<field name="product_uom" groups="uom.group_uom"
options="{'no_open': True}" />
</div>
<label for="price" string="Unit Price" />
<div class="o_row">
<field name="price" class="oe_inline" widget="monetary"
options="{'currency_field': 'currency_id'}" />
<field name="currency_id" groups="base.group_multi_currency"
options="{'no_open': True}" />
</div>
<label for="date_start" string="Validity" />
<div class="o_row">
<field name="date_start" class="oe_inline" /> to <field
name="date_end" class="oe_inline" />
</div>
<field name="discount" />
<field name="company_id" options="{'no_create': True}"
groups="base.group_multi_company" />
</group>
</group>
<!-- Section personnalisée pour les informations fournisseur -->
<group>
<group string="Supplier Pricing" col="2">
<field name="supplier_list_price" widget="monetary"
options="{'currency_field': 'currency_id'}" />
<field name="supplier_discount_percent" widget="percentage" />
</group>
<group string="Notes" col="1">
<field name="purchasing_notes" nolabel="1"
placeholder="Add purchasing notes, terms, conditions, or special instructions..." />
</group>
</group>
</sheet>
<!-- Chatter pour la traçabilité -->
<div class="oe_chatter">
<field name="message_follower_ids" groups="base.group_user" />
<field name="activity_ids" />
<field name="message_ids" />
</div>
</form>
</field>
</record>
<!-- Surcharge simple pour ajouter groups au champ company_id de la vue standard -->
<record id="product_supplierinfo_company_groups" model="ir.ui.view">
<field name="name">product.supplierinfo.company.groups</field>
<field name="model">product.supplierinfo</field>
<field name="inherit_id" ref="product.product_supplierinfo_form_view" />
<field name="arch" type="xml">
<field name="company_id" position="attributes">
<attribute name="groups">base.group_multi_company</attribute>
</field>
</field>
</record>
</data>
</odoo>