19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:31:28 +01:00
parent ff721d030e
commit 7721452493
1826 changed files with 124775 additions and 274114 deletions

View file

@ -1,6 +1,6 @@
# Argentinean eCommerce
Be able to see Identification Type and AFIP Responsibility in ecommerce checkout form.
Bridge Website Sale for Argentina
## Installation
@ -10,36 +10,15 @@ pip install odoo-bringout-oca-ocb-l10n_ar_website_sale
## Dependencies
This addon depends on:
- website_sale
- l10n_ar
## Manifest Information
- **Name**: Argentinean eCommerce
- **Version**: 1.0
- **Category**: Accounting/Localizations/Website
- **License**: LGPL-3
- **Installable**: True
## Source
Based on [OCA/OCB](https://github.com/OCA/OCB) branch 16.0, addon `l10n_ar_website_sale`.
- Repository: https://github.com/OCA/OCB
- Branch: 19.0
- Path: addons/l10n_ar_website_sale
## License
This package maintains the original LGPL-3 license from the upstream Odoo project.
## Documentation
- Overview: doc/OVERVIEW.md
- Architecture: doc/ARCHITECTURE.md
- Models: doc/MODELS.md
- Controllers: doc/CONTROLLERS.md
- Wizards: doc/WIZARDS.md
- Install: doc/INSTALL.md
- Usage: doc/USAGE.md
- Configuration: doc/CONFIGURATION.md
- Dependencies: doc/DEPENDENCIES.md
- Troubleshooting: doc/TROUBLESHOOTING.md
- FAQ: doc/FAQ.md
This package preserves the original LGPL-3 license.

View file

@ -1,4 +1 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import controllers
from . import models
from . import tests

View file

@ -1,23 +1,26 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': 'Argentinean eCommerce',
'version': '1.0',
'category': 'Accounting/Localizations/Website',
'sequence': 14,
'author': 'Odoo, ADHOC SA',
'description': """Be able to see Identification Type and AFIP Responsibility in ecommerce checkout form.""",
'countries': ['ar'],
'icon': '/base/static/img/country_flags/ar.png',
'description': """Bridge Website Sale for Argentina""",
'depends': [
'website_sale',
'l10n_ar',
],
'data': [
'data/ir_model_fields.xml',
'views/res_config_settings_views.xml',
'views/templates.xml',
],
'demo': [
'demo/website_demo.xml',
],
'assets': {
'web.assets_frontend': [
'l10n_ar_website_sale/static/src/interactions/**/*',
'l10n_ar_website_sale/static/src/scss/*.scss',
]
},
'installable': True,
'auto_install': True,
'author': 'Odoo S.A.',
'license': 'LGPL-3',
}

View file

@ -1,2 +0,0 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import main

View file

@ -1,59 +0,0 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import _
from odoo.addons.website_sale.controllers.main import WebsiteSale
from odoo.http import request
class L10nARWebsiteSale(WebsiteSale):
def _get_mandatory_fields_billing(self, country_id=False):
"""Extend mandatory fields to add new identification and responsibility fields when company is argentina"""
res = super()._get_mandatory_fields_billing(country_id)
if request.website.sudo().company_id.country_id.code == "AR":
res += ["l10n_latam_identification_type_id", "l10n_ar_afip_responsibility_type_id", "vat"]
return res
def _get_country_related_render_values(self, kw, render_values):
res = super()._get_country_related_render_values(kw, render_values)
if request.website.sudo().company_id.country_id.code == "AR":
res.update({'identification': kw.get('l10n_latam_identification_type_id'),
'responsibility': kw.get('l10n_ar_afip_responsibility_type_id'),
'responsibility_types': request.env['l10n_ar.afip.responsibility.type'].search([]),
'identification_types': request.env['l10n_latam.identification.type'].search(
['|', ('country_id', '=', False), ('country_id.code', '=', 'AR')])})
return res
def _get_vat_validation_fields(self, data):
res = super()._get_vat_validation_fields(data)
if request.website.sudo().company_id.country_id.code == "AR":
res.update({'l10n_latam_identification_type_id': int(data['l10n_latam_identification_type_id'])
if data.get('l10n_latam_identification_type_id') else False})
res.update({'name': data['name'] if data.get('name') else False})
return res
def checkout_form_validate(self, mode, all_form_values, data):
""" We extend the method to add a new validation. If AFIP Resposibility is:
* Final Consumer or Foreign Customer: then it can select any identification type.
* Any other (Monotributista, RI, etc): should select always "CUIT" identification type"""
error, error_message = super().checkout_form_validate(mode, all_form_values, data)
# Identification type and AFIP Responsibility Combination
if request.website.sudo().company_id.country_id.code == "AR":
if mode[1] == 'billing':
if error and any(field in error for field in ['l10n_latam_identification_type_id', 'l10n_ar_afip_responsibility_type_id']):
return error, error_message
id_type_id = data.get("l10n_latam_identification_type_id")
afip_resp_id = data.get("l10n_ar_afip_responsibility_type_id")
id_type = request.env['l10n_latam.identification.type'].browse(id_type_id) if id_type_id else False
afip_resp = request.env['l10n_ar.afip.responsibility.type'].browse(afip_resp_id) if afip_resp_id else False
cuit_id_type = request.env.ref('l10n_ar.it_cuit')
# Check if the AFIP responsibility is different from Final Consumer or Foreign Customer,
# and if the identification type is different from CUIT
if afip_resp.code not in ['5', '9'] and id_type != cuit_id_type:
error["l10n_latam_identification_type_id"] = 'error'
error_message.append(_('For the selected AFIP Responsibility you will need to set CUIT Identification Type'))
return error, error_message

