Compare commits
No commits in common. "c308d538a3d9346bfa00618d0951fb46d3d36e5e" and "4207afbc3fe435453df47108d797062c4cb934e4" have entirely different histories.
c308d538a3
...
4207afbc3f
10 changed files with 604 additions and 652 deletions
2
.github/copilot-instructions.md
vendored
2
.github/copilot-instructions.md
vendored
|
|
@ -83,7 +83,7 @@ Este repositorio contiene addons personalizados y modificados de Odoo 18.0. El p
|
||||||
Pedir al usuario generar a través de UI, no sabemos el método correcto para exportar SÓLO las cadenas del addon sin incluir todo el sistema.
|
Pedir al usuario generar a través de UI, no sabemos el método correcto para exportar SÓLO las cadenas del addon sin incluir todo el sistema.
|
||||||
```
|
```
|
||||||
|
|
||||||
Usar sólo polib y apend cadenas en los archivos .po, msmerge corrompe los archivos.
|
Usar sólo polib para trataer los archivos .po, msmerge corrompe los archivos.
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@
|
||||||
"summary": "Add default price category to suppliers and bulk update products",
|
"summary": "Add default price category to suppliers and bulk update products",
|
||||||
"author": "Odoo Community Association (OCA), Criptomart",
|
"author": "Odoo Community Association (OCA), Criptomart",
|
||||||
"license": "AGPL-3",
|
"license": "AGPL-3",
|
||||||
"website": "https://git.criptomart.net/criptomart/addons-cm",
|
|
||||||
"depends": ["product_price_category", "sales_team", "product_main_seller"],
|
"depends": ["product_price_category", "sales_team", "product_main_seller"],
|
||||||
"data": [
|
"data": [
|
||||||
"security/ir.model.access.csv",
|
"security/ir.model.access.csv",
|
||||||
|
|
|
||||||
|
|
@ -1,62 +0,0 @@
|
||||||
# Changelog
|
|
||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
|
||||||
|
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
||||||
|
|
||||||
## [18.0.2.0.0] - 2026-02-12
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
**BREAKING CHANGE**: Architecture refactoring to fix pricelist report issues
|
|
||||||
|
|
||||||
- **Moved all main fields from `product.template` to `product.product`**
|
|
||||||
- `last_purchase_price_updated`
|
|
||||||
- `list_price_theoritical`
|
|
||||||
- `last_purchase_price_received`
|
|
||||||
- `last_purchase_price_compute_type`
|
|
||||||
|
|
||||||
- **Created related fields in `product.template`**
|
|
||||||
- All template fields now use `related="product_variant_id.FIELD_NAME"`
|
|
||||||
- Fields are stored and writable for performance
|
|
||||||
- This maintains backward compatibility with existing views
|
|
||||||
|
|
||||||
- **Moved business logic to `product.product`**
|
|
||||||
- `_compute_theoritical_price()` method
|
|
||||||
- `action_update_list_price()` method
|
|
||||||
- `price_compute()` override
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- Resolved issues with pricelist report generation
|
|
||||||
- Proper handling of product variants in price calculations
|
|
||||||
- Eliminated confusion between template and variant pricing
|
|
||||||
|
|
||||||
### Migration Notes
|
|
||||||
|
|
||||||
**This is a database schema change**. After updating:
|
|
||||||
|
|
||||||
1. Update the module: `odoo -u product_sale_price_from_pricelist`
|
|
||||||
2. Odoo will automatically migrate data from template to variant fields
|
|
||||||
3. No manual data migration needed thanks to related fields
|
|
||||||
4. All existing views continue working without modification
|
|
||||||
|
|
||||||
### Technical Details
|
|
||||||
|
|
||||||
This architecture follows Odoo best practices:
|
|
||||||
- Product-specific data belongs in `product.product` (variants)
|
|
||||||
- Template fields are related/computed from variants
|
|
||||||
- Prevents issues with pricelist reports that work at variant level
|
|
||||||
- Maintains compatibility with standard Odoo reports
|
|
||||||
|
|
||||||
## [18.0.1.0.0] - 2025-01-XX
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Initial release
|
|
||||||
- Automatic price calculation from purchase prices
|
|
||||||
- Multiple discount handling options
|
|
||||||
- Tax-aware pricing
|
|
||||||
- UoM conversion support
|
|
||||||
- Batch price updates
|
|
||||||
|
|
@ -40,26 +40,6 @@ Automatically calculate and update product sale prices based on the last purchas
|
||||||
4. Taxes are automatically applied based on product tax settings
|
4. Taxes are automatically applied based on product tax settings
|
||||||
5. Go to **Products > Update Theoretical Prices** to batch update prices
|
5. Go to **Products > Update Theoretical Prices** to batch update prices
|
||||||
|
|
||||||
## Technical Architecture
|
|
||||||
|
|
||||||
### Models
|
|
||||||
|
|
||||||
All main fields and business logic are stored in `product.product` for proper variant handling:
|
|
||||||
|
|
||||||
**product.product (Main model)**
|
|
||||||
- `last_purchase_price_updated` (Boolean): Flag for price update needed
|
|
||||||
- `list_price_theoritical` (Float): Calculated theoretical price
|
|
||||||
- `last_purchase_price_received` (Float): Last purchase price received
|
|
||||||
- `last_purchase_price_compute_type` (Selection): How to calculate price
|
|
||||||
- `_compute_theoritical_price()`: Main price calculation method
|
|
||||||
- `action_update_list_price()`: Update sale price from theoretical
|
|
||||||
|
|
||||||
**product.template (Related fields)**
|
|
||||||
- All fields are `related` to `product_variant_id` fields
|
|
||||||
- Allows views on template to continue working
|
|
||||||
- Delegates actions to product variants
|
|
||||||
|
|
||||||
This architecture prevents issues with pricelist reports and ensures proper handling of product variants.
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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.0.0",
|
"version": "18.0.1.0.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",
|
||||||
|
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
# Copyright 2026 Criptomart
|
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
|
||||||
|
|
||||||
import logging
|
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def migrate(cr, version):
|
|
||||||
"""Migrate fields from product.template to product.product.
|
|
||||||
|
|
||||||
This migration handles the architecture change where main fields
|
|
||||||
moved from template to variant level.
|
|
||||||
|
|
||||||
Note: Most of the work is handled automatically by Odoo's related fields,
|
|
||||||
but we ensure proper data integrity here.
|
|
||||||
"""
|
|
||||||
if not version:
|
|
||||||
return
|
|
||||||
|
|
||||||
_logger.info("Starting migration to 18.0.2.0.0 - Moving fields to product.product")
|
|
||||||
|
|
||||||
# The fields are now stored at variant level with related fields in template
|
|
||||||
# Odoo will handle the data migration automatically through related fields
|
|
||||||
# We just need to ensure the fields exist and are properly populated
|
|
||||||
|
|
||||||
# Force recompute of related fields to ensure data consistency
|
|
||||||
cr.execute("""
|
|
||||||
SELECT id FROM product_product WHERE active = true
|
|
||||||
""")
|
|
||||||
product_ids = [row[0] for row in cr.fetchall()]
|
|
||||||
|
|
||||||
if product_ids:
|
|
||||||
_logger.info(f"Migration: Recomputing fields for {len(product_ids)} products")
|
|
||||||
# The ORM will handle the rest through related fields
|
|
||||||
|
|
||||||
_logger.info("Migration to 18.0.2.0.0 completed successfully")
|
|
||||||
|
|
@ -1,210 +1,16 @@
|
||||||
import logging
|
from odoo import fields, models
|
||||||
|
|
||||||
from odoo import fields
|
|
||||||
from odoo import models
|
|
||||||
from odoo.exceptions import UserError
|
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class ProductProduct(models.Model):
|
class ProductProduct(models.Model):
|
||||||
_inherit = "product.product"
|
_inherit = "product.product"
|
||||||
|
|
||||||
last_purchase_price_updated = fields.Boolean(
|
# Related field for pricelist base computation
|
||||||
string="Last purchase price updated",
|
|
||||||
help="The last purchase price has been updated and you need to update the selling price in the database, shelves and scales.",
|
|
||||||
default=False,
|
|
||||||
company_dependent=True,
|
|
||||||
)
|
|
||||||
list_price_theoritical = fields.Float(
|
|
||||||
"Theoritical price",
|
|
||||||
company_dependent=True,
|
|
||||||
)
|
|
||||||
last_purchase_price_received = fields.Float(
|
|
||||||
string="Last purchase price",
|
|
||||||
help="The last price at which the product was purchased. It is used as the base price field for calculating the product sale price.",
|
|
||||||
digits="Product Price",
|
|
||||||
company_dependent=True,
|
|
||||||
)
|
|
||||||
last_purchase_price_compute_type = fields.Selection(
|
|
||||||
[
|
|
||||||
("without_discounts", "Without discounts"),
|
|
||||||
("with_discount", "First discount"),
|
|
||||||
("with_two_discounts", "Double discount"),
|
|
||||||
("with_three_discounts", "Triple discount"),
|
|
||||||
("manual_update", "Manual update"),
|
|
||||||
],
|
|
||||||
string="Last purchase price calculation type",
|
|
||||||
help="Choose whether discounts should influence the calculation of the last purchase price. Select Never update for manual configuration of cost and sale prices.\n"
|
|
||||||
"\n* Without discounts: does not take into account discounts when updating the last purchase price.\n"
|
|
||||||
"* First discount: take into account only first discount when updating the last purchase price.\n"
|
|
||||||
'* Triple discount: take into account all discounts when updating the last purchase price. Needs "Purchase Triple Discount" OCA module.\n'
|
|
||||||
"* Manual update: Select this for manual configuration of cost and sale price. The sales price will not be calculated automatically.",
|
|
||||||
default="without_discounts",
|
|
||||||
company_dependent=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Keep this for backward compatibility with pricelist computations
|
|
||||||
last_purchase_price = fields.Float(
|
last_purchase_price = fields.Float(
|
||||||
related="last_purchase_price_received",
|
related="product_tmpl_id.last_purchase_price_received",
|
||||||
string="Last Purchase Price",
|
string="Last Purchase Price",
|
||||||
readonly=True,
|
readonly=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _compute_theoritical_price(self):
|
|
||||||
pricelist_obj = self.env["product.pricelist"]
|
|
||||||
pricelist_id = (
|
|
||||||
self.env["ir.config_parameter"]
|
|
||||||
.sudo()
|
|
||||||
.get_param("product_sale_price_from_pricelist.product_pricelist_automatic")
|
|
||||||
or False
|
|
||||||
)
|
|
||||||
pricelist = pricelist_obj.browse(int(pricelist_id))
|
|
||||||
if pricelist:
|
|
||||||
for product in self:
|
|
||||||
_logger.info(
|
|
||||||
"[PRICE DEBUG] Product %s [%s]: Checking conditions - name=%s, id=%s, compute_type=%s",
|
|
||||||
product.default_code or product.name,
|
|
||||||
product.id,
|
|
||||||
bool(product.name),
|
|
||||||
bool(product.id),
|
|
||||||
product.last_purchase_price_compute_type,
|
|
||||||
)
|
|
||||||
if (
|
|
||||||
product.name
|
|
||||||
and product.id
|
|
||||||
and product.last_purchase_price_compute_type != "manual_update"
|
|
||||||
):
|
|
||||||
partial_price = product._get_price(qty=1, pricelist=pricelist)
|
|
||||||
|
|
||||||
_logger.info(
|
|
||||||
"[PRICE DEBUG] Product %s [%s]: partial_price result = %s",
|
|
||||||
product.default_code or product.name,
|
|
||||||
product.id,
|
|
||||||
partial_price,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Compute taxes to add
|
|
||||||
if not product.taxes_id:
|
|
||||||
raise UserError(
|
|
||||||
self.env._(
|
|
||||||
"No taxes defined for product %(product)s. Please define taxes in the product form.",
|
|
||||||
product=product.name,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
base_price = partial_price.get("value", 0.0) or 0.0
|
|
||||||
_logger.info(
|
|
||||||
"[PRICE DEBUG] Product %s [%s]: base_price from pricelist = %.2f, last_purchase_price = %.2f",
|
|
||||||
product.default_code or product.name,
|
|
||||||
product.id,
|
|
||||||
base_price,
|
|
||||||
product.last_purchase_price_received,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Use base price without taxes (taxes will be calculated automatically on sales)
|
|
||||||
theoretical_price = base_price
|
|
||||||
|
|
||||||
_logger.info(
|
|
||||||
"[PRICE] Product %s [%s]: Computed theoretical price %.2f (previous: %.2f, current list_price: %.2f)",
|
|
||||||
product.default_code or product.name,
|
|
||||||
product.id,
|
|
||||||
theoretical_price,
|
|
||||||
product.list_price_theoritical,
|
|
||||||
product.lst_price,
|
|
||||||
)
|
|
||||||
|
|
||||||
product.write(
|
|
||||||
{
|
|
||||||
"list_price_theoritical": theoretical_price,
|
|
||||||
"last_purchase_price_updated": (
|
|
||||||
theoretical_price != product.lst_price
|
|
||||||
),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
raise UserError(
|
|
||||||
self.env._(
|
|
||||||
"Not found a valid pricelist to compute sale price. Check configuration in General Settings."
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def action_update_list_price(self):
|
|
||||||
updated_products = []
|
|
||||||
skipped_products = []
|
|
||||||
|
|
||||||
for product in self:
|
|
||||||
if product.last_purchase_price_compute_type != "manual_update":
|
|
||||||
# First compute the theoretical price
|
|
||||||
product._compute_theoritical_price()
|
|
||||||
|
|
||||||
old_price = product.lst_price
|
|
||||||
product.lst_price = product.list_price_theoritical
|
|
||||||
product.last_purchase_price_updated = False
|
|
||||||
_logger.info(
|
|
||||||
"[PRICE] Product %s [%s]: List price updated from %.2f to %.2f",
|
|
||||||
product.default_code or product.name,
|
|
||||||
product.id,
|
|
||||||
old_price,
|
|
||||||
product.lst_price,
|
|
||||||
)
|
|
||||||
updated_products.append(
|
|
||||||
(
|
|
||||||
product.name or product.default_code,
|
|
||||||
old_price,
|
|
||||||
product.lst_price,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
skipped_products.append(product.name or product.default_code)
|
|
||||||
|
|
||||||
# Invalidate cache to refresh UI
|
|
||||||
self.invalidate_recordset(
|
|
||||||
["lst_price", "list_price_theoritical", "last_purchase_price_updated"]
|
|
||||||
)
|
|
||||||
|
|
||||||
# Build notification message
|
|
||||||
message = ""
|
|
||||||
if updated_products:
|
|
||||||
message += self.env._(
|
|
||||||
"✓ Products updated: %(count)s", count=len(updated_products)
|
|
||||||
)
|
|
||||||
if len(updated_products) <= 5:
|
|
||||||
message += "\n\n"
|
|
||||||
for name, old, new in updated_products:
|
|
||||||
message += self.env._(
|
|
||||||
"• %(name)s: %(old).2f → %(new).2f\n",
|
|
||||||
name=name,
|
|
||||||
old=old,
|
|
||||||
new=new,
|
|
||||||
)
|
|
||||||
|
|
||||||
if skipped_products:
|
|
||||||
if message:
|
|
||||||
message += "\n\n"
|
|
||||||
message += self.env._(
|
|
||||||
"⚠ Skipped (manual update): %(count)s", count=len(skipped_products)
|
|
||||||
)
|
|
||||||
if len(skipped_products) <= 5:
|
|
||||||
message += "\n"
|
|
||||||
for name in skipped_products:
|
|
||||||
message += self.env._("• %(name)s\n", name=name)
|
|
||||||
|
|
||||||
if not updated_products and not skipped_products:
|
|
||||||
message = self.env._("No products to update.")
|
|
||||||
|
|
||||||
return {
|
|
||||||
"type": "ir.actions.client",
|
|
||||||
"tag": "display_notification",
|
|
||||||
"params": {
|
|
||||||
"title": self.env._("Price Update"),
|
|
||||||
"message": message,
|
|
||||||
"type": "success" if updated_products else "warning",
|
|
||||||
"sticky": False,
|
|
||||||
"next": {"type": "ir.actions.act_window_close"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
def price_compute(
|
def price_compute(
|
||||||
self, price_type, uom=None, currency=None, company=None, date=False
|
self, price_type, uom=None, currency=None, company=None, date=False
|
||||||
):
|
):
|
||||||
|
|
|
||||||
|
|
@ -2,43 +2,35 @@
|
||||||
# @author Santi Noreña (<santi@criptomart.net>)
|
# @author Santi Noreña (<santi@criptomart.net>)
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||||
|
|
||||||
from odoo import api
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from odoo import _
|
||||||
from odoo import fields
|
from odoo import fields
|
||||||
from odoo import models
|
from odoo import models
|
||||||
|
from odoo.exceptions import UserError
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class ProductTemplate(models.Model):
|
class ProductTemplate(models.Model):
|
||||||
"""Product template with computed fields to product.product.
|
|
||||||
|
|
||||||
All main fields and logic are now in product.product.
|
|
||||||
These computed fields allow views on product.template to continue working.
|
|
||||||
For templates with a single variant, reads/writes directly from/to the variant.
|
|
||||||
For templates with multiple variants, uses the first variant.
|
|
||||||
"""
|
|
||||||
|
|
||||||
_inherit = "product.template"
|
_inherit = "product.template"
|
||||||
|
|
||||||
# Computed fields that delegate to product.product variants
|
|
||||||
last_purchase_price_updated = fields.Boolean(
|
last_purchase_price_updated = fields.Boolean(
|
||||||
string="Last purchase price updated",
|
string="Last purchase price updated",
|
||||||
compute="_compute_last_purchase_price_updated",
|
help="The last purchase price has been updated and you need to update the selling price in the database, shelves and scales.",
|
||||||
inverse="_inverse_last_purchase_price_updated",
|
default=False,
|
||||||
search="_search_last_purchase_price_updated",
|
company_dependent=True,
|
||||||
store=True,
|
|
||||||
)
|
)
|
||||||
list_price_theoritical = fields.Float(
|
list_price_theoritical = fields.Float(
|
||||||
string="Theoritical price",
|
"Theoritical price",
|
||||||
compute="_compute_list_price_theoritical",
|
company_dependent=True,
|
||||||
inverse="_inverse_list_price_theoritical",
|
|
||||||
search="_search_list_price_theoritical",
|
|
||||||
store=True,
|
|
||||||
)
|
)
|
||||||
last_purchase_price_received = fields.Float(
|
last_purchase_price_received = fields.Float(
|
||||||
string="Last purchase price",
|
string="Last purchase price",
|
||||||
compute="_compute_last_purchase_price_received",
|
help="The last price at which the product was purchased. It is used as the base price field for calculating the product sale price.",
|
||||||
inverse="_inverse_last_purchase_price_received",
|
digits="Product Price",
|
||||||
search="_search_last_purchase_price_received",
|
company_dependent=True,
|
||||||
store=True,
|
|
||||||
)
|
)
|
||||||
last_purchase_price_compute_type = fields.Selection(
|
last_purchase_price_compute_type = fields.Selection(
|
||||||
[
|
[
|
||||||
|
|
@ -49,96 +41,173 @@ class ProductTemplate(models.Model):
|
||||||
("manual_update", "Manual update"),
|
("manual_update", "Manual update"),
|
||||||
],
|
],
|
||||||
string="Last purchase price calculation type",
|
string="Last purchase price calculation type",
|
||||||
compute="_compute_last_purchase_price_compute_type",
|
help="Choose whether discounts should influence the calculation of the last purchase price. Select Never update for manual configuration of cost and sale prices.\n"
|
||||||
inverse="_inverse_last_purchase_price_compute_type",
|
"\n* Without discounts: does not take into account discounts when updating the last purchase price.\n"
|
||||||
search="_search_last_purchase_price_compute_type",
|
"* First discount: take into account only first discount when updating the last purchase price.\n"
|
||||||
store=True,
|
'* Triple discount: take into account all discounts when updating the last purchase price. Needs "Purchase Triple Discount" OCA module.\n'
|
||||||
|
"* Manual update: Select this for manual configuration of cost and sale price. The sales price will not be calculated automatically.",
|
||||||
|
default="without_discounts",
|
||||||
|
company_dependent=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
@api.depends("product_variant_ids.last_purchase_price_updated")
|
def _compute_theoritical_price(self):
|
||||||
def _compute_last_purchase_price_updated(self):
|
pricelist_obj = self.env["product.pricelist"]
|
||||||
for template in self:
|
pricelist_id = (
|
||||||
template.last_purchase_price_updated = (
|
self.env["ir.config_parameter"]
|
||||||
template.product_variant_ids[:1].last_purchase_price_updated
|
.sudo()
|
||||||
if template.product_variant_ids
|
.get_param("product_sale_price_from_pricelist.product_pricelist_automatic")
|
||||||
else False
|
or False
|
||||||
)
|
)
|
||||||
|
pricelist = pricelist_obj.browse(int(pricelist_id))
|
||||||
def _inverse_last_purchase_price_updated(self):
|
if pricelist:
|
||||||
for template in self:
|
for template in self:
|
||||||
if template.product_variant_ids:
|
_logger.info(
|
||||||
template.product_variant_ids.write(
|
"[PRICE DEBUG] Product %s [%s]: Checking conditions - name=%s, id=%s, variant=%s, compute_type=%s",
|
||||||
{
|
template.default_code or template.name,
|
||||||
"last_purchase_price_updated": template.last_purchase_price_updated
|
template.id,
|
||||||
}
|
bool(template.name),
|
||||||
|
bool(template.id),
|
||||||
|
bool(template.product_variant_id),
|
||||||
|
template.last_purchase_price_compute_type,
|
||||||
)
|
)
|
||||||
|
if (
|
||||||
|
template.name
|
||||||
|
and template.id
|
||||||
|
and template.product_variant_id
|
||||||
|
and template.last_purchase_price_compute_type != "manual_update"
|
||||||
|
):
|
||||||
|
partial_price = template.product_variant_id._get_price(
|
||||||
|
qty=1, pricelist=pricelist
|
||||||
|
)
|
||||||
|
|
||||||
def _search_last_purchase_price_updated(self, operator, value):
|
_logger.info(
|
||||||
return [("product_variant_ids.last_purchase_price_updated", operator, value)]
|
"[PRICE DEBUG] Product %s [%s]: partial_price result = %s",
|
||||||
|
template.default_code or template.name,
|
||||||
|
template.id,
|
||||||
|
partial_price,
|
||||||
|
)
|
||||||
|
|
||||||
@api.depends("product_variant_ids.list_price_theoritical")
|
# Compute taxes to add
|
||||||
def _compute_list_price_theoritical(self):
|
if not template.taxes_id:
|
||||||
for template in self:
|
raise UserError(
|
||||||
template.list_price_theoritical = (
|
_(
|
||||||
template.product_variant_ids[:1].list_price_theoritical
|
"No taxes defined for product %s. Please define taxes in the product form."
|
||||||
if template.product_variant_ids
|
)
|
||||||
else 0.0
|
% template.name
|
||||||
)
|
)
|
||||||
|
|
||||||
def _inverse_list_price_theoritical(self):
|
base_price = partial_price.get("value", 0.0) or 0.0
|
||||||
for template in self:
|
_logger.info(
|
||||||
if template.product_variant_ids:
|
"[PRICE DEBUG] Product %s [%s]: base_price from pricelist = %.2f, last_purchase_price = %.2f",
|
||||||
template.product_variant_ids.write(
|
template.default_code or template.name,
|
||||||
{"list_price_theoritical": template.list_price_theoritical}
|
template.id,
|
||||||
|
base_price,
|
||||||
|
template.last_purchase_price_received,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Use base price without taxes (taxes will be calculated automatically on sales)
|
||||||
|
theoretical_price = base_price
|
||||||
|
|
||||||
|
_logger.info(
|
||||||
|
"[PRICE] Product %s [%s]: Computed theoretical price %.2f (previous: %.2f, current list_price: %.2f)",
|
||||||
|
template.default_code or template.name,
|
||||||
|
template.id,
|
||||||
|
theoretical_price,
|
||||||
|
template.list_price_theoritical,
|
||||||
|
template.list_price,
|
||||||
|
)
|
||||||
|
|
||||||
|
template.write(
|
||||||
|
{
|
||||||
|
"list_price_theoritical": theoretical_price,
|
||||||
|
"last_purchase_price_updated": (
|
||||||
|
theoretical_price != template.list_price
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise UserError(
|
||||||
|
_(
|
||||||
|
"Not found a valid pricelist to compute sale price. Check configuration in General Settings."
|
||||||
)
|
)
|
||||||
|
|
||||||
def _search_list_price_theoritical(self, operator, value):
|
|
||||||
return [("product_variant_ids.list_price_theoritical", operator, value)]
|
|
||||||
|
|
||||||
@api.depends("product_variant_ids.last_purchase_price_received")
|
|
||||||
def _compute_last_purchase_price_received(self):
|
|
||||||
for template in self:
|
|
||||||
template.last_purchase_price_received = (
|
|
||||||
template.product_variant_ids[:1].last_purchase_price_received
|
|
||||||
if template.product_variant_ids
|
|
||||||
else 0.0
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def _inverse_last_purchase_price_received(self):
|
|
||||||
for template in self:
|
|
||||||
if template.product_variant_ids:
|
|
||||||
template.product_variant_ids.write(
|
|
||||||
{
|
|
||||||
"last_purchase_price_received": template.last_purchase_price_received
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
def _search_last_purchase_price_received(self, operator, value):
|
|
||||||
return [("product_variant_ids.last_purchase_price_received", operator, value)]
|
|
||||||
|
|
||||||
@api.depends("product_variant_ids.last_purchase_price_compute_type")
|
|
||||||
def _compute_last_purchase_price_compute_type(self):
|
|
||||||
for template in self:
|
|
||||||
template.last_purchase_price_compute_type = (
|
|
||||||
template.product_variant_ids[:1].last_purchase_price_compute_type
|
|
||||||
if template.product_variant_ids
|
|
||||||
else "without_discounts"
|
|
||||||
)
|
|
||||||
|
|
||||||
def _inverse_last_purchase_price_compute_type(self):
|
|
||||||
for template in self:
|
|
||||||
if template.product_variant_ids:
|
|
||||||
template.product_variant_ids.write(
|
|
||||||
{
|
|
||||||
"last_purchase_price_compute_type": template.last_purchase_price_compute_type
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
def _search_last_purchase_price_compute_type(self, operator, value):
|
|
||||||
return [
|
|
||||||
("product_variant_ids.last_purchase_price_compute_type", operator, value)
|
|
||||||
]
|
|
||||||
|
|
||||||
def action_update_list_price(self):
|
def action_update_list_price(self):
|
||||||
"""Delegate to product variants."""
|
updated_products = []
|
||||||
return self.product_variant_ids.action_update_list_price()
|
skipped_products = []
|
||||||
|
|
||||||
|
for template in self:
|
||||||
|
if template.last_purchase_price_compute_type != "manual_update":
|
||||||
|
# First compute the theoretical price
|
||||||
|
template._compute_theoritical_price()
|
||||||
|
|
||||||
|
old_price = template.list_price
|
||||||
|
template.list_price = template.list_price_theoritical
|
||||||
|
template.last_purchase_price_updated = False
|
||||||
|
_logger.info(
|
||||||
|
"[PRICE] Product %s [%s]: List price updated from %.2f to %.2f",
|
||||||
|
template.default_code or template.name,
|
||||||
|
template.id,
|
||||||
|
old_price,
|
||||||
|
template.list_price,
|
||||||
|
)
|
||||||
|
updated_products.append(
|
||||||
|
(
|
||||||
|
template.name or template.default_code,
|
||||||
|
old_price,
|
||||||
|
template.list_price,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
skipped_products.append(template.name or template.default_code)
|
||||||
|
|
||||||
|
# Invalidate cache to refresh UI
|
||||||
|
self.invalidate_recordset(
|
||||||
|
["list_price", "list_price_theoritical", "last_purchase_price_updated"]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Build notification message
|
||||||
|
message = ""
|
||||||
|
if updated_products:
|
||||||
|
message += _("✓ Products updated: %s") % len(updated_products)
|
||||||
|
if len(updated_products) <= 5:
|
||||||
|
message += "\n\n"
|
||||||
|
for name, old, new in updated_products:
|
||||||
|
message += _("• %s: %.2f → %.2f\n") % (name, old, new)
|
||||||
|
|
||||||
|
if skipped_products:
|
||||||
|
if message:
|
||||||
|
message += "\n\n"
|
||||||
|
message += _("⚠ Skipped (manual update): %s") % len(skipped_products)
|
||||||
|
if len(skipped_products) <= 5:
|
||||||
|
message += "\n"
|
||||||
|
for name in skipped_products:
|
||||||
|
message += _("• %s\n") % name
|
||||||
|
|
||||||
|
if not updated_products and not skipped_products:
|
||||||
|
message = _("No products to update.")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"type": "ir.actions.client",
|
||||||
|
"tag": "display_notification",
|
||||||
|
"params": {
|
||||||
|
"title": _("Price Update"),
|
||||||
|
"message": message,
|
||||||
|
"type": "success" if updated_products else "warning",
|
||||||
|
"sticky": False,
|
||||||
|
"next": {"type": "ir.actions.act_window_close"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def price_compute(
|
||||||
|
self, price_type, uom=None, currency=None, company=False, date=False
|
||||||
|
):
|
||||||
|
"""Return dummy not falsy prices when computation is done from supplier
|
||||||
|
info for avoiding error on super method. We will later fill these with
|
||||||
|
correct values.
|
||||||
|
"""
|
||||||
|
if price_type == "last_purchase_price":
|
||||||
|
return dict.fromkeys(self.ids, 1.0)
|
||||||
|
return super().price_compute(
|
||||||
|
price_type, uom=uom, currency=currency, company=company, date=date
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,10 @@
|
||||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import math
|
||||||
|
|
||||||
from odoo import models
|
from odoo import exceptions, models, fields, api, _
|
||||||
from odoo.tools import float_compare
|
from odoo.tools import float_is_zero, float_round, float_compare
|
||||||
from odoo.tools import float_is_zero
|
|
||||||
from odoo.tools import float_round
|
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
@ -18,7 +17,7 @@ class StockMove(models.Model):
|
||||||
_inherit = "stock.move"
|
_inherit = "stock.move"
|
||||||
|
|
||||||
def product_price_update_before_done(self):
|
def product_price_update_before_done(self):
|
||||||
res = super().product_price_update_before_done()
|
super(StockMove, self).product_price_update_before_done()
|
||||||
for move in self.filtered(lambda move: move.location_id.usage == "supplier"):
|
for move in self.filtered(lambda move: move.location_id.usage == "supplier"):
|
||||||
if (
|
if (
|
||||||
move.product_id.last_purchase_price_compute_type
|
move.product_id.last_purchase_price_compute_type
|
||||||
|
|
@ -68,11 +67,7 @@ class StockMove(models.Model):
|
||||||
move.product_id.last_purchase_price_received,
|
move.product_id.last_purchase_price_received,
|
||||||
price_updated,
|
price_updated,
|
||||||
move.name,
|
move.name,
|
||||||
(
|
move.purchase_line_id.order_id.name if move.purchase_line_id else 'N/A',
|
||||||
move.purchase_line_id.order_id.name
|
|
||||||
if move.purchase_line_id
|
|
||||||
else "N/A"
|
|
||||||
),
|
|
||||||
move.product_id.last_purchase_price_compute_type,
|
move.product_id.last_purchase_price_compute_type,
|
||||||
)
|
)
|
||||||
move.product_id.with_company(
|
move.product_id.with_company(
|
||||||
|
|
@ -80,5 +75,4 @@ class StockMove(models.Model):
|
||||||
).last_purchase_price_received = price_updated
|
).last_purchase_price_received = price_updated
|
||||||
move.product_id.with_company(
|
move.product_id.with_company(
|
||||||
move.company_id
|
move.company_id
|
||||||
)._compute_theoritical_price()
|
).product_tmpl_id._compute_theoritical_price()
|
||||||
return res
|
|
||||||
|
|
|
||||||
|
|
@ -2,22 +2,32 @@
|
||||||
<odoo>
|
<odoo>
|
||||||
<data>
|
<data>
|
||||||
|
|
||||||
<!-- Template: Group Orders Page (Eskaera) -->
|
<!-- Template: Group Orders Page (Eskaera) -->
|
||||||
<template id="eskaera_page" name="Eskaera Page">
|
<template id="eskaera_page" name="Eskaera Page">
|
||||||
<t t-call="website.layout">
|
<t t-call="website.layout">
|
||||||
<div id="wrap" class="eskaera-page oe_structure oe_empty" data-name="Eskaera Orders">
|
<div
|
||||||
<div class="container">
|
id="wrap"
|
||||||
<div class="row">
|
class="eskaera-page oe_structure oe_empty"
|
||||||
<div class="col-lg-12">
|
data-name="Eskaera Orders"
|
||||||
<h1>Available Orders</h1>
|
>
|
||||||
<p class="text-muted" role="status">Browse and select an order to view its products.</p>
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-12">
|
||||||
|
<h1>Available Orders</h1>
|
||||||
|
<p
|
||||||
|
class="text-muted"
|
||||||
|
role="status"
|
||||||
|
>Browse and select an order to view its products.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row mt-4">
|
<div class="row mt-4">
|
||||||
<div class="col-lg-12">
|
<div class="col-lg-12">
|
||||||
<!-- Editable area: Above orders list -->
|
<!-- Editable area: Above orders list -->
|
||||||
<div class="oe_structure oe_empty" data-name="Before Orders" />
|
<div
|
||||||
|
class="oe_structure oe_empty"
|
||||||
|
data-name="Before Orders"
|
||||||
|
/>
|
||||||
|
|
||||||
<t t-if="active_orders">
|
<t t-if="active_orders">
|
||||||
<div
|
<div
|
||||||
|
|
@ -305,13 +315,18 @@
|
||||||
</t>
|
</t>
|
||||||
</div>
|
</div>
|
||||||
</t>
|
</t>
|
||||||
<t t-else="">
|
<t t-else="">
|
||||||
<div class="eskaera-empty-state">
|
<div class="eskaera-empty-state">
|
||||||
<div class="alert alert-info" role="status" aria-live="polite">
|
<div
|
||||||
<p>No group orders available this week.</p>
|
class="alert alert-info"
|
||||||
</div>
|
role="status"
|
||||||
</div>
|
aria-live="polite"
|
||||||
</t>
|
>
|
||||||
|
<p
|
||||||
|
>No group orders available this week.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
|
||||||
<!-- Editable area: Below orders list -->
|
<!-- Editable area: Below orders list -->
|
||||||
<div
|
<div
|
||||||
|
|
@ -364,58 +379,89 @@
|
||||||
</t>
|
</t>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- Small QWeb snippets used to render translated confirmation strings
|
<!-- Small QWeb snippets used to render translated confirmation strings
|
||||||
Rendered via ir.ui.view._render_template() with lang in context
|
Rendered via ir.ui.view._render_template() with lang in context
|
||||||
to ensure server-side translation regardless of call stack. -->
|
to ensure server-side translation regardless of call stack. -->
|
||||||
<template id="confirm_message_snippet" name="Confirm Message Snippet">
|
<template id="confirm_message_snippet" name="Confirm Message Snippet">
|
||||||
<t t-esc="_('Thank you! Your order has been confirmed.')" />
|
<t t-esc="_('Thank you! Your order has been confirmed.')" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template id="confirm_pickup_label_snippet" name="Confirm Pickup Label Snippet">
|
<template
|
||||||
<t t-esc="_('Pickup Day')" />
|
id="confirm_pickup_label_snippet"
|
||||||
</template>
|
name="Confirm Pickup Label Snippet"
|
||||||
|
>
|
||||||
|
<t t-esc="_('Pickup Day')" />
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- Shared template: Order Header -->
|
<!-- Shared template: Order Header -->
|
||||||
<template id="order_header" name="Order Header">
|
<template id="order_header" name="Order Header">
|
||||||
<div t-att-class="header_class or 'eskaera-order-header'">
|
<div t-att-class="header_class or 'eskaera-order-header'">
|
||||||
<div class="d-flex gap-5 align-items-center mb-4">
|
<div class="d-flex gap-5 align-items-center mb-4">
|
||||||
<t t-set="image_to_show" t-value="group_order.image or (group_order.group_ids[0].image_1920 if group_order.group_ids else False)" />
|
<t
|
||||||
<t t-if="image_to_show">
|
t-set="image_to_show"
|
||||||
<img t-att-src="image_data_uri(image_to_show)" alt="Order image" class="order-thumbnail-md" />
|
t-value="group_order.image or (group_order.group_ids[0].image_1920 if group_order.group_ids else False)"
|
||||||
</t>
|
/>
|
||||||
<div class="flex-grow-1">
|
<t t-if="image_to_show">
|
||||||
<h1 class="mb-2"><t t-esc="header_title or group_order.name" /></h1>
|
<img
|
||||||
<t t-if="group_order.description">
|
t-att-src="image_data_uri(image_to_show)"
|
||||||
<p class="text-muted mb-0 order-desc-full"><t t-esc="group_order.description" /></p>
|
alt="Order image"
|
||||||
</t>
|
class="order-thumbnail-md"
|
||||||
</div>
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- Template: Group Order Shop (Eskaera) -->
|
|
||||||
<template id="eskaera_shop" name="Eskaera Shop">
|
|
||||||
<t t-call="website.layout">
|
|
||||||
<div id="wrap" class="eskaera-shop-page oe_structure oe_empty" data-name="Eskaera Shop">
|
|
||||||
<div class="container">
|
|
||||||
<!-- Order Header Info Panel -->
|
|
||||||
<div class="row mb-4">
|
|
||||||
<div class="col-lg-12">
|
|
||||||
<t t-call="website_sale_aplicoop.order_header">
|
|
||||||
<t t-set="header_class" t-value="'eskaera-order-header'" />
|
|
||||||
</t>
|
|
||||||
<div class="eskaera-order-header">
|
|
||||||
<div class="order-info-grid">
|
|
||||||
<div class="info-item">
|
|
||||||
<span t-att-class="'info-label'">Consumer Groups</span>
|
|
||||||
<span class="info-value"><t t-esc="', '.join(group_order.group_ids.mapped('name'))" /></span>
|
|
||||||
</div>
|
|
||||||
<t t-if="group_order.cutoff_day">
|
|
||||||
<div class="info-item">
|
|
||||||
<span t-att-class="'info-label'">Cutoff Day</span>
|
|
||||||
<span class="info-value"><t t-esc="day_names[int(group_order.cutoff_day) % 7]" /> (<t t-esc="group_order.cutoff_date.strftime('%d/%m/%Y')" />)</span>
|
|
||||||
</div>
|
|
||||||
</t>
|
</t>
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<h1 class="mb-2"><t
|
||||||
|
t-esc="header_title or group_order.name"
|
||||||
|
/></h1>
|
||||||
|
<t t-if="group_order.description">
|
||||||
|
<p class="text-muted mb-0 order-desc-full"><t
|
||||||
|
t-esc="group_order.description"
|
||||||
|
/></p>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Template: Group Order Shop (Eskaera) -->
|
||||||
|
<template id="eskaera_shop" name="Eskaera Shop">
|
||||||
|
<t t-call="website.layout">
|
||||||
|
<div
|
||||||
|
id="wrap"
|
||||||
|
class="eskaera-shop-page oe_structure oe_empty"
|
||||||
|
data-name="Eskaera Shop"
|
||||||
|
>
|
||||||
|
<div class="container">
|
||||||
|
<!-- Order Header Info Panel -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-lg-12">
|
||||||
|
<t t-call="website_sale_aplicoop.order_header">
|
||||||
|
<t
|
||||||
|
t-set="header_class"
|
||||||
|
t-value="'eskaera-order-header'"
|
||||||
|
/>
|
||||||
|
</t>
|
||||||
|
<div class="eskaera-order-header">
|
||||||
|
<div class="order-info-grid">
|
||||||
|
<div class="info-item">
|
||||||
|
<span
|
||||||
|
t-att-class="'info-label'"
|
||||||
|
>Consumer Groups</span>
|
||||||
|
<span class="info-value"><t
|
||||||
|
t-esc="', '.join(group_order.group_ids.mapped('name'))"
|
||||||
|
/></span>
|
||||||
|
</div>
|
||||||
|
<t t-if="group_order.cutoff_day">
|
||||||
|
<div class="info-item">
|
||||||
|
<span
|
||||||
|
t-att-class="'info-label'"
|
||||||
|
>Cutoff Day</span>
|
||||||
|
<span class="info-value"><t
|
||||||
|
t-esc="day_names[int(group_order.cutoff_day) % 7]"
|
||||||
|
/> (<t
|
||||||
|
t-esc="group_order.cutoff_date.strftime('%d/%m/%Y')"
|
||||||
|
/>)</span>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
<t t-if="group_order.pickup_day">
|
<t t-if="group_order.pickup_day">
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
<span
|
<span
|
||||||
|
|
@ -470,21 +516,38 @@
|
||||||
<!-- Search and Filter Bar (Full Width, Above Products/Cart) -->
|
<!-- Search and Filter Bar (Full Width, Above Products/Cart) -->
|
||||||
<div class="mb-3" id="realtimeSearch-filters">
|
<div class="mb-3" id="realtimeSearch-filters">
|
||||||
<div class="row g-2">
|
<div class="row g-2">
|
||||||
<div class="col-md-7">
|
<div class="col-md-7">
|
||||||
<!-- CRITICAL: This input is NOT inside a form to prevent Odoo from transforming it -->
|
<!-- CRITICAL: This input is NOT inside a form to prevent Odoo from transforming it -->
|
||||||
<!-- It must remain a pure HTML input element for realtime_search.js to detect value changes -->
|
<!-- It must remain a pure HTML input element for realtime_search.js to detect value changes -->
|
||||||
<input type="text" id="realtime-search-input" class="form-control realtime-search-box search-input-styled" placeholder="Search products..." autocomplete="off" />
|
<input
|
||||||
</div>
|
type="text"
|
||||||
<div class="col-md-5">
|
id="realtime-search-input"
|
||||||
<select name="category" id="realtime-category-select" class="form-select">
|
class="form-control realtime-search-box search-input-styled"
|
||||||
<option value="">Browse Product Categories</option>
|
placeholder="Search products..."
|
||||||
<!-- Macro para renderizar categorías recursivamente -->
|
autocomplete="off"
|
||||||
<t t-call="website_sale_aplicoop.category_hierarchy_options">
|
/>
|
||||||
<t t-set="categories" t-value="category_hierarchy" />
|
</div>
|
||||||
<t t-set="depth" t-value="0" />
|
<div class="col-md-5">
|
||||||
</t>
|
<select
|
||||||
</select>
|
name="category"
|
||||||
</div>
|
id="realtime-category-select"
|
||||||
|
class="form-select"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
value=""
|
||||||
|
>Browse Product Categories</option>
|
||||||
|
<!-- Macro para renderizar categorías recursivamente -->
|
||||||
|
<t
|
||||||
|
t-call="website_sale_aplicoop.category_hierarchy_options"
|
||||||
|
>
|
||||||
|
<t
|
||||||
|
t-set="categories"
|
||||||
|
t-value="category_hierarchy"
|
||||||
|
/>
|
||||||
|
<t t-set="depth" t-value="0" />
|
||||||
|
</t>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<t t-if="available_tags">
|
<t t-if="available_tags">
|
||||||
<div class="row mt-3">
|
<div class="row mt-3">
|
||||||
|
|
@ -543,8 +606,11 @@
|
||||||
<div class="row g-2">
|
<div class="row g-2">
|
||||||
<!-- Products Column -->
|
<!-- Products Column -->
|
||||||
<div class="col-lg-9">
|
<div class="col-lg-9">
|
||||||
<!-- Editable area: Above search/filter -->
|
<!-- Editable area: Above search/filter -->
|
||||||
<div class="oe_structure oe_empty" data-name="Before Products Filter" />
|
<div
|
||||||
|
class="oe_structure oe_empty"
|
||||||
|
data-name="Before Products Filter"
|
||||||
|
/>
|
||||||
|
|
||||||
<t t-if="products">
|
<t t-if="products">
|
||||||
<div class="products-grid">
|
<div class="products-grid">
|
||||||
|
|
@ -760,11 +826,15 @@
|
||||||
</t>
|
</t>
|
||||||
</div>
|
</div>
|
||||||
</t>
|
</t>
|
||||||
<t t-else="">
|
<t t-else="">
|
||||||
<div class="alert alert-warning" role="status" aria-live="polite">
|
<div
|
||||||
<p>No products available in this order.</p>
|
class="alert alert-warning"
|
||||||
</div>
|
role="status"
|
||||||
</t>
|
aria-live="polite"
|
||||||
|
>
|
||||||
|
<p>No products available in this order.</p>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
<!-- Editable area: Below products list -->
|
<!-- Editable area: Below products list -->
|
||||||
<div
|
<div
|
||||||
class="oe_structure oe_empty mt-4"
|
class="oe_structure oe_empty mt-4"
|
||||||
|
|
@ -781,11 +851,26 @@
|
||||||
<div
|
<div
|
||||||
class="card-header d-flex justify-content-between align-items-center gap-1"
|
class="card-header d-flex justify-content-between align-items-center gap-1"
|
||||||
>
|
>
|
||||||
<h6 class="mb-0 cart-title-sm" id="cart-title">My Cart</h6>
|
<h6
|
||||||
<div class="btn-group cart-btn-group gap-0" role="group">
|
class="mb-0 cart-title-sm"
|
||||||
<button type="button" class="btn btn-primary cart-btn-compact" id="save-cart-btn" t-attf-data-order-id="{{ group_order.id }}" data-bs-title="Save Cart" data-bs-toggle="tooltip">
|
id="cart-title"
|
||||||
<i class="fa fa-save cart-icon-size" />
|
>My Cart</h6>
|
||||||
</button>
|
<div
|
||||||
|
class="btn-group cart-btn-group gap-0"
|
||||||
|
role="group"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-primary cart-btn-compact"
|
||||||
|
id="save-cart-btn"
|
||||||
|
t-attf-data-order-id="{{ group_order.id }}"
|
||||||
|
data-bs-title="Save Cart"
|
||||||
|
data-bs-toggle="tooltip"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
class="fa fa-save cart-icon-size"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-info cart-btn-compact"
|
class="btn btn-info cart-btn-compact"
|
||||||
|
|
@ -812,31 +897,63 @@
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body cart-body-lg" id="cart-items-container" t-attf-data-order-id="{{ group_order.id }}" aria-labelledby="cart-title" aria-live="polite" aria-relevant="additions removals">
|
<div
|
||||||
<p class="text-muted">This order's cart is empty</p>
|
class="card-body cart-body-lg"
|
||||||
</div>
|
id="cart-items-container"
|
||||||
<div class="card-footer bg-white text-center">
|
t-attf-data-order-id="{{ group_order.id }}"
|
||||||
<a t-attf-href="/eskaera/{{ group_order.id }}/checkout" class="btn btn-success checkout-btn-lg" data-bs-title="Proceed to Checkout" data-bs-toggle="tooltip">
|
aria-labelledby="cart-title"
|
||||||
Proceed to Checkout
|
aria-live="polite"
|
||||||
</a>
|
aria-relevant="additions removals"
|
||||||
</div>
|
>
|
||||||
|
<p class="text-muted">
|
||||||
|
This order's cart is empty
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer bg-white text-center">
|
||||||
|
<a
|
||||||
|
t-attf-href="/eskaera/{{ group_order.id }}/checkout"
|
||||||
|
class="btn btn-success checkout-btn-lg"
|
||||||
|
data-bs-title="Proceed to Checkout"
|
||||||
|
data-bs-toggle="tooltip"
|
||||||
|
>
|
||||||
|
Proceed to Checkout
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Scripts (in dependency order) -->
|
<!-- Scripts (in dependency order) -->
|
||||||
<!-- Load i18n_manager first - fetches translations from server -->
|
<!-- Load i18n_manager first - fetches translations from server -->
|
||||||
<script type="text/javascript" src="/website_sale_aplicoop/static/src/js/i18n_manager.js" />
|
<script
|
||||||
<!-- Keep legacy helpers for backwards compatibility -->
|
type="text/javascript"
|
||||||
<script type="text/javascript" src="/website_sale_aplicoop/static/src/js/i18n_helpers.js" />
|
src="/website_sale_aplicoop/static/src/js/i18n_manager.js"
|
||||||
<!-- Main shop functionality (depends on i18nManager) -->
|
/>
|
||||||
<script type="text/javascript" src="/website_sale_aplicoop/static/src/js/website_sale.js" />
|
<!-- Keep legacy helpers for backwards compatibility -->
|
||||||
<!-- UI enhancements -->
|
<script
|
||||||
<script type="text/javascript" src="/website_sale_aplicoop/static/src/js/checkout_labels.js" />
|
type="text/javascript"
|
||||||
<script type="text/javascript" src="/website_sale_aplicoop/static/src/js/home_delivery.js" />
|
src="/website_sale_aplicoop/static/src/js/i18n_helpers.js"
|
||||||
<script type="text/javascript" src="/website_sale_aplicoop/static/src/js/realtime_search.js" />
|
/>
|
||||||
|
<!-- Main shop functionality (depends on i18nManager) -->
|
||||||
|
<script
|
||||||
|
type="text/javascript"
|
||||||
|
src="/website_sale_aplicoop/static/src/js/website_sale.js"
|
||||||
|
/>
|
||||||
|
<!-- UI enhancements -->
|
||||||
|
<script
|
||||||
|
type="text/javascript"
|
||||||
|
src="/website_sale_aplicoop/static/src/js/checkout_labels.js"
|
||||||
|
/>
|
||||||
|
<script
|
||||||
|
type="text/javascript"
|
||||||
|
src="/website_sale_aplicoop/static/src/js/home_delivery.js"
|
||||||
|
/>
|
||||||
|
<script
|
||||||
|
type="text/javascript"
|
||||||
|
src="/website_sale_aplicoop/static/src/js/realtime_search.js"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- Initialize tooltips using native title attribute -->
|
<!-- Initialize tooltips using native title attribute -->
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
|
|
@ -876,51 +993,77 @@
|
||||||
</t>
|
</t>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- Sub-template: Checkout Order Summary Table with Translations -->
|
<!-- Sub-template: Checkout Order Summary Table with Translations -->
|
||||||
<template id="eskaera_checkout_summary" name="Checkout Order Summary">
|
<template id="eskaera_checkout_summary" name="Checkout Order Summary">
|
||||||
<div class="checkout-summary-container">
|
<div class="checkout-summary-container">
|
||||||
<table class="table table-hover checkout-summary-table" id="checkout-summary-table">
|
<table
|
||||||
<thead class="table-dark">
|
class="table table-hover checkout-summary-table"
|
||||||
<tr>
|
id="checkout-summary-table"
|
||||||
<th class="col-name">Product</th>
|
>
|
||||||
<th class="col-qty text-center">Quantity</th>
|
<thead class="table-dark">
|
||||||
<th class="col-price text-right">Price</th>
|
<tr>
|
||||||
<th class="col-subtotal text-right">Subtotal</th>
|
<th class="col-name">Product</th>
|
||||||
</tr>
|
<th class="col-qty text-center">Quantity</th>
|
||||||
</thead>
|
<th class="col-price text-right">Price</th>
|
||||||
<tbody id="checkout-summary-tbody">
|
<th class="col-subtotal text-right">Subtotal</th>
|
||||||
<tr id="checkout-empty-row" class="empty-message">
|
</tr>
|
||||||
<td colspan="4" class="text-center text-muted py-4">
|
</thead>
|
||||||
<i class="fa fa-inbox fa-2x mb-2" />
|
<tbody id="checkout-summary-tbody">
|
||||||
<p>This order's cart is empty</p>
|
<tr id="checkout-empty-row" class="empty-message">
|
||||||
</td>
|
<td colspan="4" class="text-center text-muted py-4">
|
||||||
</tr>
|
<i class="fa fa-inbox fa-2x mb-2" />
|
||||||
</tbody>
|
<p>This order's cart is empty</p>
|
||||||
</table>
|
</td>
|
||||||
<div class="checkout-total-section">
|
</tr>
|
||||||
<div class="total-row">
|
</tbody>
|
||||||
<span class="total-label">Total</span>:
|
</table>
|
||||||
<span class="total-amount" id="checkout-total-amount">0.00</span>
|
<div class="checkout-total-section">
|
||||||
<span class="currency">€</span>
|
<div class="total-row">
|
||||||
</div>
|
<span class="total-label">Total</span>:
|
||||||
</div>
|
<span
|
||||||
</div>
|
class="total-amount"
|
||||||
</template>
|
id="checkout-total-amount"
|
||||||
|
>0.00</span>
|
||||||
<!-- Template: Group Order Checkout (Eskaera) -->
|
<span class="currency">€</span>
|
||||||
<template id="eskaera_checkout" name="Eskaera Checkout">
|
</div>
|
||||||
<t t-call="website.layout">
|
|
||||||
<div id="wrap" class="eskaera-checkout-page oe_structure oe_empty" data-name="Eskaera Checkout" t-attf-data-delivery-product-id="{{ delivery_product_id }}" t-attf-data-delivery-product-name="{{ delivery_product_name }}" t-attf-data-delivery-product-price="{{ delivery_product_price }}" t-attf-data-home-delivery-enabled="{{ 'true' if group_order.home_delivery else 'false' }}" t-attf-data-pickup-day="{{ group_order.pickup_day }}" t-attf-data-pickup-date="{{ group_order.pickup_date.strftime('%d/%m/%Y') if group_order.pickup_date else '' }}" t-attf-data-delivery-notice="{{ (group_order.delivery_notice or '').replace(chr(10), ' ').replace(chr(13), ' ') }}">
|
|
||||||
<div class="container mt-5">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-lg-10 offset-lg-1">
|
|
||||||
<!-- Header Section -->
|
|
||||||
<div class="mb-4">
|
|
||||||
<t t-call="website_sale_aplicoop.order_header">
|
|
||||||
<t t-set="header_class" t-value="'checkout-header'" />
|
|
||||||
<t t-set="header_title">Confirm Order: <t t-esc="group_order.name" /></t>
|
|
||||||
</t>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Template: Group Order Checkout (Eskaera) -->
|
||||||
|
<template id="eskaera_checkout" name="Eskaera Checkout">
|
||||||
|
<t t-call="website.layout">
|
||||||
|
<div
|
||||||
|
id="wrap"
|
||||||
|
class="eskaera-checkout-page oe_structure oe_empty"
|
||||||
|
data-name="Eskaera Checkout"
|
||||||
|
t-attf-data-delivery-product-id="{{ delivery_product_id }}"
|
||||||
|
t-attf-data-delivery-product-name="{{ delivery_product_name }}"
|
||||||
|
t-attf-data-delivery-product-price="{{ delivery_product_price }}"
|
||||||
|
t-attf-data-home-delivery-enabled="{{ 'true' if group_order.home_delivery else 'false' }}"
|
||||||
|
t-attf-data-pickup-day="{{ group_order.pickup_day }}"
|
||||||
|
t-attf-data-pickup-date="{{ group_order.pickup_date.strftime('%d/%m/%Y') if group_order.pickup_date else '' }}"
|
||||||
|
t-attf-data-delivery-notice="{{ (group_order.delivery_notice or '').replace(chr(10), ' ').replace(chr(13), ' ') }}"
|
||||||
|
>
|
||||||
|
<div class="container mt-5">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-10 offset-lg-1">
|
||||||
|
<!-- Header Section -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<t
|
||||||
|
t-call="website_sale_aplicoop.order_header"
|
||||||
|
>
|
||||||
|
<t
|
||||||
|
t-set="header_class"
|
||||||
|
t-value="'checkout-header'"
|
||||||
|
/>
|
||||||
|
<t
|
||||||
|
t-set="header_title"
|
||||||
|
>Confirm Order: <t
|
||||||
|
t-esc="group_order.name"
|
||||||
|
/></t>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Order Info Card -->
|
<!-- Order Info Card -->
|
||||||
<div
|
<div
|
||||||
|
|
@ -1061,13 +1204,21 @@
|
||||||
data-name="After Summary"
|
data-name="After Summary"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Home Delivery Checkbox -->
|
<!-- Home Delivery Checkbox -->
|
||||||
<div class="card border-0 shadow-sm mb-4">
|
<div class="card border-0 shadow-sm mb-4">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input type="checkbox" class="form-check-input" id="home-delivery-checkbox" name="home_delivery" />
|
<input
|
||||||
<label class="form-check-label fw-bold" for="home-delivery-checkbox">Home Delivery</label>
|
type="checkbox"
|
||||||
</div>
|
class="form-check-input"
|
||||||
|
id="home-delivery-checkbox"
|
||||||
|
name="home_delivery"
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
class="form-check-label fw-bold"
|
||||||
|
for="home-delivery-checkbox"
|
||||||
|
>Home Delivery</label>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
id="delivery-info-alert"
|
id="delivery-info-alert"
|
||||||
class="alert alert-info mt-3 d-none"
|
class="alert alert-info mt-3 d-none"
|
||||||
|
|
@ -1143,17 +1294,43 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Action Buttons -->
|
<!-- Action Buttons -->
|
||||||
<div class="checkout-actions d-grid gap-3" id="checkout-form-labels">
|
<div
|
||||||
<button class="btn btn-success btn-lg" id="confirm-order-btn" t-attf-data-order-id="{{ group_order.id }}" data-confirmed-label="Order confirmed" data-pickup-label="Pickup Day" aria-label="Confirm and send order" data-bs-title="Confirm Order" data-bs-toggle="tooltip">
|
class="checkout-actions d-grid gap-3"
|
||||||
<i class="fa fa-check-circle" aria-hidden="true" t-translation="off" />
|
id="checkout-form-labels"
|
||||||
<span>Confirm Order</span>
|
>
|
||||||
</button>
|
<button
|
||||||
<a t-attf-href="/eskaera/{{ group_order.id }}" class="btn btn-outline-secondary btn-lg" aria-label="Back to cart page" data-bs-title="Back to Cart" data-bs-toggle="tooltip">
|
class="btn btn-success btn-lg"
|
||||||
<i class="fa fa-arrow-left" aria-hidden="true" t-translation="off" />
|
id="confirm-order-btn"
|
||||||
<span>Back to Cart</span>
|
t-attf-data-order-id="{{ group_order.id }}"
|
||||||
</a>
|
data-confirmed-label="Order confirmed"
|
||||||
</div>
|
data-pickup-label="Pickup Day"
|
||||||
|
aria-label="Confirm and send order"
|
||||||
|
data-bs-title="Confirm Order"
|
||||||
|
data-bs-toggle="tooltip"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
class="fa fa-check-circle"
|
||||||
|
aria-hidden="true"
|
||||||
|
t-translation="off"
|
||||||
|
/>
|
||||||
|
<span>Confirm Order</span>
|
||||||
|
</button>
|
||||||
|
<a
|
||||||
|
t-attf-href="/eskaera/{{ group_order.id }}"
|
||||||
|
class="btn btn-outline-secondary btn-lg"
|
||||||
|
aria-label="Back to cart page"
|
||||||
|
data-bs-title="Back to Cart"
|
||||||
|
data-bs-toggle="tooltip"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
class="fa fa-arrow-left"
|
||||||
|
aria-hidden="true"
|
||||||
|
t-translation="off"
|
||||||
|
/>
|
||||||
|
<span>Back to Cart</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1171,17 +1348,35 @@
|
||||||
console.log('[LABELS] Initialized from server:', window.groupOrderShop.labels);
|
console.log('[LABELS] Initialized from server:', window.groupOrderShop.labels);
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<!-- Scripts (in dependency order) -->
|
<!-- Scripts (in dependency order) -->
|
||||||
<!-- Load i18n_manager first - fetches translations from server -->
|
<!-- Load i18n_manager first - fetches translations from server -->
|
||||||
<script type="text/javascript" src="/website_sale_aplicoop/static/src/js/i18n_manager.js" />
|
<script
|
||||||
<!-- Keep legacy helpers for backwards compatibility -->
|
type="text/javascript"
|
||||||
<script type="text/javascript" src="/website_sale_aplicoop/static/src/js/i18n_helpers.js" />
|
src="/website_sale_aplicoop/static/src/js/i18n_manager.js"
|
||||||
<!-- Main shop functionality (depends on i18nManager) -->
|
/>
|
||||||
<script type="text/javascript" src="/website_sale_aplicoop/static/src/js/website_sale.js" />
|
<!-- Keep legacy helpers for backwards compatibility -->
|
||||||
<!-- UI enhancements -->
|
<script
|
||||||
<script type="text/javascript" src="/website_sale_aplicoop/static/src/js/checkout_labels.js" />
|
type="text/javascript"
|
||||||
<script type="text/javascript" src="/website_sale_aplicoop/static/src/js/home_delivery.js" />
|
src="/website_sale_aplicoop/static/src/js/i18n_helpers.js"
|
||||||
<script type="text/javascript" src="/website_sale_aplicoop/static/src/js/checkout_summary.js" />
|
/>
|
||||||
|
<!-- Main shop functionality (depends on i18nManager) -->
|
||||||
|
<script
|
||||||
|
type="text/javascript"
|
||||||
|
src="/website_sale_aplicoop/static/src/js/website_sale.js"
|
||||||
|
/>
|
||||||
|
<!-- UI enhancements -->
|
||||||
|
<script
|
||||||
|
type="text/javascript"
|
||||||
|
src="/website_sale_aplicoop/static/src/js/checkout_labels.js"
|
||||||
|
/>
|
||||||
|
<script
|
||||||
|
type="text/javascript"
|
||||||
|
src="/website_sale_aplicoop/static/src/js/home_delivery.js"
|
||||||
|
/>
|
||||||
|
<script
|
||||||
|
type="text/javascript"
|
||||||
|
src="/website_sale_aplicoop/static/src/js/checkout_summary.js"
|
||||||
|
/>
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
// Auto-load cart from localStorage when accessing checkout directly
|
// Auto-load cart from localStorage when accessing checkout directly
|
||||||
(function() {
|
(function() {
|
||||||
|
|
@ -1217,38 +1412,46 @@
|
||||||
</t>
|
</t>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- Recursive macro to render category hierarchy for select dropdown -->
|
<!-- Recursive macro to render category hierarchy for select dropdown -->
|
||||||
<template id="category_hierarchy_options" name="Category Hierarchy Options">
|
<template
|
||||||
<!--
|
id="category_hierarchy_options"
|
||||||
Macro para renderizar recursivamente la jerarquía de categorías.
|
name="Category Hierarchy Options"
|
||||||
Todas las categorías son seleccionables, indentadas por profundidad.
|
>
|
||||||
|
<!--
|
||||||
|
Macro para renderizar recursivamente la jerarquía de categorías.
|
||||||
|
Todas las categorías son seleccionables, indentadas por profundidad.
|
||||||
|
|
||||||
Parámetros:
|
Parámetros:
|
||||||
- categories: lista de categorías a renderizar
|
- categories: lista de categorías a renderizar
|
||||||
- depth: nivel de profundidad actual (para padding/indentación)
|
- depth: nivel de profundidad actual (para padding/indentación)
|
||||||
-->
|
-->
|
||||||
<t t-foreach="categories" t-as="cat">
|
<t t-foreach="categories" t-as="cat">
|
||||||
<!-- Calcular padding basado en profundidad: 20px por nivel -->
|
<!-- Calcular padding basado en profundidad: 20px por nivel -->
|
||||||
<t t-set="padding_px" t-value="depth * 20" />
|
<t t-set="padding_px" t-value="depth * 20" />
|
||||||
<!-- Crear prefijo visual con flechas según profundidad -->
|
<!-- Crear prefijo visual con flechas según profundidad -->
|
||||||
<t t-set="prefix">
|
<t t-set="prefix">
|
||||||
<t t-foreach="range(depth)" t-as="i">↳ </t>
|
<t t-foreach="range(depth)" t-as="i">↳ </t>
|
||||||
</t>
|
</t>
|
||||||
|
|
||||||
<!-- Renderizar como opción indentada y seleccionable -->
|
<!-- Renderizar como opción indentada y seleccionable -->
|
||||||
<option t-att-value="str(cat['id'])" t-attf-style="padding-left: {{ padding_px }}px;">
|
<option
|
||||||
<t t-esc="prefix" /><t t-esc="cat['name']" />
|
t-att-value="str(cat['id'])"
|
||||||
</option>
|
t-attf-style="padding-left: {{ padding_px }}px;"
|
||||||
|
>
|
||||||
|
<t t-esc="prefix" /><t t-esc="cat['name']" />
|
||||||
|
</option>
|
||||||
|
|
||||||
<!-- Renderizar hijos recursivamente si existen -->
|
<!-- Renderizar hijos recursivamente si existen -->
|
||||||
<t t-if="cat['children']">
|
<t t-if="cat['children']">
|
||||||
<t t-call="website_sale_aplicoop.category_hierarchy_options">
|
<t
|
||||||
<t t-set="categories" t-value="cat['children']" />
|
t-call="website_sale_aplicoop.category_hierarchy_options"
|
||||||
<t t-set="depth" t-value="depth + 1" />
|
>
|
||||||
</t>
|
<t t-set="categories" t-value="cat['children']" />
|
||||||
</t>
|
<t t-set="depth" t-value="depth + 1" />
|
||||||
</t>
|
</t>
|
||||||
</template>
|
</t>
|
||||||
|
</t>
|
||||||
|
</template>
|
||||||
|
|
||||||
</data>
|
</data>
|
||||||
</odoo>
|
</odoo>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue