mirror of
https://gitlab.com/flectra-hq/flectra.git
synced 2026-08-19 01:34:43 -05:00
[PATCH] Upstream patch - 06082022
This commit is contained in:
@@ -294,7 +294,7 @@ class AccountMove(models.Model):
|
|||||||
default=_get_default_invoice_incoterm,
|
default=_get_default_invoice_incoterm,
|
||||||
help='International Commercial Terms are a series of predefined commercial terms used in international transactions.')
|
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')
|
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(),
|
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.")
|
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.exceptions import UserError
|
||||||
from flectra.addons.account.tests.common import AccountTestInvoicingCommon
|
from flectra.addons.account.tests.common import AccountTestInvoicingCommon
|
||||||
from flectra.tests import tagged
|
from flectra.tests import tagged
|
||||||
|
from flectra import fields
|
||||||
|
|
||||||
@tagged('post_install', '-at_install')
|
@tagged('post_install', '-at_install')
|
||||||
class TestSEPAQRCode(AccountTestInvoicingCommon):
|
class TestSEPAQRCode(AccountTestInvoicingCommon):
|
||||||
@@ -62,3 +62,16 @@ class TestSEPAQRCode(AccountTestInvoicingCommon):
|
|||||||
"""
|
"""
|
||||||
self.sepa_qr_invoice.generate_qr_code()
|
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.")
|
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"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
<flectra>
|
<flectra>
|
||||||
<data noupdate="1">
|
<data noupdate="1">
|
||||||
|
|
||||||
<!-- 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_12" model="account.tax.group">
|
<!-- Account Tax Group -->
|
||||||
<field name="name">TVA 12%</field>
|
<record id="tax_group_tva_21" model="account.tax.group">
|
||||||
</record>
|
<field name="name">TVA 21%</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
<record id="tax_group_tva_6" model="account.tax.group">
|
<record id="tax_group_tva_12" model="account.tax.group">
|
||||||
<field name="name">TVA 6%</field>
|
<field name="name">TVA 12%</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
<record id="tax_group_tva_0" model="account.tax.group">
|
<record id="tax_group_tva_6" model="account.tax.group">
|
||||||
<field name="name">TVA 0%</field>
|
<field name="name">TVA 6%</field>
|
||||||
</record>
|
</record>
|
||||||
|
|
||||||
</data>
|
<record id="tax_group_tva_0" model="account.tax.group">
|
||||||
</flectra>
|
<field name="name">TVA 0%</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</data>
|
||||||
|
</flectra>
|
||||||
|
|||||||
@@ -12,6 +12,10 @@
|
|||||||
<field name="zip">12345</field>
|
<field name="zip">12345</field>
|
||||||
</record>
|
</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">
|
<record id="partner_demo_it" model="res.partner">
|
||||||
<field name="name">Palazzo dell'Arte</field>
|
<field name="name">Palazzo dell'Arte</field>
|
||||||
<field name="vat">IT00000010215</field>
|
<field name="vat">IT00000010215</field>
|
||||||
|
|||||||
@@ -3,6 +3,12 @@
|
|||||||
<data>
|
<data>
|
||||||
|
|
||||||
<template id="account_invoice_line_it_FatturaPA">
|
<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>
|
<DettaglioLinee>
|
||||||
<NumeroLinea t-esc="line_counter"/>
|
<NumeroLinea t-esc="line_counter"/>
|
||||||
<CodiceArticolo t-if="line.product_id.barcode">
|
<CodiceArticolo t-if="line.product_id.barcode">
|
||||||
@@ -19,12 +25,12 @@
|
|||||||
</Descrizione>
|
</Descrizione>
|
||||||
<Quantita t-esc="format_numbers(line.quantity)"/>
|
<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)"/>
|
<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">
|
<ScontoMaggiorazione t-if="line.discount != 0">
|
||||||
<Tipo t-esc="discount_type(line.discount)"/>
|
<Tipo t-esc="discount_type(line.discount)"/>
|
||||||
<Percentuale t-esc="format_numbers(abs(line.discount))"/>
|
<Percentuale t-esc="format_numbers(abs(line.discount))"/>
|
||||||
</ScontoMaggiorazione>
|
</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-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'"/>
|
<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"/>
|
<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)"/>
|
<AliquotaIVA t-esc="format_numbers(tax.amount)"/>
|
||||||
<Natura t-if="has_exoneration" t-esc="kind_exoneration"/>
|
<Natura t-if="has_exoneration" t-esc="kind_exoneration"/>
|
||||||
<Arrotondamento t-if="tax_dict.get('rounding')" t-esc="format_numbers(tax_dict['rounding'])"/>
|
<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)"/>
|
<t t-if="rc_refund">
|
||||||
<Imposta t-esc="format_monetary(abs(tax_dict['tax_amount_currency']), currency)"/>
|
<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"/>
|
<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])"/>
|
<RiferimentoNormativo t-if="has_exoneration" t-esc="format_alphanumeric(tax.l10n_it_law_reference[:100])"/>
|
||||||
</DatiRiepilogo>
|
</DatiRiepilogo>
|
||||||
|
|||||||
@@ -212,14 +212,14 @@ class AccountEdiFormat(models.Model):
|
|||||||
|
|
||||||
def _l10n_it_document_type_mapping(self):
|
def _l10n_it_document_type_mapping(self):
|
||||||
return {
|
return {
|
||||||
'TD01': dict(move_type='out_invoice', import_type='in_invoice'),
|
'TD01': dict(move_types=['out_invoice'], import_type='in_invoice'),
|
||||||
'TD04': dict(move_type='out_refund', import_type='in_refund'),
|
'TD04': dict(move_types=['out_refund'], import_type='in_refund'),
|
||||||
'TD07': dict(move_type='out_invoice', import_type='in_invoice', simplified=True),
|
'TD07': dict(move_types=['out_invoice'], import_type='in_invoice', simplified=True),
|
||||||
'TD08': dict(move_type='out_refund', import_type='in_refund', simplified=True),
|
'TD08': dict(move_types=['out_refund'], import_type='in_refund', simplified=True),
|
||||||
'TD09': dict(move_type='out_invoice', import_type='in_invoice', simplified=True),
|
'TD09': dict(move_types=['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"),
|
'TD17': dict(move_types=['in_invoice', 'in_refund'], 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),
|
'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_type='in_invoice', import_type='out_invoice', self_invoice=True, services_or_goods="consu", goods_in_italy=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):
|
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_services_or_goods = infos.get('services_or_goods', "both")
|
||||||
info_partner_in_eu = infos.get('partner_in_eu', False)
|
info_partner_in_eu = infos.get('partner_in_eu', False)
|
||||||
if all([
|
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_self_invoice == infos.get('self_invoice', False),
|
||||||
is_simplified == infos.get('simplified', False),
|
is_simplified == infos.get('simplified', False),
|
||||||
info_services_or_goods in ("both", services_or_goods),
|
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 (partner.country_id.code == 'IT' and '0000000')
|
||||||
or 'XXXXXXX')
|
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
|
# Self-invoices are technically -100%/+100% repartitioned
|
||||||
# but functionally need to be exported as 100%
|
# but functionally need to be exported as 100%
|
||||||
document_total = self.amount_total
|
document_total = self.amount_total
|
||||||
if is_self_invoice:
|
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.
|
# Create file content.
|
||||||
template_values = {
|
template_values = {
|
||||||
@@ -225,7 +230,8 @@ class AccountMove(models.Model):
|
|||||||
'normalize_codice_fiscale': partner._l10n_it_normalize_codice_fiscale,
|
'normalize_codice_fiscale': partner._l10n_it_normalize_codice_fiscale,
|
||||||
'get_vat_number': get_vat_number,
|
'get_vat_number': get_vat_number,
|
||||||
'get_vat_country': get_vat_country,
|
'get_vat_country': get_vat_country,
|
||||||
'in_eu': in_eu
|
'in_eu': in_eu,
|
||||||
|
'rc_refund': rc_refund,
|
||||||
}
|
}
|
||||||
return template_values
|
return template_values
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
# -*- coding:utf-8 -*-
|
# -*- coding:utf-8 -*-
|
||||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
# 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 import api, fields, models, _
|
||||||
from flectra.exceptions import UserError, ValidationError
|
from flectra.exceptions import UserError, ValidationError
|
||||||
@@ -45,7 +46,6 @@ class ResPartner(models.Model):
|
|||||||
|
|
||||||
@api.constrains('l10n_it_codice_fiscale')
|
@api.constrains('l10n_it_codice_fiscale')
|
||||||
def validate_codice_fiscale(self):
|
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:
|
for record in self:
|
||||||
if record.l10n_it_codice_fiscale and not p.match(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 '12345678901' or 'IT12345678901' for businesses.", 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.
|
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||||
|
|
||||||
from . import test_ir_mail_server
|
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',
|
'amount_type': 'percent',
|
||||||
'type_tax_use': 'purchase',
|
'type_tax_use': 'purchase',
|
||||||
'invoice_repartition_line_ids': repartition_lines(
|
'invoice_repartition_line_ids': repartition_lines(
|
||||||
RepartitionLine(100, 'base', ('+03',)),
|
RepartitionLine(100, 'base', ('+03', '+vj9')),
|
||||||
RepartitionLine(100, 'tax', ('+vj9',)),
|
RepartitionLine(100, 'tax', ('+5v',)),
|
||||||
RepartitionLine(-100, 'tax', False)),
|
RepartitionLine(-100, 'tax', ('-4v',))),
|
||||||
'refund_repartition_line_ids': repartition_lines(
|
'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))
|
RepartitionLine(-100, 'tax', False)),
|
||||||
}
|
}
|
||||||
# Purchase tax 4% with Reverse Charge
|
# Purchase tax 4% with Reverse Charge
|
||||||
cls.purchase_tax_4p = cls.env['account.tax'].with_company(cls.company).create(tax_data)
|
cls.purchase_tax_4p = cls.env['account.tax'].with_company(cls.company).create(tax_data)
|
||||||
@@ -74,10 +74,15 @@ class TestItEdiReverseCharge(TestItEdi):
|
|||||||
**tax_data,
|
**tax_data,
|
||||||
'name': 'Tax 4% purchase Reverse Charge, in Italy',
|
'name': 'Tax 4% purchase Reverse Charge, in Italy',
|
||||||
'invoice_repartition_line_ids': repartition_lines(
|
'invoice_repartition_line_ids': repartition_lines(
|
||||||
RepartitionLine(100, 'base', ('+03',)),
|
RepartitionLine(100, 'base', ('+03', '+vj3')),
|
||||||
RepartitionLine(100, 'tax', ('+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)),
|
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.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 = cls.standard_line.copy()
|
||||||
cls.line_tax_4p_already_in_italy['tax_ids'] = [(6, 0, cls.purchase_tax_4p_already_in_italy.ids)]
|
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_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 -----------
|
# Posting moves -----------
|
||||||
cls.reverse_charge_invoice._post()
|
cls.reverse_charge_invoice._post()
|
||||||
cls.reverse_charge_bill._post()
|
cls.reverse_charge_bill._post()
|
||||||
cls.reverse_charge_bill_2._post()
|
cls.reverse_charge_bill_2._post()
|
||||||
|
cls.reverse_charge_refund._post()
|
||||||
|
|
||||||
def _cleanup_etree(self, content, xpaths=None):
|
def _cleanup_etree(self, content, xpaths=None):
|
||||||
xpaths = {
|
xpaths = {
|
||||||
@@ -193,3 +202,26 @@ class TestItEdiReverseCharge(TestItEdi):
|
|||||||
"(//DettaglioLinee/Descrizione)[2]": "<Descrizione/>",
|
"(//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");
|
var $snippets_menu = $snippetsSideBar.find("#snippets_menu");
|
||||||
|
|
||||||
for (const button of $snippets_menu.get(0).children) {
|
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';
|
button.style.display = 'none';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -531,7 +531,7 @@
|
|||||||
<field name="view_id" eval="False"/>
|
<field name="view_id" eval="False"/>
|
||||||
<field name="search_view_id" ref="view_mrp_production_filter"/>
|
<field name="search_view_id" ref="view_mrp_production_filter"/>
|
||||||
<field name="domain">[('picking_type_id', '=', active_id)]</field>
|
<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>
|
||||||
|
|
||||||
<record id="mrp_production_action_unreserve_tree" model="ir.actions.server">
|
<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();
|
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(){
|
get_price_without_tax: function(){
|
||||||
return this.get_all_prices().priceWithoutTax;
|
return this.get_all_prices().priceWithoutTax;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -37,9 +37,9 @@
|
|||||||
<span> </span><t t-esc="props.line.get_unit().name" />
|
<span> </span><t t-esc="props.line.get_unit().name" />
|
||||||
at
|
at
|
||||||
<t t-if="props.line.display_discount_policy() == 'without_discount' and
|
<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>
|
<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>
|
</s>
|
||||||
<t t-esc="env.pos.format_currency(props.line.get_unit_display_price(),'Product Price')" />
|
<t t-esc="env.pos.format_currency(props.line.get_unit_display_price(),'Product Price')" />
|
||||||
</t>
|
</t>
|
||||||
|
|||||||
@@ -335,7 +335,8 @@ class PurchaseOrderLine(models.Model):
|
|||||||
result = super(PurchaseOrderLine, self).write(values)
|
result = super(PurchaseOrderLine, self).write(values)
|
||||||
if 'price_unit' in values:
|
if 'price_unit' in values:
|
||||||
for line in lines:
|
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()})
|
moves.write({'price_unit': line._get_stock_move_price_unit()})
|
||||||
if 'product_qty' in values:
|
if 'product_qty' in values:
|
||||||
lines.with_context(previous_product_qty=previous_product_qty)._create_or_update_picking()
|
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):
|
elif not float_is_zero(line.qty_to_invoice, precision_digits=precision):
|
||||||
line.invoice_status = 'to invoice'
|
line.invoice_status = 'to invoice'
|
||||||
elif line.state == 'sale' and line.product_id.invoice_policy == 'order' and\
|
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:
|
float_compare(line.qty_delivered, line.product_uom_qty, precision_digits=precision) == 1:
|
||||||
line.invoice_status = 'upselling'
|
line.invoice_status = 'upselling'
|
||||||
elif float_compare(line.qty_invoiced, line.product_uom_qty, precision_digits=precision) >= 0:
|
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.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')
|
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):
|
def test_sale_sequence(self):
|
||||||
self.env['ir.sequence'].search([
|
self.env['ir.sequence'].search([
|
||||||
('code', '=', 'sale.order'),
|
('code', '=', 'sale.order'),
|
||||||
|
|||||||
@@ -90,6 +90,8 @@ class ResCompany(models.Model):
|
|||||||
('default_location_src_id.usage', '=', 'supplier'),
|
('default_location_src_id.usage', '=', 'supplier'),
|
||||||
('default_location_dest_id.usage', '=', 'customer'),
|
('default_location_dest_id.usage', '=', 'customer'),
|
||||||
], limit=1, order='sequence')
|
], limit=1, order='sequence')
|
||||||
|
if not dropship_picking_type:
|
||||||
|
continue
|
||||||
dropship_vals.append({
|
dropship_vals.append({
|
||||||
'name': '%s → %s' % (supplier_location.name, customer_location.name),
|
'name': '%s → %s' % (supplier_location.name, customer_location.name),
|
||||||
'action': 'buy',
|
'action': 'buy',
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
flectra.define('web.DatePickerOwl', function (require) {
|
flectra.define('web.DatePickerOwl', function (require) {
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
|
const config = require('web.config');
|
||||||
const field_utils = require('web.field_utils');
|
const field_utils = require('web.field_utils');
|
||||||
const time = require('web.time');
|
const time = require('web.time');
|
||||||
const { useAutofocus } = require('web.custom_hooks');
|
const { useAutofocus } = require('web.custom_hooks');
|
||||||
@@ -167,7 +168,7 @@ flectra.define('web.DatePickerOwl', function (require) {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
_onWindowScroll(ev) {
|
_onWindowScroll(ev) {
|
||||||
if (ev.target !== this.inputRef.el) {
|
if (!config.device.isIOS && ev.target !== this.inputRef.el) {
|
||||||
this._datetimepicker('hide');
|
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
|
* @throws {Error} if no float is found or if parameter does not respect monetary condition
|
||||||
*/
|
*/
|
||||||
function parseMonetary(value, field, options) {
|
function parseMonetary(value, field, options) {
|
||||||
var values = value.split(' ');
|
if (!value.includes(NBSP) && !value.includes(' ')) {
|
||||||
if (values.length === 1) {
|
|
||||||
values = value.split(NBSP);
|
|
||||||
}
|
|
||||||
if (values.length === 1) {
|
|
||||||
return parseFloat(value);
|
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 || {};
|
options = options || {};
|
||||||
var currency = options.currency;
|
var currency = options.currency;
|
||||||
if (!currency) {
|
if (!currency) {
|
||||||
@@ -635,7 +628,18 @@ function parseMonetary(value, field, options) {
|
|||||||
}
|
}
|
||||||
currency = session.get_currency(currency_id);
|
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) {
|
QUnit.test('parse monetary', function(assert) {
|
||||||
assert.expect(11);
|
assert.expect(13);
|
||||||
var originalCurrencies = session.currencies;
|
var originalCurrencies = session.currencies;
|
||||||
|
const originalParameters = _.clone(core._t.database.parameters);
|
||||||
session.currencies = {
|
session.currencies = {
|
||||||
1: {
|
1: {
|
||||||
digits: [69, 2],
|
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", {}, {currency_id: 1})}, /is not a correct/);
|
||||||
assert.throws(function() {fieldUtils.parse.monetary("$ 12.00 34", {}, {currency_id: 3})}, /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;
|
session.currencies = originalCurrencies;
|
||||||
|
core._t.database.parameters = originalParameters;
|
||||||
});
|
});
|
||||||
|
|
||||||
QUnit.test('parse percentage', function(assert) {
|
QUnit.test('parse percentage', function(assert) {
|
||||||
|
|||||||
@@ -77,7 +77,16 @@ function _buildElement(tagName, title, options) {
|
|||||||
*/
|
*/
|
||||||
function _buildTitleElement(title) {
|
function _buildTitleElement(title) {
|
||||||
const titleEl = document.createElement('we-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;
|
return titleEl;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1286,10 +1286,14 @@ header {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Language selector
|
// Language selector
|
||||||
.js_language_selector {
|
#wrapwrap:not(.o_rtl) {
|
||||||
.dropdown-menu {
|
.js_language_selector {
|
||||||
min-width: 0;
|
.dropdown-menu {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
.js_language_selector {
|
||||||
a.list-inline-item {
|
a.list-inline-item {
|
||||||
padding: 3px 0;
|
padding: 3px 0;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user