mirror of
https://gitlab.com/flectra-hq/flectra.git
synced 2026-08-18 17:24:42 -05:00
[PATCH] Upstream patch - 06082022
This commit is contained in:
@@ -294,7 +294,7 @@ class AccountMove(models.Model):
|
||||
default=_get_default_invoice_incoterm,
|
||||
help='International Commercial Terms are a series of predefined commercial terms used in international transactions.')
|
||||
display_qr_code = fields.Boolean(string="Display QR-code", related='company_id.qr_code')
|
||||
qr_code_method = fields.Selection(string="Payment QR-code",
|
||||
qr_code_method = fields.Selection(string="Payment QR-code", copy=False,
|
||||
selection=lambda self: self.env['res.partner.bank'].get_available_qr_methods_in_sequence(),
|
||||
help="Type of QR-code to be generated for the payment of this invoice, when printing it. If left blank, the first available and usable method will be used.")
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from flectra.exceptions import UserError
|
||||
from flectra.addons.account.tests.common import AccountTestInvoicingCommon
|
||||
from flectra.tests import tagged
|
||||
|
||||
from flectra import fields
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestSEPAQRCode(AccountTestInvoicingCommon):
|
||||
@@ -62,3 +62,16 @@ class TestSEPAQRCode(AccountTestInvoicingCommon):
|
||||
"""
|
||||
self.sepa_qr_invoice.generate_qr_code()
|
||||
self.assertEqual(self.sepa_qr_invoice.qr_code_method, 'sct_qr', "SEPA QR-code generator should have been chosen for this invoice.")
|
||||
|
||||
def test_out_invoice_create_refund_qr_code(self):
|
||||
self.sepa_qr_invoice.generate_qr_code()
|
||||
self.sepa_qr_invoice.action_post()
|
||||
move_reversal = self.env['account.move.reversal'].with_context(active_model="account.move", active_ids=self.sepa_qr_invoice.ids).create({
|
||||
'date': fields.Date.from_string('2019-02-01'),
|
||||
'reason': 'no reason',
|
||||
'refund_method': 'refund',
|
||||
})
|
||||
reversal = move_reversal.reverse_moves()
|
||||
reverse_move = self.env['account.move'].browse(reversal['res_id'])
|
||||
|
||||
self.assertFalse(reverse_move.qr_code_method, "qr_code_method for credit note should be None")
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<flectra>
|
||||
<data noupdate="1">
|
||||
|
||||
<!-- Account Tax Group -->
|
||||
<record id="tax_group_tva_21" model="account.tax.group">
|
||||
<field name="name">TVA 21%</field>
|
||||
</record>
|
||||
<data noupdate="1">
|
||||
|
||||
<record id="tax_group_tva_12" model="account.tax.group">
|
||||
<field name="name">TVA 12%</field>
|
||||
</record>
|
||||
<!-- Account Tax Group -->
|
||||
<record id="tax_group_tva_21" model="account.tax.group">
|
||||
<field name="name">TVA 21%</field>
|
||||
</record>
|
||||
|
||||
<record id="tax_group_tva_6" model="account.tax.group">
|
||||
<field name="name">TVA 6%</field>
|
||||
</record>
|
||||
<record id="tax_group_tva_12" model="account.tax.group">
|
||||
<field name="name">TVA 12%</field>
|
||||
</record>
|
||||
|
||||
<record id="tax_group_tva_0" model="account.tax.group">
|
||||
<field name="name">TVA 0%</field>
|
||||
</record>
|
||||
<record id="tax_group_tva_6" model="account.tax.group">
|
||||
<field name="name">TVA 6%</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</flectra>
|
||||
<record id="tax_group_tva_0" model="account.tax.group">
|
||||
<field name="name">TVA 0%</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</flectra>
|
||||
|
||||
@@ -12,6 +12,10 @@
|
||||
<field name="zip">12345</field>
|
||||
</record>
|
||||
|
||||
<record id="l10n_it.partner_demo_company_it" model="res.partner">
|
||||
<field name="l10n_it_pa_index">0803HR0</field>
|
||||
</record>
|
||||
|
||||
<record id="partner_demo_it" model="res.partner">
|
||||
<field name="name">Palazzo dell'Arte</field>
|
||||
<field name="vat">IT00000010215</field>
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
<data>
|
||||
|
||||
<template id="account_invoice_line_it_FatturaPA">
|
||||
<t t-if="rc_refund">
|
||||
<t t-set="price_subtotal" t-value="-line.price_subtotal"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t t-set="price_subtotal" t-value="line.price_subtotal"/>
|
||||
</t>
|
||||
<DettaglioLinee>
|
||||
<NumeroLinea t-esc="line_counter"/>
|
||||
<CodiceArticolo t-if="line.product_id.barcode">
|
||||
@@ -19,12 +25,12 @@
|
||||
</Descrizione>
|
||||
<Quantita t-esc="format_numbers(line.quantity)"/>
|
||||
<UnitaMisura t-if="line.product_uom_id.category_id != env.ref('uom.product_uom_categ_unit')" t-esc="format_alphanumeric(line.product_uom_id.name)"/>
|
||||
<PrezzoUnitario t-esc="'%.06f' % (line.price_subtotal / (( 1 - (line.discount or 0.0) / 100.0) * line.quantity) if line.quantity and line.discount != 100.0 else line.price_unit)"/>
|
||||
<PrezzoUnitario t-esc="'%.06f' % (price_subtotal / (( 1 - (line.discount or 0.0) / 100.0) * line.quantity) if line.quantity and line.discount != 100.0 else line.price_unit)"/>
|
||||
<ScontoMaggiorazione t-if="line.discount != 0">
|
||||
<Tipo t-esc="discount_type(line.discount)"/>
|
||||
<Percentuale t-esc="format_numbers(abs(line.discount))"/>
|
||||
</ScontoMaggiorazione>
|
||||
<PrezzoTotale t-esc="format_monetary(line.price_subtotal, currency)"/>
|
||||
<PrezzoTotale t-esc="format_monetary(price_subtotal, currency)"/>
|
||||
<AliquotaIVA t-if="line.tax_ids.amount_type == 'percent'" t-esc="format_numbers(line.tax_ids.amount)"/>
|
||||
<AliquotaIVA t-elif="line.tax_ids.amount_type != 'percent'" t-esc="'0.00'"/>
|
||||
<Natura t-if="line.tax_ids.l10n_it_has_exoneration" t-esc="line.tax_ids.l10n_it_kind_exoneration"/>
|
||||
@@ -158,8 +164,14 @@
|
||||
<AliquotaIVA t-esc="format_numbers(tax.amount)"/>
|
||||
<Natura t-if="has_exoneration" t-esc="kind_exoneration"/>
|
||||
<Arrotondamento t-if="tax_dict.get('rounding')" t-esc="format_numbers(tax_dict['rounding'])"/>
|
||||
<ImponibileImporto t-esc="format_monetary(abs(tax_dict['base_amount_currency']), currency)"/>
|
||||
<Imposta t-esc="format_monetary(abs(tax_dict['tax_amount_currency']), currency)"/>
|
||||
<t t-if="rc_refund">
|
||||
<ImponibileImporto t-esc="format_monetary(tax_dict['base_amount_currency'], currency)"/>
|
||||
<Imposta t-esc="format_monetary(tax_dict['tax_amount_currency'], currency)"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<ImponibileImporto t-esc="format_monetary(abs(tax_dict['base_amount_currency']), currency)"/>
|
||||
<Imposta t-esc="format_monetary(abs(tax_dict['tax_amount_currency']), currency)"/>
|
||||
</t>
|
||||
<EsigibilitaIVA t-if="not has_exoneration or kind_exoneration == 'N6'" t-esc="tax.l10n_it_vat_due_date"/>
|
||||
<RiferimentoNormativo t-if="has_exoneration" t-esc="format_alphanumeric(tax.l10n_it_law_reference[:100])"/>
|
||||
</DatiRiepilogo>
|
||||
|
||||
@@ -212,14 +212,14 @@ class AccountEdiFormat(models.Model):
|
||||
|
||||
def _l10n_it_document_type_mapping(self):
|
||||
return {
|
||||
'TD01': dict(move_type='out_invoice', import_type='in_invoice'),
|
||||
'TD04': dict(move_type='out_refund', import_type='in_refund'),
|
||||
'TD07': dict(move_type='out_invoice', import_type='in_invoice', simplified=True),
|
||||
'TD08': dict(move_type='out_refund', import_type='in_refund', simplified=True),
|
||||
'TD09': dict(move_type='out_invoice', import_type='in_invoice', simplified=True),
|
||||
'TD17': dict(move_type='in_invoice', import_type='out_invoice', self_invoice=True, services_or_goods="service"),
|
||||
'TD18': dict(move_type='in_invoice', import_type='out_invoice', self_invoice=True, services_or_goods="consu", partner_in_eu=True),
|
||||
'TD19': dict(move_type='in_invoice', import_type='out_invoice', self_invoice=True, services_or_goods="consu", goods_in_italy=True),
|
||||
'TD01': dict(move_types=['out_invoice'], import_type='in_invoice'),
|
||||
'TD04': dict(move_types=['out_refund'], import_type='in_refund'),
|
||||
'TD07': dict(move_types=['out_invoice'], import_type='in_invoice', simplified=True),
|
||||
'TD08': dict(move_types=['out_refund'], import_type='in_refund', simplified=True),
|
||||
'TD09': dict(move_types=['out_invoice'], import_type='in_invoice', simplified=True),
|
||||
'TD17': dict(move_types=['in_invoice', 'in_refund'], import_type='out_invoice', self_invoice=True, services_or_goods="service"),
|
||||
'TD18': dict(move_types=['in_invoice', 'in_refund'], import_type='out_invoice', self_invoice=True, services_or_goods="consu", partner_in_eu=True),
|
||||
'TD19': dict(move_types=['in_invoice', 'in_refund'], import_type='out_invoice', self_invoice=True, services_or_goods="consu", goods_in_italy=True),
|
||||
}
|
||||
|
||||
def _l10n_it_get_document_type(self, invoice):
|
||||
@@ -232,7 +232,7 @@ class AccountEdiFormat(models.Model):
|
||||
info_services_or_goods = infos.get('services_or_goods', "both")
|
||||
info_partner_in_eu = infos.get('partner_in_eu', False)
|
||||
if all([
|
||||
invoice.move_type == infos.get('move_type', False),
|
||||
invoice.move_type in infos.get('move_types', False),
|
||||
is_self_invoice == infos.get('self_invoice', False),
|
||||
is_simplified == infos.get('simplified', False),
|
||||
info_services_or_goods in ("both", services_or_goods),
|
||||
|
||||
@@ -183,11 +183,16 @@ class AccountMove(models.Model):
|
||||
or (partner.country_id.code == 'IT' and '0000000')
|
||||
or 'XXXXXXX')
|
||||
|
||||
# Represent if the document is a reverse charge refund in a single variable
|
||||
rc_refund = self.move_type == 'in_refund' and document_type in ['TD16', 'TD17', 'TD18']
|
||||
|
||||
# Self-invoices are technically -100%/+100% repartitioned
|
||||
# but functionally need to be exported as 100%
|
||||
document_total = self.amount_total
|
||||
if is_self_invoice:
|
||||
document_total += sum([v['tax_amount_currency'] for k, v in tax_details['tax_details'].items()])
|
||||
document_total += sum([abs(v['tax_amount_currency']) for k, v in tax_details['tax_details'].items()])
|
||||
if rc_refund:
|
||||
document_total = -abs(document_total)
|
||||
|
||||
# Create file content.
|
||||
template_values = {
|
||||
@@ -225,7 +230,8 @@ class AccountMove(models.Model):
|
||||
'normalize_codice_fiscale': partner._l10n_it_normalize_codice_fiscale,
|
||||
'get_vat_number': get_vat_number,
|
||||
'get_vat_country': get_vat_country,
|
||||
'in_eu': in_eu
|
||||
'in_eu': in_eu,
|
||||
'rc_refund': rc_refund,
|
||||
}
|
||||
return template_values
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||
from stdnum.it import codicefiscale, iva
|
||||
|
||||
from flectra import api, fields, models, _
|
||||
from flectra.exceptions import UserError, ValidationError
|
||||
@@ -45,7 +46,6 @@ class ResPartner(models.Model):
|
||||
|
||||
@api.constrains('l10n_it_codice_fiscale')
|
||||
def validate_codice_fiscale(self):
|
||||
p = re.compile(r'^([A-Za-z]{6}[0-9]{2}[A-Za-z]{1}[0-9]{2}[A-Za-z]{1}[0-9]{3}[A-Za-z]{1}$)|([0-9]{11})|(IT[0-9]{11})$')
|
||||
for record in self:
|
||||
if record.l10n_it_codice_fiscale and not p.match(record.l10n_it_codice_fiscale):
|
||||
raise UserError(_("Invalid Codice Fiscale '%s': should be like 'MRTMTT91D08F205J' for physical person and '12345678901' or 'IT12345678901' for businesses.", record.l10n_it_codice_fiscale))
|
||||
if record.l10n_it_codice_fiscale and (not codicefiscale.is_valid(record.l10n_it_codice_fiscale) and not iva.is_valid(record.l10n_it_codice_fiscale)):
|
||||
raise UserError(_("Invalid Codice Fiscale '%s': should be like 'MRTMTT91D08F205J' for physical person and '12345670546' or 'IT12345670546' for businesses.", record.l10n_it_codice_fiscale))
|
||||
|
||||
@@ -2,3 +2,4 @@
|
||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import test_ir_mail_server
|
||||
from . import test_res_partner
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||
from flectra.exceptions import UserError
|
||||
from flectra.tests.common import TransactionCase, tagged
|
||||
|
||||
|
||||
@tagged('post_install_l10n', 'post_install', '-at_install')
|
||||
class TestResPartner(TransactionCase):
|
||||
|
||||
def test_validate_fiscal_code(self):
|
||||
valid_codes = [
|
||||
"AORTHV05P30V295L",
|
||||
"SPDTHB43S93F42VH",
|
||||
"MDRTUV99H14X2MNU",
|
||||
"XPTDRX73R64YPLUD",
|
||||
"LOLXDR40T3MZRTSV",
|
||||
"GJTIUG55DLQZRTSS",
|
||||
"CDEOTG5PBLQZRTSE",
|
||||
"PERTLELPALQZRTSN",
|
||||
"IT12345678887",
|
||||
"IT12345670546",
|
||||
"IT95286931217",
|
||||
"IT95867361206",
|
||||
"IT94567689990",
|
||||
"12345670546",
|
||||
"95286931217",
|
||||
"95867361206",
|
||||
"94567689990",
|
||||
]
|
||||
|
||||
invalid_codes = [
|
||||
"AORTHV05P34V295U",
|
||||
"SPDTHB43O93F42VH",
|
||||
"MDRTUVV9H14X2MNU",
|
||||
"XPTDRX73RS4YPLUD",
|
||||
"LOLXDRQ0T3QZRTSJ",
|
||||
"GJTIUGR5DLQZRTSS",
|
||||
"CDEOTG5PBLQZRTSS",
|
||||
"PERTLEZPALQZRTSN",
|
||||
"IT12345678901",
|
||||
"IT12345678885",
|
||||
"IT45689349992",
|
||||
"IT78239131204",
|
||||
"IT45692151219",
|
||||
"12345678901",
|
||||
"12345678885",
|
||||
"45689349992",
|
||||
"78239131204",
|
||||
"45692151219",
|
||||
]
|
||||
|
||||
partners = self.env['res.partner']
|
||||
|
||||
for i, code in enumerate(invalid_codes):
|
||||
with self.assertRaises(UserError):
|
||||
partners += self.env['res.partner'].create({'name': f'partner_{i}', 'l10n_it_codice_fiscale': code})
|
||||
|
||||
for i, code in enumerate(valid_codes):
|
||||
partners += self.env['res.partner'].create({'name': f'partner_{i}', 'l10n_it_codice_fiscale': code})
|
||||
|
||||
self.assertEqual(len(partners), len(valid_codes))
|
||||
@@ -55,13 +55,13 @@ class TestItEdiReverseCharge(TestItEdi):
|
||||
'amount_type': 'percent',
|
||||
'type_tax_use': 'purchase',
|
||||
'invoice_repartition_line_ids': repartition_lines(
|
||||
RepartitionLine(100, 'base', ('+03',)),
|
||||
RepartitionLine(100, 'tax', ('+vj9',)),
|
||||
RepartitionLine(-100, 'tax', False)),
|
||||
RepartitionLine(100, 'base', ('+03', '+vj9')),
|
||||
RepartitionLine(100, 'tax', ('+5v',)),
|
||||
RepartitionLine(-100, 'tax', ('-4v',))),
|
||||
'refund_repartition_line_ids': repartition_lines(
|
||||
RepartitionLine(100, 'base', False),
|
||||
RepartitionLine(100, 'base', ('-03', '-vj9')),
|
||||
RepartitionLine(100, 'tax', False),
|
||||
RepartitionLine(-100, 'tax', False))
|
||||
RepartitionLine(-100, 'tax', False)),
|
||||
}
|
||||
# Purchase tax 4% with Reverse Charge
|
||||
cls.purchase_tax_4p = cls.env['account.tax'].with_company(cls.company).create(tax_data)
|
||||
@@ -74,10 +74,15 @@ class TestItEdiReverseCharge(TestItEdi):
|
||||
**tax_data,
|
||||
'name': 'Tax 4% purchase Reverse Charge, in Italy',
|
||||
'invoice_repartition_line_ids': repartition_lines(
|
||||
RepartitionLine(100, 'base', ('+03',)),
|
||||
RepartitionLine(100, 'tax', ('+vj3',)),
|
||||
RepartitionLine(100, 'base', ('+03', '+vj3')),
|
||||
RepartitionLine(100, 'tax', ('+5v',)),
|
||||
RepartitionLine(-100, 'tax', ('-4v',))),
|
||||
'refund_repartition_line_ids': repartition_lines(
|
||||
RepartitionLine(100, 'base', ('-03', '-vj3')),
|
||||
RepartitionLine(100, 'tax', False),
|
||||
RepartitionLine(-100, 'tax', False)),
|
||||
}
|
||||
|
||||
cls.purchase_tax_4p_already_in_italy = cls.env['account.tax'].with_company(cls.company).create(tax_data_4p_already_in_italy)
|
||||
cls.line_tax_4p_already_in_italy = cls.standard_line.copy()
|
||||
cls.line_tax_4p_already_in_italy['tax_ids'] = [(6, 0, cls.purchase_tax_4p_already_in_italy.ids)]
|
||||
@@ -151,11 +156,15 @@ class TestItEdiReverseCharge(TestItEdi):
|
||||
),
|
||||
}
|
||||
cls.reverse_charge_bill_2 = cls.env['account.move'].with_company(cls.company).create(bill_data_2)
|
||||
cls.reverse_charge_refund = cls.reverse_charge_bill.with_company(cls.company)._reverse_moves([{
|
||||
'invoice_date': fields.Date.from_string('2022-03-24'),
|
||||
}])
|
||||
|
||||
# Posting moves -----------
|
||||
cls.reverse_charge_invoice._post()
|
||||
cls.reverse_charge_bill._post()
|
||||
cls.reverse_charge_bill_2._post()
|
||||
cls.reverse_charge_refund._post()
|
||||
|
||||
def _cleanup_etree(self, content, xpaths=None):
|
||||
xpaths = {
|
||||
@@ -193,3 +202,26 @@ class TestItEdiReverseCharge(TestItEdi):
|
||||
"(//DettaglioLinee/Descrizione)[2]": "<Descrizione/>",
|
||||
}
|
||||
)
|
||||
|
||||
def test_reverse_charge_refund(self):
|
||||
self._test_invoice_with_sample_file(
|
||||
self.reverse_charge_refund,
|
||||
"reverse_charge_bill.xml",
|
||||
xpaths_result={
|
||||
"//DatiGeneraliDocumento/Numero": "<Numero/>",
|
||||
"//DatiPagamento/DettaglioPagamento/DataScadenzaPagamento": "<DataScadenzaPagamento/>",
|
||||
},
|
||||
xpaths_file={
|
||||
"//DatiGeneraliDocumento/Numero": "<Numero/>",
|
||||
"//DatiGeneraliDocumento/ImportoTotaleDocumento": "<ImportoTotaleDocumento>-1808.91</ImportoTotaleDocumento>",
|
||||
"//DatiPagamento/DettaglioPagamento/DataScadenzaPagamento": "<DataScadenzaPagamento/>",
|
||||
"(//DettaglioLinee/PrezzoUnitario)[1]": "<PrezzoUnitario>-800.400000</PrezzoUnitario>",
|
||||
"(//DettaglioLinee/PrezzoUnitario)[2]": "<PrezzoUnitario>-800.400000</PrezzoUnitario>",
|
||||
"(//DettaglioLinee/PrezzoTotale)[1]": "<PrezzoTotale>-800.40</PrezzoTotale>",
|
||||
"(//DettaglioLinee/PrezzoTotale)[2]": "<PrezzoTotale>-800.40</PrezzoTotale>",
|
||||
"(//DatiRiepilogo/ImponibileImporto)[1]": "<ImponibileImporto>-800.40</ImponibileImporto>",
|
||||
"(//DatiRiepilogo/ImponibileImporto)[2]": "<ImponibileImporto>-800.40</ImponibileImporto>",
|
||||
"(//DatiRiepilogo/Imposta)[1]": "<Imposta>-176.09</Imposta>",
|
||||
"(//DatiRiepilogo/Imposta)[2]": "<Imposta>-32.02</Imposta>",
|
||||
}
|
||||
)
|
||||
|
||||
@@ -347,7 +347,7 @@ var MassMailingFieldHtml = FieldHtml.extend({
|
||||
var $snippets_menu = $snippetsSideBar.find("#snippets_menu");
|
||||
|
||||
for (const button of $snippets_menu.get(0).children) {
|
||||
if (button.textContent === 'Select a template') {
|
||||
if (!button.hasAttribute('tabindex') && !button.hasAttribute('accesskey')) {
|
||||
button.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -531,7 +531,7 @@
|
||||
<field name="view_id" eval="False"/>
|
||||
<field name="search_view_id" ref="view_mrp_production_filter"/>
|
||||
<field name="domain">[('picking_type_id', '=', active_id)]</field>
|
||||
<field name="context">{}</field>
|
||||
<field name="context">{'default_picking_type_id': active_id}</field>
|
||||
</record>
|
||||
|
||||
<record id="mrp_production_action_unreserve_tree" model="ir.actions.server">
|
||||
|
||||
@@ -2268,6 +2268,16 @@ exports.Orderline = Backbone.Model.extend({
|
||||
return this.get_base_price();
|
||||
}
|
||||
},
|
||||
get_taxed_lst_unit_price: function(){
|
||||
var lst_price = this.get_lst_price();
|
||||
if (this.pos.config.iface_tax_included === 'total') {
|
||||
var product = this.get_product();
|
||||
var taxes_ids = product.taxes_id;
|
||||
var product_taxes = this.get_taxes_after_fp(taxes_ids);
|
||||
return this.compute_all(product_taxes, lst_price, 1, this.pos.currency.rounding).total_included;
|
||||
}
|
||||
return lst_price;
|
||||
},
|
||||
get_price_without_tax: function(){
|
||||
return this.get_all_prices().priceWithoutTax;
|
||||
},
|
||||
|
||||
@@ -37,9 +37,9 @@
|
||||
<span> </span><t t-esc="props.line.get_unit().name" />
|
||||
at
|
||||
<t t-if="props.line.display_discount_policy() == 'without_discount' and
|
||||
props.line.get_unit_display_price() < props.line.get_lst_price()">
|
||||
props.line.get_unit_display_price() < props.line.get_taxed_lst_unit_price()">
|
||||
<s>
|
||||
<t t-esc="env.pos.format_currency(props.line.get_fixed_lst_price(),'Product Price')" />
|
||||
<t t-esc="env.pos.format_currency(props.line.get_taxed_lst_unit_price(),'Product Price')" />
|
||||
</s>
|
||||
<t t-esc="env.pos.format_currency(props.line.get_unit_display_price(),'Product Price')" />
|
||||
</t>
|
||||
|
||||
@@ -335,7 +335,8 @@ class PurchaseOrderLine(models.Model):
|
||||
result = super(PurchaseOrderLine, self).write(values)
|
||||
if 'price_unit' in values:
|
||||
for line in lines:
|
||||
moves = line.move_ids.filtered(lambda s: s.state not in ('cancel', 'done'))
|
||||
# Avoid updating kit components' stock.move
|
||||
moves = line.move_ids.filtered(lambda s: s.state not in ('cancel', 'done') and s.product_id == line.product_id)
|
||||
moves.write({'price_unit': line._get_stock_move_price_unit()})
|
||||
if 'product_qty' in values:
|
||||
lines.with_context(previous_product_qty=previous_product_qty)._create_or_update_picking()
|
||||
|
||||
@@ -1188,6 +1188,7 @@ class SaleOrderLine(models.Model):
|
||||
elif not float_is_zero(line.qty_to_invoice, precision_digits=precision):
|
||||
line.invoice_status = 'to invoice'
|
||||
elif line.state == 'sale' and line.product_id.invoice_policy == 'order' and\
|
||||
line.product_uom_qty >= 0.0 and\
|
||||
float_compare(line.qty_delivered, line.product_uom_qty, precision_digits=precision) == 1:
|
||||
line.invoice_status = 'upselling'
|
||||
elif float_compare(line.qty_invoiced, line.product_uom_qty, precision_digits=precision) >= 0:
|
||||
|
||||
@@ -172,6 +172,20 @@ class TestSaleOrder(TestSaleCommon):
|
||||
self.assertEqual(mail_message.author_id, mail_message.partner_ids, 'Sale: author should be in composer recipients thanks to "partner_to" field set on template')
|
||||
self.assertEqual(mail_message.partner_ids, mail_message.sudo().mail_ids.recipient_ids, 'Sale: author should receive mail due to presence in composer recipients')
|
||||
|
||||
def test_invoice_state_when_ordered_quantity_is_negative(self):
|
||||
"""When you invoice a SO line with a product that is invoiced on ordered quantities and has negative ordered quantity,
|
||||
this test ensures that the invoicing status of the SO line is 'invoiced' (and not 'upselling')."""
|
||||
sale_order = self.env['sale.order'].create({
|
||||
'partner_id': self.partner_a.id,
|
||||
'order_line': [(0, 0, {
|
||||
'product_id': self.company_data['product_order_no'].id,
|
||||
'product_uom_qty': -1,
|
||||
})]
|
||||
})
|
||||
sale_order.action_confirm()
|
||||
sale_order._create_invoices(final=True)
|
||||
self.assertTrue(sale_order.invoice_status == 'invoiced', 'Sale: The invoicing status of the SO should be "invoiced"')
|
||||
|
||||
def test_sale_sequence(self):
|
||||
self.env['ir.sequence'].search([
|
||||
('code', '=', 'sale.order'),
|
||||
|
||||
@@ -90,6 +90,8 @@ class ResCompany(models.Model):
|
||||
('default_location_src_id.usage', '=', 'supplier'),
|
||||
('default_location_dest_id.usage', '=', 'customer'),
|
||||
], limit=1, order='sequence')
|
||||
if not dropship_picking_type:
|
||||
continue
|
||||
dropship_vals.append({
|
||||
'name': '%s → %s' % (supplier_location.name, customer_location.name),
|
||||
'action': 'buy',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
flectra.define('web.DatePickerOwl', function (require) {
|
||||
"use strict";
|
||||
|
||||
const config = require('web.config');
|
||||
const field_utils = require('web.field_utils');
|
||||
const time = require('web.time');
|
||||
const { useAutofocus } = require('web.custom_hooks');
|
||||
@@ -167,7 +168,7 @@ flectra.define('web.DatePickerOwl', function (require) {
|
||||
* @private
|
||||
*/
|
||||
_onWindowScroll(ev) {
|
||||
if (ev.target !== this.inputRef.el) {
|
||||
if (!config.device.isIOS && ev.target !== this.inputRef.el) {
|
||||
this._datetimepicker('hide');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -615,16 +615,9 @@ function parseFloat(value) {
|
||||
* @throws {Error} if no float is found or if parameter does not respect monetary condition
|
||||
*/
|
||||
function parseMonetary(value, field, options) {
|
||||
var values = value.split(' ');
|
||||
if (values.length === 1) {
|
||||
values = value.split(NBSP);
|
||||
}
|
||||
if (values.length === 1) {
|
||||
if (!value.includes(NBSP) && !value.includes(' ')) {
|
||||
return parseFloat(value);
|
||||
}
|
||||
else if (values.length !== 2) {
|
||||
throw new Error(_.str.sprintf(core._t("'%s' is not a correct monetary field"), value));
|
||||
}
|
||||
options = options || {};
|
||||
var currency = options.currency;
|
||||
if (!currency) {
|
||||
@@ -635,7 +628,18 @@ function parseMonetary(value, field, options) {
|
||||
}
|
||||
currency = session.get_currency(currency_id);
|
||||
}
|
||||
return parseFloat(values[0] === currency.symbol ? values[1] : values[0]);
|
||||
if (!value.includes(currency.symbol)) {
|
||||
throw new Error(_.str.sprintf(core._t("'%s' is not a correct monetary field"), value));
|
||||
}
|
||||
if (currency.position === 'before') {
|
||||
return parseFloat(value
|
||||
.replace(`${ currency.symbol }${ NBSP }`, '')
|
||||
.replace(`${ currency.symbol } `, ''));
|
||||
} else {
|
||||
return parseFloat(value
|
||||
.replace(`${ NBSP }${ currency.symbol }`, '')
|
||||
.replace(` ${ currency.symbol }`, ''));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -261,8 +261,9 @@ QUnit.test('parse integer', function(assert) {
|
||||
});
|
||||
|
||||
QUnit.test('parse monetary', function(assert) {
|
||||
assert.expect(11);
|
||||
assert.expect(13);
|
||||
var originalCurrencies = session.currencies;
|
||||
const originalParameters = _.clone(core._t.database.parameters);
|
||||
session.currencies = {
|
||||
1: {
|
||||
digits: [69, 2],
|
||||
@@ -288,7 +289,18 @@ QUnit.test('parse monetary', function(assert) {
|
||||
assert.throws(function() {fieldUtils.parse.monetary("$ 12.00", {}, {currency_id: 1})}, /is not a correct/);
|
||||
assert.throws(function() {fieldUtils.parse.monetary("$ 12.00 34", {}, {currency_id: 3})}, /is not a correct/);
|
||||
|
||||
// In some languages, the non-breaking space character is used as thousands separator.
|
||||
const nbsp = '\u00a0';
|
||||
_.extend(core._t.database.parameters, {
|
||||
grouping: [3, 0],
|
||||
decimal_point: ',',
|
||||
thousands_sep: nbsp,
|
||||
});
|
||||
assert.strictEqual(fieldUtils.parse.monetary(`1${nbsp}000.00${nbsp}€`, {}, {currency_id: 1}), 1000);
|
||||
assert.strictEqual(fieldUtils.parse.monetary(`$${nbsp}1${nbsp}000.00`, {}, {currency_id: 3}), 1000);
|
||||
|
||||
session.currencies = originalCurrencies;
|
||||
core._t.database.parameters = originalParameters;
|
||||
});
|
||||
|
||||
QUnit.test('parse percentage', function(assert) {
|
||||
|
||||
@@ -77,7 +77,16 @@ function _buildElement(tagName, title, options) {
|
||||
*/
|
||||
function _buildTitleElement(title) {
|
||||
const titleEl = document.createElement('we-title');
|
||||
titleEl.textContent = title;
|
||||
// As a stable fix, to not touch XML templates and break existing
|
||||
// translations, the ⌙ character is automatically replaced by └ which makes
|
||||
// more sense for the usecase and should work properly in all browsers. The
|
||||
// ⌙ character is actually rendered mirrored on Windows 11 Chrome (and
|
||||
// others) as the font used for those unicode characters is left to the
|
||||
// browser. We could force a font of our own but it's probably not worth it.
|
||||
// TODO a better solution with a SVG or CSS solution has to be done in
|
||||
// master. That would unify the look of the symbol across all browsers and
|
||||
// also prevent special characters to be placed in translations.
|
||||
titleEl.textContent = title.replace(/⌙/g, '└');
|
||||
return titleEl;
|
||||
}
|
||||
/**
|
||||
|
||||
@@ -1286,10 +1286,14 @@ header {
|
||||
}
|
||||
|
||||
// Language selector
|
||||
.js_language_selector {
|
||||
.dropdown-menu {
|
||||
min-width: 0;
|
||||
#wrapwrap:not(.o_rtl) {
|
||||
.js_language_selector {
|
||||
.dropdown-menu {
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
.js_language_selector {
|
||||
a.list-inline-item {
|
||||
padding: 3px 0;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user