View file

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<function model="ir.model.fields" name="formbuilder_whitelist">
<value>res.partner</value>
<value eval="[
'l10n_ar_afip_responsibility_type_id', 'l10n_latam_identification_type_id',
]"/>
</function>
</odoo>

View file

@ -1,23 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo noupdate="1">
<record id="default_website_ri" model="website">
<field name="name">(AR) Responsable Inscripto Website</field>
<field name="company_id" ref="l10n_ar.company_ri"/>
<field name="logo" type="base64" file="l10n_ar_website_sale/static/description/icon.png"/>
<field name="domain" model="ir.config_parameter" eval="obj().env['ir.config_parameter'].sudo().get_param('web.base.url')"/>
</record>
<function model="payment.provider" name="copy">
<value eval="[ref('payment.payment_provider_transfer')]"/>
<value eval="{'company_id': ref('l10n_ar.company_ri'), 'state': 'enabled'}"/>
</function>
<function model="product.pricelist" name="write">
<value model="product.pricelist" eval="obj().search([('currency_id', '=', ref('base.ARS')), ('company_id', '=', ref('l10n_ar.company_ri'))]).id"/>
<value model="website" eval="{'sequence': 1, 'website_id': ref('l10n_ar_website_sale.default_website_ri')}"/>
</function>
<function model="product.product" name="write">
<value model="product.product" eval="obj().search([('taxes_id', '=', False)]).ids"/>
<value model="account.tax" eval="{'taxes_id': [(4, obj().search([('type_tax_use', '=', 'sale'), ('tax_group_id', '=', ref('l10n_ar.tax_group_iva_21')), ('company_id', '=', ref('l10n_ar.company_ri'))]).id)]}"/>
</function>
</odoo>

View file

@ -1,86 +0,0 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * l10n_ar_website_sale
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.0+e\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-11-15 10:56+0000\n"
"PO-Revision-Date: 2024-11-15 10:56+0000\n"
"Last-Translator: María Fernanda Alvarez Ramírez <mfar@odoo.com>\n"
"Language-Team: \n"
"Language: es\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#. module: l10n_ar_website_sale
#: model:website,contact_us_button_url:l10n_ar_website_sale.default_website_ri
msgid "/contactus"
msgstr "/contacto"
#. module: l10n_ar_website_sale
#: model_terms:ir.ui.view,arch_db:l10n_ar_website_sale.partner_info
msgid "<option value=\"\">AFIP Responsibility...</option>"
msgstr "<option value=\"\">Responsabilidad AFIP...</option>"
#. module: l10n_ar_website_sale
#: model_terms:ir.ui.view,arch_db:l10n_ar_website_sale.partner_info
msgid "<option value=\"\">Identification Type...</option>"
msgstr "<option value=\"\">Tipo de identificación...</option>"
#. module: l10n_ar_website_sale
#: model_terms:ir.ui.view,arch_db:l10n_ar_website_sale.partner_info
msgid "AFIP Responsibility"
msgstr "Responsabilidad AFIP"
#. module: l10n_ar_website_sale
#: model_terms:ir.ui.view,arch_db:l10n_ar_website_sale.partner_info
msgid ""
"Changing AFIP Responsibility type is not allowed once document(s) have been "
"issued for your account. Please contact us directly for this operation."
msgstr ""
"No puede cambiar el tipo de responsabilidad AFIP después de que haya emitido "
"documentos para su cuenta. Contáctenos para realizar esta operación."
#. module: l10n_ar_website_sale
#: model_terms:ir.ui.view,arch_db:l10n_ar_website_sale.partner_info
msgid ""
"Changing Identification type is not allowed once document(s) have been "
"issued for your account. Please contact us directly for this operation."
msgstr ""
"No puede cambiar el tipo de identificación después de que haya emitido "
"documentos para su cuenta. Contáctenos para realizar esta operación."
#. module: l10n_ar_website_sale
#. odoo-python
#: code:addons/l10n_ar_website_sale/controllers/main.py:0
#, python-format
msgid ""
"For the selected AFIP Responsibility you will need to set CUIT "
"Identification Type"
msgstr ""
"Para la responsabilidad AFIP seleccionada debe elegir el tipo de "
"identificación CUIT"
#. module: l10n_ar_website_sale
#: model_terms:ir.ui.view,arch_db:l10n_ar_website_sale.partner_info
msgid "Identification Type"
msgstr "Tipo de identificación"
#. module: l10n_ar_website_sale
#: model:website,prevent_zero_price_sale_text:l10n_ar_website_sale.default_website_ri
msgid "Not Available For Sale"
msgstr "No está disponible para la venta"
#. module: l10n_ar_website_sale
#: model_terms:ir.ui.view,arch_db:l10n_ar_website_sale.address
msgid "Number"
msgstr "Número"
#. module: l10n_ar_website_sale
#: model:ir.model,name:l10n_ar_website_sale.model_website
msgid "Website"
msgstr "Sitio web"

