mirror of
https://gitlab.com/flectra-hq/flectra.git
synced 2026-08-18 17:24:42 -05:00
[PATCH] Upstream patch - 22042022
This commit is contained in:
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
""",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user