[ADD] product_sale_price_from_pricelist: update sale orders from list price
This commit is contained in:
parent
ced21cc489
commit
55497327e8
7 changed files with 292 additions and 7 deletions
|
|
@ -2,7 +2,7 @@
|
|||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
{ # noqa: B018
|
||||
"name": "Product Sale Price from Pricelist",
|
||||
"version": "18.0.2.6.0",
|
||||
"version": "18.0.2.7.0",
|
||||
"category": "product",
|
||||
"summary": "Set sale price from pricelist based on last purchase price",
|
||||
"author": "Odoo Community Association (OCA), Criptomart",
|
||||
|
|
@ -20,5 +20,6 @@
|
|||
"data": [
|
||||
"views/actions.xml",
|
||||
"views/product_view.xml",
|
||||
"views/sale_order_view.xml",
|
||||
],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -187,6 +187,32 @@ msgstr "Triple descuento"
|
|||
msgid "Update Sales Price"
|
||||
msgstr "Actualizar Precio de Venta"
|
||||
|
||||
#. module: product_sale_price_from_pricelist
|
||||
#: model:ir.actions.server,name:product_sale_price_from_pricelist.action_sale_update_from_list_price
|
||||
#: model_terms:ir.ui.view,arch_db:product_sale_price_from_pricelist.view_order_form_update_list_price
|
||||
msgid "Update with List Price"
|
||||
msgstr "Actualizar con precio de venta"
|
||||
|
||||
#. module: product_sale_price_from_pricelist
|
||||
#: model_terms:ir.ui.view,arch_db:product_sale_price_from_pricelist.view_order_form_update_list_price
|
||||
msgid "Update skipped"
|
||||
msgstr "Actualización omitida"
|
||||
|
||||
#. module: product_sale_price_from_pricelist
|
||||
#: model_terms:ir.ui.view,arch_db:product_sale_price_from_pricelist.view_order_form_update_list_price
|
||||
msgid "No sales orders to update. Only non-invoiced and non-canceled orders are processed."
|
||||
msgstr "No hay pedidos de venta que actualizar. Solo se procesan los no facturados ni cancelados."
|
||||
|
||||
#. module: product_sale_price_from_pricelist
|
||||
#: model_terms:ir.ui.view,arch_db:product_sale_price_from_pricelist.view_order_form_update_list_price
|
||||
msgid "Sales price update"
|
||||
msgstr "Actualización de precios de venta"
|
||||
|
||||
#. module: product_sale_price_from_pricelist
|
||||
#: model_terms:ir.ui.view,arch_db:product_sale_price_from_pricelist.view_order_form_update_list_price
|
||||
msgid "Updated %(lines)s lines across %(orders)s sales orders."
|
||||
msgstr "Se actualizaron %(lines)s líneas en %(orders)s pedidos de venta."
|
||||
|
||||
#. module: product_sale_price_from_pricelist
|
||||
#: model:ir.model.fields.selection,name:product_sale_price_from_pricelist.selection__product_template__last_purchase_price_compute_type__without_discounts
|
||||
msgid "Without discounts"
|
||||
|
|
|
|||
43
product_sale_price_from_pricelist/i18n/eu.po
Normal file
43
product_sale_price_from_pricelist/i18n/eu.po
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# \t* product_sale_price_from_pricelist
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 18.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-02-27 00:00+0000\n"
|
||||
"PO-Revision-Date: 2026-02-27 00:00+0000\n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"Language: eu\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
|
||||
#. module: product_sale_price_from_pricelist
|
||||
#: model:ir.actions.server,name:product_sale_price_from_pricelist.action_sale_update_from_list_price
|
||||
#: model_terms:ir.ui.view,arch_db:product_sale_price_from_pricelist.view_order_form_update_list_price
|
||||
msgid "Update with List Price"
|
||||
msgstr "Eguneratu tarifa-prezioarekin"
|
||||
|
||||
#. module: product_sale_price_from_pricelist
|
||||
#: model_terms:ir.ui.view,arch_db:product_sale_price_from_pricelist.view_order_form_update_list_price
|
||||
msgid "Update skipped"
|
||||
msgstr "Eguneratzea saltatu da"
|
||||
|
||||
#. module: product_sale_price_from_pricelist
|
||||
#: model_terms:ir.ui.view,arch_db:product_sale_price_from_pricelist.view_order_form_update_list_price
|
||||
msgid "No sales orders to update. Only non-invoiced and non-canceled orders are processed."
|
||||
msgstr "Ez dago eguneratzeko salmenta-agindurik. Soilik fakturatu gabe eta bertan behera utzi gabe daudenak prozesatzen dira."
|
||||
|
||||
#. module: product_sale_price_from_pricelist
|
||||
#: model_terms:ir.ui.view,arch_db:product_sale_price_from_pricelist.view_order_form_update_list_price
|
||||
msgid "Sales price update"
|
||||
msgstr "Salmenta-prezioaren eguneraketa"
|
||||
|
||||
#. module: product_sale_price_from_pricelist
|
||||
#: model_terms:ir.ui.view,arch_db:product_sale_price_from_pricelist.view_order_form_update_list_price
|
||||
msgid "Updated %(lines)s lines across %(orders)s sales orders."
|
||||
msgstr "Eguneratu dira %(orders)s salmenta-agindutako %(lines)s lerro."
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
from . import product_pricelist
|
||||
from . import product_pricelist_item
|
||||
from . import product_product
|
||||
from . import product_template
|
||||
from . import res_config
|
||||
from . import stock_move
|
||||
from . import product_pricelist # noqa: F401
|
||||
from . import product_pricelist_item # noqa: F401
|
||||
from . import product_product # noqa: F401
|
||||
from . import product_template # noqa: F401
|
||||
from . import res_config # noqa: F401
|
||||
from . import stock_move # noqa: F401
|
||||
from . import sale_order # noqa: F401
|
||||
|
|
|
|||
72
product_sale_price_from_pricelist/models/sale_order.py
Normal file
72
product_sale_price_from_pricelist/models/sale_order.py
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
import logging
|
||||
|
||||
from odoo import models
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SaleOrder(models.Model):
|
||||
_inherit = "sale.order"
|
||||
|
||||
def action_update_lines_from_list_price(self):
|
||||
"""Copy current product list price into sale order lines.
|
||||
|
||||
Applies to orders that are not fully invoiced. Skips display and
|
||||
downpayment lines, keeps discounts. Totals/taxes are recomputed by
|
||||
field dependencies when the lines change.
|
||||
"""
|
||||
|
||||
eligible_orders = self.filtered(
|
||||
lambda so: so.invoice_status not in ("invoiced", "up_to_date")
|
||||
and so.state != "cancel"
|
||||
)
|
||||
|
||||
if not eligible_orders:
|
||||
return {
|
||||
"type": "ir.actions.client",
|
||||
"tag": "display_notification",
|
||||
"params": {
|
||||
"title": self.env._("Update skipped"),
|
||||
"message": self.env._(
|
||||
"No sales orders to update. Only non-invoiced and non-canceled orders are processed."
|
||||
),
|
||||
"type": "warning",
|
||||
"sticky": False,
|
||||
},
|
||||
}
|
||||
|
||||
total_lines = 0
|
||||
for order in eligible_orders:
|
||||
lines = order.order_line.filtered(
|
||||
lambda line: not line.display_type
|
||||
and not line.is_downpayment
|
||||
and line.product_id
|
||||
)
|
||||
for line in lines:
|
||||
price_unit = line.product_id.uom_id._compute_price(
|
||||
line.product_id.lst_price, line.product_uom
|
||||
)
|
||||
line.price_unit = price_unit
|
||||
|
||||
total_lines += len(lines)
|
||||
_logger.info(
|
||||
"[SALE PRICE UPDATE] Order %s (%s): updated %s lines",
|
||||
order.name,
|
||||
order.id,
|
||||
len(lines),
|
||||
)
|
||||
|
||||
return {
|
||||
"type": "ir.actions.client",
|
||||
"tag": "display_notification",
|
||||
"params": {
|
||||
"title": self.env._("Sales price update"),
|
||||
"message": self.env._(
|
||||
"Updated %(lines)s lines across %(orders)s sales orders.",
|
||||
lines=total_lines,
|
||||
orders=len(eligible_orders),
|
||||
),
|
||||
"type": "success" if total_lines else "warning",
|
||||
"sticky": False,
|
||||
},
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
# Copyright (C) 2026: Criptomart (https://criptomart.net)
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
from odoo.tests import tagged
|
||||
from odoo.tests.common import TransactionCase
|
||||
|
||||
|
||||
@tagged("post_install", "-at_install")
|
||||
class TestSaleOrderUpdate(TransactionCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.partner = cls.env.ref("base.res_partner_1")
|
||||
|
||||
cls.product = cls.env["product.product"].create(
|
||||
{
|
||||
"name": "Test Product SO",
|
||||
"list_price": 120.0,
|
||||
"standard_price": 80.0,
|
||||
}
|
||||
)
|
||||
|
||||
cls.downpayment_product = cls.env["product.product"].create(
|
||||
{
|
||||
"name": "Downpayment Product",
|
||||
"type": "service",
|
||||
"list_price": 10.0,
|
||||
}
|
||||
)
|
||||
|
||||
def _create_sale_order(self, values=None):
|
||||
vals = {
|
||||
"partner_id": self.partner.id,
|
||||
"order_line": [
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"product_id": self.product.id,
|
||||
"product_uom": self.product.uom_id.id,
|
||||
"product_uom_qty": 2.0,
|
||||
"price_unit": 50.0,
|
||||
"tax_id": [(6, 0, self.product.taxes_id.ids)],
|
||||
},
|
||||
),
|
||||
(0, 0, {"display_type": "line_note", "name": "Note"}),
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"product_id": self.downpayment_product.id,
|
||||
"product_uom": self.downpayment_product.uom_id.id,
|
||||
"product_uom_qty": 1.0,
|
||||
"price_unit": 10.0,
|
||||
"is_downpayment": True,
|
||||
},
|
||||
),
|
||||
],
|
||||
}
|
||||
if values:
|
||||
vals.update(values)
|
||||
return self.env["sale.order"].create(vals)
|
||||
|
||||
def test_update_lines_copies_list_price(self):
|
||||
order = self._create_sale_order()
|
||||
|
||||
order.action_update_lines_from_list_price()
|
||||
|
||||
line = order.order_line.filtered(lambda line: line.product_id == self.product)
|
||||
self.assertTrue(line)
|
||||
self.assertEqual(line.price_unit, self.product.list_price)
|
||||
|
||||
# Downpayment and display lines remain unchanged
|
||||
down_line = order.order_line.filtered("is_downpayment")
|
||||
self.assertTrue(down_line)
|
||||
self.assertEqual(down_line.price_unit, 10.0)
|
||||
|
||||
def test_invoiced_orders_are_skipped(self):
|
||||
order = self._create_sale_order()
|
||||
order.action_confirm()
|
||||
invoice = order._create_invoices()
|
||||
invoice.action_post()
|
||||
order._compute_invoice_status()
|
||||
self.assertEqual(order.invoice_status, "invoiced")
|
||||
|
||||
# Change product price to ensure potential change
|
||||
order.order_line.filtered(
|
||||
lambda line: line.product_id == self.product
|
||||
).price_unit = 5.0
|
||||
|
||||
order.action_update_lines_from_list_price()
|
||||
|
||||
# Price should remain untouched because order is invoiced
|
||||
line = order.order_line.filtered(lambda line: line.product_id == self.product)
|
||||
self.assertEqual(line.price_unit, 5.0)
|
||||
47
product_sale_price_from_pricelist/views/sale_order_view.xml
Normal file
47
product_sale_price_from_pricelist/views/sale_order_view.xml
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<data>
|
||||
|
||||
<!-- Server action for mass update from list view -->
|
||||
<record id="action_sale_update_from_list_price" model="ir.actions.server">
|
||||
<field name="name">Update with List Price</field>
|
||||
<field name="model_id" ref="sale.model_sale_order" />
|
||||
<field name="binding_model_id" ref="sale.model_sale_order" />
|
||||
<field name="binding_view_types">list,form</field>
|
||||
<field name="state">code</field>
|
||||
<field name="code">records.action_update_lines_from_list_price()</field>
|
||||
</record>
|
||||
|
||||
<!-- Button in sale order form -->
|
||||
<record id="view_order_form_update_list_price" model="ir.ui.view">
|
||||
<field name="name">sale.order.form.update.list.price</field>
|
||||
<field name="model">sale.order</field>
|
||||
<field name="inherit_id" ref="sale.view_order_form" />
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//button[@name='action_update_prices']" position="after">
|
||||
<button
|
||||
name="action_update_lines_from_list_price"
|
||||
type="object"
|
||||
string="Update with List Price"
|
||||
class="btn-link mb-1 px-0"
|
||||
icon="fa-refresh"
|
||||
invisible="invoice_status in ('invoiced','up_to_date') or state == 'cancel'"
|
||||
/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Action menu entry in tree/list view -->
|
||||
<record id="view_order_tree_update_list_price" model="ir.ui.view">
|
||||
<field name="name">sale.order.tree.update.list.price</field>
|
||||
<field name="model">sale.order</field>
|
||||
<field name="inherit_id" ref="sale.view_quotation_tree" />
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//list" position="attributes">
|
||||
<attribute name="decoration-muted">state == 'cancel'</attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
Loading…
Add table
Add a link
Reference in a new issue