View file

@ -0,0 +1,65 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * l10n_ar_website_sale
#
# "Fernanda Alvarez (mfar)" <mfar@odoo.com>, 2026.
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 18.4a1+e\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-12-30 19:04+0000\n"
"PO-Revision-Date: 2026-01-31 10:05+0000\n"
"Last-Translator: \"Fernanda Alvarez (mfar)\" <mfar@odoo.com>\n"
"Language-Team: Spanish (Latin America) <https://translate.odoo.com/projects/"
"odoo-19-l10n/l10n_ar_website_sale/es_419/>\n"
"Language: es_419\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.14.3\n"
#. module: l10n_ar_website_sale
#: model:ir.model,name:l10n_ar_website_sale.model_res_config_settings
msgid "Config Settings"
msgstr "Ajustes de configuración"
#. module: l10n_ar_website_sale
#: model:ir.model.fields,field_description:l10n_ar_website_sale.field_product_template__display_name
#: model:ir.model.fields,field_description:l10n_ar_website_sale.field_res_config_settings__display_name
#: model:ir.model.fields,field_description:l10n_ar_website_sale.field_website__display_name
msgid "Display Name"
msgstr "Nombre Mostrado"
#. module: l10n_ar_website_sale
#: model:ir.model.fields,field_description:l10n_ar_website_sale.field_res_config_settings__l10n_ar_website_sale_show_both_prices
#: model:ir.model.fields,field_description:l10n_ar_website_sale.field_website__l10n_ar_website_sale_show_both_prices
msgid "Display Price without National Taxes"
msgstr "Mostrar Precio sin Impuestos Nacionales"
#. module: l10n_ar_website_sale
#: model:ir.model.fields,field_description:l10n_ar_website_sale.field_product_template__id
#: model:ir.model.fields,field_description:l10n_ar_website_sale.field_res_config_settings__id
#: model:ir.model.fields,field_description:l10n_ar_website_sale.field_website__id
msgid "ID"
msgstr "ID"
#. module: l10n_ar_website_sale
#: model_terms:ir.ui.view,arch_db:l10n_ar_website_sale.l10n_ar_website_sale_products_item_inherit
msgid "Precio s/Imp. Nac."
msgstr "Precio s/Imp. Nac."
#. module: l10n_ar_website_sale
#: model_terms:ir.ui.view,arch_db:l10n_ar_website_sale.l10n_ar_website_sale_product_price_inherit
msgid "Precio s/Imp. Nac.:"
msgstr "Precio s/Imp. Nac.:"
#. module: l10n_ar_website_sale
#: model:ir.model,name:l10n_ar_website_sale.model_product_template
msgid "Product"
msgstr "Producto"
#. module: l10n_ar_website_sale
#: model:ir.model,name:l10n_ar_website_sale.model_website
msgid "Website"
msgstr "Website"

View file

