product_supplierinfo_tracking
This commit is contained in:
parent
e5e90d6c7e
commit
583e25e092
21 changed files with 764 additions and 188 deletions
27
product_pricelist_supplierinfo_chatter/__manifest__.py.backup
Executable file
27
product_pricelist_supplierinfo_chatter/__manifest__.py.backup
Executable 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:
|
||||
|
|
@ -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):
|
||||
BIN
product_pricelist_supplierinfo_chatter/static/description/icon.png
Executable file → Normal file
BIN
product_pricelist_supplierinfo_chatter/static/description/icon.png
Executable file → Normal file
Binary file not shown.
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 3.6 KiB |
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
3
product_supplierinfo_pricing/__init__.py
Normal file
3
product_supplierinfo_pricing/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import models
|
||||
24
product_supplierinfo_pricing/__manifest__.py
Normal file
24
product_supplierinfo_pricing/__manifest__.py
Normal 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:
|
||||
3
product_supplierinfo_pricing/models/__init__.py
Normal file
3
product_supplierinfo_pricing/models/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import product_supplierinfo
|
||||
57
product_supplierinfo_pricing/models/product_supplierinfo.py
Normal file
57
product_supplierinfo_pricing/models/product_supplierinfo.py
Normal 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)
|
||||
|
|
@ -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>
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
from . import product_supplierinfo
|
||||
from . import product_supplier_info
|
||||
88
product_supplierinfo_tracking/models/product_supplier_info.py
Executable file
88
product_supplierinfo_tracking/models/product_supplier_info.py
Executable 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 |
3
product_supplierinfo_tracking/tests/__init__.py
Normal file
3
product_supplierinfo_tracking/tests/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import test_product_supplierinfo_tracking
|
||||
|
|
@ -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 --> 80.0', message_body)
|
||||
self.assertIn('min_qty --> 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 --> 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 --> 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')
|
||||
|
|
@ -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>
|
||||
27
product_supplierinfo_tracking/views/supplierinfo.xml
Normal file
27
product_supplierinfo_tracking/views/supplierinfo.xml
Normal 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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
Loading…
Reference in a new issue