[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).
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
{ # noqa: B018
|
{ # noqa: B018
|
||||||
"name": "Product Sale Price from Pricelist",
|
"name": "Product Sale Price from Pricelist",
|
||||||
"version": "18.0.2.6.0",
|
"version": "18.0.2.7.0",
|
||||||
"category": "product",
|
"category": "product",
|
||||||
"summary": "Set sale price from pricelist based on last purchase price",
|
"summary": "Set sale price from pricelist based on last purchase price",
|
||||||
"author": "Odoo Community Association (OCA), Criptomart",
|
"author": "Odoo Community Association (OCA), Criptomart",
|
||||||
|
|
@ -20,5 +20,6 @@
|
||||||
"data": [
|
"data": [
|
||||||
"views/actions.xml",
|
"views/actions.xml",
|
||||||
"views/product_view.xml",
|
"views/product_view.xml",
|
||||||
|
"views/sale_order_view.xml",
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -187,6 +187,32 @@ msgstr "Triple descuento"
|
||||||
msgid "Update Sales Price"
|
msgid "Update Sales Price"
|
||||||
msgstr "Actualizar Precio de Venta"
|
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
|
#. 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
|
#: model:ir.model.fields.selection,name:product_sale_price_from_pricelist.selection__product_template__last_purchase_price_compute_type__without_discounts
|
||||||
msgid "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 # noqa: F401
|
||||||
from . import product_pricelist_item
|
from . import product_pricelist_item # noqa: F401
|
||||||
from . import product_product
|
from . import product_product # noqa: F401
|
||||||
from . import product_template
|
from . import product_template # noqa: F401
|
||||||
from . import res_config
|
from . import res_config # noqa: F401
|
||||||
from . import stock_move
|
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