@ -4,10 +4,10 @@
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.0+e\n"
"Project-Id-Version: Odoo Server 19.0+e\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-11-15 10:56+0000\n"
"PO-Revision-Date: 2024-11-15 10:56+0000\n"
"POT-Creation-Date: 2025-12-30 19:04+0000\n"
"PO-Revision-Date: 2025-12-30 19:04+0000\n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
@ -16,61 +16,43 @@ msgstr ""
"Plural-Forms: \n"
#. module: l10n_ar_website_sale
#: model:website,contact_us_button_url:l10n_ar_website_sale.default_website_ri
msgid "/contactus"
#: model:ir.model,name:l10n_ar_website_sale.model_res_config_settings
msgid "Config Settings"
msgstr ""
#. module: l10n_ar_website_sale
#: model_terms:ir.ui.view,arch_db:l10n_ar_website_sale.partner_info
msgid "<option value=\"\">AFIP Responsibility...</option>"
#: model:ir.model.fields,field_description:l10n_ar_website_sale.field_product_template__display_name
#: model:ir.model.fields,field_description:l10n_ar_website_sale.field_res_config_settings__display_name
#: model:ir.model.fields,field_description:l10n_ar_website_sale.field_website__display_name
msgid "Display Name"
msgstr ""
#. module: l10n_ar_website_sale
#: model_terms:ir.ui.view,arch_db:l10n_ar_website_sale.partner_info
msgid "<option value=\"\">Identification Type...</option>"
#: model:ir.model.fields,field_description:l10n_ar_website_sale.field_res_config_settings__l10n_ar_website_sale_show_both_prices
#: model:ir.model.fields,field_description:l10n_ar_website_sale.field_website__l10n_ar_website_sale_show_both_prices
msgid "Display Price without National Taxes"
msgstr ""
#. module: l10n_ar_website_sale
#: model_terms:ir.ui.view,arch_db:l10n_ar_website_sale.partner_info
msgid "AFIP Responsibility"
#: model:ir.model.fields,field_description:l10n_ar_website_sale.field_product_template__id
#: model:ir.model.fields,field_description:l10n_ar_website_sale.field_res_config_settings__id
#: model:ir.model.fields,field_description:l10n_ar_website_sale.field_website__id
msgid "ID"
msgstr ""
#. module: l10n_ar_website_sale
#: model_terms:ir.ui.view,arch_db:l10n_ar_website_sale.partner_info
msgid ""
"Changing AFIP Responsibility type is not allowed once document(s) have been "
"issued for your account. Please contact us directly for this operation."
#: model_terms:ir.ui.view,arch_db:l10n_ar_website_sale.l10n_ar_website_sale_products_item_inherit
msgid "Precio s/Imp. Nac."
msgstr ""
#. module: l10n_ar_website_sale
#: model_terms:ir.ui.view,arch_db:l10n_ar_website_sale.partner_info
msgid ""
"Changing Identification type is not allowed once document(s) have been "
"issued for your account. Please contact us directly for this operation."
#: model_terms:ir.ui.view,arch_db:l10n_ar_website_sale.l10n_ar_website_sale_product_price_inherit
msgid "Precio s/Imp. Nac.:"
msgstr ""
#. module: l10n_ar_website_sale
#. odoo-python
#: code:addons/l10n_ar_website_sale/controllers/main.py:0
#, python-format
msgid ""
"For the selected AFIP Responsibility you will need to set CUIT "
"Identification Type"
msgstr ""
#. module: l10n_ar_website_sale
#: model_terms:ir.ui.view,arch_db:l10n_ar_website_sale.partner_info
msgid "Identification Type"
msgstr ""
#. module: l10n_ar_website_sale
#: model:website,prevent_zero_price_sale_text:l10n_ar_website_sale.default_website_ri
msgid "Not Available For Sale"
msgstr ""
#. module: l10n_ar_website_sale
#: model_terms:ir.ui.view,arch_db:l10n_ar_website_sale.address
msgid "Number"
#: model:ir.model,name:l10n_ar_website_sale.model_product_template
msgid "Product"
msgstr ""
#. module: l10n_ar_website_sale

View file

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import product_template
from . import res_config_settings
from . import website
from . import sale_order

View file

