[PATCH] Upstream patch - 22042022

This commit is contained in:
Parthiv Patel
2022-04-22 08:35:39 +00:00
parent 6213b40c4e
commit b48ac72eb8
30 changed files with 797 additions and 366 deletions
+1 -1
View File
@@ -589,7 +589,7 @@ class Lead(models.Model):
# stage change with new stage: update probability and date_closed
if vals.get('probability', 0) >= 100 or not vals.get('active', True):
vals['date_closed'] = fields.Datetime.now()
elif 'probability' in vals:
elif tools.float_compare(vals.get('probability', 0), 0, precision_digits=2) > 0:
vals['date_closed'] = False
if any(field in ['active', 'stage_id'] for field in vals):
+11
View File
@@ -1,6 +1,9 @@
# -*- coding: utf-8 -*-
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
from datetime import datetime
from freezegun import freeze_time
from flectra.addons.crm.models.crm_lead import PARTNER_FIELDS_TO_SYNC, PARTNER_ADDRESS_FIELDS_TO_SYNC
from flectra.addons.crm.tests.common import TestCrmCommon, INCOMING_EMAIL
from flectra.addons.phone_validation.tools.phone_validation import phone_format
@@ -370,6 +373,14 @@ class TestCRMLead(TestCrmCommon):
self.assertEqual(lead.probability, 100.0)
self.assertEqual(lead.stage_id, self.stage_gen_won) # generic won stage has lower sequence than team won stage
@users('user_sales_leads')
@freeze_time("2012-01-14")
def test_crm_lead_lost_date_closed(self):
self.assertFalse(self.lead_1.date_closed, "Initially, closed date is not set")
# Mark the lead as lost
self.lead_1.action_set_lost()
self.assertEqual(self.lead_1.date_closed, datetime.now(), "Closed date is updated after marking lead as lost")
@users('user_sales_leads')
def test_crm_lead_update_contact(self):
# ensure initial data, especially for corner cases
+1
View File
@@ -17,6 +17,7 @@ After installing this module, you'll have access to :
* Finnish chart of account
* Fiscal positions
* Invoice Payment Reference Types (Finnish Standard Reference & Finnish Creditor Reference (RF))
* Finnish Reference format for Sale Orders
Set the payment reference type from the Sales Journal.
""",
+1 -1
View File
@@ -4,7 +4,7 @@ from flectra.addons.account.tests.common import AccountTestInvoicingCommon
from flectra.exceptions import UserError
@tagged('standard', 'at_install')
@tagged('post_install', '-at_install')
class PaymentReferenceTest(AccountTestInvoicingCommon):
"""
All references validated with the reference calculator by Nordea Bank
+14 -12
View File
@@ -20,7 +20,7 @@
</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="line.product_uom_id.name"/>
<PrezzoUnitario t-esc="'%.06f' % (line.price_subtotal / (( 1 - (line.discount or 0.0) / 100.0) * line.quantity) if line.discount != 100.0 else line.price_unit)"/>
<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)"/>
<ScontoMaggiorazione t-if="line.discount != 0">
<!-- [2.2.1.10] -->
<Tipo t-esc="discount_type(line.discount)"/>
@@ -176,19 +176,21 @@
</DatiRiepilogo>
</t>
</DatiBeniServizi>
<DatiPagamento>
<t t-set="company_bank_account" t-value="record.partner_bank_id"/>
<DatiPagamento t-if="company_bank_account and record.move_type != 'out_refund'">
<t t-set="payments" t-value="record.line_ids.filtered(lambda line: line.account_id.user_type_id.type in ('receivable', 'payable'))"/>
<CondizioniPagamento><t t-if="len(payments) == 1">TP02</t><t t-else="">TP01</t></CondizioniPagamento>
<DettaglioPagamento>
<t t-set="company_bank_account" t-value="record.partner_bank_id"/>
<ModalitaPagamento t-if="company_bank_account">MP05</ModalitaPagamento>
<DataScadenzaPagamento t-esc="format_date(record.invoice_date_due)"/>
<ImportoPagamento t-esc="format_numbers_two(record.amount_total)"/>
<IstitutoFinanziario t-if="company_bank_account.bank_id" t-esc="company_bank_account.bank_id.name[:80]"/>
<IBAN t-if="company_bank_account.acc_type == 'iban'" t-esc="company_bank_account.sanitized_acc_number"/>
<BIC t-if="company_bank_account.acc_type == 'bank' and company_bank_account.bank_id.bic" t-esc="company_bank_account.bank_id.bic"/>
<CodicePagamento t-esc="record.payment_reference[:60]"/>
</DettaglioPagamento>
<t t-foreach="payments" t-as="payment">
<DettaglioPagamento>
<ModalitaPagamento>MP05</ModalitaPagamento>
<DataScadenzaPagamento t-esc="format_date(payment.date_maturity)"/>
<ImportoPagamento t-esc="format_monetary(abs(payment.price_total), currency)"/>
<IstitutoFinanziario t-if="company_bank_account.bank_id" t-esc="company_bank_account.bank_id.name[:80]"/>
<IBAN t-if="company_bank_account.acc_type == 'iban'" t-esc="company_bank_account.sanitized_acc_number"/>
<BIC t-if="company_bank_account.acc_type == 'bank' and company_bank_account.bank_id.bic" t-esc="company_bank_account.bank_id.bic"/>
<CodicePagamento t-esc="record.payment_reference[:60]"/>
</DettaglioPagamento>
</t>
</DatiPagamento>
<Allegati t-if="pdf">
<NomeAttachment t-esc="pdf_name"/>
@@ -120,9 +120,6 @@ class AccountEdiFormat(models.Model):
if not tax_line.tax_line_id.l10n_it_kind_exoneration and tax_line.tax_line_id.amount == 0:
errors.append(_("%s has an amount of 0.0, you must indicate the kind of exoneration.", tax_line.name))
if not invoice.partner_bank_id:
errors.append(_("The seller must have a bank account."))
return errors
# -------------------------------------------------------------------------
@@ -5,7 +5,6 @@ import datetime
import logging
from collections import namedtuple
from unittest.mock import patch
from lxml import etree
from freezegun import freeze_time
from flectra import tools
@@ -52,7 +51,6 @@ class PecMailServerTests(AccountEdiTestCommon):
# Initialize the company's codice fiscale
cls.company.l10n_it_codice_fiscale = '01234560157'
cls.company.vat = 'IT01234560157'
# Build test data.
# invoice_filename1 is used for vendor bill receipts tests
@@ -85,97 +83,9 @@ class PecMailServerTests(AccountEdiTestCommon):
'server_type': 'imap',
'l10n_it_is_pec': True})
cls.price_included_tax = cls.env['account.tax'].create({
'name': '22% price included tax',
'amount': 22.0,
'amount_type': 'percent',
'price_include': True,
'include_base_amount': True,
'company_id': cls.company.id,
})
cls.italian_partner_a = cls.env['res.partner'].create({
'name': 'Alessi',
'vat': 'IT00465840031',
'l10n_it_codice_fiscale': '00465840031',
'country_id': cls.env.ref('base.it').id,
'street': 'Via Privata Alessi 6',
'zip': '28887',
'company_id': cls.company.id,
})
cls.standard_line = {
'name': 'standard_line',
'quantity': 1,
'price_unit': 800.40,
'tax_ids': [(6, 0, [cls.company.account_sale_tax_id.id])]
}
cls.price_included_invoice = cls.env['account.move'].with_company(cls.company).create({
'move_type': 'out_invoice',
'invoice_date': datetime.date(2022, 3, 24),
'partner_id': cls.italian_partner_a.id,
'invoice_line_ids': [
(0, 0, {
**cls.standard_line,
'name': 'something price included',
'tax_ids': [(6, 0, [cls.price_included_tax.id])]
}),
(0, 0, {
**cls.standard_line,
'name': 'something else price included',
'tax_ids': [(6, 0, [cls.price_included_tax.id])]
}),
(0, 0, {
**cls.standard_line,
'name': 'something not price included',
}),
],
})
cls.partial_discount_invoice = cls.env['account.move'].with_company(cls.company).create({
'move_type': 'out_invoice',
'invoice_date': datetime.date(2022, 3, 24),
'partner_id': cls.italian_partner_a.id,
'invoice_line_ids': [
(0, 0, {
**cls.standard_line,
'name': 'no discount',
}),
(0, 0, {
**cls.standard_line,
'name': 'special discount',
'discount': 50,
}),
(0, 0, {
**cls.standard_line,
'name': "an offer you can't refuse",
'discount': 100,
}),
],
})
cls.full_discount_invoice = cls.env['account.move'].with_company(cls.company).create({
'move_type': 'out_invoice',
'invoice_date': datetime.date(2022, 3, 24),
'partner_id': cls.italian_partner_a.id,
'invoice_line_ids': [
(0, 0, {
**cls.standard_line,
'name': 'nothing shady just a gift for my friend',
'discount': 100,
}),
],
})
# post the invoices
cls.price_included_invoice._post()
cls.partial_discount_invoice._post()
cls.full_discount_invoice._post()
cls.test_invoice_xmls = {k: cls._get_test_file_content(v) for k, v in [
('normal_1', 'IT01234567890_FPR01.xml'),
('signed', 'IT01234567890_FPR01.xml.p7m'),
('export_basis', 'IT00470550013_basis.xml'),
]}
@classmethod
@@ -254,173 +164,3 @@ class PecMailServerTests(AccountEdiTestCommon):
def test_decorrenza_termini(self):
""" Test a receipt adapted from https://www.fatturapa.gov.it/export/documenti/messaggi/v1.0/IT01234567890_11111_DT_001.xml """
self._test_receipt('DT', 'delivered', 'delivered_expired')
@freeze_time('2020-03-24')
def test_price_included_taxes(self):
""" When the tax is price included, there should be a rounding value added to the xml, if the sum(subtotals) * tax_rate is not
equal to taxable base * tax rate (there is a constraint in the edi where taxable base * tax rate = tax amount, but also
taxable base = sum(subtotals) + rounding amount)
"""
# In this case, the first two lines use a price_include tax the
# subtotals should be 800.40 / (100 + 22.0) * 100 = 656.065564..,
# where 22.0 is the tax rate.
#
# Since the subtotals are rounded we actually have 656.07
lines = self.price_included_invoice.line_ids
price_included_lines = lines.filtered(lambda line: line.tax_ids == self.price_included_tax)
self.assertEqual([line.price_subtotal for line in price_included_lines], [656.07, 656.07])
# So the taxable a base the edi expects (for this tax) is actually 1312.14
price_included_tax_line = lines.filtered(lambda line: line.tax_line_id == self.price_included_tax)
self.assertEqual(price_included_tax_line.tax_base_amount, 1312.14)
# The tax amount of the price included tax should be:
# per line: 800.40 - (800.40 / (100 + 22) * 100) = 144.33
# tax amount: 144.33 * 2 = 288.66
self.assertEqual(price_included_tax_line.price_total, 288.66)
expected_etree = self.with_applied_xpath(
etree.fromstring(self.test_invoice_xmls['export_basis']),
'''
<xpath expr="//FatturaElettronicaBody//DatiBeniServizi" position="replace">
<DatiBeniServizi>
<DettaglioLinee>
<NumeroLinea>1</NumeroLinea>
<Descrizione>something price included</Descrizione>
<Quantita>1.00</Quantita>
<PrezzoUnitario>656.070000</PrezzoUnitario>
<PrezzoTotale>656.07</PrezzoTotale>
<AliquotaIVA>22.00</AliquotaIVA>
</DettaglioLinee>
<DettaglioLinee>
<NumeroLinea>2</NumeroLinea>
<Descrizione>something else price included</Descrizione>
<Quantita>1.00</Quantita>
<PrezzoUnitario>656.070000</PrezzoUnitario>
<PrezzoTotale>656.07</PrezzoTotale>
<AliquotaIVA>22.00</AliquotaIVA>
</DettaglioLinee>
<DettaglioLinee>
<NumeroLinea>3</NumeroLinea>
<Descrizione>something not price included</Descrizione>
<Quantita>1.00</Quantita>
<PrezzoUnitario>800.400000</PrezzoUnitario>
<PrezzoTotale>800.40</PrezzoTotale>
<AliquotaIVA>22.00</AliquotaIVA>
</DettaglioLinee>
<DatiRiepilogo>
<AliquotaIVA>22.00</AliquotaIVA>
<Arrotondamento>-0.04909091</Arrotondamento>
<ImponibileImporto>1312.09</ImponibileImporto>
<Imposta>288.66</Imposta>
<EsigibilitaIVA>I</EsigibilitaIVA>
</DatiRiepilogo>
<DatiRiepilogo>
<AliquotaIVA>22.00</AliquotaIVA>
<ImponibileImporto>800.40</ImponibileImporto>
<Imposta>176.09</Imposta>
<EsigibilitaIVA>I</EsigibilitaIVA>
</DatiRiepilogo>
</DatiBeniServizi>
</xpath>
<xpath expr="//DettaglioPagamento//ImportoPagamento" position="inside">
2577.29
</xpath>
''')
invoice_etree = etree.fromstring(self.price_included_invoice._export_as_xml())
# Remove the attachment and its details
invoice_etree = self.with_applied_xpath(invoice_etree, "<xpath expr='.//Allegati' position='replace'/>")
self.assertXmlTreeEqual(invoice_etree, expected_etree)
@freeze_time('2020-03-24')
def test_partially_discounted_invoice(self):
# The EDI can account for discounts, but a line with, for example, a 100% discount should still have
# a corresponding tax with a base amount of 0
invoice_etree = etree.fromstring(self.partial_discount_invoice._export_as_xml())
expected_etree = self.with_applied_xpath(
etree.fromstring(self.test_invoice_xmls['export_basis']),
'''
<xpath expr="//FatturaElettronicaBody//DatiBeniServizi" position="replace">
<DatiBeniServizi>
<DettaglioLinee>
<NumeroLinea>1</NumeroLinea>
<Descrizione>no discount</Descrizione>
<Quantita>1.00</Quantita>
<PrezzoUnitario>800.400000</PrezzoUnitario>
<PrezzoTotale>800.40</PrezzoTotale>
<AliquotaIVA>22.00</AliquotaIVA>
</DettaglioLinee>
<DettaglioLinee>
<NumeroLinea>2</NumeroLinea>
<Descrizione>special discount</Descrizione>
<Quantita>1.00</Quantita>
<PrezzoUnitario>800.400000</PrezzoUnitario>
<ScontoMaggiorazione>
<Tipo>SC</Tipo>
<Percentuale>50.00</Percentuale>
</ScontoMaggiorazione>
<PrezzoTotale>400.20</PrezzoTotale>
<AliquotaIVA>22.00</AliquotaIVA>
</DettaglioLinee>
<DettaglioLinee>
<NumeroLinea>3</NumeroLinea>
<Descrizione>an offer you can't refuse</Descrizione>
<Quantita>1.00</Quantita>
<PrezzoUnitario>800.400000</PrezzoUnitario>
<ScontoMaggiorazione>
<Tipo>SC</Tipo>
<Percentuale>100.00</Percentuale>
</ScontoMaggiorazione>
<PrezzoTotale>0.00</PrezzoTotale>
<AliquotaIVA>22.00</AliquotaIVA>
</DettaglioLinee>
<DatiRiepilogo>
<AliquotaIVA>22.00</AliquotaIVA>
<ImponibileImporto>1200.60</ImponibileImporto>
<Imposta>264.13</Imposta>
<EsigibilitaIVA>I</EsigibilitaIVA>
</DatiRiepilogo>
</DatiBeniServizi>
</xpath>
<xpath expr="//DettaglioPagamento//ImportoPagamento" position="inside">
1464.73
</xpath>
''')
invoice_etree = self.with_applied_xpath(invoice_etree, "<xpath expr='.//Allegati' position='replace'/>")
self.assertXmlTreeEqual(invoice_etree, expected_etree)
@freeze_time('2020-03-24')
def test_fully_discounted_inovice(self):
invoice_etree = etree.fromstring(self.full_discount_invoice._export_as_xml())
expected_etree = self.with_applied_xpath(
etree.fromstring(self.test_invoice_xmls['export_basis']),
'''
<xpath expr="//FatturaElettronicaBody//DatiBeniServizi" position="replace">
<DatiBeniServizi>
<DettaglioLinee>
<NumeroLinea>1</NumeroLinea>
<Descrizione>nothing shady just a gift for my friend</Descrizione>
<Quantita>1.00</Quantita>
<PrezzoUnitario>800.400000</PrezzoUnitario>
<ScontoMaggiorazione>
<Tipo>SC</Tipo>
<Percentuale>100.00</Percentuale>
</ScontoMaggiorazione>
<PrezzoTotale>0.00</PrezzoTotale>
<AliquotaIVA>22.00</AliquotaIVA>
</DettaglioLinee>
<DatiRiepilogo>
<AliquotaIVA>22.00</AliquotaIVA>
<ImponibileImporto>0.00</ImponibileImporto>
<Imposta>0.00</Imposta>
<EsigibilitaIVA>I</EsigibilitaIVA>
</DatiRiepilogo>
</DatiBeniServizi>
</xpath>
<xpath expr="//DettaglioPagamento//ImportoPagamento" position="inside">
0.00
</xpath>
''')
invoice_etree = self.with_applied_xpath(invoice_etree, "<xpath expr='.//Allegati' position='replace'/>")
self.assertXmlTreeEqual(invoice_etree, expected_etree)
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
from . import test_edi_xml
@@ -0,0 +1,73 @@
<p:FatturaElettronica xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://ivaservizi.agenziaentrate.gov.it/docs/xsd/fatture/v1.2" xsi:schemaLocation="http://ivaservizi.agenziaentrate.gov.it/docs/xsd/fatture/v1.2 http://www.fatturapa.gov.it/export/fatturazione/sdi/fatturapa/v1.2/Schema_del_file_xml_FatturaPA_versione_1.2.xsd" versione="FPR12">
<FatturaElettronicaHeader>
<DatiTrasmissione>
<IdTrasmittente>
<IdPaese>IT</IdPaese>
<IdCodice>01234560157</IdCodice>
</IdTrasmittente>
<ProgressivoInvio>___ignore___</ProgressivoInvio>
<FormatoTrasmissione>FPR12</FormatoTrasmissione>
<CodiceDestinatario>0000000</CodiceDestinatario>
<ContattiTrasmittente>
</ContattiTrasmittente>
</DatiTrasmissione>
<CedentePrestatore>
<DatiAnagrafici>
<IdFiscaleIVA>
<IdPaese>IT</IdPaese>
<IdCodice>01234560157</IdCodice>
</IdFiscaleIVA>
<CodiceFiscale>01234560157</CodiceFiscale>
<Anagrafica>
<Denominazione>company_2_data</Denominazione>
</Anagrafica>
<RegimeFiscale>RF01</RegimeFiscale>
</DatiAnagrafici>
<Sede>
<Indirizzo>1234 Test Street </Indirizzo>
<CAP>12345</CAP>
<Comune>Prova</Comune>
<Nazione>IT</Nazione>
</Sede>
</CedentePrestatore>
<CessionarioCommittente>
<DatiAnagrafici>
<IdFiscaleIVA>
<IdPaese>IT</IdPaese>
<IdCodice>00465840031</IdCodice>
</IdFiscaleIVA>
<Anagrafica>
<Nome>Alessi</Nome>
<Cognome></Cognome>
</Anagrafica>
</DatiAnagrafici>
<Sede>
<Indirizzo>Via Privata Alessi 6 </Indirizzo>
<CAP>28887</CAP>
<Comune>Milan</Comune>
<Nazione>IT</Nazione>
</Sede>
</CessionarioCommittente>
</FatturaElettronicaHeader>
<FatturaElettronicaBody>
<DatiGenerali>
<DatiGeneraliDocumento>
<TipoDocumento>TD01</TipoDocumento>
<Divisa>EUR</Divisa>
<Data>2022-03-24</Data>
<Numero>___ignore___</Numero>
</DatiGeneraliDocumento>
</DatiGenerali>
<DatiBeniServizi>
</DatiBeniServizi>
<DatiPagamento>
<CondizioniPagamento>TP02</CondizioniPagamento>
<DettaglioPagamento>
<ModalitaPagamento>MP05</ModalitaPagamento>
<DataScadenzaPagamento>2022-03-24</DataScadenzaPagamento>
<ImportoPagamento></ImportoPagamento>
<CodicePagamento>___ignore___</CodicePagamento>
</DettaglioPagamento>
</DatiPagamento>
</FatturaElettronicaBody>
</p:FatturaElettronica>
@@ -0,0 +1,318 @@
# -*- coding: utf-8 -*-
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
import datetime
import logging
from lxml import etree
from freezegun import freeze_time
from flectra import tools
from flectra.tests import tagged
from flectra.addons.account_edi.tests.common import AccountEdiTestCommon
_logger = logging.getLogger(__name__)
@tagged('post_install_l10n', 'post_install', '-at_install')
class TestItEdi(AccountEdiTestCommon):
@classmethod
def setUpClass(cls):
super().setUpClass(chart_template_ref='l10n_it.l10n_it_chart_template_generic',
edi_format_ref='l10n_it_edi.edi_fatturaPA')
# Use the company_data_2 to test that the e-invoice is imported for the right company
cls.company = cls.company_data_2['company']
cls.company.l10n_it_codice_fiscale = '01234560157'
cls.company.vat = 'IT01234560157'
cls.test_bank = cls.env['res.partner.bank'].with_company(cls.company).create({
'partner_id': cls.company.partner_id.id,
'acc_number': 'IT1212341234123412341234123',
'bank_name': 'BIG BANK',
'bank_bic': 'BIGGBANQ',
})
cls.company.l10n_it_tax_system = "RF01"
cls.company.street = "1234 Test Street"
cls.company.zip = "12345"
cls.company.city = "Prova"
cls.company.country_id = cls.env.ref('base.it')
cls.price_included_tax = cls.env['account.tax'].create({
'name': '22% price included tax',
'amount': 22.0,
'amount_type': 'percent',
'price_include': True,
'include_base_amount': True,
'company_id': cls.company.id,
})
cls.italian_partner_a = cls.env['res.partner'].create({
'name': 'Alessi',
'vat': 'IT00465840031',
'l10n_it_codice_fiscale': '00465840031',
'country_id': cls.env.ref('base.it').id,
'street': 'Via Privata Alessi 6',
'zip': '28887',
'city': 'Milan',
'company_id': cls.company.id,
})
cls.standard_line = {
'name': 'standard_line',
'quantity': 1,
'price_unit': 800.40,
'tax_ids': [(6, 0, [cls.company.account_sale_tax_id.id])]
}
cls.price_included_invoice = cls.env['account.move'].with_company(cls.company).create({
'move_type': 'out_invoice',
'invoice_date': datetime.date(2022, 3, 24),
'partner_id': cls.italian_partner_a.id,
'partner_bank_id': cls.test_bank.id,
'invoice_line_ids': [
(0, 0, {
**cls.standard_line,
'name': 'something price included',
'tax_ids': [(6, 0, [cls.price_included_tax.id])]
}),
(0, 0, {
**cls.standard_line,
'name': 'something else price included',
'tax_ids': [(6, 0, [cls.price_included_tax.id])]
}),
(0, 0, {
**cls.standard_line,
'name': 'something not price included',
}),
],
})
cls.partial_discount_invoice = cls.env['account.move'].with_company(cls.company).create({
'move_type': 'out_invoice',
'invoice_date': datetime.date(2022, 3, 24),
'partner_id': cls.italian_partner_a.id,
'partner_bank_id': cls.test_bank.id,
'invoice_line_ids': [
(0, 0, {
**cls.standard_line,
'name': 'no discount',
}),
(0, 0, {
**cls.standard_line,
'name': 'special discount',
'discount': 50,
}),
(0, 0, {
**cls.standard_line,
'name': "an offer you can't refuse",
'discount': 100,
}),
],
})
cls.full_discount_invoice = cls.env['account.move'].with_company(cls.company).create({
'move_type': 'out_invoice',
'invoice_date': datetime.date(2022, 3, 24),
'partner_id': cls.italian_partner_a.id,
'partner_bank_id': cls.test_bank.id,
'invoice_line_ids': [
(0, 0, {
**cls.standard_line,
'name': 'nothing shady just a gift for my friend',
'discount': 100,
}),
],
})
# We create this because we are unable to post without a proxy user existing
cls.proxy_user = cls.env['account_edi_proxy_client.user'].create({
'id_client': 'l10n_it_edi_sdicoop_test',
'company_id': cls.company.id,
'edi_format_id': cls.edi_format.id,
'edi_identification': 'l10n_it_edi_sdicoop_test',
'private_key': 'l10n_it_edi_sdicoop_test',
})
# post the invoices
cls.price_included_invoice._post()
cls.partial_discount_invoice._post()
cls.full_discount_invoice._post()
cls.edi_basis_xml = cls._get_test_file_content('IT00470550013_basis.xml')
@classmethod
def _get_test_file_content(cls, filename):
""" Get the content of a test file inside this module """
path = 'l10n_it_edi_sdicoop/tests/expected_xmls/' + filename
with tools.file_open(path, mode='rb') as test_file:
return test_file.read()
@freeze_time('2020-03-24')
def test_price_included_taxes(self):
""" When the tax is price included, there should be a rounding value added to the xml, if the sum(subtotals) * tax_rate is not
equal to taxable base * tax rate (there is a constraint in the edi where taxable base * tax rate = tax amount, but also
taxable base = sum(subtotals) + rounding amount)
"""
# In this case, the first two lines use a price_include tax the
# subtotals should be 800.40 / (100 + 22.0) * 100 = 656.065564..,
# where 22.0 is the tax rate.
#
# Since the subtotals are rounded we actually have 656.07
lines = self.price_included_invoice.line_ids
price_included_lines = lines.filtered(lambda line: line.tax_ids == self.price_included_tax)
self.assertEqual([line.price_subtotal for line in price_included_lines], [656.07, 656.07])
# So the taxable a base the edi expects (for this tax) is actually 1312.14
price_included_tax_line = lines.filtered(lambda line: line.tax_line_id == self.price_included_tax)
self.assertEqual(price_included_tax_line.tax_base_amount, 1312.14)
# The tax amount of the price included tax should be:
# per line: 800.40 - (800.40 / (100 + 22) * 100) = 144.33
# tax amount: 144.33 * 2 = 288.66
self.assertEqual(price_included_tax_line.price_total, 288.66)
expected_etree = self.with_applied_xpath(
etree.fromstring(self.edi_basis_xml),
'''
<xpath expr="//FatturaElettronicaBody//DatiBeniServizi" position="replace">
<DatiBeniServizi>
<DettaglioLinee>
<NumeroLinea>1</NumeroLinea>
<Descrizione>something price included</Descrizione>
<Quantita>1.00</Quantita>
<PrezzoUnitario>656.070000</PrezzoUnitario>
<PrezzoTotale>656.07</PrezzoTotale>
<AliquotaIVA>22.00</AliquotaIVA>
</DettaglioLinee>
<DettaglioLinee>
<NumeroLinea>2</NumeroLinea>
<Descrizione>something else price included</Descrizione>
<Quantita>1.00</Quantita>
<PrezzoUnitario>656.070000</PrezzoUnitario>
<PrezzoTotale>656.07</PrezzoTotale>
<AliquotaIVA>22.00</AliquotaIVA>
</DettaglioLinee>
<DettaglioLinee>
<NumeroLinea>3</NumeroLinea>
<Descrizione>something not price included</Descrizione>
<Quantita>1.00</Quantita>
<PrezzoUnitario>800.400000</PrezzoUnitario>
<PrezzoTotale>800.40</PrezzoTotale>
<AliquotaIVA>22.00</AliquotaIVA>
</DettaglioLinee>
<DatiRiepilogo>
<AliquotaIVA>22.00</AliquotaIVA>
<Arrotondamento>-0.04909091</Arrotondamento>
<ImponibileImporto>1312.09</ImponibileImporto>
<Imposta>288.66</Imposta>
<EsigibilitaIVA>I</EsigibilitaIVA>
</DatiRiepilogo>
<DatiRiepilogo>
<AliquotaIVA>22.00</AliquotaIVA>
<ImponibileImporto>800.40</ImponibileImporto>
<Imposta>176.09</Imposta>
<EsigibilitaIVA>I</EsigibilitaIVA>
</DatiRiepilogo>
</DatiBeniServizi>
</xpath>
<xpath expr="//DettaglioPagamento//ImportoPagamento" position="inside">
2577.29
</xpath>
''')
invoice_etree = etree.fromstring(self.price_included_invoice._export_as_xml())
# Remove the attachment and its details
invoice_etree = self.with_applied_xpath(invoice_etree, "<xpath expr='.//Allegati' position='replace'/>")
self.assertXmlTreeEqual(invoice_etree, expected_etree)
@freeze_time('2020-03-24')
def test_partially_discounted_invoice(self):
# The EDI can account for discounts, but a line with, for example, a 100% discount should still have
# a corresponding tax with a base amount of 0
invoice_etree = etree.fromstring(self.partial_discount_invoice._export_as_xml())
expected_etree = self.with_applied_xpath(
etree.fromstring(self.edi_basis_xml),
'''
<xpath expr="//FatturaElettronicaBody//DatiBeniServizi" position="replace">
<DatiBeniServizi>
<DettaglioLinee>
<NumeroLinea>1</NumeroLinea>
<Descrizione>no discount</Descrizione>
<Quantita>1.00</Quantita>
<PrezzoUnitario>800.400000</PrezzoUnitario>
<PrezzoTotale>800.40</PrezzoTotale>
<AliquotaIVA>22.00</AliquotaIVA>
</DettaglioLinee>
<DettaglioLinee>
<NumeroLinea>2</NumeroLinea>
<Descrizione>special discount</Descrizione>
<Quantita>1.00</Quantita>
<PrezzoUnitario>800.400000</PrezzoUnitario>
<ScontoMaggiorazione>
<Tipo>SC</Tipo>
<Percentuale>50.00</Percentuale>
</ScontoMaggiorazione>
<PrezzoTotale>400.20</PrezzoTotale>
<AliquotaIVA>22.00</AliquotaIVA>
</DettaglioLinee>
<DettaglioLinee>
<NumeroLinea>3</NumeroLinea>
<Descrizione>an offer you can't refuse</Descrizione>
<Quantita>1.00</Quantita>
<PrezzoUnitario>800.400000</PrezzoUnitario>
<ScontoMaggiorazione>
<Tipo>SC</Tipo>
<Percentuale>100.00</Percentuale>
</ScontoMaggiorazione>
<PrezzoTotale>0.00</PrezzoTotale>
<AliquotaIVA>22.00</AliquotaIVA>
</DettaglioLinee>
<DatiRiepilogo>
<AliquotaIVA>22.00</AliquotaIVA>
<ImponibileImporto>1200.60</ImponibileImporto>
<Imposta>264.13</Imposta>
<EsigibilitaIVA>I</EsigibilitaIVA>
</DatiRiepilogo>
</DatiBeniServizi>
</xpath>
<xpath expr="//DettaglioPagamento//ImportoPagamento" position="inside">
1464.73
</xpath>
''')
invoice_etree = self.with_applied_xpath(invoice_etree, "<xpath expr='.//Allegati' position='replace'/>")
self.assertXmlTreeEqual(invoice_etree, expected_etree)
@freeze_time('2020-03-24')
def test_fully_discounted_inovice(self):
invoice_etree = etree.fromstring(self.full_discount_invoice._export_as_xml())
expected_etree = self.with_applied_xpath(
etree.fromstring(self.edi_basis_xml),
'''
<xpath expr="//FatturaElettronicaBody//DatiBeniServizi" position="replace">
<DatiBeniServizi>
<DettaglioLinee>
<NumeroLinea>1</NumeroLinea>
<Descrizione>nothing shady just a gift for my friend</Descrizione>
<Quantita>1.00</Quantita>
<PrezzoUnitario>800.400000</PrezzoUnitario>
<ScontoMaggiorazione>
<Tipo>SC</Tipo>
<Percentuale>100.00</Percentuale>
</ScontoMaggiorazione>
<PrezzoTotale>0.00</PrezzoTotale>
<AliquotaIVA>22.00</AliquotaIVA>
</DettaglioLinee>
<DatiRiepilogo>
<AliquotaIVA>22.00</AliquotaIVA>
<ImponibileImporto>0.00</ImponibileImporto>
<Imposta>0.00</Imposta>
<EsigibilitaIVA>I</EsigibilitaIVA>
</DatiRiepilogo>
</DatiBeniServizi>
</xpath>
<xpath expr="//DettaglioPagamento//ImportoPagamento" position="inside">
0.00
</xpath>
''')
invoice_etree = self.with_applied_xpath(invoice_etree, "<xpath expr='.//Allegati' position='replace'/>")
self.assertXmlTreeEqual(invoice_etree, expected_etree)
@@ -67,6 +67,7 @@ class ChatWindow extends Component {
// the following are passed as props to children
this._onAutocompleteSelect = this._onAutocompleteSelect.bind(this);
this._onAutocompleteSource = this._onAutocompleteSource.bind(this);
this._saveThreadScrollTop = this._saveThreadScrollTop.bind(this);
this._constructor(...args);
}
@@ -17,6 +17,7 @@
chatWindowLocalId="chatWindow.localId"
hasCloseAsBackButton="props.hasCloseAsBackButton"
isExpandable="props.isExpandable"
saveThreadScrollTop="_saveThreadScrollTop"
t-on-o-clicked="_onClickedHeader"
t-ref="header"
/>
@@ -85,6 +85,9 @@ class ChatWindowHeader extends Component {
*/
_onClickShiftLeft(ev) {
ev.stopPropagation();
if (this.props.saveThreadScrollTop) {
this.props.saveThreadScrollTop();
}
this.chatWindow.shiftLeft();
}
@@ -94,6 +97,9 @@ class ChatWindowHeader extends Component {
*/
_onClickShiftRight(ev) {
ev.stopPropagation();
if (this.props.saveThreadScrollTop) {
this.props.saveThreadScrollTop();
}
this.chatWindow.shiftRight();
}
@@ -109,6 +115,10 @@ Object.assign(ChatWindowHeader, {
chatWindowLocalId: String,
hasCloseAsBackButton: Boolean,
isExpandable: Boolean,
saveThreadScrollTop: {
type: Function,
optional: true,
},
},
template: 'mail.ChatWindowHeader',
});
@@ -2416,7 +2416,7 @@ QUnit.test('chat window should remain folded when new message is received', asyn
);
});
QUnit.test('chat window scroll position should remain the same after switching left', async function (assert) {
QUnit.skip('chat window scroll position should remain the same after switching left', async function (assert) {
assert.expect(2);
this.data['mail.channel'].records.push({
@@ -2474,7 +2474,7 @@ QUnit.test('chat window scroll position should remain the same after switching l
);
});
QUnit.test('chat window scroll position should remain the same after switching right', async function (assert) {
QUnit.skip('chat window scroll position should remain the same after switching right', async function (assert) {
assert.expect(2);
this.data['mail.channel'].records.push({
+1
View File
@@ -2,6 +2,7 @@
from . import payment_acquirer
from . import account_invoice
from . import account_journal
from . import res_partner
from . import account_payment
from . import chart_template
+14
View File
@@ -0,0 +1,14 @@
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
from flectra import api, models, _
from flectra.exceptions import ValidationError
class AccountJournal(models.Model):
_inherit = 'account.journal'
@api.constrains('type')
def _check_journal_type_change(self):
acquirer_incompatible_journals = self.filtered(lambda j: j.type not in ('bank', 'cash'))
if acquirer_incompatible_journals and self.env['payment.acquirer'].search_count([('journal_id', 'in', acquirer_incompatible_journals.ids)]):
raise ValidationError(_("An acquirer is using this journal. Only bank and cash types are allowed."))
@@ -310,8 +310,20 @@ flectra.define('point_of_sale.ProductScreen', function(require) {
this.currentOrder.updatePricelist(newClient);
}
}
_onClickPay() {
this.showScreen('PaymentScreen');
async _onClickPay() {
if (this.env.pos.get_order().orderlines.any(line => line.get_product().tracking !== 'none' && !line.has_valid_product_lot() && (this.env.pos.picking_type.use_create_lots || this.env.pos.picking_type.use_existing_lots))) {
const { confirmed } = await this.showPopup('ConfirmPopup', {
title: this.env._t('Some Serial/Lot Numbers are missing'),
body: this.env._t('You are trying to sell products with serial/lot numbers, but some of them are not set.\nWould you like to proceed anyway?'),
confirmText: this.env._t('Yes'),
cancelText: this.env._t('No')
});
if (confirmed) {
this.showScreen('PaymentScreen');
}
} else {
this.showScreen('PaymentScreen');
}
}
switchPane() {
if (this.mobile_pane === "left") {
+10 -1
View File
@@ -142,7 +142,16 @@ class TestPurchaseInvoice(AccountTestInvoicingCommon):
"""Only purchase managers can approve a purchase order when double
validation is enabled"""
group_purchase_manager = self.env.ref('purchase.group_purchase_manager')
order = self.env.ref("purchase.purchase_order_1")
order = self.env['purchase.order'].create({
"partner_id": self.vendor.id,
"order_line": [
(0, 0, {
'product_id': self.product.id,
'name': f'{self.product.name} {1:05}',
'price_unit': 79.80,
'product_qty': 15.0,
}),
]})
company = order.sudo().company_id
company.po_double_validation = 'two_step'
company.po_double_validation_amount = 0
+1 -1
View File
@@ -51,7 +51,7 @@
<separator string="Warning on the Sales Order" colspan="4" />
<field name="sale_warn" nolabel="1" />
<field name="sale_warn_msg" colspan="3" nolabel="1"
attrs="{'required':[('sale_warn', '!=', False), ('sale_warn','!=','no-message')],'readonly':[('sale_warn','=','no-message')]}"/>
attrs="{'required':[('sale_warn', '!=', False), ('sale_warn','!=','no-message')], 'invisible':[('sale_warn','in',(False,'no-message'))]}"/>
</group>
</page>
</field>
@@ -132,6 +132,7 @@
data-model="project.task"
data-views='[[false, "list"], [false, "form"]]'
t-att-data-domain="json.dumps([['project_id', 'in', projects.ids], ['sale_line_id', '=', False]])"
t-att-data-context="json.dumps({'active_test': False})"
>
<span class="btn-link"
style="font-weight:normal;"
@@ -365,7 +365,7 @@
<span t-esc="choice_data['row'].value"></span>
</td>
<td class="survey_answer" t-foreach="choice_data['columns']" t-as="column_data">
<span t-esc="round(column_data['count'] * 100.0/ (len(question_data['answer_line_done_ids']) or 1), 2)"></span> %
<span t-esc="round(column_data['count'] * 100.0/ (len(question_data['answer_input_done_ids']) or 1), 2)"></span> %
<span class="badge badge-primary" t-esc="column_data['count']"></span>
<i class="fa fa-filter text-primary survey_filter"
t-att-data-question_id="question.id"
@@ -934,7 +934,9 @@ var FieldDate = InputField.extend({
let value = this.$input.val();
try {
value = this._parseValue(value);
value.add(-this.getSession().getTZOffset(value), "minutes");
if (this.field.type === "datetime") {
value.add(-this.getSession().getTZOffset(value), "minutes");
}
} catch (err) {}
await this._setValue(value);
this._render();
@@ -1472,11 +1472,11 @@ var BasicModel = AbstractModel.extend({
var makeDefaultRecords = [];
if (additionalContexts){
_.each(additionalContexts, function (context) {
params.context = self._getContext(list, {additionalContext: context});
params.context = self._getContext(list, {additionalContext: context, sanitize_default_values: true});
makeDefaultRecords.push(self._makeDefaultRecord(list.model, params));
});
} else {
params.context = self._getContext(list);
params.context = self._getContext(list, {sanitize_default_values: true});
makeDefaultRecords.push(self._makeDefaultRecord(list.model, params));
}
@@ -1870,7 +1870,7 @@ var BasicModel = AbstractModel.extend({
});
}
});
var def = self._readUngroupedList(list).then(function () {
var def = self._readUngroupedList(list).then(function (list) {
var x2ManysDef = self._fetchX2ManysBatched(list);
var referencesDef = self._fetchReferencesBatched(list);
return Promise.all([x2ManysDef, referencesDef]);
@@ -3449,7 +3449,12 @@ var BasicModel = AbstractModel.extend({
context.set_eval_context(this._getEvalContext(element));
if (options.full || !(options.fieldName || options.additionalContext)) {
context.add(element.context);
var context_to_add = options.sanitize_default_values ?
_.omit(element.context, function (val, key) {
return _.str.startsWith(key, 'default_');
})
: element.context;
context.add(context_to_add);
}
if (options.fieldName) {
var viewType = options.viewType || element.viewType;
@@ -4243,6 +4243,11 @@ QUnit.module('basic_fields', {
viewOptions: {
mode: 'edit',
},
session: {
getTZOffset: function () {
return 120;
},
},
});
const year = (new Date()).getFullYear();
@@ -3674,6 +3674,56 @@ QUnit.module('relational_fields', {
form.destroy();
});
QUnit.test('Check onchange with two consecutive many2one', async function (assert) {
assert.expect(2);
this.data.product.fields.product_partner_ids = { string: "User", type: 'one2many', relation: 'partner' };
this.data.product.records[0].product_partner_ids = [1];
this.data.product.records[1].product_partner_ids = [2];
this.data.turtle.fields.product_ids = { string: "Product", type: "one2many", relation: 'product' };
this.data.turtle.fields.user_ids = { string: "Product", type: "one2many", relation: 'user' };
this.data.turtle.onchanges = {
turtle_trululu: function (record) {
record.product_ids = [37];
record.user_ids = [17, 19];
},
};
var form = await createView({
View: FormView,
model: 'turtle',
data: this.data,
arch:
'<form string="Turtles">' +
'<field string="Product" name="turtle_trululu"/>' +
'<field readonly="1" string="Related field" name="product_ids">' +
'<tree>' +
'<field widget="many2many_tags" name="product_partner_ids"/>' +
'</tree>' +
'</field>' +
'<field readonly="1" string="Second related field" name="user_ids">' +
'<tree>' +
'<field widget="many2many_tags" name="partner_ids"/>' +
'</tree>' +
'</field>' +
'</form>',
res_id: 1,
});
await testUtils.form.clickEdit(form);
await testUtils.fields.many2one.clickOpenDropdown("turtle_trululu");
await testUtils.fields.many2one.searchAndClickItem('turtle_trululu', {search: 'first record'});
const getElementTextContent = name => [...document.querySelectorAll(`.o_field_many2manytags[name="${name}"] .badge.o_tag_color_0 > span`)]
.map(x=>x.textContent);
assert.deepEqual(
getElementTextContent('product_partner_ids'),
['first record'],
"should have the correct value in the many2many tag widget");
assert.deepEqual(
getElementTextContent('partner_ids'),
['first record', 'second record'],
"should have the correct values in the many2many tag widget");
form.destroy();
});
});
});
});
@@ -2352,6 +2352,44 @@ QUnit.module('Views', {
form.destroy();
});
QUnit.test('remove default value in subviews', async function (assert) {
assert.expect(2);
this.data.product.onchanges = {}
this.data.product.onchanges.name = function () {};
var form = await createView({
View: FormView,
model: 'partner',
data: this.data,
viewOptions: {
context: {default_state: "ab"}
},
arch: '<form string="Partners">' +
'<field name="product_ids" context="{\'default_product_uom_qty\': 68}">' +
'<tree editable="top">' +
'<field name="name"/>' +
'</tree>' +
'</field>' +
'</form>',
mockRPC: function (route, args) {
if (route === "/web/dataset/call_kw/partner/onchange") {
assert.deepEqual(args.kwargs.context, {
default_state: 'ab',
})
}
else if (route === "/web/dataset/call_kw/product/onchange") {
assert.deepEqual(args.kwargs.context, {
default_product_uom_qty: 68,
})
}
return this._super.apply(this, arguments);
},
});
await testUtils.dom.click(form.$('.o_field_x2many_list_row_add a'));
form.destroy();
});
QUnit.test('reference field in one2many list', async function (assert) {
assert.expect(1);
@@ -1423,7 +1423,13 @@ var VideoWidget = MediaWidget.extend({
const fullscreen = options.hide_fullscreen ? '&fs=0' : '';
const ytLoop = loop ? loop + `&playlist=${matches.youtube[2]}` : '';
const logo = options.hide_yt_logo ? '&modestbranding=1' : '';
embedURL = `//www.youtube${matches.youtube[1] || ''}.com/embed/${matches.youtube[2]}${autoplay}&rel=0${ytLoop}${controls}${fullscreen}${logo}`;
// The youtube js api is needed for autoplay on mobile. Note: this
// was added as a fix, old customers may have autoplay videos
// without this, which will make their video autoplay on desktop
// but not in mobile (so no behavior change was done in stable,
// this should not be migrated).
const enablejsapi = options.autoplay ? '&enablejsapi=1' : '';
embedURL = `//www.youtube${matches.youtube[1] || ''}.com/embed/${matches.youtube[2]}${autoplay}${enablejsapi}&rel=0${ytLoop}${controls}${fullscreen}${logo}`;
type = 'youtube';
} else if (matches.instagram && matches.instagram[2].length) {
embedURL = `//www.instagram.com/p/${matches.instagram[2]}/embed/`;
@@ -5,6 +5,7 @@ flectra.define('website.content.snippets.animation', function (require) {
* Provides a way to start JS code for snippets' initialization and animations.
*/
const ajax = require('web.ajax');
var Class = require('web.Class');
var config = require('web.config');
var core = require('web.core');
@@ -596,7 +597,55 @@ registry.Parallax = Animation.extend({
},
});
registry.mediaVideo = publicWidget.Widget.extend({
const MobileYoutubeAutoplayMixin = {
/**
* Takes care of any necessary setup for autoplaying video. In practice,
* this method will load the youtube iframe API for mobile environments
* because mobile environments don't support the youtube autoplay param
* passed in the url.
*
* @private
* @param {string} src - The source url of the video
*/
_setupAutoplay: function (src) {
let promise = Promise.resolve();
this.isYoutubeVideo = src.indexOf('youtube') >= 0;
this.isMobileEnv = config.device.size_class <= config.device.SIZES.LG && config.device.touch;
if (this.isYoutubeVideo && this.isMobileEnv && !window.YT) {
const oldOnYoutubeIframeAPIReady = window.onYouTubeIframeAPIReady;
promise = new Promise(resolve => {
window.onYouTubeIframeAPIReady = () => {
if (oldOnYoutubeIframeAPIReady) {
oldOnYoutubeIframeAPIReady();
}
return resolve();
};
});
ajax.loadJS('https://www.youtube.com/iframe_api');
}
return promise;
},
/**
* @private
* @param {DOMElement} iframeEl - the iframe containing the video player
*/
_triggerAutoplay: function (iframeEl) {
// YouTube does not allow to auto-play video in mobile devices, so we
// have to play the video manually.
if (this.isMobileEnv && this.isYoutubeVideo) {
new window.YT.Player(iframeEl, {
events: {
onReady: ev => ev.target.playVideo(),
}
});
}
},
};
registry.mediaVideo = publicWidget.Widget.extend(MobileYoutubeAutoplayMixin, {
selector: '.media_iframe_video',
/**
@@ -606,15 +655,36 @@ registry.mediaVideo = publicWidget.Widget.extend({
// TODO: this code should be refactored to make more sense and be better
// integrated with Flectra (this refactoring should be done in master).
var def = this._super.apply(this, arguments);
if (this.$target.children('iframe').length) {
// There already is an <iframe/>, do nothing. This is the normal
// case. The whole code that follows is only there to ensure
// compatibility with videos added before bug fixes or new Flectra
// versions where the <iframe/> element is properly saved.
return def;
const proms = [this._super.apply(this, arguments)];
let iframeEl = this.$target[0].querySelector(':scope > iframe');
// The following code is only there to ensure compatibility with
// videos added before bug fixes or new Flectra versions where the
// <iframe/> element is properly saved.
if (!iframeEl) {
iframeEl = this._generateIframe();
}
if (!iframeEl) {
// Something went wrong: no iframe is present in the DOM and the
// widget was unable to create one on the fly.
return Promise.all(proms);
}
proms.push(this._setupAutoplay(iframeEl.getAttribute('src')));
return Promise.all(proms).then(() => {
this._triggerAutoplay(iframeEl);
});
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* @private
*/
_generateIframe: function () {
// Bug fix / compatibility: empty the <div/> element as all information
// to rebuild the iframe should have been saved on the <div/> element
this.$target.empty();
@@ -634,25 +704,23 @@ registry.mediaVideo = publicWidget.Widget.extend({
var m = src.match(/^(?:https?:)?\/\/([^/?#]+)/);
if (!m) {
// Unsupported protocol or wrong URL format, don't inject iframe
return def;
return;
}
var domain = m[1].replace(/^www\./, '');
var supportedDomains = ['youtu.be', 'youtube.com', 'youtube-nocookie.com', 'instagram.com', 'vine.co', 'player.vimeo.com', 'vimeo.com', 'dailymotion.com', 'player.youku.com', 'youku.com'];
if (!_.contains(supportedDomains, domain)) {
// Unsupported domain, don't inject iframe
return def;
return;
}
this.$target.append($('<iframe/>', {
return this.$target.append($('<iframe/>', {
src: src,
frameborder: '0',
allowfullscreen: 'allowfullscreen',
}));
return def;
}))[0];
},
});
registry.backgroundVideo = publicWidget.Widget.extend({
registry.backgroundVideo = publicWidget.Widget.extend(MobileYoutubeAutoplayMixin, {
selector: '.o_background_video',
xmlDependencies: ['/website/static/src/xml/website.background.video.xml'],
disabledInEditableMode: false,
@@ -665,26 +733,13 @@ registry.backgroundVideo = publicWidget.Widget.extend({
this.videoSrc = this.el.dataset.bgVideoSrc;
this.iframeID = _.uniqueId('o_bg_video_iframe_');
this.isYoutubeVideo = this.videoSrc.indexOf('youtube') >= 0;
this.isMobileEnv = config.device.size_class <= config.device.SIZES.LG && config.device.touch;
if (this.isYoutubeVideo && this.isMobileEnv) {
this.videoSrc = this.videoSrc + "&enablejsapi=1";
if (!window.YT) {
var oldOnYoutubeIframeAPIReady = window.onYouTubeIframeAPIReady;
proms.push(new Promise(resolve => {
window.onYouTubeIframeAPIReady = () => {
if (oldOnYoutubeIframeAPIReady) {
oldOnYoutubeIframeAPIReady();
}
return resolve();
};
}));
$('<script/>', {
src: 'https://www.youtube.com/iframe_api',
}).appendTo('head');
}
proms.push(this._setupAutoplay(this.videoSrc));
if (this.isYoutubeVideo && this.isMobileEnv && !this.videoSrc.includes('enablejsapi=1')) {
// Compatibility: when choosing an autoplay youtube video via the
// media manager, the API was not automatically enabled before but
// only enabled here in the case of background videos.
// TODO migrate those old cases so this code can be removed?
this.videoSrc += '&enablejsapi=1';
}
var throttledUpdate = _.throttle(() => this._adjustIframe(), 50);
@@ -785,16 +840,7 @@ registry.backgroundVideo = publicWidget.Widget.extend({
$oldContainer.remove();
this._adjustIframe();
// YouTube does not allow to auto-play video in mobile devices, so we
// have to play the video manually.
if (this.isMobileEnv && this.isYoutubeVideo) {
new window.YT.Player(this.iframeID, {
events: {
onReady: ev => ev.target.playVideo(),
}
});
}
this._triggerAutoplay(this.$iframe[0]);
},
});
@@ -2,39 +2,111 @@ flectra.define('website_hr_recruitment.tour', function(require) {
'use strict';
var tour = require("web_tour.tour");
function applyForAJob(jobName, application) {
return [{
content: "Select Job",
trigger: `.oe_website_jobs h3 span:contains(${jobName})`,
}, {
content: "Apply",
trigger: ".js_hr_recruitment a:contains('Apply')",
}, {
content: "Complete name",
trigger: "input[name=partner_name]",
run: `text ${application.name}`,
}, {
content: "Complete Email",
trigger: "input[name=email_from]",
run: `text ${application.email}`,
}, {
content: "Complete phone number",
trigger: "input[name=partner_phone]",
run: `text ${application.phone}`,
}, {
content: "Complete Subject",
trigger: "textarea[name=description]",
run: `text ${application.subject}`,
}, { // TODO: Upload a file ?
content: "Send the form",
trigger: ".s_website_form_send",
}, {
content: "Check the form is submitted without errors",
trigger: ".oe_structure:has(h1:contains('Congratulations'))",
}];
}
tour.register('website_hr_recruitment_tour', {
test: true,
url: '/jobs',
}, [
...applyForAJob('Guru', {
name: 'John Smith',
email: 'john@smith.com',
phone: '118.218',
subject: '### [GURU] HR RECRUITMENT TEST DATA ###',
}),
{
content: "Go back to the jobs page",
trigger: "body",
run: () => {
window.location.href = '/jobs';
},
},
...applyForAJob('Internship', {
name: 'Jack Doe',
email: 'jack@doe.com',
phone: '118.712',
subject: '### HR [INTERN] RECRUITMENT TEST DATA ###',
}),
]);
tour.register('website_hr_recruitment_tour_edit_form', {
test: true,
url: '/jobs',
}, [{
content: "Select Job",
trigger: ".oe_website_jobs h3 span:contains('A Test Job')"
content: 'Go to the Guru job page',
trigger: 'a[href*="guru"]',
}, {
content: "Apply",
trigger: ".js_hr_recruitment a:contains('Apply')"
content: 'Go to the Guru job form',
trigger: 'a[href*="apply"]',
}, {
content: "Complete name",
trigger: "input[name=partner_name]",
run: "text John Smith"
content: 'Check if the Guru form is present',
trigger: 'form'
}, {
content: "Complete Email",
trigger: "input[name=email_from]",
run: "text john@smith.com"
content: 'Enter in edit mode',
trigger: 'a[data-action="edit"]',
}, {
content: "Complete phone number",
trigger: "input[name=partner_phone]",
run: "text 118.218"
content: 'Edit the form',
trigger: 'input[type="file"]',
extra_trigger: '#oe_snippets.o_loaded',
}, {
content: "Complete Subject",
trigger: "textarea[name=description]",
run: "text ### HR RECRUITMENT TEST DATA ###"
}, { // TODO: Upload a file ?
content: "Send the form",
trigger: ".s_website_form_send"
content: 'Add a new field',
trigger: 'we-button[data-add-field]',
}, {
content: "Check the form is submited without errors",
trigger: ".oe_structure:has(h1:contains('Congratulations'))"
}]);
content: 'Save',
trigger: 'button[data-action="save"]',
}, {
content: 'Go back to /jobs page after save',
trigger: 'a[data-action="edit"]',
run: () => {
window.location.href = '/jobs';
}
}, {
content: 'Go to the Internship job page',
trigger: 'a[href*="internship"]',
}, {
content: 'Go to the Internship job form',
trigger: 'a[href*="apply"]',
}, {
content: 'Check that a job_id has been loaded',
trigger: 'form',
run: () => {
const selector = 'input[name="job_id"]:not([value=""])';
if (!document.querySelector(selector)) {
console.error('The job_id field has a wrong value');
}
}
},
]);
return {};
});
@@ -7,16 +7,28 @@ import flectra.tests
@flectra.tests.tagged('post_install', '-at_install')
class TestWebsiteHrRecruitmentForm(flectra.tests.HttpCase):
def test_tour(self):
job = self.env['hr.job'].create({
'name': 'A Test Job',
job_guru = self.env['hr.job'].create({
'name': 'Guru',
'is_published': True,
})
self.start_tour("/", 'website_hr_recruitment_tour')
job_intern = self.env['hr.job'].create({
'name': 'Internship',
'is_published': True,
})
self.start_tour('/', 'website_hr_recruitment_tour_edit_form', login='admin')
self.start_tour('/', 'website_hr_recruitment_tour')
# check result
record = self.env['hr.applicant'].search([('description', '=', '### HR RECRUITMENT TEST DATA ###')])
self.assertEqual(len(record), 1)
self.assertEqual(record.partner_name, "John Smith")
self.assertEqual(record.email_from, "john@smith.com")
self.assertEqual(record.partner_phone, '118.218')
guru_applicant = self.env['hr.applicant'].search([('description', '=', '### [GURU] HR RECRUITMENT TEST DATA ###'),
('job_id', '=', job_guru.id),])
self.assertEqual(len(guru_applicant), 1)
self.assertEqual(guru_applicant.partner_name, 'John Smith')
self.assertEqual(guru_applicant.email_from, 'john@smith.com')
self.assertEqual(guru_applicant.partner_phone, '118.218')
internship_applicant = self.env['hr.applicant'].search([('description', '=', '### HR [INTERN] RECRUITMENT TEST DATA ###'),
('job_id', '=', job_intern.id),])
self.assertEqual(len(internship_applicant), 1)
self.assertEqual(internship_applicant.partner_name, 'Jack Doe')
self.assertEqual(internship_applicant.email_from, 'jack@doe.com')
self.assertEqual(internship_applicant.partner_phone, '118.712')