@ -0,0 +1,79 @@
from odoo import models
from odoo.http import request
class ProductTemplate(models.Model):
_inherit = 'product.template'
def _get_sales_prices(self, website):
'''
Resolution 4/2025 requires us to display both prices on the e-commerce site:
- Price including taxes
- Price excluding taxes
If the website is configured to use tax-included pricing, we calculate the tax-excluded
price separately. This tax-excluded price is displayed on the shop page (on both list and grid views).
'''
res = super()._get_sales_prices(website)
fiscal_position_id = request.fiscal_position
pricelist_prices = request.pricelist._compute_price_rule(self, 1.0)
if (
website
and website.company_id.country_code == 'AR'
and website.l10n_ar_website_sale_show_both_prices
and website.show_line_subtotals_tax_selection == 'tax_included'
):
for template_id, template_val in res.items():
# Get applicable taxes for the product and map them using the website's FPOS
template = self.env['product.template'].browse(template_id)
product_taxes = template.sudo().taxes_id._filter_taxes_by_company(self.env.company)
mapped_taxes = fiscal_position_id.map_tax(product_taxes)
# Compute the tax-excluded value
total_excluded_value = mapped_taxes.compute_all(
price_unit=pricelist_prices[template.id][0],
currency=website.currency_id,
product=template,
)['total_excluded']
# Store the tax-excluded price in the res for use in showing both prices
res[template_id]['l10n_ar_price_tax_excluded'] = total_excluded_value
return res
def _get_additionnal_combination_info(self, product_or_template, quantity, uom, date, website):
combination_info = super()._get_additionnal_combination_info(
product_or_template, quantity, uom, date, website
)
if (
website
and website.company_id.country_code == 'AR'
and website.l10n_ar_website_sale_show_both_prices
and website.show_line_subtotals_tax_selection == 'tax_included'
):
# Get applicable taxes for the product and map them using the website's FPOS
product_taxes = product_or_template.sudo().taxes_id._filter_taxes_by_company(self.env.company)
mapped_taxes = request.fiscal_position.map_tax(product_taxes)
# Compute price per unit of product or template
pricelist_prices = request.pricelist._compute_price_rule(product_or_template, quantity)
unit_price = pricelist_prices[product_or_template.id][0]
# Compute the tax-excluded value
total_excluded_value = mapped_taxes.compute_all(
price_unit=unit_price,
currency=website.currency_id,
product=product_or_template,
)['total_excluded']
# Check if a discount is applied and adjust the tax-excluded price accordingly
if combination_info['has_discounted_price']:
discount_percent = (combination_info['list_price'] - combination_info['price']) / combination_info['list_price']
total_excluded_value = total_excluded_value * (1 - discount_percent)
# Store the tax-excluded price in the res for use in showing both prices
combination_info['l10n_ar_price_tax_excluded'] = total_excluded_value
return combination_info

View file

@ -0,0 +1,11 @@
from odoo import fields, models
class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
# Website Dependent Settings
l10n_ar_website_sale_show_both_prices = fields.Boolean(
related='website_id.l10n_ar_website_sale_show_both_prices',
readonly=False,
)

View file

@ -1,25 +0,0 @@
import pytz
from odoo import fields, models
class SaleOrder(models.Model):
_inherit = "sale.order"
def _create_invoices(self, grouped=False, final=False, date=None):
""" EXTENDS 'sale'
Necessary because if someone creates an invoice after 9 pm Argentina time, if the invoice is created
automatically, then it is created with the date of the next day (UTC date) instead of today.
This fix is necessary because it causes problems validating invoices in ARCA (ex AFIP), since when generating
the invoice with the date of the next day, no more invoices could be generated with today's date.
We took the same approach that was used in the POS module to set the date, in this case always forcing the
Argentina timezone """
invoices = super()._create_invoices(grouped=grouped, final=final, date=date)
for invoice in invoices:
if invoice.country_code == 'AR':
timezone = pytz.timezone('America/Buenos_Aires')
context_today_ar = fields.Datetime.now().astimezone(timezone).date()
invoice.invoice_date = context_today_ar
return invoices

View file

@ -1,13 +1,27 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
from odoo import api, fields, models
class Website(models.Model):
_inherit = "website"
_inherit = 'website'
def _display_partner_b2b_fields(self):
""" Argentinean localization must always display b2b fields """
self.ensure_one()
return self.company_id.country_id.code == "AR" or super()._display_partner_b2b_fields()
l10n_ar_website_sale_show_both_prices = fields.Boolean(
string="Display Price without National Taxes",
compute='_compute_l10n_ar_website_sale_show_both_prices',
readonly=False,
store=True,
)
@api.depends('company_id')
def _compute_l10n_ar_website_sale_show_both_prices(self):
for website in self:
website.l10n_ar_website_sale_show_both_prices = (
website.company_id.account_fiscal_country_id.code == 'AR'
)
@api.depends('company_id.account_fiscal_country_id')
def _compute_show_line_subtotals_tax_selection(self):
# EXTENDS 'website_sale'
super()._compute_show_line_subtotals_tax_selection()
for website in self:
if website.company_id.account_fiscal_country_id.code == 'AR':
website.show_line_subtotals_tax_selection = 'tax_included'

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -0,0 +1,22 @@
import { patch } from '@web/core/utils/patch';
import { WebsiteSale } from '@website_sale/interactions/website_sale';
patch(WebsiteSale.prototype, {
/**
* @override
* Updates the product's excluded price based on the selected variant.
* Ensuring availability info stays accurate.
*/
_onChangeCombination(ev, parent, combination) {
super._onChangeCombination(...arguments);
const currencyValue = parent.querySelector(
'.o_l10n_ar_price_tax_excluded .oe_currency_value'
);
if (currencyValue) {
const { currency_precision, l10n_ar_price_tax_excluded } = combination;
currencyValue.textContent = this._priceToStr(
l10n_ar_price_tax_excluded, currency_precision,
);
}
},
})

View file

@ -0,0 +1,11 @@
.o_wsale_products_opt_layout_list{
.o_wsale_product_sub{
.o_wsale_product_btn:has(.btn) + .o_l10n_ar_product_price {
text-align: end;
}
.o_wsale_product_btn:not(:has(.btn)) + .o_l10n_ar_product_price {
text-align: start;
}
}
}

View file

@ -1 +1 @@
from . import test_invoice
from . import test_l10n_ar_website_sale

View file

@ -1,45 +0,0 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields
from odoo.tests import tagged
from odoo.tools import mute_logger
from freezegun import freeze_time
from odoo.addons.account_payment.tests.common import AccountPaymentCommon
from odoo.addons.sale.tests.common import SaleCommon
from odoo.addons.l10n_ar.tests.common import TestAr
@tagged('-at_install', 'post_install', 'post_install_l10n')
class TestWebsiteSaleInvoice(AccountPaymentCommon, SaleCommon, TestAr):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.website = cls.env['website'].create({'name': 'Test AR Website'})
def test_website_automatic_invoice_date(self):
# Set automatic invoice
self.env['ir.config_parameter'].sudo().set_param('sale.automatic_invoice', 'True')
self.frozen_today = "2025-01-24T21:10:00"
with freeze_time(self.frozen_today, tz_offset=3):
# Prepare values needed for AR invoice generation: Tax in all lines, and AFIP responsibility partner
self.sale_order.order_line.write({'tax_id': self.company_data['default_tax_sale']})
self.sale_order.partner_id = self.partner_cf
self.sale_order.currency_id = self.env.ref('base.ARS')
# Create SO on Test Website
self.sale_order.website_id = self.website.id
# Create the payment and invoices
self.amount = self.sale_order.amount_total
tx = self._create_transaction(flow='redirect', sale_order_ids=[self.sale_order.id], state='done')
with mute_logger('odoo.addons.sale.models.payment_transaction'):
tx.with_context(l10n_ar_invoice_skip_commit=True)._reconcile_after_done()
invoice = self.sale_order.invoice_ids
self.assertTrue(invoice, "Do not create the invoice")
self.assertEqual(invoice.state, "posted", "the invoice was not posted")
self.assertEqual(fields.Datetime.now().date().strftime("%Y-%m-%d"), '2025-01-25', "UCT should be next day")
self.assertEqual(invoice.invoice_date.strftime('%Y-%m-%d'), '2025-01-24', "Should be AR current date")

View file

@ -0,0 +1,150 @@
from datetime import datetime
from odoo.fields import Command
from odoo.tests import tagged
from odoo.addons.l10n_ar.tests.common import TestArCommon
from odoo.addons.website_sale.tests.common import MockRequest
@tagged('post_install_l10n', 'post_install', '-at_install')
class TestL10nArWebsiteSale(TestArCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
# Set up Argentina-specific test company and website
cls.ar_company = cls.company_data['company']
cls.ar_website = cls.env['website'].create({
'name': 'AR Website',
'company_id': cls.ar_company.id,
})
# Create a base product template with default tax
cls.product_1 = cls.env['product.template'].create({
'name': 'Product 1',
'is_published': True,
'list_price': 1000,
'taxes_id': cls.env['account.chart.template'].ref('ri_tax_vat_21_ventas'),
})
# Create color attribute and values
cls.color_attribute = cls.env['product.attribute'].create({
'name': 'Color',
'display_type': 'color',
})
cls.color_white = cls.env['product.attribute.value'].create({
'name': 'White',
'html_color': '#FFFFFF',
'attribute_id': cls.color_attribute.id,
})
cls.color_black = cls.env['product.attribute.value'].create({
'name': 'Black',
'html_color': '#000000',
'attribute_id': cls.color_attribute.id,
})
def assertDictContains(self, actual_dict, expected_subset):
"""Assert that actual_dict contains all key-value pairs from expected_subset."""
for key, expected_value in expected_subset.items():
self.assertEqual(actual_dict.get(key), expected_value)
def _get_combination_info(self, product_id=None, quantity=1):
"""Helper method to retrieve combination info for a product."""
with MockRequest(self.env, website=self.ar_website):
return self.product_1._get_additionnal_combination_info(
product_or_template=product_id or self.product_1,
quantity=quantity,
uom=self.uom_unit,
date=datetime(2025, 5, 21),
website=self.ar_website
)
def test_default_website_sale_legal_values(self):
"""Ensure legal default values are applied on AR website."""
self.assertEqual(self.ar_website.l10n_ar_website_sale_show_both_prices, True)
self.assertEqual(self.ar_website.show_line_subtotals_tax_selection, 'tax_included')
def test_price_calculation_with_tax_changes(self):
"""Test list price and tax excluded price calculations for various tax setups."""
with self.subTest(scenario="Single 21% VAT - tax excluded"):
combo = self._get_combination_info()
self.assertDictContains(combo, {
'list_price': 1210.00, # 1000 + 21%
'l10n_ar_price_tax_excluded': 1000.00,
})
with self.subTest(scenario="Mixed taxes - 10.5% excluded + 27% included"):
template = self.env['account.chart.template']
tax_27_included = template.ref('ri_tax_vat_27_ventas')
tax_10_5_excluded = template.ref('ri_tax_vat_10_ventas')
tax_27_included.price_include = True
tax_10_5_excluded.price_include = False
self.product_1.taxes_id = (tax_27_included + tax_10_5_excluded).ids
combo = self._get_combination_info()
self.assertDictContains(combo, {
'list_price': 1082.68, # Computed price including all taxes
'l10n_ar_price_tax_excluded': 787.40, # Reverse calculated base price
})
def test_price_calculation_with_pricelist_rules(self):
"""Check that pricelist rules are taken into account."""
self._enable_pricelists()
self.pricelist.update({
'website_id': self.ar_website.id,
'item_ids': [
Command.create({
'compute_price': 'fixed',
'fixed_price': 888.0,
'min_quantity': 5.0,
'applied_on': '1_product',
'product_tmpl_id': self.product_1.id,
}),
Command.create({
'compute_price': 'formula',
'price_surcharge': 2.0,
'applied_on': '3_global',
}),
],
})
info_qty_3 = self._get_combination_info(quantity=3)
self.assertEqual(info_qty_3['l10n_ar_price_tax_excluded'], 1002.0)
info_qty_5 = self._get_combination_info(quantity=5)
self.assertEqual(info_qty_5['l10n_ar_price_tax_excluded'], 888.0)
def test_product_variant_prices_with_attributes(self):
"""Test variant-specific price calculation with color attribute values."""
self.product_1.taxes_id = self.env['account.chart.template'].ref('ri_tax_vat_21_ventas')
# Add attribute line and values to product template
attribute_line = self.env['product.template.attribute.line'].create({
'product_tmpl_id': self.product_1.id,
'attribute_id': self.color_attribute.id,
'value_ids': [(6, 0, [self.color_white.id, self.color_black.id])]
})
# Set price extras for each variant
attribute_line.product_template_value_ids[0].price_extra = 100 # White
attribute_line.product_template_value_ids[1].price_extra = 200 # Black
white_variant = self.product_1.product_variant_ids[0]
black_variant = self.product_1.product_variant_ids[1]
with self.subTest(scenario="White variant with 100 extra + 21% VAT"):
combo = self._get_combination_info(product_id=white_variant)
self.assertDictContains(combo, {
'list_price': 1331.00, # (1000+100) + 21%
'l10n_ar_price_tax_excluded': 1100.00,
})
with self.subTest(scenario="Black variant with 200 extra + 21% VAT"):
combo = self._get_combination_info(product_id=black_variant)
self.assertDictContains(combo, {
'list_price': 1452.00, # (1000+200) + 21%
'l10n_ar_price_tax_excluded': 1200.00,
})

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id='res_config_settings_view_form' model='ir.ui.view'>
<field name='name'>res.config.settings.view.form.inherit.website.sale</field>
<field name='model'>res.config.settings</field>
<field name='inherit_id' ref='website_sale.res_config_settings_view_form'/>
<field name='arch' type='xml'>
<setting id='website_tax_inclusion_setting' position='inside'>
<div invisible='country_code != "AR" or show_line_subtotals_tax_selection != "tax_included"'>
<field name='l10n_ar_website_sale_show_both_prices'/>
<label for='l10n_ar_website_sale_show_both_prices'/>
</div>
</setting>
</field>
</record>
</odoo>

View file

@ -1,63 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="partner_info" name="Argentinean partner">
<!-- show afip responsibility -->
<div t-attf-class="mb-3 #{error.get('l10n_ar_afip_responsibility_type_id') and 'o_has_error' or ''} col-xl-6">
<label class="col-form-label" for="l10n_ar_afip_responsibility_type_id">AFIP Responsibility</label>
<t t-if="partner.can_edit_vat()">
<select name="l10n_ar_afip_responsibility_type_id" t-attf-class="form-select #{error.get('l10n_ar_afip_responsibility_type_id') and 'is-invalid' or ''}">
<option value="">AFIP Responsibility...</option>
<t t-foreach="responsibility_types or []" t-as="resp_type">
<option t-att-value="resp_type.id" t-att-selected="resp_type.id == int(responsibility) if responsibility else resp_type.id == partner.l10n_ar_afip_responsibility_type_id.id">
<t t-esc="resp_type.name"/>
</option>
</t>
</select>
</t>
<t t-else="">
<p class="form-control" t-esc="partner.l10n_ar_afip_responsibility_type_id.name" readonly="1" title="Changing AFIP Responsibility type is not allowed once document(s) have been issued for your account. Please contact us directly for this operation."/>
<input name="l10n_ar_afip_responsibility_type_id" class="form-control" t-att-value="partner.l10n_ar_afip_responsibility_type_id.id" type='hidden'/>
</t>
</div>
<!-- show identification type -->
<div t-attf-class="mb-3 #{error.get('l10n_latam_identification_type_id') and 'o_has_error' or ''} col-xl-6">
<label class="col-form-label" for="l10n_latam_identification_type_id">Identification Type</label>
<t t-if="partner.can_edit_vat()">
<select name="l10n_latam_identification_type_id" t-attf-class="form-select #{error.get('l10n_latam_identification_type_id') and 'is-invalid' or ''}">
<option value="">Identification Type...</option>
<t t-foreach="identification_types or []" t-as="id_type">
<option t-att-value="id_type.id" t-att-selected="id_type.id == int(identification) if identification else id_type.id == partner.l10n_latam_identification_type_id.id">
<t t-esc="id_type.name"/>
</option>
</t>
</select>
</t>
<t t-else="">
<p class="form-control" t-esc="partner.l10n_latam_identification_type_id.name" readonly="1" title="Changing Identification type is not allowed once document(s) have been issued for your account. Please contact us directly for this operation."/>
<input name="l10n_latam_identification_type_id" class="form-control" t-att-value="partner.l10n_latam_identification_type_id.id" type='hidden'/>
</t>
</div>
<template id='l10n_ar_website_sale_product_price_inherit' inherit_id='website_sale.product_price'>
<xpath expr='//div[1]'>
<small t-if='combination_info.get("l10n_ar_price_tax_excluded")' class='h6 text-muted'>
Precio s/Imp. Nac.:
<span
class='o_l10n_ar_price_tax_excluded'
t-out='combination_info["l10n_ar_price_tax_excluded"]'
t-options='{
"widget": "monetary",
"display_currency": website.currency_id,
}'
/>
</small>
</xpath>
</template>
<template id="address" inherit_id="website_sale.address">
<xpath expr="//input[@name='vat']/.." position="before">
<t t-if="mode[1] == 'billing'" positon="inside">
<t t-if="res_company.country_id.code == 'AR'">
<t t-set="partner" t-value="website_sale_order.partner_id"/>
<t t-call="l10n_ar_website_sale.partner_info"/>
</t>
</t>
<template id='l10n_ar_website_sale_products_item_inherit' inherit_id='website_sale.products_item'>
<xpath expr='//div[hasclass("product_price")]' position='attributes'>
<attribute name='t-attf-class' add='o_l10n_ar_product_price'/>
</xpath>
<xpath expr='//div[hasclass("product_price")]' position='inside'>
<small t-if='template_price_vals.get("l10n_ar_price_tax_excluded")' class='d-block mt-1 text-muted'>
Precio s/Imp. Nac.
<span
class='o_l10n_ar_price_tax_excluded'
t-out='template_price_vals["l10n_ar_price_tax_excluded"]'
t-options='{
"widget": "monetary",
"display_currency": website.currency_id,
}'
/>
</small>
</xpath>
<label for="vat" position="attributes">
<attribute name="t-if">res_company.country_id.code != 'AR'</attribute>
</label>
<label for="vat" position="after">
<label t-if="res_company.country_id.code == 'AR'" class="col-form-label label-optional" for="vat">Number</label>
</label>
</template>
</odoo>

View file

@ -1,13 +1,15 @@
[project]
name = "odoo-bringout-oca-ocb-l10n_ar_website_sale"
version = "16.0.0"
description = "Argentinean eCommerce - Odoo addon"
description = "Argentinean eCommerce -
Odoo addon
"
authors = [
{ name = "Ernad Husremovic", email = "hernad@bring.out.ba" }
]
dependencies = [
"odoo-bringout-oca-ocb-website_sale>=16.0.0",
"odoo-bringout-oca-ocb-l10n_ar>=16.0.0",
"odoo-bringout-oca-ocb-website_sale>=19.0.0",
"odoo-bringout-oca-ocb-l10n_ar>=19.0.0",
"requests>=2.25.1"
]
readme = "README.md"
@ -17,7 +19,7 @@ classifiers = [
"Intended Audience :: Developers",
"License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Office/Business",
]