mirror of
https://gitlab.com/flectra-hq/flectra.git
synced 2026-11-03 12:46:39 -06:00
[PATCH] Upstream patch - 16102021
This commit is contained in:
1 parent
f045c58122
commit
f0f3085cb9
33 files changed
+2600
-641
No files matched your search
@@ -277,7 +277,7 @@ class AccountReconcileModel(models.Model):
|
||||
self.ensure_one()
|
||||
balance = base_line_dict['balance']
|
||||
tax_type = tax.type_tax_use
|
||||
is_refund = (tax_type == 'sale' and balance < 0) or (tax_type == 'purchase' and balance > 0)
|
||||
is_refund = (tax_type == 'sale' and balance > 0) or (tax_type == 'purchase' and balance < 0)
|
||||
|
||||
res = tax.compute_all(balance, is_refund=is_refund)
|
||||
|
||||
|
||||
@@ -199,7 +199,7 @@
|
||||
<p t-if="o.invoice_incoterm_id" name="incoterm">
|
||||
<strong>Incoterm: </strong><span t-field="o.invoice_incoterm_id.code"/> - <span t-field="o.invoice_incoterm_id.name"/>
|
||||
</p>
|
||||
<div id="qrcode" t-if="o.display_qr_code">
|
||||
<div id="qrcode" t-if="o.display_qr_code and o.amount_residual > 0">
|
||||
<p t-if="qr_code_urls.get(o.id)">
|
||||
<strong class="text-center">Scan me with your banking app.</strong><br/><br/>
|
||||
<img class="border border-dark rounded" t-att-src="qr_code_urls[o.id]"/>
|
||||
|
||||
@@ -82,6 +82,8 @@ class ProviderGrid(models.Model):
|
||||
total_delivery += line.price_total
|
||||
if not line.product_id or line.is_delivery:
|
||||
continue
|
||||
if line.product_id.type == "service":
|
||||
continue
|
||||
qty = line.product_uom._compute_quantity(line.product_uom_qty, line.product_id.uom_id)
|
||||
weight += (line.product_id.weight or 0.0) * qty
|
||||
volume += (line.product_id.volume or 0.0) * qty
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||
from . import common
|
||||
from . import test_manual
|
||||
@@ -0,0 +1,694 @@
|
||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||
from flectra import fields
|
||||
from flectra.tests.common import Form
|
||||
from flectra.addons.account.tests.common import AccountTestInvoicingCommon
|
||||
import random
|
||||
import logging
|
||||
import time
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TestAr(AccountTestInvoicingCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls, chart_template_ref='l10n_ar.l10nar_ri_chart_template'):
|
||||
super(TestAr, cls).setUpClass(chart_template_ref=chart_template_ref)
|
||||
|
||||
# ==== Company ====
|
||||
cls.company_data['company'].write({
|
||||
'parent_id': cls.env.ref('base.main_company').id,
|
||||
'currency_id': cls.env.ref('base.ARS').id,
|
||||
'name': '(AR) Responsable Inscripto (Unit Tests)',
|
||||
"l10n_ar_afip_start_date": time.strftime('%Y-01-01'),
|
||||
'l10n_ar_gross_income_type': 'local',
|
||||
'l10n_ar_gross_income_number': '901-21885123',
|
||||
})
|
||||
cls.company_ri = cls.company_data['company']
|
||||
|
||||
cls.company_ri.partner_id.write({
|
||||
'name': '(AR) Responsable Inscripto (Unit Tests)',
|
||||
'l10n_ar_afip_responsibility_type_id': cls.env.ref("l10n_ar.res_IVARI").id,
|
||||
'l10n_latam_identification_type_id': cls.env.ref("l10n_ar.it_cuit").id,
|
||||
'vat': '30111111118',
|
||||
"street": 'Calle Falsa 123',
|
||||
"city": 'Rosario',
|
||||
"country_id": cls.env.ref("base.ar").id,
|
||||
"state_id": cls.env.ref("base.state_ar_s").id,
|
||||
"zip": '2000',
|
||||
"phone": '+1 555 123 7073',
|
||||
"email": 'info@example.com',
|
||||
"website": 'www.example.com',
|
||||
})
|
||||
cls.partner_ri = cls.company_ri.partner_id
|
||||
|
||||
# ==== Company MONO ====
|
||||
cls.company_mono = cls.setup_company_data('(AR) Monotributista (Unit Tests)', chart_template=cls.env.ref('l10n_ar.l10nar_base_chart_template'))['company']
|
||||
cls.company_mono.write({
|
||||
'parent_id': cls.env.ref('base.main_company').id,
|
||||
'currency_id': cls.env.ref('base.ARS').id,
|
||||
'name': '(AR) Monotributista (Unit Tests)',
|
||||
"l10n_ar_afip_start_date": time.strftime('%Y-01-01'),
|
||||
'l10n_ar_gross_income_type': 'exempt',
|
||||
})
|
||||
cls.company_mono.partner_id.write({
|
||||
'name': '(AR) Monotributista (Unit Tests)',
|
||||
'l10n_ar_afip_responsibility_type_id': cls.env.ref("l10n_ar.res_RM").id,
|
||||
'l10n_latam_identification_type_id': cls.env.ref("l10n_ar.it_cuit").id,
|
||||
'vat': '20222222223',
|
||||
"street": 'Calle Falsa 123',
|
||||
"city": 'Rosario',
|
||||
"country_id": cls.env.ref("base.ar").id,
|
||||
"state_id": cls.env.ref("base.state_ar_s").id,
|
||||
"zip": '2000',
|
||||
"phone": '+1 555 123 7073',
|
||||
"email": 'info@example.com',
|
||||
"website": 'www.example.com',
|
||||
})
|
||||
cls.partner_mono = cls.company_mono.partner_id
|
||||
|
||||
# ==== Bank Account ====
|
||||
cls.bank_account_ri = cls.env['res.partner.bank'].create({
|
||||
'acc_number': '7982898111100056688080',
|
||||
'partner_id': cls.company_ri.partner_id.id,
|
||||
'company_id': cls.company_ri.id,
|
||||
})
|
||||
|
||||
# ==== Partners / Customers ====
|
||||
cls.res_partner_adhoc = cls.env['res.partner'].create({
|
||||
"name": "ADHOC SA",
|
||||
"is_company": 1,
|
||||
"city": "Rosario",
|
||||
"zip": "2000",
|
||||
"state_id": cls.env.ref("base.state_ar_s").id,
|
||||
"country_id": cls.env.ref("base.ar").id,
|
||||
"street": "Ovidio Lagos 41 bis",
|
||||
"email": "info@adhoc.com.ar",
|
||||
"phone": "(+54) (341) 208 0203",
|
||||
"website": "http://www.adhoc.com.ar",
|
||||
'l10n_latam_identification_type_id': cls.env.ref("l10n_ar.it_cuit").id,
|
||||
'vat': "30714295698",
|
||||
'l10n_ar_afip_responsibility_type_id': cls.env.ref("l10n_ar.res_IVARI").id,
|
||||
})
|
||||
cls.partner_cf = cls.env['res.partner'].create({
|
||||
"name": "Consumidor Final Anónimo",
|
||||
"l10n_latam_identification_type_id": cls.env.ref('l10n_ar.it_Sigd').id,
|
||||
"l10n_ar_afip_responsibility_type_id": cls.env.ref("l10n_ar.res_CF").id,
|
||||
})
|
||||
cls.res_partner_gritti_mono = cls.env['res.partner'].create({
|
||||
"name": "Gritti Agrimensura (Monotributo)",
|
||||
"is_company": 1,
|
||||
"city": "Rosario",
|
||||
"zip": "2000",
|
||||
"state_id": cls.env.ref("base.state_ar_s").id,
|
||||
"country_id": cls.env.ref("base.ar").id,
|
||||
"street": "Calle Falsa 123",
|
||||
"email": "info@example.com.ar",
|
||||
"phone": "(+54) (341) 111 2222",
|
||||
"website": "http://www.grittiagrimensura.com",
|
||||
'l10n_latam_identification_type_id': cls.env.ref("l10n_ar.it_cuit").id,
|
||||
'vat': "27320732811",
|
||||
'l10n_ar_afip_responsibility_type_id': cls.env.ref("l10n_ar.res_RM").id,
|
||||
})
|
||||
cls.res_partner_cerrocastor = cls.env['res.partner'].create({
|
||||
"name": "Cerro Castor (Tierra del Fuego)",
|
||||
"is_company": 1,
|
||||
"city": "Ushuaia",
|
||||
"state_id": cls.env.ref("base.state_ar_v").id,
|
||||
"country_id": cls.env.ref("base.ar").id,
|
||||
"street": "Ruta 3 km 26",
|
||||
"email": "info@cerrocastor.com",
|
||||
"phone": "(+00) (11) 4444 5556",
|
||||
"website": "http://www.cerrocastor.com",
|
||||
'l10n_latam_identification_type_id': cls.env.ref("l10n_ar.it_cuit").id,
|
||||
'vat': "27333333339",
|
||||
'l10n_ar_afip_responsibility_type_id': cls.env.ref("l10n_ar.res_IVA_LIB").id,
|
||||
})
|
||||
cls.res_partner_cmr = cls.env['res.partner'].create({
|
||||
"name": "Concejo Municipal de Rosario (IVA Sujeto Exento)",
|
||||
"is_company": 1,
|
||||
"city": "Rosario",
|
||||
"zip": "2000",
|
||||
"state_id": cls.env.ref("base.state_ar_s").id,
|
||||
"country_id": cls.env.ref("base.ar").id,
|
||||
"street": "Cordoba 501",
|
||||
"email": "info@example.com.ar",
|
||||
"phone": "(+54) (341) 222 3333",
|
||||
"website": "http://www.concejorosario.gov.ar/",
|
||||
'l10n_latam_identification_type_id': cls.env.ref("l10n_ar.it_cuit").id,
|
||||
'vat': "30684679372",
|
||||
'l10n_ar_afip_responsibility_type_id': cls.env.ref("l10n_ar.res_IVAE").id,
|
||||
})
|
||||
cls.res_partner_expresso = cls.env['res.partner'].create({
|
||||
"name": "Expresso",
|
||||
"is_company": 1,
|
||||
"city": "Barcelona",
|
||||
"zip": "11002",
|
||||
"country_id": cls.env.ref("base.es").id,
|
||||
"street": "La gran avenida 123",
|
||||
"email": "info@expresso.com",
|
||||
"phone": "(+00) (11) 222 3333",
|
||||
"website": "http://www.expresso.com/",
|
||||
'l10n_latam_identification_type_id': cls.env.ref("l10n_latam_base.it_fid").id,
|
||||
'vat': "2222333344445555",
|
||||
'l10n_ar_afip_responsibility_type_id': cls.env.ref("l10n_ar.res_EXT").id,
|
||||
})
|
||||
cls.partner_mipyme = cls.env['res.partner'].create({
|
||||
"name": "Belgrano Cargas Y Logistica S (Mipyme)",
|
||||
"is_company": 1,
|
||||
"city": "Buenos Aires",
|
||||
"zip": "1425",
|
||||
"state_id": cls.env.ref("base.state_ar_c").id,
|
||||
"country_id": cls.env.ref("base.ar").id,
|
||||
"street": "Av. Santa Fe 4636",
|
||||
"email": "mipyme@example.com",
|
||||
"phone": "(123)-456-7890",
|
||||
"website": "http://www.mypime-inc.com",
|
||||
'l10n_latam_identification_type_id': cls.env.ref("l10n_ar.it_cuit").id,
|
||||
'vat': "30714101443",
|
||||
'l10n_ar_afip_responsibility_type_id': cls.env.ref("l10n_ar.res_IVARI").id,
|
||||
})
|
||||
cls.partner_mipyme_ex = cls.partner_mipyme.copy({'name': 'MiPyme Exento', 'l10n_ar_afip_responsibility_type_id': cls.env.ref('l10n_ar.res_IVAE').id})
|
||||
|
||||
# ==== Taxes ====
|
||||
cls.tax_21 = cls._search_tax(cls, 'iva_21')
|
||||
cls.tax_27 = cls._search_tax(cls, 'iva_27')
|
||||
cls.tax_0 = cls._search_tax(cls, 'iva_0')
|
||||
cls.tax_10_5 = cls._search_tax(cls, 'iva_105')
|
||||
cls.tax_no_gravado = cls._search_tax(cls, 'iva_no_gravado')
|
||||
cls.tax_perc_iibb = cls._search_tax(cls, 'percepcion_iibb_ba')
|
||||
cls.tax_iva_exento = cls._search_tax(cls, 'iva_exento')
|
||||
|
||||
# ==== Products ====
|
||||
uom_unit = cls.env.ref('uom.product_uom_unit')
|
||||
uom_hour = cls.env.ref('uom.product_uom_hour')
|
||||
|
||||
cls.product_iva_21 = cls.env['product.product'].create({
|
||||
'name': 'Large Cabinet (VAT 21)',
|
||||
'uom_id': uom_unit.id,
|
||||
'uom_po_id': uom_unit.id,
|
||||
'lst_price': 320.0,
|
||||
'type': "consu",
|
||||
'default_code': 'E-COM07',
|
||||
})
|
||||
cls.service_iva_27 = cls.env['product.product'].create({
|
||||
# demo 'product_product_telefonia'
|
||||
'name': 'Telephone service (VAT 27)',
|
||||
'uom_id': uom_unit.id,
|
||||
'uom_po_id': uom_unit.id,
|
||||
'lst_price': 130.0,
|
||||
'type': 'service',
|
||||
'default_code': 'TELEFONIA',
|
||||
'taxes_id': [(6, 0, cls.tax_27.ids)],
|
||||
})
|
||||
cls.product_iva_cero = cls.env['product.product'].create({
|
||||
# demo 'product_product_cero'
|
||||
'name': 'Non-industrialized animals and vegetables (VAT Zero)',
|
||||
'uom_id': uom_unit.id,
|
||||
'uom_po_id': uom_unit.id,
|
||||
'list_price': 160.0,
|
||||
'type': 'consu',
|
||||
'default_code': 'CERO',
|
||||
'taxes_id': [(6, 0, cls.tax_0.ids)],
|
||||
})
|
||||
cls.product_iva_105 = cls.env['product.product'].create({
|
||||
# demo 'product.product_product_27'
|
||||
'name': 'Laptop Customized (VAT 10,5)',
|
||||
'uom_id': uom_unit.id,
|
||||
'uom_po_id': uom_unit.id,
|
||||
'type': 'consu',
|
||||
'default_code': '10,5',
|
||||
'taxes_id': [(6, 0, cls.tax_10_5.ids)],
|
||||
})
|
||||
cls.service_iva_21 = cls.env['product.product'].create({
|
||||
# demo data product.product_product_2
|
||||
'name': 'Virtual Home Staging (VAT 21)',
|
||||
'uom_id': uom_hour.id,
|
||||
'uom_po_id': uom_hour.id,
|
||||
'list_price': 38.25,
|
||||
'type': 'service',
|
||||
'default_code': 'VAT 21',
|
||||
'taxes_id': [(6, 0, cls.tax_21.ids)],
|
||||
})
|
||||
cls.product_no_gravado = cls.env['product.product'].create({
|
||||
# demo data product_product_no_gravado
|
||||
'name': 'Untaxed concepts (VAT NT)',
|
||||
'uom_id': uom_unit.id,
|
||||
'uom_po_id': uom_unit.id,
|
||||
'list_price': 40.00,
|
||||
'type': 'consu',
|
||||
'default_code': 'NOGRAVADO',
|
||||
'taxes_id': [(6, 0, cls.tax_no_gravado.ids)],
|
||||
})
|
||||
cls.product_iva_105_perc = cls.product_iva_105.copy({
|
||||
# product.product_product_25
|
||||
"name": "Laptop E5023 (VAT 10,5)",
|
||||
# agregamos percecipn aplicada y sufrida tambien
|
||||
'taxes_id': [(6, 0, [cls.tax_10_5.id, cls.tax_perc_iibb.id])],
|
||||
})
|
||||
cls.product_iva_exento = cls.env['product.product'].create({
|
||||
# demo product_product_exento
|
||||
'name': 'Book: Development in Flectra (VAT Exempt)',
|
||||
'uom_id': uom_unit.id,
|
||||
'uom_po_id': uom_unit.id,
|
||||
"list_price": 80.0,
|
||||
'type': 'consu',
|
||||
'default_code': 'EXENTO',
|
||||
'taxes_id': [(6, 0, cls.tax_iva_exento.ids)],
|
||||
})
|
||||
|
||||
# ==== Document Types ====
|
||||
cls.document_type = {
|
||||
'invoice_a': cls.env.ref('l10n_ar.dc_a_f'),
|
||||
'credit_note_a': cls.env.ref('l10n_ar.dc_a_nc'),
|
||||
'invoice_b': cls.env.ref('l10n_ar.dc_b_f'),
|
||||
'credit_note_b': cls.env.ref('l10n_ar.dc_b_nc'),
|
||||
'invoice_e': cls.env.ref('l10n_ar.dc_e_f'),
|
||||
'invoice_mipyme_a': cls.env.ref('l10n_ar.dc_fce_a_f'),
|
||||
'invoice_mipyme_b': cls.env.ref('l10n_ar.dc_fce_b_f'),
|
||||
}
|
||||
|
||||
# ==== Journals ====
|
||||
cls.sale_expo_journal_ri = cls.env["account.journal"].create({
|
||||
'name': "Expo Sales Journal",
|
||||
'company_id': cls.company_ri.id,
|
||||
'type': "sale",
|
||||
'code': "S0002",
|
||||
'l10n_latam_use_documents': "True",
|
||||
'l10n_ar_afip_pos_number': 2,
|
||||
'l10n_ar_afip_pos_partner_id': cls.partner_ri.id,
|
||||
'l10n_ar_afip_pos_system': "FEERCEL",
|
||||
'refund_sequence': False,
|
||||
})
|
||||
|
||||
# ==== Invoices ====
|
||||
cls.demo_invoices = {}
|
||||
|
||||
def _create_test_invoices_like_demo(self, use_current_date=True):
|
||||
""" Create in the unit tests the same invoices created in demo data """
|
||||
payment_term_id = self.env.ref("account.account_payment_term_end_following_month")
|
||||
invoice_user_id = self.env.user
|
||||
incoterm = self.env.ref("account.incoterm_EXW")
|
||||
|
||||
invoices_to_create = {
|
||||
'test_invoice_1': {
|
||||
"ref": "test_invoice_1: Invoice to gritti support service, vat 21",
|
||||
"partner_id": self.res_partner_gritti_mono,
|
||||
"invoice_user_id": invoice_user_id,
|
||||
"invoice_payment_term_id": payment_term_id,
|
||||
"move_type": "out_invoice",
|
||||
"company_id": self.company_ri,
|
||||
"invoice_line_ids": [
|
||||
{'product_id': self.service_iva_21}
|
||||
],
|
||||
},
|
||||
'test_invoice_2': {
|
||||
"ref": "test_invoice_2: Invoice to CMR with vat 21, 27 and 10,5",
|
||||
"partner_id": self.res_partner_cmr,
|
||||
"invoice_user_id": invoice_user_id,
|
||||
"invoice_payment_term_id": payment_term_id,
|
||||
"move_type": "out_invoice",
|
||||
"company_id": self.company_ri,
|
||||
"invoice_line_ids": [
|
||||
{'product_id': self.product_iva_105, 'price_unit': 642.0, 'quantity': 5},
|
||||
{'product_id': self.service_iva_27, 'price_unit': 250.0, 'quantity': 1},
|
||||
{'product_id': self.product_iva_105_perc, 'price_unit': 3245.0, 'quantity': 2}
|
||||
],
|
||||
},
|
||||
'test_invoice_3': {
|
||||
"ref": "test_invoice_3: Invoice to ADHOC with vat cero and 21",
|
||||
"partner_id": self.res_partner_adhoc,
|
||||
"invoice_user_id": invoice_user_id,
|
||||
"invoice_payment_term_id": payment_term_id,
|
||||
"move_type": 'out_invoice',
|
||||
"company_id": self.company_ri,
|
||||
"invoice_line_ids": [
|
||||
{'product_id': self.product_iva_105, 'price_unit': 642.0, 'quantity': 5},
|
||||
{'product_id': self.product_iva_cero, 'price_unit': 200.0, 'quantity': 1}
|
||||
],
|
||||
},
|
||||
'test_invoice_4': {
|
||||
'ref': 'test_invoice_4: Invoice to ADHOC with vat exempt and 21',
|
||||
"partner_id": self.res_partner_adhoc,
|
||||
"invoice_user_id": invoice_user_id,
|
||||
"invoice_payment_term_id": payment_term_id,
|
||||
"move_type": 'out_invoice',
|
||||
"company_id": self.company_ri,
|
||||
"invoice_line_ids": [
|
||||
{'product_id': self.product_iva_105, 'price_unit': 642.0, 'quantity': 5},
|
||||
{'product_id': self.product_iva_exento, 'price_unit': 100.0, 'quantity': 1},
|
||||
],
|
||||
},
|
||||
'test_invoice_5': {
|
||||
'ref': 'test_invoice_5: Invoice to ADHOC with all type of taxes',
|
||||
"partner_id": self.res_partner_adhoc,
|
||||
"invoice_user_id": invoice_user_id,
|
||||
"invoice_payment_term_id": payment_term_id,
|
||||
"move_type": 'out_invoice',
|
||||
"company_id": self.company_ri,
|
||||
"invoice_line_ids": [
|
||||
{'product_id': self.product_iva_105, 'price_unit': 642.0, 'quantity': 5},
|
||||
{'product_id': self.service_iva_27, 'price_unit': 250.0, 'quantity': 1},
|
||||
{'product_id': self.product_iva_105_perc, 'price_unit': 3245.0, 'quantity': 2},
|
||||
{'product_id': self.product_no_gravado, 'price_unit': 50.0, 'quantity': 10},
|
||||
{'product_id': self.product_iva_cero, 'price_unit': 200.0, 'quantity': 1},
|
||||
{'product_id': self.product_iva_exento, 'price_unit': 100.0, 'quantity': 1}
|
||||
],
|
||||
},
|
||||
'test_invoice_6': {
|
||||
'ref': 'test_invoice_6: Invoice to cerro castor, fiscal position changes taxes to exempt',
|
||||
"partner_id": self.res_partner_cerrocastor,
|
||||
"journal_id": self.sale_expo_journal_ri,
|
||||
"invoice_user_id": invoice_user_id,
|
||||
"invoice_payment_term_id": payment_term_id,
|
||||
"move_type": 'out_invoice',
|
||||
"company_id": self.company_ri,
|
||||
"invoice_incoterm_id": incoterm,
|
||||
"invoice_line_ids": [
|
||||
{'product_id': self.product_iva_105, 'price_unit': 642.0, 'quantity': 5},
|
||||
{'product_id': self.service_iva_27, 'price_unit': 250.0, 'quantity': 1},
|
||||
{'product_id': self.product_iva_105_perc, 'price_unit': 3245.0, 'quantity': 2},
|
||||
{'product_id': self.product_no_gravado, 'price_unit': 50.0, 'quantity': 10},
|
||||
{'product_id': self.product_iva_cero, 'price_unit': 200.0, 'quantity': 1},
|
||||
{'product_id': self.product_iva_exento, 'price_unit': 100.0, 'quantity': 1},
|
||||
],
|
||||
},
|
||||
'test_invoice_7': {
|
||||
'ref': 'test_invoice_7: Export invoice to expresso, fiscal position changes tax to exempt (type 4 because it have services)',
|
||||
"partner_id": self.res_partner_expresso,
|
||||
"journal_id": self.sale_expo_journal_ri,
|
||||
"invoice_user_id": invoice_user_id,
|
||||
"invoice_payment_term_id": payment_term_id,
|
||||
"move_type": 'out_invoice',
|
||||
"company_id": self.company_ri,
|
||||
"invoice_incoterm_id": incoterm,
|
||||
"invoice_line_ids": [
|
||||
{'product_id': self.product_iva_105, 'price_unit': 642.0, 'quantity': 5},
|
||||
{'product_id': self.service_iva_27, 'price_unit': 250.0, 'quantity': 1},
|
||||
{'product_id': self.product_iva_105_perc, 'price_unit': 3245.0, 'quantity': 2},
|
||||
{'product_id': self.product_no_gravado, 'price_unit': 50.0, 'quantity': 10},
|
||||
{'product_id': self.product_iva_cero, 'price_unit': 200.0, 'quantity': 1},
|
||||
{'product_id': self.product_iva_exento, 'price_unit': 100.0, 'quantity': 1},
|
||||
],
|
||||
},
|
||||
'test_invoice_8': {
|
||||
'ref': 'test_invoice_8: Invoice to consumidor final',
|
||||
"partner_id": self.partner_cf,
|
||||
"invoice_user_id": invoice_user_id,
|
||||
"invoice_payment_term_id": payment_term_id,
|
||||
"move_type": 'out_invoice',
|
||||
"company_id": self.company_ri,
|
||||
"invoice_line_ids": [
|
||||
{'product_id': self.service_iva_21, 'price_unit': 642.0, 'quantity': 1},
|
||||
],
|
||||
},
|
||||
'test_invoice_10': {
|
||||
'ref': 'test_invoice_10; Invoice to ADHOC in USD and vat 21',
|
||||
"partner_id": self.res_partner_adhoc,
|
||||
"invoice_user_id": invoice_user_id,
|
||||
"invoice_payment_term_id": payment_term_id,
|
||||
"move_type": 'out_invoice',
|
||||
"company_id": self.company_ri,
|
||||
"invoice_line_ids": [
|
||||
{'product_id': self.product_iva_105, 'price_unit': 1000.0, 'quantity': 5},
|
||||
],
|
||||
"currency_id": self.env.ref("base.USD"),
|
||||
},
|
||||
'test_invoice_11': {
|
||||
'ref': 'test_invoice_11: Invoice to ADHOC with many lines in order to prove rounding error, with 4 decimals of precision for the currency and 2 decimals for the product the error apperar',
|
||||
"partner_id": self.res_partner_adhoc,
|
||||
"invoice_user_id": invoice_user_id,
|
||||
"invoice_payment_term_id": payment_term_id,
|
||||
"move_type": 'out_invoice',
|
||||
"company_id": self.company_ri,
|
||||
"invoice_line_ids": [
|
||||
{'product_id': self.service_iva_21, 'price_unit': 1.12, 'quantity': 1, 'name': 'Support Services 1'},
|
||||
{'product_id': self.service_iva_21, 'price_unit': 1.12, 'quantity': 1, 'name': 'Support Services 2'},
|
||||
{'product_id': self.service_iva_21, 'price_unit': 1.12, 'quantity': 1, 'name': 'Support Services 3'},
|
||||
{'product_id': self.service_iva_21, 'price_unit': 1.12, 'quantity': 1, 'name': 'Support Services 4'},
|
||||
],
|
||||
},
|
||||
'test_invoice_12': {
|
||||
'ref': 'test_invoice_12: Invoice to ADHOC with many lines in order to test rounding error, it is required to use a 4 decimal precision in prodct in order to the error occur',
|
||||
"partner_id": self.res_partner_adhoc,
|
||||
"invoice_user_id": invoice_user_id,
|
||||
"invoice_payment_term_id": payment_term_id,
|
||||
"move_type": 'out_invoice',
|
||||
"company_id": self.company_ri,
|
||||
"invoice_line_ids": [
|
||||
{'product_id': self.service_iva_21, 'price_unit': 15.7076, 'quantity': 1, 'name': 'Support Services 1'},
|
||||
{'product_id': self.service_iva_21, 'price_unit': 5.3076, 'quantity': 2, 'name': 'Support Services 2'},
|
||||
{'product_id': self.service_iva_21, 'price_unit': 3.5384, 'quantity': 2, 'name': 'Support Services 3'},
|
||||
{'product_id': self.service_iva_21, 'price_unit': 1.6376, 'quantity': 2, 'name': 'Support Services 4'},
|
||||
],
|
||||
},
|
||||
'test_invoice_13': {
|
||||
'ref': 'test_invoice_13: Invoice to ADHOC with many lines in order to test zero amount invoices y rounding error. it is required to set the product decimal precision to 4 and change 260.59 for 260.60 in order to reproduce the error',
|
||||
"partner_id": self.res_partner_adhoc,
|
||||
"invoice_user_id": invoice_user_id,
|
||||
"invoice_payment_term_id": payment_term_id,
|
||||
"move_type": 'out_invoice',
|
||||
"company_id": self.company_ri,
|
||||
"invoice_line_ids": [
|
||||
{'product_id': self.service_iva_21, 'price_unit': 24.3, 'quantity': 3, 'name': 'Support Services 1'},
|
||||
{'product_id': self.service_iva_21, 'price_unit': 260.59, 'quantity': 1, 'name': 'Support Services 2'},
|
||||
{'product_id': self.service_iva_21, 'price_unit': 48.72, 'quantity': 1, 'name': 'Support Services 3'},
|
||||
{'product_id': self.service_iva_21, 'price_unit': 13.666, 'quantity': 1, 'name': 'Support Services 4'},
|
||||
{'product_id': self.service_iva_21, 'price_unit': 11.329, 'quantity': 2, 'name': 'Support Services 5'},
|
||||
{'product_id': self.service_iva_21, 'price_unit': 68.9408, 'quantity': 1, 'name': 'Support Services 6'},
|
||||
{'product_id': self.service_iva_21, 'price_unit': 4.7881, 'quantity': 2, 'name': 'Support Services 7'},
|
||||
{'product_id': self.service_iva_21, 'price_unit': 12.0625, 'quantity': 2, 'name': 'Support Services 8'},
|
||||
],
|
||||
},
|
||||
'test_invoice_14': {
|
||||
'ref': 'test_invoice_14: Export invoice to expresso, fiscal position changes tax to exempt (type 1 because only products)',
|
||||
"partner_id": self.res_partner_expresso,
|
||||
"journal_id": self.sale_expo_journal_ri,
|
||||
"invoice_user_id": invoice_user_id,
|
||||
"invoice_payment_term_id": payment_term_id,
|
||||
"move_type": 'out_invoice',
|
||||
"company_id": self.company_ri,
|
||||
"invoice_incoterm_id": incoterm,
|
||||
"invoice_line_ids": [
|
||||
{'product_id': self.product_iva_105, 'price_unit': 642.0, 'quantity': 5},
|
||||
],
|
||||
},
|
||||
'test_invoice_15': {
|
||||
'ref': 'test_invoice_15: Export invoice to expresso, fiscal position changes tax to exempt (type 2 because only service)',
|
||||
"partner_id": self.res_partner_expresso,
|
||||
"journal_id": self.sale_expo_journal_ri,
|
||||
"invoice_user_id": invoice_user_id,
|
||||
"invoice_payment_term_id": payment_term_id,
|
||||
"move_type": 'out_invoice',
|
||||
"company_id": self.company_ri,
|
||||
"invoice_incoterm_id": incoterm,
|
||||
"invoice_line_ids": [
|
||||
{'product_id': self.service_iva_27, 'price_unit': 250.0, 'quantity': 1},
|
||||
],
|
||||
},
|
||||
'test_invoice_16': {
|
||||
'ref': 'test_invoice_16: Export invoice to expresso, fiscal position changes tax to exempt (type 1 because it have products only, used to test refund of expo)',
|
||||
"partner_id": self.res_partner_expresso,
|
||||
"journal_id": self.sale_expo_journal_ri,
|
||||
"invoice_user_id": invoice_user_id,
|
||||
"invoice_payment_term_id": payment_term_id,
|
||||
"move_type": 'out_invoice',
|
||||
"company_id": self.company_ri,
|
||||
"invoice_incoterm_id": incoterm,
|
||||
"invoice_line_ids": [
|
||||
{'product_id': self.product_iva_105, 'price_unit': 642.0, 'quantity': 5},
|
||||
],
|
||||
},
|
||||
'test_invoice_17': {
|
||||
'ref': 'test_invoice_17: Invoice to ADHOC with 100%% of discount',
|
||||
"partner_id": self.res_partner_adhoc,
|
||||
"invoice_user_id": invoice_user_id,
|
||||
"invoice_payment_term_id": payment_term_id,
|
||||
"move_type": 'out_invoice',
|
||||
"company_id": self.company_ri,
|
||||
"invoice_line_ids": [
|
||||
{'product_id': self.service_iva_21, 'price_unit': 24.3, 'quantity': 3, 'name': 'Support Services 8', 'discount': 100},
|
||||
],
|
||||
},
|
||||
'test_invoice_18': {
|
||||
'ref': 'test_invoice_18: Invoice to ADHOC with 100%% of discount and with different VAT aliquots',
|
||||
"partner_id": self.res_partner_adhoc,
|
||||
"invoice_user_id": invoice_user_id,
|
||||
"invoice_payment_term_id": payment_term_id,
|
||||
"move_type": 'out_invoice',
|
||||
"company_id": self.company_ri,
|
||||
"invoice_line_ids": [
|
||||
{'product_id': self.service_iva_21, 'price_unit': 24.3, 'quantity': 3, 'name': 'Support Services 8', 'discount': 100},
|
||||
{'product_id': self.service_iva_27, 'price_unit': 250.0, 'quantity': 1, 'discount': 100},
|
||||
{'product_id': self.product_iva_105_perc, 'price_unit': 3245.0, 'quantity': 1},
|
||||
],
|
||||
},
|
||||
'test_invoice_19': {
|
||||
'ref': 'test_invoice_19: Invoice to ADHOC with multiple taxes and perceptions',
|
||||
"partner_id": self.res_partner_adhoc,
|
||||
"invoice_user_id": invoice_user_id,
|
||||
"invoice_payment_term_id": payment_term_id,
|
||||
"move_type": 'out_invoice',
|
||||
"company_id": self.company_ri,
|
||||
"invoice_line_ids": [
|
||||
{'product_id': self.service_iva_21, 'price_unit': 24.3, 'quantity': 3, 'name': 'Support Services 8'},
|
||||
{'product_id': self.service_iva_27, 'price_unit': 250.0, 'quantity': 1},
|
||||
{'product_id': self.product_iva_105_perc, 'price_unit': 3245.0, 'quantity': 1},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
for key, values in invoices_to_create.items():
|
||||
with Form(self.env['account.move'].with_context(default_move_type=values['move_type'])) as invoice_form:
|
||||
invoice_form.ref = values['ref']
|
||||
invoice_form.partner_id = values['partner_id']
|
||||
invoice_form.invoice_user_id = values['invoice_user_id']
|
||||
invoice_form.invoice_payment_term_id = values['invoice_payment_term_id']
|
||||
if not use_current_date:
|
||||
invoice_form.invoice_date = values['invoice_date']
|
||||
if values.get('invoice_incoterm_id'):
|
||||
invoice_form.invoice_incoterm_id = values['invoice_incoterm_id']
|
||||
for line in values['invoice_line_ids']:
|
||||
with invoice_form.invoice_line_ids.new() as line_form:
|
||||
line_form.product_id = line.get('product_id')
|
||||
line_form.price_unit = line.get('price_unit')
|
||||
line_form.quantity = line.get('quantity')
|
||||
line_form.name = 'xxxx'
|
||||
line_form.account_id = self.company_data['default_account_revenue']
|
||||
invoice = invoice_form.save()
|
||||
self.demo_invoices[key] = invoice
|
||||
|
||||
# Helpers
|
||||
|
||||
@classmethod
|
||||
def _get_afip_pos_system_real_name(cls):
|
||||
return {'PREPRINTED': 'II_IM'}
|
||||
|
||||
def _create_journal(self, afip_ws, data=None):
|
||||
""" Create a journal of a given AFIP ws type.
|
||||
If there is a problem because we are using a AFIP certificate that is already been in use then change the certificate and try again """
|
||||
data = data or {}
|
||||
afip_ws = afip_ws.upper()
|
||||
pos_number = str(random.randint(0, 99999))
|
||||
if 'l10n_ar_afip_pos_number' in data:
|
||||
pos_number = data.get('l10n_ar_afip_pos_number')
|
||||
values = {'name': '%s %s' % (afip_ws.replace('WS', ''), pos_number),
|
||||
'type': 'sale',
|
||||
'code': afip_ws,
|
||||
'l10n_ar_afip_pos_system': self._get_afip_pos_system_real_name().get(afip_ws),
|
||||
'l10n_ar_afip_pos_number': pos_number,
|
||||
'l10n_latam_use_documents': True,
|
||||
'company_id': self.env.company.id,
|
||||
'l10n_ar_afip_pos_partner_id': self.partner_ri.id,
|
||||
'sequence': 1}
|
||||
values.update(data)
|
||||
|
||||
journal = self.env['account.journal'].create(values)
|
||||
_logger.info('Created journal %s for company %s' % (journal.name, self.env.company.name))
|
||||
return journal
|
||||
|
||||
def _create_invoice(self, data=None, invoice_type='out_invoice'):
|
||||
data = data or {}
|
||||
with Form(self.env['account.move'].with_context(default_move_type=invoice_type)) as invoice_form:
|
||||
invoice_form.partner_id = data.get('partner', self.partner)
|
||||
if 'in_' not in invoice_type:
|
||||
invoice_form.journal_id = data.get('journal', self.journal)
|
||||
|
||||
if data.get('document_type'):
|
||||
invoice_form.l10n_latam_document_type_id = data.get('document_type')
|
||||
if data.get('document_number'):
|
||||
invoice_form.l10n_latam_document_number = data.get('document_number')
|
||||
if data.get('incoterm'):
|
||||
invoice_form.invoice_incoterm_id = data.get('incoterm')
|
||||
if data.get('currency'):
|
||||
invoice_form.currency_id = data.get('currency')
|
||||
for line in data.get('lines', [{}]):
|
||||
with invoice_form.invoice_line_ids.new() as invoice_line_form:
|
||||
if line.get('display_type'):
|
||||
invoice_line_form.display_type = line.get('display_type')
|
||||
invoice_line_form.name = line.get('name', 'not invoice line')
|
||||
else:
|
||||
invoice_line_form.product_id = line.get('product', self.product_iva_21)
|
||||
invoice_line_form.quantity = line.get('quantity', 1)
|
||||
invoice_line_form.price_unit = line.get('price_unit', 100)
|
||||
invoice_form.invoice_date = invoice_form.date
|
||||
invoice = invoice_form.save()
|
||||
return invoice
|
||||
|
||||
def _create_invoice_product(self, data=None):
|
||||
data = data or {}
|
||||
return self._create_invoice(data)
|
||||
|
||||
def _create_invoice_service(self, data=None):
|
||||
data = data or {}
|
||||
newlines = []
|
||||
for line in data.get('lines', [{}]):
|
||||
line.update({'product': self.service_iva_27})
|
||||
newlines.append(line)
|
||||
data.update({'lines': newlines})
|
||||
return self._create_invoice(data)
|
||||
|
||||
def _create_invoice_product_service(self, data=None):
|
||||
data = data or {}
|
||||
newlines = []
|
||||
for line in data.get('lines', [{}]):
|
||||
line.update({'product': self.product_iva_21})
|
||||
newlines.append(line)
|
||||
data.update({'lines': newlines + [{'product': self.service_iva_27}]})
|
||||
return self._create_invoice(data)
|
||||
|
||||
def _create_credit_note(self, invoice, data=None):
|
||||
data = data or {}
|
||||
refund_wizard = self.env['account.move.reversal'].with_context({'active_ids': [invoice.id], 'active_model': 'account.move'}).create({
|
||||
'reason': data.get('reason', 'Mercadería defectuosa'),
|
||||
'refund_method': data.get('refund_method', 'refund')})
|
||||
|
||||
forced_document_type = data.get('document_type')
|
||||
if forced_document_type:
|
||||
refund_wizard.l10n_latam_document_type_id = forced_document_type.id
|
||||
|
||||
res = refund_wizard.reverse_moves()
|
||||
refund = self.env['account.move'].browse(res['res_id'])
|
||||
return refund
|
||||
|
||||
def _create_debit_note(self, invoice, data=None):
|
||||
data = data or {}
|
||||
debit_note_wizard = self.env['account.debit.note'].with_context(
|
||||
{'active_ids': [invoice.id], 'active_model': 'account.move', 'default_copy_lines': True}).create({
|
||||
'reason': data.get('reason', 'Mercadería defectuosa')})
|
||||
res = debit_note_wizard.create_debit()
|
||||
debit_note = self.env['account.move'].browse(res['res_id'])
|
||||
return debit_note
|
||||
|
||||
def _search_tax(self, tax_type):
|
||||
res = self.env['account.tax'].with_context(active_test=False).search([
|
||||
('type_tax_use', '=', 'sale'),
|
||||
('company_id', '=', self.env.company.id),
|
||||
('tax_group_id', '=', self.env.ref('l10n_ar.tax_group_' + tax_type).id)], limit=1)
|
||||
self.assertTrue(res, '%s Tax was not found' % (tax_type))
|
||||
return res
|
||||
|
||||
def _search_fp(self, name):
|
||||
return self.env['account.fiscal.position'].search([('company_id', '=', self.env.company.id), ('name', '=', name)])
|
||||
|
||||
def _post(self, invoice):
|
||||
invoice.action_post()
|
||||
self.assertEqual(invoice.state, 'posted')
|
||||
|
||||
def _prepare_multicurrency_values(self):
|
||||
# Enable multi currency
|
||||
self.env.user.write({'groups_id': [(4, self.env.ref('base.group_multi_currency').id)]})
|
||||
# Set ARS as main currency
|
||||
self._set_today_rate(self.env.ref('base.ARS'), 1.0)
|
||||
# Set Rates for USD currency
|
||||
self._set_today_rate(self.env.ref('base.USD'), 1.0 / 162.013)
|
||||
|
||||
def _set_today_rate(self, currency, value):
|
||||
rate_obj = self.env['res.currency.rate']
|
||||
rate = rate_obj.search([('currency_id', '=', currency.id), ('name', '=', fields.Date.to_string(fields.Date.today())),
|
||||
('company_id', '=', self.env.company.id)])
|
||||
if rate:
|
||||
rate.rate = value
|
||||
else:
|
||||
rate_obj.create({'company_id': self.env.company.id, 'currency_id': currency.id, 'rate': value})
|
||||
_logger.log(25, 'Using %s rate %s' % (currency.name, currency.rate))
|
||||
@@ -0,0 +1,99 @@
|
||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||
from . import common
|
||||
from flectra.tests import tagged
|
||||
|
||||
|
||||
@tagged('-at_install', 'post_install')
|
||||
class TestManual(common.TestAr):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestManual, cls).setUpClass()
|
||||
cls.journal = cls._create_journal(cls, 'preprinted')
|
||||
cls.partner = cls.res_partner_adhoc
|
||||
cls._create_test_invoices_like_demo(cls)
|
||||
|
||||
def test_01_create_invoice(self):
|
||||
""" Create and validate an invoice for a Responsable Inscripto
|
||||
|
||||
* Proper set the current user company
|
||||
* Properly set the tax amount of the product / partner
|
||||
* Proper fiscal position (this case not fiscal position is selected)
|
||||
"""
|
||||
invoice = self._create_invoice()
|
||||
self.assertEqual(invoice.company_id, self.company_ri, 'created with wrong company')
|
||||
self.assertEqual(invoice.amount_tax, 21, 'invoice taxes are not properly set')
|
||||
self.assertEqual(invoice.amount_total, 121.0, 'invoice taxes has not been applied to the total')
|
||||
self.assertEqual(invoice.l10n_latam_document_type_id, self.document_type['invoice_a'], 'selected document type should be Factura A')
|
||||
self._post(invoice)
|
||||
self.assertEqual(invoice.state, 'posted', 'invoice has not been validate in Flectra')
|
||||
self.assertEqual(invoice.name, 'FA-A %05d-00000002' % self.journal.l10n_ar_afip_pos_number, 'Invoice number is wrong')
|
||||
|
||||
def test_02_fiscal_position(self):
|
||||
# ADHOC SA > IVA Responsable Inscripto > Without Fiscal Positon
|
||||
invoice = self._create_invoice({'partner': self.partner})
|
||||
self.assertFalse(invoice.fiscal_position_id, 'Fiscal position should be set to empty')
|
||||
|
||||
# Consumidor Final > IVA Responsable Inscripto > Without Fiscal Positon
|
||||
invoice = self._create_invoice({'partner': self.partner_cf})
|
||||
self.assertFalse(invoice.fiscal_position_id, 'Fiscal position should be set to empty')
|
||||
|
||||
# Cerro Castor > IVA Liberado – Ley Nº 19.640 > Compras / Ventas Zona Franca > IVA Exento
|
||||
invoice = self._create_invoice({'partner': self.res_partner_cerrocastor})
|
||||
self.assertEqual(invoice.fiscal_position_id, self._search_fp('Compras / Ventas Zona Franca'))
|
||||
|
||||
# Expresso > Cliente / Proveedor del Exterior > > IVA Exento
|
||||
invoice = self._create_invoice({'partner': self.res_partner_expresso})
|
||||
self.assertEqual(invoice.fiscal_position_id, self._search_fp('Compras / Ventas al exterior'))
|
||||
|
||||
def test_03_corner_cases(self):
|
||||
""" Mono partner of type Service and VAT 21 """
|
||||
self._post(self.demo_invoices['test_invoice_1'])
|
||||
|
||||
def test_04_corner_cases(self):
|
||||
""" Exento partner with multiple VAT types 21, 27 and 10,5' """
|
||||
self._post(self.demo_invoices['test_invoice_2'])
|
||||
|
||||
def test_05_corner_cases(self):
|
||||
""" RI partner with VAT 0 and 21 """
|
||||
self._post(self.demo_invoices['test_invoice_3'])
|
||||
|
||||
def test_06_corner_cases(self):
|
||||
""" RI partner with VAT exempt and 21 """
|
||||
self._post(self.demo_invoices['test_invoice_4'])
|
||||
|
||||
def test_07_corner_cases(self):
|
||||
""" RI partner with all type of taxes """
|
||||
self._post(self.demo_invoices['test_invoice_5'])
|
||||
|
||||
def test_08_corner_cases(self):
|
||||
""" Consumidor Final """
|
||||
self._post(self.demo_invoices['test_invoice_8'])
|
||||
|
||||
def test_09_corner_cases(self):
|
||||
""" RI partner with many lines in order to prove rounding error, with 4 decimals of precision for the
|
||||
currency and 2 decimals for the product the error appear """
|
||||
self._post(self.demo_invoices['test_invoice_11'])
|
||||
|
||||
def test_10_corner_cases(self):
|
||||
""" RI partner with many lines in order to test rounding error, it is required to use a 4 decimal precision
|
||||
in product in order to the error occur """
|
||||
self._post(self.demo_invoices['test_invoice_12'])
|
||||
|
||||
def test_11_corner_cases(self):
|
||||
""" RI partner with many lines in order to test zero amount invoices y rounding error. it is required to
|
||||
set the product decimal precision to 4 and change 260,59 for 260.60 in order to reproduce the error """
|
||||
self._post(self.demo_invoices['test_invoice_13'])
|
||||
|
||||
def test_12_corner_cases(self):
|
||||
""" RI partner with 100%% of discount """
|
||||
self._post(self.demo_invoices['test_invoice_17'])
|
||||
|
||||
def test_13_corner_cases(self):
|
||||
""" RI partner with 100%% of discount and with different VAT aliquots """
|
||||
self._post(self.demo_invoices['test_invoice_18'])
|
||||
|
||||
def test_14_corner_cases(self):
|
||||
""" Responsable Inscripto" in USD and VAT 21 """
|
||||
self._prepare_multicurrency_values()
|
||||
self._post(self.demo_invoices['test_invoice_10'])
|
||||
@@ -4,13 +4,13 @@
|
||||
|
||||
<!-- Account Tax Group -->
|
||||
<record id="tax_group_0" model="account.tax.group">
|
||||
<field name="name">Thuế GTGT 0%</field>
|
||||
<field name="name">VAT 0%</field>
|
||||
</record>
|
||||
<record id="tax_group_5" model="account.tax.group">
|
||||
<field name="name">Thuế GTGT 5%</field>
|
||||
<field name="name">VAT 5%</field>
|
||||
</record>
|
||||
<record id="tax_group_10" model="account.tax.group">
|
||||
<field name="name">Thuế GTGT 10%</field>
|
||||
<field name="name">VAT 10%</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
<!-- for purchase -->
|
||||
<record id="tax_purchase_vat10" model="account.tax.template">
|
||||
<field name="chart_template_id" ref="vn_template"/>
|
||||
<field name="name">Thuế GTGT được khấu trừ 10%</field>
|
||||
<field name="description">Thuế GTGT được khấu trừ 10%</field>
|
||||
<field name="name">Deductible VAT 10%</field>
|
||||
<field name="description">Deductible VAT 10%</field>
|
||||
<field name="amount" eval="10"/>
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="type_tax_use">purchase</field>
|
||||
@@ -39,8 +39,8 @@
|
||||
</record>
|
||||
<record id="tax_purchase_vat5" model="account.tax.template">
|
||||
<field name="chart_template_id" ref="vn_template"/>
|
||||
<field name="name">Thuế GTGT được khấu trừ 5%</field>
|
||||
<field name="description">Thuế GTGT được khấu trừ 5%</field>
|
||||
<field name="name">Deductible VAT 5%</field>
|
||||
<field name="description">Deductible VAT 5%</field>
|
||||
<field name="amount" eval="5"/>
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="type_tax_use">purchase</field>
|
||||
@@ -74,8 +74,8 @@
|
||||
</record>
|
||||
<record id="tax_purchase_vat0" model="account.tax.template">
|
||||
<field name="chart_template_id" ref="vn_template"/>
|
||||
<field name="name">Thuế GTGT được khấu trừ 0%</field>
|
||||
<field name="description">Thuế GTGT được khấu trừ 0%</field>
|
||||
<field name="name">Deductible VAT 0%</field>
|
||||
<field name="description">Deductible VAT 0%</field>
|
||||
<field name="amount" eval="0"/>
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="type_tax_use">purchase</field>
|
||||
@@ -107,8 +107,8 @@
|
||||
<!-- for sale -->
|
||||
<record id="tax_sale_vat10" model="account.tax.template">
|
||||
<field name="chart_template_id" ref="vn_template"/>
|
||||
<field name="name">Thuế GTGT phải nộp 10%</field>
|
||||
<field name="description">Thuế GTGT phải nộp 10%</field>
|
||||
<field name="name">Value Added Tax (VAT) 10%</field>
|
||||
<field name="description">Value Added Tax (VAT) 10%</field>
|
||||
<field name="amount" eval="10"/>
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="type_tax_use">sale</field>
|
||||
@@ -142,8 +142,8 @@
|
||||
</record>
|
||||
<record id="tax_sale_vat5" model="account.tax.template">
|
||||
<field name="chart_template_id" ref="vn_template"/>
|
||||
<field name="name">Thuế GTGT phải nộp 5%</field>
|
||||
<field name="description">Thuế GTGT phải nộp 5%</field>
|
||||
<field name="name">Value Added Tax (VAT) 5%</field>
|
||||
<field name="description">Value Added Tax (VAT) 5%</field>
|
||||
<field name="amount" eval="5"/>
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="type_tax_use">sale</field>
|
||||
@@ -177,8 +177,8 @@
|
||||
</record>
|
||||
<record id="tax_sale_vat0" model="account.tax.template">
|
||||
<field name="chart_template_id" ref="vn_template"/>
|
||||
<field name="name">Thuế GTGT phải nộp 0%</field>
|
||||
<field name="description">Thuế GTGT phải nộp 0%</field>
|
||||
<field name="name">Value Added Tax (VAT) 0%</field>
|
||||
<field name="description">Value Added Tax (VAT) 0%</field>
|
||||
<field name="amount" eval="0"/>
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="type_tax_use">sale</field>
|
||||
|
||||
@@ -7,136 +7,136 @@
|
||||
</record>
|
||||
|
||||
<record id="account_tax_report_line_01_vn" model="account.tax.report.line">
|
||||
<field name="name">HHDV mua vào</field>
|
||||
<field name="name">Purchase of Goods and Services</field>
|
||||
<field name="report_id" ref="tax_report"/>
|
||||
<field name="sequence" eval="1"/>
|
||||
</record>
|
||||
|
||||
<record id="account_tax_report_line_01_01_vn" model="account.tax.report.line">
|
||||
<field name="name">Thuế GTGT HHDV mua vào</field>
|
||||
<field name="name">VAT on purchase of goods and services</field>
|
||||
<field name="report_id" ref="tax_report"/>
|
||||
<field name="sequence" eval="1"/>
|
||||
<field name="parent_id" ref="account_tax_report_line_01_vn"/>
|
||||
</record>
|
||||
|
||||
<record id="account_tax_report_line_01_01_01_vn" model="account.tax.report.line">
|
||||
<field name="name">Thuế GTGT HHDV mua vào chịu thuế 0%</field>
|
||||
<field name="tag_name">Thuế GTGT HHDV mua vào chịu thuế 0%</field>
|
||||
<field name="name">VAT on purchase of goods and services 0%</field>
|
||||
<field name="tag_name">VAT on purchase of goods and services 0%</field>
|
||||
<field name="report_id" ref="tax_report"/>
|
||||
<field name="sequence" eval="1"/>
|
||||
<field name="parent_id" ref="account_tax_report_line_01_01_vn"/>
|
||||
</record>
|
||||
|
||||
<record id="account_tax_report_line_02_01_01_vn" model="account.tax.report.line">
|
||||
<field name="name">Thuế GTGT HHDV mua vào chịu thuế 5%</field>
|
||||
<field name="tag_name">Thuế GTGT HHDV mua vào chịu thuế 5%</field>
|
||||
<field name="name">VAT on purchase of goods and services 5%</field>
|
||||
<field name="tag_name">VAT on purchase of goods and services 5%</field>
|
||||
<field name="report_id" ref="tax_report"/>
|
||||
<field name="sequence" eval="2"/>
|
||||
<field name="parent_id" ref="account_tax_report_line_01_01_vn"/>
|
||||
</record>
|
||||
|
||||
<record id="account_tax_report_line_03_01_01_vn" model="account.tax.report.line">
|
||||
<field name="name">Thuế GTGT HHDV mua vào chịu thuế 10%</field>
|
||||
<field name="tag_name">Thuế GTGT HHDV mua vào chịu thuế 10%</field>
|
||||
<field name="name">VAT on purchase of goods and services 10%</field>
|
||||
<field name="tag_name">VAT on purchase of goods and services 10%</field>
|
||||
<field name="report_id" ref="tax_report"/>
|
||||
<field name="sequence" eval="3"/>
|
||||
<field name="parent_id" ref="account_tax_report_line_01_01_vn"/>
|
||||
</record>
|
||||
|
||||
<record id="account_tax_report_line_02_01_vn" model="account.tax.report.line">
|
||||
<field name="name">Giá trị HHDV mua vào</field>
|
||||
<field name="name">Untaxed Purchase of Goods and Services</field>
|
||||
<field name="report_id" ref="tax_report"/>
|
||||
<field name="sequence" eval="2"/>
|
||||
<field name="parent_id" ref="account_tax_report_line_01_vn"/>
|
||||
</record>
|
||||
|
||||
<record id="account_tax_report_line_01_02_01_vn" model="account.tax.report.line">
|
||||
<field name="name">Giá trị HHDV mua vào chịu thuế 0%</field>
|
||||
<field name="tag_name">Giá trị HHDV mua vào chịu thuế 0%</field>
|
||||
<field name="name">Untaxed Purchase of Goods and Services taxed 0%</field>
|
||||
<field name="tag_name">Untaxed Purchase of Goods and Services taxed 0%</field>
|
||||
<field name="report_id" ref="tax_report"/>
|
||||
<field name="sequence" eval="1"/>
|
||||
<field name="parent_id" ref="account_tax_report_line_02_01_vn"/>
|
||||
</record>
|
||||
|
||||
<record id="account_tax_report_line_02_02_01_vn" model="account.tax.report.line">
|
||||
<field name="name">Giá trị HHDV mua vào chịu thuế 5%</field>
|
||||
<field name="tag_name">Giá trị HHDV mua vào chịu thuế 5%</field>
|
||||
<field name="name">Untaxed Purchase of Goods and Services taxed 5%</field>
|
||||
<field name="tag_name">Untaxed Purchase of Goods and Services taxed 5%</field>
|
||||
<field name="report_id" ref="tax_report"/>
|
||||
<field name="sequence" eval="2"/>
|
||||
<field name="parent_id" ref="account_tax_report_line_02_01_vn"/>
|
||||
</record>
|
||||
|
||||
<record id="account_tax_report_line_03_02_01_vn" model="account.tax.report.line">
|
||||
<field name="name">Giá trị HHDV mua vào chịu thuế 10%</field>
|
||||
<field name="tag_name">Giá trị HHDV mua vào chịu thuế 10%</field>
|
||||
<field name="name">Untaxed Purchase of Goods and Services taxed 10%</field>
|
||||
<field name="tag_name">Untaxed Purchase of Goods and Services taxed 10%</field>
|
||||
<field name="report_id" ref="tax_report"/>
|
||||
<field name="sequence" eval="3"/>
|
||||
<field name="parent_id" ref="account_tax_report_line_02_01_vn"/>
|
||||
</record>
|
||||
|
||||
<record id="account_tax_report_line_02_vn" model="account.tax.report.line">
|
||||
<field name="name">HHDV bán ra</field>
|
||||
<field name="name">Sales of Goods and Services</field>
|
||||
<field name="report_id" ref="tax_report"/>
|
||||
<field name="sequence" eval="2"/>
|
||||
</record>
|
||||
|
||||
<record id="account_tax_report_line_01_02_vn" model="account.tax.report.line">
|
||||
<field name="name">Thuế GTGT HHDV bán ra</field>
|
||||
<field name="name">VAT on sales of goods and services</field>
|
||||
<field name="report_id" ref="tax_report"/>
|
||||
<field name="sequence" eval="1"/>
|
||||
<field name="parent_id" ref="account_tax_report_line_02_vn"/>
|
||||
</record>
|
||||
|
||||
<record id="account_tax_report_line_01_01_02_vn" model="account.tax.report.line">
|
||||
<field name="name">Thuế GTGT HHDV bán ra chịu thuế 0%</field>
|
||||
<field name="tag_name">Thuế GTGT HHDV bán ra chịu thuế 0%</field>
|
||||
<field name="name">VAT on sales of goods and services 0%</field>
|
||||
<field name="tag_name">VAT on sales of goods and services 0%</field>
|
||||
<field name="report_id" ref="tax_report"/>
|
||||
<field name="sequence" eval="1"/>
|
||||
<field name="parent_id" ref="account_tax_report_line_01_02_vn"/>
|
||||
</record>
|
||||
|
||||
<record id="account_tax_report_line_02_01_02_vn" model="account.tax.report.line">
|
||||
<field name="name">Thuế GTGT HHDV bán ra chịu thuế 5%</field>
|
||||
<field name="tag_name">Thuế GTGT HHDV bán ra chịu thuế 5%</field>
|
||||
<field name="name">VAT on sales of goods and services 5%</field>
|
||||
<field name="tag_name">VAT on sales of goods and services 5%</field>
|
||||
<field name="report_id" ref="tax_report"/>
|
||||
<field name="sequence" eval="2"/>
|
||||
<field name="parent_id" ref="account_tax_report_line_01_02_vn"/>
|
||||
</record>
|
||||
|
||||
<record id="account_tax_report_line_03_01_02_vn" model="account.tax.report.line">
|
||||
<field name="name">Thuế GTGT HHDV bán ra chịu thuế 10%</field>
|
||||
<field name="tag_name">Thuế GTGT HHDV bán ra chịu thuế 10%</field>
|
||||
<field name="name">VAT on sales of goods and services 10%</field>
|
||||
<field name="tag_name">VAT on sales of goods and services 10%</field>
|
||||
<field name="report_id" ref="tax_report"/>
|
||||
<field name="sequence" eval="3"/>
|
||||
<field name="parent_id" ref="account_tax_report_line_01_02_vn"/>
|
||||
</record>
|
||||
|
||||
<record id="account_tax_report_line_02_02_vn" model="account.tax.report.line">
|
||||
<field name="name">Giá trị HHDV bán ra</field>
|
||||
<field name="name">Untaxed Sales of Goods and Services</field>
|
||||
<field name="report_id" ref="tax_report"/>
|
||||
<field name="sequence" eval="2"/>
|
||||
<field name="parent_id" ref="account_tax_report_line_02_vn"/>
|
||||
</record>
|
||||
|
||||
<record id="account_tax_report_line_01_02_02_vn" model="account.tax.report.line">
|
||||
<field name="name">Giá trị HHDV bán ra chịu thuế 0%</field>
|
||||
<field name="tag_name">Giá trị HHDV bán ra chịu thuế 0%</field>
|
||||
<field name="name">Untaxed sales of goods and services taxed 0%</field>
|
||||
<field name="tag_name">Untaxed sales of goods and services taxed 0%</field>
|
||||
<field name="report_id" ref="tax_report"/>
|
||||
<field name="sequence" eval="1"/>
|
||||
<field name="parent_id" ref="account_tax_report_line_02_02_vn"/>
|
||||
</record>
|
||||
|
||||
<record id="account_tax_report_line_02_02_02_vn" model="account.tax.report.line">
|
||||
<field name="name">Giá trị HHDV bán ra chịu thuế 5%</field>
|
||||
<field name="tag_name">Giá trị HHDV bán ra chịu thuế 5%</field>
|
||||
<field name="name">Untaxed sales of goods and services taxed 5%</field>
|
||||
<field name="tag_name">Untaxed sales of goods and services taxed 5%</field>
|
||||
<field name="report_id" ref="tax_report"/>
|
||||
<field name="sequence" eval="2"/>
|
||||
<field name="parent_id" ref="account_tax_report_line_02_02_vn"/>
|
||||
</record>
|
||||
|
||||
<record id="account_tax_report_line_03_02_02_vn" model="account.tax.report.line">
|
||||
<field name="name">Giá trị HHDV bán ra chịu thuế 10%</field>
|
||||
<field name="tag_name">Giá trị HHDV bán ra chịu thuế 10%</field>
|
||||
<field name="name">Untaxed sales of goods and services taxed 10%</field>
|
||||
<field name="tag_name">Untaxed sales of goods and services taxed 10%</field>
|
||||
<field name="report_id" ref="tax_report"/>
|
||||
<field name="sequence" eval="3"/>
|
||||
<field name="parent_id" ref="account_tax_report_line_02_02_vn"/>
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
+376
-179
File diff suppressed because it is too large.
Load diff
@@ -395,7 +395,7 @@ class StockMove(models.Model):
|
||||
# Then, we collect every relevant moves related to a specific component
|
||||
# to know how many are considered delivered.
|
||||
uom_qty_per_kit = bom_line_data['qty'] / bom_line_data['original_qty']
|
||||
qty_per_kit = bom_line.product_uom_id._compute_quantity(uom_qty_per_kit, bom_line.product_id.uom_id)
|
||||
qty_per_kit = bom_line.product_uom_id._compute_quantity(uom_qty_per_kit, bom_line.product_id.uom_id, round=False)
|
||||
if not qty_per_kit:
|
||||
continue
|
||||
incoming_moves = bom_line_moves.filtered(filters['incoming_moves'])
|
||||
|
||||
@@ -135,6 +135,8 @@ class MrpProduction(models.Model):
|
||||
return False
|
||||
if not all(line.lot_id for line in mo.move_raw_ids.filtered(lambda sm: sm.has_tracking != 'none').move_line_ids):
|
||||
return False
|
||||
if mo.product_id.tracking != 'none' and not mo.lot_producing_id:
|
||||
return False
|
||||
return True
|
||||
|
||||
return self.filtered(filter_in)
|
||||
@@ -735,3 +735,75 @@ class TestSubcontractingTracking(TransactionCase):
|
||||
self.assertEqual(avail_qty_comp1, -todo_nb)
|
||||
self.assertEqual(avail_qty_comp2, -todo_nb)
|
||||
self.assertEqual(avail_qty_finished, todo_nb)
|
||||
|
||||
def test_flow_tracked_backorder02(self):
|
||||
""" Both component and finished product are tracked by lot. """
|
||||
todo_nb = 4
|
||||
resupply_sub_on_order_route = self.env['stock.location.route'].search([('name', '=', 'Resupply Subcontractor on Order')])
|
||||
finished_product, component = self.env['product.product'].create([{
|
||||
'name': 'SuperProduct',
|
||||
'type': 'product',
|
||||
'tracking': 'lot',
|
||||
}, {
|
||||
'name': 'Component',
|
||||
'type': 'product',
|
||||
'tracking': 'lot',
|
||||
'route_ids': [(4, resupply_sub_on_order_route.id)],
|
||||
}])
|
||||
|
||||
bom_form = Form(self.env['mrp.bom'])
|
||||
bom_form.type = 'subcontract'
|
||||
bom_form.subcontractor_ids.add(self.subcontractor_partner1)
|
||||
bom_form.product_tmpl_id = finished_product.product_tmpl_id
|
||||
with bom_form.bom_line_ids.new() as bom_line:
|
||||
bom_line.product_id = component
|
||||
bom_line.product_qty = 1
|
||||
bom = bom_form.save()
|
||||
|
||||
finished_lot, component_lot = self.env['stock.production.lot'].create([{
|
||||
'name': 'lot_%s' % product.name,
|
||||
'product_id': product.id,
|
||||
'company_id': self.env.company.id,
|
||||
} for product in [finished_product, component]])
|
||||
|
||||
self.env['stock.quant']._update_available_quantity(component, self.env.ref('stock.stock_location_stock'), todo_nb, lot_id=component_lot)
|
||||
|
||||
# Create a receipt picking from the subcontractor
|
||||
picking_form = Form(self.env['stock.picking'])
|
||||
picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
|
||||
picking_form.partner_id = self.subcontractor_partner1
|
||||
with picking_form.move_ids_without_package.new() as move:
|
||||
move.product_id = finished_product
|
||||
move.product_uom_qty = todo_nb
|
||||
picking_receipt = picking_form.save()
|
||||
picking_receipt.action_confirm()
|
||||
|
||||
mo = self.env['mrp.production'].search([('bom_id', '=', bom.id)])
|
||||
|
||||
# Process the delivery of the components
|
||||
compo_picking = mo.picking_ids
|
||||
compo_picking.action_assign()
|
||||
wizard_data = compo_picking.button_validate()
|
||||
wizard = Form(self.env[wizard_data['res_model']].with_context(wizard_data['context'])).save()
|
||||
wizard.process()
|
||||
|
||||
for qty in [3, 1]:
|
||||
# Record the receiption of <qty> finished products
|
||||
picking_receipt = self.env['stock.picking'].search([('partner_id', '=', self.subcontractor_partner1.id), ('state', '!=', 'done')])
|
||||
action = picking_receipt.action_record_components()
|
||||
mo = self.env['mrp.production'].browse(action['res_id'])
|
||||
mo_form = Form(mo.with_context(**action['context']), view=action['view_id'])
|
||||
mo_form.qty_producing = qty
|
||||
mo_form.lot_producing_id = finished_lot
|
||||
with mo_form.move_line_raw_ids.edit(0) as ml:
|
||||
ml.lot_id = component_lot
|
||||
mo = mo_form.save()
|
||||
mo.subcontracting_record_component()
|
||||
|
||||
# Validate the picking and create a backorder
|
||||
wizard_data = picking_receipt.button_validate()
|
||||
if qty == 3:
|
||||
wizard = Form(self.env[wizard_data['res_model']].with_context(wizard_data['context'])).save()
|
||||
wizard.process()
|
||||
|
||||
self.assertEqual(picking_receipt.state, 'done')
|
||||
@@ -1,5 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from random import randint
|
||||
from datetime import datetime
|
||||
|
||||
from flectra import fields, tools
|
||||
from flectra.addons.stock_account.tests.test_anglo_saxon_valuation_reconciliation_common import ValuationReconciliationTestCommon
|
||||
@@ -272,6 +273,7 @@ class TestPoSCommon(ValuationReconciliationTestCommon):
|
||||
cls.env['res.currency.rate'].create({
|
||||
'rate': 0.5,
|
||||
'currency_id': cls.other_currency.id,
|
||||
'name': datetime.today().date(),
|
||||
})
|
||||
other_cash_journal = cls.env['account.journal'].create({
|
||||
'name': 'Cash Other',
|
||||
|
||||
@@ -40,8 +40,8 @@
|
||||
<attribute name="invisible">0</attribute>
|
||||
</xpath>
|
||||
<group name="purchase" position="before">
|
||||
<field name="seller_ids" context="{'default_product_tmpl_id':context.get('product_tmpl_id',active_id), 'product_template_invisible_variant': True, 'tree_view_ref':'purchase.product_supplierinfo_tree_view2'}" nolabel="1" attrs="{'invisible': [('product_variant_count','>',1)]}"/>
|
||||
<field name="variant_seller_ids" context="{'default_product_tmpl_id': context.get('product_tmpl_id', active_id), 'tree_view_ref':'purchase.product_supplierinfo_tree_view2'}" nolabel="1" attrs="{'invisible': [('product_variant_count','<=',1)]}"/>
|
||||
<field name="seller_ids" context="{'default_product_tmpl_id':context.get('product_tmpl_id',active_id), 'product_template_invisible_variant': True, 'tree_view_ref':'purchase.product_supplierinfo_tree_view2'}" nolabel="1" attrs="{'invisible': [('product_variant_count','>',1)], 'readonly': [('product_variant_count','>',1)]}"/>
|
||||
<field name="variant_seller_ids" context="{'default_product_tmpl_id': context.get('product_tmpl_id', active_id), 'tree_view_ref':'purchase.product_supplierinfo_tree_view2'}" nolabel="1" attrs="{'invisible': [('product_variant_count','<=',1)], 'readonly': [('product_variant_count','<=',1)]}"/>
|
||||
</group>
|
||||
<group name="bill" position="attributes">
|
||||
<attribute name="groups">purchase.group_purchase_manager</attribute>
|
||||
|
||||
@@ -296,3 +296,50 @@ class TestSaleMrpKitBom(TransactionCase):
|
||||
purchase_price = line.product_id.with_company(line.company_id)._compute_average_price(0, line.product_uom_qty, line.move_ids)
|
||||
self.assertEqual(purchase_price, 92, "The purchase price must be the total cost of the components multiplied by their unit of measure")
|
||||
|
||||
def test_qty_delivered_with_bom(self):
|
||||
"""Check the quantity delivered, when a bom line has a non integer quantity"""
|
||||
|
||||
self.env['ir.model.data'].xmlid_to_object('product.decimal_product_uom').digits = 5
|
||||
|
||||
self.kit = self._create_product('Kit', 'product', 0.00)
|
||||
self.comp = self._create_product('Component', 'product', 0.00)
|
||||
|
||||
# Create BoM for Kit
|
||||
bom_product_form = Form(self.env['mrp.bom'])
|
||||
bom_product_form.product_id = self.kit
|
||||
bom_product_form.product_tmpl_id = self.kit.product_tmpl_id
|
||||
bom_product_form.product_qty = 1.0
|
||||
bom_product_form.type = 'phantom'
|
||||
with bom_product_form.bom_line_ids.new() as bom_line:
|
||||
bom_line.product_id = self.comp
|
||||
bom_line.product_qty = 0.08600
|
||||
self.bom = bom_product_form.save()
|
||||
|
||||
|
||||
self.customer = self.env['res.partner'].create({
|
||||
'name': 'customer',
|
||||
})
|
||||
|
||||
so = self.env['sale.order'].create({
|
||||
'partner_id': self.customer.id,
|
||||
'order_line': [
|
||||
(0, 0, {
|
||||
'name': self.kit.name,
|
||||
'product_id': self.kit.id,
|
||||
'product_uom_qty': 10.0,
|
||||
'product_uom': self.kit.uom_id.id,
|
||||
'price_unit': 1,
|
||||
'tax_id': False,
|
||||
})],
|
||||
})
|
||||
so.action_confirm()
|
||||
|
||||
self.assertTrue(so.picking_ids)
|
||||
self.assertEqual(so.order_line.qty_delivered, 0)
|
||||
|
||||
picking = so.picking_ids
|
||||
picking.move_lines.quantity_done = 0.86000
|
||||
picking.button_validate()
|
||||
|
||||
# Checks the delivery amount (must be 10).
|
||||
self.assertEqual(so.order_line.qty_delivered, 10)
|
||||
@@ -162,7 +162,8 @@ class SaleOrder(models.Model):
|
||||
@api.onchange('user_id')
|
||||
def onchange_user_id(self):
|
||||
super().onchange_user_id()
|
||||
self.warehouse_id = self.user_id.with_company(self.company_id.id)._get_default_warehouse_id().id
|
||||
if self.state in ['draft','sent']:
|
||||
self.warehouse_id = self.user_id.with_company(self.company_id.id)._get_default_warehouse_id().id
|
||||
|
||||
@api.onchange('partner_shipping_id')
|
||||
def _onchange_partner_shipping_id(self):
|
||||
|
||||
@@ -202,10 +202,11 @@ class ProjectTask(models.Model):
|
||||
# override sale_order_id and make it computed stored field instead of regular field.
|
||||
sale_order_id = fields.Many2one(compute='_compute_sale_order_id', store=True, readonly=False,
|
||||
domain="['|', '|', ('partner_id', '=', partner_id), ('partner_id', 'child_of', commercial_partner_id), ('partner_id', 'parent_of', partner_id)]")
|
||||
# content in related parameter of the field definition is removed to manually define the compute and the search method.
|
||||
project_sale_order_id = fields.Many2one(compute='_compute_project_sale_order_id', search='_search_project_sale_order_id', related=None)
|
||||
analytic_account_id = fields.Many2one('account.analytic.account', related='sale_order_id.analytic_account_id')
|
||||
bill_type = fields.Selection(related="project_id.bill_type")
|
||||
pricing_type = fields.Selection(related="project_id.pricing_type")
|
||||
sale_line_id = fields.Many2one(domain="[('company_id', '=', company_id), ('is_service', '=', True), ('order_partner_id', 'child_of', commercial_partner_id), ('is_expense', '=', False), ('state', 'in', ['sale', 'done']), '|', ('project_id.bill_type', '=', 'customer_task'), ('order_id', '=?', project_sale_order_id)]")
|
||||
is_project_map_empty = fields.Boolean("Is Project map empty", compute='_compute_is_project_map_empty')
|
||||
has_multi_sol = fields.Boolean(compute='_compute_has_multi_sol', compute_sudo=True)
|
||||
allow_billable = fields.Boolean(related="project_id.allow_billable")
|
||||
@@ -271,6 +272,14 @@ class ProjectTask(models.Model):
|
||||
for task in self:
|
||||
task.analytic_account_active = task.analytic_account_active or task.analytic_account_id.active
|
||||
|
||||
@api.depends('project_id.bill_type', 'project_id.sale_order_id')
|
||||
def _compute_project_sale_order_id(self):
|
||||
for task in self:
|
||||
if task.bill_type != 'customer_task':
|
||||
task.project_sale_order_id = task.project_id.sale_order_id
|
||||
else:
|
||||
task.project_sale_order_id = False
|
||||
|
||||
@api.depends('sale_line_id', 'project_id', 'allow_billable', 'non_allow_billable')
|
||||
def _compute_sale_order_id(self):
|
||||
for task in self:
|
||||
@@ -309,6 +318,9 @@ class ProjectTask(models.Model):
|
||||
if not self.sale_line_id:
|
||||
self.sale_line_id = self.project_id.sale_line_id
|
||||
|
||||
def _search_project_sale_order_id(self, operator, value):
|
||||
return [('bill_type', '!=', 'customer_task'), ('project_id.sale_order_id', operator, value)]
|
||||
|
||||
def write(self, values):
|
||||
res = super(ProjectTask, self).write(values)
|
||||
# Done after super to avoid constraints on field recomputation
|
||||
|
||||
@@ -101,9 +101,10 @@ class Project(models.Model):
|
||||
'amount_untaxed_to_invoice': 'to_invoice',
|
||||
'timesheet_cost': 'cost',
|
||||
'expense_cost': 'expense_cost',
|
||||
'expense_amount_untaxed_invoiced': 'expense_amount_untaxed_invoiced',
|
||||
'expense_amount_untaxed_invoiced': 'expense_amount_untaxed_invoiced',
|
||||
'expense_amount_untaxed_to_invoice': 'expense_amount_untaxed_to_invoice',
|
||||
'other_revenues': 'other_revenues'
|
||||
}
|
||||
}
|
||||
profit = dict.fromkeys(list(field_map.values()) + ['other_revenues', 'total'], 0.0)
|
||||
profitability_raw_data = self.env['project.profitability.report'].read_group([('project_id', 'in', self.ids)], ['project_id'] + list(field_map), ['project_id'])
|
||||
for data in profitability_raw_data:
|
||||
|
||||
@@ -132,7 +132,6 @@ class ProfitabilityAnalysis(models.Model):
|
||||
SUM(timesheet_cost) AS timesheet_cost,
|
||||
SUM(expense_cost) AS expense_cost,
|
||||
SUM(other_revenues) AS other_revenues,
|
||||
SUM(downpayment_invoiced) AS downpayment_invoiced,
|
||||
SUM(expense_amount_untaxed_to_invoice) AS expense_amount_untaxed_to_invoice,
|
||||
SUM(expense_amount_untaxed_invoiced) AS expense_amount_untaxed_invoiced,
|
||||
SUM(amount_untaxed_to_invoice) AS amount_untaxed_to_invoice,
|
||||
@@ -148,7 +147,6 @@ class ProfitabilityAnalysis(models.Model):
|
||||
TS.amount AS timesheet_cost,
|
||||
0.0 AS other_revenues,
|
||||
0.0 AS expense_cost,
|
||||
0.0 AS downpayment_invoiced,
|
||||
0.0 AS expense_amount_untaxed_to_invoice,
|
||||
0.0 AS expense_amount_untaxed_invoiced,
|
||||
0.0 AS amount_untaxed_to_invoice,
|
||||
@@ -159,7 +157,7 @@ class ProfitabilityAnalysis(models.Model):
|
||||
|
||||
UNION ALL
|
||||
|
||||
-- Get the other revenues
|
||||
-- Get the other revenues (products that are not services)
|
||||
SELECT
|
||||
P.id AS project_id,
|
||||
P.analytic_account_id AS analytic_account_id,
|
||||
@@ -168,7 +166,6 @@ class ProfitabilityAnalysis(models.Model):
|
||||
0.0 AS timesheet_cost,
|
||||
AAL.amount AS other_revenues,
|
||||
0.0 AS expense_cost,
|
||||
0.0 AS downpayment_invoiced,
|
||||
0.0 AS expense_amount_untaxed_to_invoice,
|
||||
0.0 AS expense_amount_untaxed_invoiced,
|
||||
0.0 AS amount_untaxed_to_invoice,
|
||||
@@ -177,16 +174,12 @@ class ProfitabilityAnalysis(models.Model):
|
||||
FROM project_project P
|
||||
JOIN account_analytic_account AA ON P.analytic_account_id = AA.id
|
||||
JOIN account_analytic_line AAL ON AAL.account_id = AA.id
|
||||
JOIN product_product PP ON PP.id = AAL.product_id
|
||||
JOIN product_template PT ON PT.id = PP.product_tmpl_id
|
||||
LEFT JOIN sale_order_line_invoice_rel SOINV ON SOINV.invoice_line_id = AAL.move_id
|
||||
LEFT JOIN sale_order_line SOL ON SOINV.order_line_id = SOL.id
|
||||
WHERE AAL.amount > 0.0 AND AAL.project_id IS NULL AND P.active = 't'
|
||||
AND P.allow_timesheets = 't'
|
||||
AND PT.service_type = 'manual' -- default value or Milestone service for services products
|
||||
AND PT.service_tracking = 'no' -- default value or not a tracking service for services products
|
||||
AND (SOL.id IS NULL
|
||||
OR (SOL.is_expense IS NOT TRUE AND SOL.is_downpayment IS NOT TRUE))
|
||||
OR (SOL.is_expense IS NOT TRUE AND SOL.is_downpayment IS NOT TRUE AND SOL.is_service IS NOT TRUE))
|
||||
|
||||
UNION ALL
|
||||
|
||||
@@ -199,64 +192,32 @@ class ProfitabilityAnalysis(models.Model):
|
||||
0.0 AS timesheet_cost,
|
||||
0.0 AS other_revenues,
|
||||
AAL.amount AS expense_cost,
|
||||
0.0 AS downpayment_invoiced,
|
||||
0.0 AS expense_amount_untaxed_to_invoice,
|
||||
0.0 AS expense_amount_untaxed_invoiced,
|
||||
0.0 AS amount_untaxed_to_invoice,
|
||||
0.0 AS amount_untaxed_invoiced,
|
||||
AAL.date AS line_date
|
||||
FROM project_project P
|
||||
LEFT JOIN account_analytic_account AA ON P.analytic_account_id = AA.id
|
||||
LEFT JOIN account_analytic_line AAL ON AAL.account_id = AA.id
|
||||
WHERE AAL.amount < 0.0 AND AAL.project_id IS NULL AND P.active = 't' AND P.allow_timesheets = 't'
|
||||
|
||||
UNION ALL
|
||||
|
||||
-- Get the invoiced downpayments
|
||||
SELECT
|
||||
P.id AS project_id,
|
||||
P.analytic_account_id AS analytic_account_id,
|
||||
MY_SOLS.id AS sale_line_id,
|
||||
0.0 AS timesheet_unit_amount,
|
||||
0.0 AS timesheet_cost,
|
||||
0.0 AS other_revenues,
|
||||
0.0 AS expense_cost,
|
||||
CASE WHEN MY_SOLS.invoice_status = 'invoiced' THEN MY_SOLS.price_reduce ELSE 0.0 END AS downpayment_invoiced,
|
||||
0.0 AS expense_amount_untaxed_to_invoice,
|
||||
0.0 AS expense_amount_untaxed_invoiced,
|
||||
0.0 AS amount_untaxed_to_invoice,
|
||||
0.0 AS amount_untaxed_invoiced,
|
||||
MY_S.date_order AS line_date
|
||||
FROM project_project P
|
||||
LEFT JOIN sale_order_line MY_SOL ON P.sale_line_id = MY_SOL.id
|
||||
LEFT JOIN sale_order MY_S ON MY_SOL.order_id = MY_S.id
|
||||
LEFT JOIN sale_order_line MY_SOLS ON MY_SOLS.order_id = MY_S.id
|
||||
WHERE MY_SOLS.is_downpayment = 't'
|
||||
|
||||
UNION ALL
|
||||
|
||||
-- Get the expense costs from sale order line
|
||||
SELECT
|
||||
P.id AS project_id,
|
||||
P.analytic_account_id AS analytic_account_id,
|
||||
OLIS.id AS sale_line_id,
|
||||
0.0 AS timesheet_unit_amount,
|
||||
0.0 AS timesheet_cost,
|
||||
0.0 AS other_revenues,
|
||||
OLIS.price_reduce AS expense_cost,
|
||||
0.0 AS downpayment_invoiced,
|
||||
0.0 AS expense_amount_untaxed_to_invoice,
|
||||
0.0 AS expense_amount_untaxed_invoiced,
|
||||
0.0 AS amount_untaxed_to_invoice,
|
||||
0.0 AS amount_untaxed_invoiced,
|
||||
ANLI.date AS line_date
|
||||
FROM project_project P
|
||||
LEFT JOIN account_analytic_account ANAC ON P.analytic_account_id = ANAC.id
|
||||
LEFT JOIN account_analytic_line ANLI ON ANAC.id = ANLI.account_id
|
||||
LEFT JOIN sale_order_line OLI ON P.sale_line_id = OLI.id
|
||||
LEFT JOIN sale_order ORD ON OLI.order_id = ORD.id
|
||||
LEFT JOIN sale_order_line OLIS ON ORD.id = OLIS.order_id
|
||||
WHERE OLIS.product_id = ANLI.product_id AND OLIS.is_downpayment = 't' AND ANLI.amount < 0.0 AND ANLI.project_id IS NULL AND P.active = 't' AND P.allow_timesheets = 't'
|
||||
JOIN account_analytic_account AA ON P.analytic_account_id = AA.id
|
||||
JOIN account_analytic_line AAL ON AAL.account_id = AA.id
|
||||
LEFT JOIN account_move_line RINVL ON AAL.move_id = RINVL.id
|
||||
AND RINVL.parent_state = 'posted'
|
||||
AND RINVL.exclude_from_invoice_tab = 'f'
|
||||
-- Check if the AAL is not related to a reversed credit note
|
||||
LEFT JOIN account_move RINV ON RINV.id = RINVL.move_id
|
||||
LEFT JOIN account_move_line INVL ON INVL.move_id = RINV.reversed_entry_id
|
||||
AND INVL.parent_state = 'posted'
|
||||
AND INVL.exclude_from_invoice_tab = 'f'
|
||||
AND INVL.product_id = RINVL.product_id
|
||||
LEFT JOIN sale_order_line_invoice_rel SOINV ON SOINV.invoice_line_id = INVL.id
|
||||
LEFT JOIN sale_order_line SOL ON SOINV.order_line_id = SOL.id
|
||||
AND SOL.product_id = AAL.product_id
|
||||
-- Check if the AAL is not related to a consumed downpayment (when the SOL is fully invoiced - with downpayment discounted.)
|
||||
LEFT JOIN sale_order_line_invoice_rel SOINVDOWN ON SOINVDOWN.invoice_line_id = RINVL.id
|
||||
LEFT JOIN sale_order_line SOLDOWN on SOINVDOWN.order_line_id = SOLDOWN.id AND SOLDOWN.is_downpayment = 't'
|
||||
WHERE AAL.amount < 0.0 AND AAL.project_id IS NULL
|
||||
AND SOL.id IS NULL AND SOLDOWN.id IS NULL -- Not linked to a credit note and not a downpayment
|
||||
AND P.active = 't' AND P.allow_timesheets = 't'
|
||||
|
||||
UNION ALL
|
||||
|
||||
@@ -270,17 +231,16 @@ class ProfitabilityAnalysis(models.Model):
|
||||
0.0 AS timesheet_cost,
|
||||
0.0 AS other_revenues,
|
||||
0.0 AS expense_cost,
|
||||
0.0 AS downpayment_invoiced,
|
||||
CASE
|
||||
WHEN SOL.qty_delivered_method = 'analytic' THEN (SOL.untaxed_amount_to_invoice / CASE COALESCE(S.currency_rate, 0) WHEN 0 THEN 1.0 ELSE S.currency_rate END)
|
||||
ELSE 0.0
|
||||
END AS expense_amount_untaxed_to_invoice,
|
||||
CASE
|
||||
WHEN SOL.qty_delivered_method = 'analytic' AND SOL.invoice_status != 'no'
|
||||
WHEN SOL.qty_delivered_method = 'analytic' AND SOL.invoice_status = 'invoiced'
|
||||
THEN
|
||||
CASE
|
||||
WHEN T.expense_policy = 'sales_price'
|
||||
THEN (SOL.price_reduce / CASE COALESCE(S.currency_rate, 0) WHEN 0 THEN 1.0 ELSE S.currency_rate END) * SOL.qty_invoiced
|
||||
THEN (SOL.untaxed_amount_invoiced / CASE COALESCE(S.currency_rate, 0) WHEN 0 THEN 1.0 ELSE S.currency_rate END)
|
||||
ELSE -AMOUNT_UNTAXED.expense_cost
|
||||
END
|
||||
ELSE 0.0
|
||||
@@ -290,31 +250,80 @@ class ProfitabilityAnalysis(models.Model):
|
||||
ELSE 0.0
|
||||
END AS amount_untaxed_to_invoice,
|
||||
CASE
|
||||
WHEN SOL.qty_delivered_method IN ('timesheet', 'manual') THEN (COALESCE(SOL.untaxed_amount_invoiced, AMOUNT_UNTAXED.downpayment_invoiced) / CASE COALESCE(S.currency_rate, 0) WHEN 0 THEN 1.0 ELSE S.currency_rate END)
|
||||
WHEN SOL.qty_delivered_method IN ('timesheet', 'manual') THEN (SOL.untaxed_amount_invoiced / CASE COALESCE(S.currency_rate, 0) WHEN 0 THEN 1.0 ELSE S.currency_rate END)
|
||||
ELSE 0.0
|
||||
END AS amount_untaxed_invoiced,
|
||||
S.date_order AS line_date
|
||||
FROM project_project P
|
||||
JOIN res_company C ON C.id = P.company_id
|
||||
LEFT JOIN (
|
||||
-- Gets SOL linked to timesheets
|
||||
SELECT
|
||||
P.id AS project_id,
|
||||
P.analytic_account_id AS analytic_account_id,
|
||||
AAL.so_line AS sale_line_id,
|
||||
0.0 AS expense_cost,
|
||||
0.0 AS downpayment_invoiced
|
||||
0.0 AS expense_cost
|
||||
FROM account_analytic_line AAL, project_project P
|
||||
WHERE AAL.project_id IS NOT NULL AND P.id = AAL.project_id AND P.active = 't'
|
||||
GROUP BY P.id, AAL.so_line
|
||||
|
||||
UNION
|
||||
-- Service SOL linked to a project task AND not yet timesheeted
|
||||
SELECT
|
||||
P.id AS project_id,
|
||||
P.analytic_account_id AS analytic_account_id,
|
||||
SOL.id AS sale_line_id,
|
||||
0.0 AS expense_cost
|
||||
FROM sale_order_line SOL
|
||||
JOIN project_task T ON T.sale_line_id = SOL.id
|
||||
JOIN project_project P ON T.project_id = P.id
|
||||
LEFT JOIN account_analytic_line AAL ON AAL.task_id = T.id
|
||||
WHERE SOL.is_service = 't'
|
||||
AND AAL.id IS NULL -- not timesheeted
|
||||
AND P.active = 't' AND P.allow_timesheets = 't'
|
||||
GROUP BY P.id, SOL.id
|
||||
UNION
|
||||
-- Service SOL linked to project AND not yet timesheeted
|
||||
SELECT
|
||||
P.id AS project_id,
|
||||
P.analytic_account_id AS analytic_account_id,
|
||||
SOL.id AS sale_line_id,
|
||||
0.0 AS expense_cost
|
||||
FROM sale_order_line SOL
|
||||
JOIN project_project P ON P.sale_line_id = SOL.id
|
||||
LEFT JOIN account_analytic_line AAL ON AAL.project_id = P.id
|
||||
LEFT JOIN project_task T ON T.sale_line_id = SOL.id
|
||||
WHERE SOL.is_service = 't'
|
||||
AND AAL.id IS NULL -- not timesheeted
|
||||
AND (T.id IS NULL OR T.project_id != P.id) -- not linked to a task in this project
|
||||
AND P.active = 't' AND P.allow_timesheets = 't'
|
||||
GROUP BY P.id, SOL.id
|
||||
UNION
|
||||
-- Service SOL linked to analytic account AND not yet timesheeted
|
||||
SELECT
|
||||
P.id AS project_id,
|
||||
P.analytic_account_id AS analytic_account_id,
|
||||
SOL.id AS sale_line_id,
|
||||
0.0 AS expense_cost
|
||||
FROM sale_order_line SOL
|
||||
JOIN sale_order SO ON SO.id = SOL.order_id
|
||||
JOIN account_analytic_account AA ON AA.id = SO.analytic_account_id
|
||||
JOIN project_project P ON P.analytic_account_id = AA.id
|
||||
LEFT JOIN project_project PSOL ON PSOL.sale_line_id = SOL.id
|
||||
LEFT JOIN project_task TSOL ON TSOL.sale_line_id = SOL.id
|
||||
LEFT JOIN account_analytic_line AAL ON AAL.so_line = SOL.id
|
||||
WHERE SOL.is_service = 't'
|
||||
AND AAL.id IS NULL -- not timesheeted
|
||||
AND TSOL.id IS NULL -- not linked to a task
|
||||
AND PSOL.id IS NULL -- not linked to a project
|
||||
AND P.active = 't' AND P.allow_timesheets = 't'
|
||||
GROUP BY P.id, SOL.id
|
||||
UNION
|
||||
|
||||
SELECT
|
||||
P.id AS project_id,
|
||||
P.analytic_account_id AS analytic_account_id,
|
||||
AAL.so_line AS sale_line_id,
|
||||
0.0 AS expense_cost,
|
||||
0.0 AS downpayment_invoiced
|
||||
0.0 AS expense_cost
|
||||
FROM project_project P
|
||||
LEFT JOIN account_analytic_account AA ON P.analytic_account_id = AA.id
|
||||
LEFT JOIN account_analytic_line AAL ON AAL.account_id = AA.id
|
||||
@@ -325,8 +334,7 @@ class ProfitabilityAnalysis(models.Model):
|
||||
P.id AS project_id,
|
||||
P.analytic_account_id AS analytic_account_id,
|
||||
AAL.so_line AS sale_line_id,
|
||||
SUM(AAL.amount) AS expense_cost,
|
||||
0.0 AS downpayment_invoiced
|
||||
SUM(AAL.amount) AS expense_cost
|
||||
FROM project_project P
|
||||
LEFT JOIN account_analytic_account AA ON P.analytic_account_id = AA.id
|
||||
LEFT JOIN account_analytic_line AAL ON AAL.account_id = AA.id
|
||||
@@ -336,37 +344,31 @@ class ProfitabilityAnalysis(models.Model):
|
||||
SELECT
|
||||
P.id AS project_id,
|
||||
P.analytic_account_id AS analytic_account_id,
|
||||
MY_SOLS.id AS sale_line_id,
|
||||
0.0 AS expense_cost,
|
||||
CASE WHEN MY_SOLS.invoice_status = 'invoiced' THEN MY_SOLS.price_reduce ELSE 0.0 END AS downpayment_invoiced
|
||||
SOLDOWN.id AS sale_line_id,
|
||||
0.0 AS expense_cost
|
||||
FROM project_project P
|
||||
LEFT JOIN sale_order_line MY_SOL ON P.sale_line_id = MY_SOL.id
|
||||
LEFT JOIN sale_order MY_S ON MY_SOL.order_id = MY_S.id
|
||||
LEFT JOIN sale_order_line MY_SOLS ON MY_SOLS.order_id = MY_S.id
|
||||
WHERE MY_SOLS.is_downpayment = 't'
|
||||
GROUP BY P.id, MY_SOLS.id
|
||||
UNION
|
||||
SELECT
|
||||
P.id AS project_id,
|
||||
P.analytic_account_id AS analytic_account_id,
|
||||
OLIS.id AS sale_line_id,
|
||||
OLIS.price_reduce AS expense_cost,
|
||||
0.0 AS downpayment_invoiced
|
||||
FROM project_project P
|
||||
LEFT JOIN account_analytic_account ANAC ON P.analytic_account_id = ANAC.id
|
||||
LEFT JOIN account_analytic_line ANLI ON ANAC.id = ANLI.account_id
|
||||
LEFT JOIN sale_order_line OLI ON P.sale_line_id = OLI.id
|
||||
LEFT JOIN sale_order ORD ON OLI.order_id = ORD.id
|
||||
LEFT JOIN sale_order_line OLIS ON ORD.id = OLIS.order_id
|
||||
WHERE OLIS.product_id = ANLI.product_id AND OLIS.is_downpayment = 't' AND ANLI.amount < 0.0 AND ANLI.project_id IS NULL AND P.active = 't' AND P.allow_timesheets = 't'
|
||||
GROUP BY P.id, OLIS.id
|
||||
LEFT JOIN sale_order_line SOL ON P.sale_line_id = SOL.id
|
||||
LEFT JOIN sale_order SO ON SO.id = SOL.order_id OR SO.analytic_account_id = P.analytic_account_id
|
||||
LEFT JOIN sale_order_line SOLDOWN ON SOLDOWN.order_id = SO.id AND SOLDOWN.is_downpayment = 't'
|
||||
LEFT JOIN sale_order_line_invoice_rel SOINV ON SOINV.order_line_id = SOLDOWN.id
|
||||
LEFT JOIN account_move_line INVL ON SOINV.invoice_line_id = INVL.id
|
||||
AND INVL.parent_state = 'posted'
|
||||
AND INVL.exclude_from_invoice_tab = 'f'
|
||||
LEFT JOIN account_move RINV ON INVL.move_id = RINV.reversed_entry_id
|
||||
LEFT JOIN account_move_line RINVL ON RINV.id = RINVL.move_id
|
||||
AND RINVL.parent_state = 'posted'
|
||||
AND RINVL.exclude_from_invoice_tab = 'f'
|
||||
AND RINVL.product_id = SOLDOWN.product_id
|
||||
LEFT JOIN account_analytic_line ANLI ON ANLI.move_id = RINVL.id AND ANLI.amount < 0.0
|
||||
WHERE ANLI.id IS NULL -- there are no credit note for this downpayment
|
||||
AND P.active = 't' AND P.allow_timesheets = 't'
|
||||
GROUP BY P.id, SOLDOWN.id
|
||||
UNION
|
||||
SELECT
|
||||
P.id AS project_id,
|
||||
P.analytic_account_id AS analytic_account_id,
|
||||
SOL.id AS sale_line_id,
|
||||
0.0 AS expense_cost,
|
||||
0.0 AS downpayment_invoiced
|
||||
0.0 AS expense_cost
|
||||
FROM sale_order_line SOL
|
||||
INNER JOIN project_project P ON SOL.project_id = P.id
|
||||
WHERE P.active = 't' AND P.allow_timesheets = 't'
|
||||
@@ -375,8 +377,7 @@ class ProfitabilityAnalysis(models.Model):
|
||||
P.id AS project_id,
|
||||
P.analytic_account_id AS analytic_account_id,
|
||||
SOL.id AS sale_line_id,
|
||||
0.0 AS expense_cost,
|
||||
0.0 AS downpayment_invoiced
|
||||
0.0 AS expense_cost
|
||||
FROM sale_order_line SOL
|
||||
INNER JOIN project_task T ON SOL.task_id = T.id
|
||||
INNER JOIN project_project P ON P.id = T.project_id
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# # Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import common
|
||||
from . import common_reporting
|
||||
from . import test_sale_timesheet
|
||||
from . import test_sale_service
|
||||
from . import test_project_billing
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||
from flectra.tools import float_is_zero, float_compare
|
||||
from flectra.addons.sale_timesheet.tests.common import TestCommonSaleTimesheet
|
||||
|
||||
|
||||
class TestCommonReporting(TestCommonSaleTimesheet):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls, chart_template_ref=None):
|
||||
super().setUpClass(chart_template_ref=chart_template_ref)
|
||||
|
||||
# expense product
|
||||
cls.product_expense = cls.env['product.product'].with_context(mail_notrack=True, mail_create_nolog=True).create({
|
||||
'name': "Expense service",
|
||||
'standard_price': 10,
|
||||
'list_price': 20,
|
||||
'type': 'service',
|
||||
'invoice_policy': 'delivery',
|
||||
'expense_policy': 'sales_price',
|
||||
'default_code': 'EXP',
|
||||
'service_type': 'manual',
|
||||
'taxes_id': False,
|
||||
'property_account_income_id': cls.company_data['default_account_revenue'].id,
|
||||
})
|
||||
|
||||
# create Analytic Accounts
|
||||
cls.analytic_account_1 = cls.env['account.analytic.account'].create({
|
||||
'name': 'Test AA 1',
|
||||
'code': 'AA1',
|
||||
'company_id': cls.company_data['company'].id,
|
||||
'partner_id': cls.partner_a.id
|
||||
})
|
||||
cls.analytic_account_2 = cls.env['account.analytic.account'].create({
|
||||
'name': 'Test AA 2',
|
||||
'code': 'AA2',
|
||||
'company_id': cls.company_data['company'].id,
|
||||
'partner_id': cls.partner_a.id
|
||||
})
|
||||
cls.analytic_account_3 = cls.env['account.analytic.account'].create({
|
||||
'name': 'Test AA 3',
|
||||
'code': 'AA3',
|
||||
'company_id': cls.company_data['company'].id,
|
||||
'partner_id': cls.partner_a.id
|
||||
})
|
||||
|
||||
# Sale orders each will create project and a task in a global project (one SO is 'delivered', the other is 'ordered')
|
||||
# and a third one using fixed_price (which is 'delivered')
|
||||
cls.sale_order_1 = cls.env['sale.order'].with_context(mail_notrack=True, mail_create_nolog=True).create({
|
||||
'partner_id': cls.partner_a.id,
|
||||
'partner_invoice_id': cls.partner_a.id,
|
||||
'partner_shipping_id': cls.partner_a.id,
|
||||
'analytic_account_id': cls.analytic_account_1.id,
|
||||
})
|
||||
cls.so_line_deliver_project = cls.env['sale.order.line'].create({
|
||||
'name': cls.product_delivery_timesheet3.name,
|
||||
'product_id': cls.product_delivery_timesheet3.id,
|
||||
'product_uom_qty': 5,
|
||||
'product_uom': cls.product_delivery_timesheet3.uom_id.id,
|
||||
'price_unit': cls.product_delivery_timesheet3.list_price,
|
||||
'order_id': cls.sale_order_1.id,
|
||||
})
|
||||
cls.so_line_deliver_task = cls.env['sale.order.line'].create({
|
||||
'name': cls.product_delivery_timesheet2.name,
|
||||
'product_id': cls.product_delivery_timesheet2.id,
|
||||
'product_uom_qty': 7,
|
||||
'product_uom': cls.product_delivery_timesheet2.uom_id.id,
|
||||
'price_unit': cls.product_delivery_timesheet2.list_price,
|
||||
'order_id': cls.sale_order_1.id,
|
||||
})
|
||||
|
||||
cls.sale_order_2 = cls.env['sale.order'].with_context(mail_notrack=True, mail_create_nolog=True).create({
|
||||
'partner_id': cls.partner_a.id,
|
||||
'partner_invoice_id': cls.partner_a.id,
|
||||
'partner_shipping_id': cls.partner_a.id,
|
||||
'analytic_account_id': cls.analytic_account_2.id,
|
||||
})
|
||||
cls.so_line_order_project = cls.env['sale.order.line'].create({
|
||||
'name': cls.product_order_timesheet3.name,
|
||||
'product_id': cls.product_order_timesheet3.id,
|
||||
'product_uom_qty': 5,
|
||||
'product_uom': cls.product_order_timesheet3.uom_id.id,
|
||||
'price_unit': cls.product_order_timesheet3.list_price,
|
||||
'order_id': cls.sale_order_2.id,
|
||||
})
|
||||
cls.so_line_order_task = cls.env['sale.order.line'].create({
|
||||
'name': cls.product_order_timesheet2.name,
|
||||
'product_id': cls.product_order_timesheet2.id,
|
||||
'product_uom_qty': 7,
|
||||
'product_uom': cls.product_order_timesheet2.uom_id.id,
|
||||
'price_unit': cls.product_order_timesheet2.list_price,
|
||||
'order_id': cls.sale_order_2.id,
|
||||
})
|
||||
|
||||
cls.sale_order_3 = cls.env['sale.order'].with_context(mail_notrack=True, mail_create_nolog=True).create({
|
||||
'partner_id': cls.partner_a.id,
|
||||
'partner_invoice_id': cls.partner_a.id,
|
||||
'partner_shipping_id': cls.partner_a.id,
|
||||
'analytic_account_id': cls.analytic_account_3.id,
|
||||
})
|
||||
cls.so_line_deliver_manual_project = cls.env['sale.order.line'].create({
|
||||
'name': cls.product_delivery_manual3.name,
|
||||
'product_id': cls.product_delivery_manual3.id,
|
||||
'product_uom_qty': 11,
|
||||
'product_uom': cls.product_delivery_manual3.uom_id.id,
|
||||
'price_unit': cls.product_delivery_manual3.list_price,
|
||||
'order_id': cls.sale_order_3.id,
|
||||
})
|
||||
|
||||
def _log_timesheet_user(self, project, unit_amount, task=False):
|
||||
""" Utility method to log timesheet """
|
||||
Timesheet = self.env['account.analytic.line']
|
||||
return Timesheet.create({
|
||||
'name': 'timesheet employee on project_so_1 only',
|
||||
'account_id': project.analytic_account_id.id,
|
||||
'project_id': project.id,
|
||||
'employee_id': self.employee_user.id,
|
||||
'unit_amount': unit_amount,
|
||||
'task_id': task.id if task else False,
|
||||
})
|
||||
|
||||
def _log_timesheet_manager(self, project, unit_amount, task=False):
|
||||
""" Utility method to log timesheet """
|
||||
Timesheet = self.env['account.analytic.line']
|
||||
return Timesheet.create({
|
||||
'name': 'timesheet employee on project_so_1 only',
|
||||
'account_id': project.analytic_account_id.id,
|
||||
'project_id': project.id,
|
||||
'employee_id': self.employee_manager.id,
|
||||
'unit_amount': unit_amount,
|
||||
'task_id': task.id if task else False,
|
||||
})
|
||||
@@ -1,13 +1,13 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from flectra.addons.sale_timesheet.tests.test_reporting import TestReporting
|
||||
from flectra.tools import float_compare
|
||||
from flectra.addons.sale_timesheet.tests.common_reporting import TestCommonReporting
|
||||
from flectra.tools import float_compare, float_is_zero
|
||||
from flectra.tests import tagged
|
||||
|
||||
|
||||
@tagged('-at_install', 'post_install')
|
||||
class TestSaleProject(TestReporting):
|
||||
class TestSaleProject(TestCommonReporting):
|
||||
|
||||
def test_project_overview_by_project(self):
|
||||
rounding = self.env.company.currency_id.rounding
|
||||
@@ -34,7 +34,6 @@ class TestSaleProject(TestReporting):
|
||||
project_so = self.so_line_order_project.project_id
|
||||
# log timesheet for billable time
|
||||
timesheet1 = self._log_timesheet_manager(project_so, 10, so_line_deliver_global_project.task_id)
|
||||
|
||||
task_so = self.so_line_order_project.task_id
|
||||
# logged some timesheets: on project only, then on tasks with different employees
|
||||
timesheet2 = self._log_timesheet_user(project_so, 2)
|
||||
@@ -89,7 +88,7 @@ class TestSaleProject(TestReporting):
|
||||
})
|
||||
|
||||
other_revenues = self.env['account.analytic.line'].create({
|
||||
'name': 'pther revenues on project_so',
|
||||
'name': 'other revenues on project_so',
|
||||
'account_id': project_so.analytic_account_id.id,
|
||||
'employee_id': self.employee_user.id,
|
||||
'unit_amount': 1,
|
||||
@@ -103,10 +102,10 @@ class TestSaleProject(TestReporting):
|
||||
|
||||
dashboard_value = timesheet2.unit_amount + timesheet3.unit_amount + timesheet4.unit_amount + timesheet5.unit_amount + timesheet1.unit_amount
|
||||
project_so_timesheet_sold_unit = timesheet3.unit_amount + timesheet4.unit_amount
|
||||
project_rate_non_billable = timesheet5.unit_amount / dashboard_value * 100
|
||||
project_rate_non_billable_project = timesheet2.unit_amount / dashboard_value * 100
|
||||
project_rate_billable_time = timesheet1.unit_amount / dashboard_value * 100
|
||||
project_rate_billable_fixed = project_so_timesheet_sold_unit / dashboard_value * 100
|
||||
project_rate_non_billable = round(timesheet5.unit_amount / dashboard_value * 100, 2)
|
||||
project_rate_non_billable_project = round(timesheet2.unit_amount / dashboard_value * 100, 2)
|
||||
project_rate_billable_time = round(timesheet1.unit_amount / dashboard_value * 100, 2)
|
||||
project_rate_billable_fixed = round(project_so_timesheet_sold_unit / dashboard_value * 100, 2)
|
||||
project_rate_total = project_rate_non_billable + project_rate_non_billable_project + project_rate_billable_time + project_rate_billable_fixed
|
||||
project_invoiced = self.so_line_order_project.price_unit * self.so_line_order_project.product_uom_qty * timesheet1.unit_amount
|
||||
project_timesheet_cost = timesheet2.amount + timesheet3.amount + timesheet4.amount + timesheet5.amount + timesheet1.amount
|
||||
@@ -122,9 +121,11 @@ class TestSaleProject(TestReporting):
|
||||
self.assertEqual(float_compare(vals['dashboard']['rates']['billable_time'], project_rate_billable_time, precision_rounding=rounding), 0, "The rate billable-time should be the one from the SO2 line, as we are in ordered quantity")
|
||||
self.assertEqual(float_compare(vals['dashboard']['rates']['billable_fixed'], project_rate_billable_fixed, precision_rounding=rounding), 0, "The rate billable-fixed should be the one from the SO2 line, as we are in ordered quantity")
|
||||
self.assertEqual(float_compare(vals['dashboard']['rates']['total'], project_rate_total, precision_rounding=rounding), 0, "The total rates should be the one from the SO2 line, as we are in ordered quantity")
|
||||
self.assertEqual(float_compare(vals['dashboard']['profit']['invoiced'], project_invoiced, precision_rounding=rounding), 0, "The amount invoiced should be the one from the SO2 line, as we are in ordered quantity")
|
||||
self.assertEqual(float_compare(vals['dashboard']['profit']['invoiced'], project_invoiced + project_other_revenues.price_total, precision_rounding=rounding), 0, "The amount invoiced should be the one from the SO2 line, as we are in ordered quantity")
|
||||
self.assertEqual(float_compare(vals['dashboard']['profit']['cost'], project_timesheet_cost, precision_rounding=rounding), 0, "The amount cost should be the one from the SO2 line, as we are in ordered quantity")
|
||||
self.assertEqual(float_compare(vals['dashboard']['profit']['expense_cost'], expense.amount, precision_rounding=rounding), 0, "The amount expense-cost should be the one from the SO2 line, as we are in ordered quantity")
|
||||
self.assertEqual(float_compare(vals['dashboard']['profit']['other_revenues'], other_revenues.amount + project_other_revenues.price_total, precision_rounding=rounding), 0, "The amount of the other revenues should be equal to the corresponding account move line and the one from the SO line")
|
||||
self.assertEqual(float_compare(vals['dashboard']['profit']['total'], project_invoiced + project_timesheet_cost + expense.amount + other_revenues.amount + project_other_revenues.price_total, precision_rounding=rounding), 0, "The total amount should be the sum of the SO2 line and the created other_revenues account analytic line")
|
||||
self.assertEqual(float_compare(vals['dashboard']['profit']['other_revenues'], other_revenues.amount, precision_rounding=rounding), 0, "The amount of the other revenues should be equal to the corresponding account move line and the one from the SO line")
|
||||
self.assertEqual(float_compare(vals['dashboard']['profit']['expense_amount_untaxed_to_invoice'], -expense.amount, precision_rounding=rounding), 0, "The amount of expense to re-invoice shoul be equal to the opposite of expense amount")
|
||||
self.assertTrue(float_is_zero(vals['dashboard']['profit']['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The amount of expense re-invoiced should be equal to 0")
|
||||
self.assertEqual(float_compare(vals['dashboard']['profit']['total'], project_invoiced + project_timesheet_cost + other_revenues.amount + project_other_revenues.price_total, precision_rounding=rounding), 0, "The total amount should be the sum of the SO2 line and the created other_revenues account analytic line")
|
||||
self.assertEqual(float_compare(vals['repartition_employee_max'], 11.0, precision_rounding=rounding), 0, "The amount of repartition-employee-max should be the one from SO2 line")
|
||||
@@ -1,115 +1,13 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||
from flectra.osv import expression
|
||||
from flectra.tools import float_is_zero, float_compare
|
||||
from flectra.addons.sale_timesheet.tests.common import TestCommonSaleTimesheet
|
||||
from flectra.addons.sale_timesheet.tests.common_reporting import TestCommonReporting
|
||||
from flectra.tests import tagged
|
||||
|
||||
|
||||
@tagged('-at_install', 'post_install')
|
||||
class TestReporting(TestCommonSaleTimesheet):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls, chart_template_ref=None):
|
||||
super().setUpClass(chart_template_ref=chart_template_ref)
|
||||
|
||||
# expense product
|
||||
cls.product_expense = cls.env['product.product'].with_context(mail_notrack=True, mail_create_nolog=True).create({
|
||||
'name': "Expense service",
|
||||
'standard_price': 10,
|
||||
'list_price': 20,
|
||||
'type': 'service',
|
||||
'invoice_policy': 'delivery',
|
||||
'expense_policy': 'sales_price',
|
||||
'default_code': 'EXP',
|
||||
'service_type': 'manual',
|
||||
'taxes_id': False,
|
||||
'property_account_income_id': cls.company_data['default_account_revenue'].id,
|
||||
})
|
||||
|
||||
# create Analytic Accounts
|
||||
cls.analytic_account_1 = cls.env['account.analytic.account'].create({
|
||||
'name': 'Test AA 1',
|
||||
'code': 'AA1',
|
||||
'company_id': cls.company_data['company'].id,
|
||||
'partner_id': cls.partner_a.id
|
||||
})
|
||||
cls.analytic_account_2 = cls.env['account.analytic.account'].create({
|
||||
'name': 'Test AA 2',
|
||||
'code': 'AA2',
|
||||
'company_id': cls.company_data['company'].id,
|
||||
'partner_id': cls.partner_a.id
|
||||
})
|
||||
|
||||
# Sale orders each will create project and a task in a global project (one SO is 'delivered', the other is 'ordered')
|
||||
cls.sale_order_1 = cls.env['sale.order'].with_context(mail_notrack=True, mail_create_nolog=True).create({
|
||||
'partner_id': cls.partner_a.id,
|
||||
'partner_invoice_id': cls.partner_a.id,
|
||||
'partner_shipping_id': cls.partner_a.id,
|
||||
'analytic_account_id': cls.analytic_account_1.id,
|
||||
})
|
||||
cls.so_line_deliver_project = cls.env['sale.order.line'].create({
|
||||
'name': cls.product_delivery_timesheet3.name,
|
||||
'product_id': cls.product_delivery_timesheet3.id,
|
||||
'product_uom_qty': 5,
|
||||
'product_uom': cls.product_delivery_timesheet3.uom_id.id,
|
||||
'price_unit': cls.product_delivery_timesheet3.list_price,
|
||||
'order_id': cls.sale_order_1.id,
|
||||
})
|
||||
cls.so_line_deliver_task = cls.env['sale.order.line'].create({
|
||||
'name': cls.product_delivery_timesheet2.name,
|
||||
'product_id': cls.product_delivery_timesheet2.id,
|
||||
'product_uom_qty': 7,
|
||||
'product_uom': cls.product_delivery_timesheet2.uom_id.id,
|
||||
'price_unit': cls.product_delivery_timesheet2.list_price,
|
||||
'order_id': cls.sale_order_1.id,
|
||||
})
|
||||
|
||||
cls.sale_order_2 = cls.env['sale.order'].with_context(mail_notrack=True, mail_create_nolog=True).create({
|
||||
'partner_id': cls.partner_a.id,
|
||||
'partner_invoice_id': cls.partner_a.id,
|
||||
'partner_shipping_id': cls.partner_a.id,
|
||||
'analytic_account_id': cls.analytic_account_2.id,
|
||||
})
|
||||
cls.so_line_order_project = cls.env['sale.order.line'].create({
|
||||
'name': cls.product_order_timesheet3.name,
|
||||
'product_id': cls.product_order_timesheet3.id,
|
||||
'product_uom_qty': 5,
|
||||
'product_uom': cls.product_order_timesheet3.uom_id.id,
|
||||
'price_unit': cls.product_order_timesheet3.list_price,
|
||||
'order_id': cls.sale_order_2.id,
|
||||
})
|
||||
cls.so_line_order_task = cls.env['sale.order.line'].create({
|
||||
'name': cls.product_order_timesheet2.name,
|
||||
'product_id': cls.product_order_timesheet2.id,
|
||||
'product_uom_qty': 7,
|
||||
'product_uom': cls.product_order_timesheet2.uom_id.id,
|
||||
'price_unit': cls.product_order_timesheet2.list_price,
|
||||
'order_id': cls.sale_order_2.id,
|
||||
})
|
||||
|
||||
def _log_timesheet_user(self, project, unit_amount, task=False):
|
||||
""" Utility method to log timesheet """
|
||||
Timesheet = self.env['account.analytic.line']
|
||||
return Timesheet.create({
|
||||
'name': 'timesheet employee on project_so_1 only',
|
||||
'account_id': project.analytic_account_id.id,
|
||||
'project_id': project.id,
|
||||
'employee_id': self.employee_user.id,
|
||||
'unit_amount': unit_amount,
|
||||
'task_id': task.id if task else False,
|
||||
})
|
||||
|
||||
def _log_timesheet_manager(self, project, unit_amount, task=False):
|
||||
""" Utility method to log timesheet """
|
||||
Timesheet = self.env['account.analytic.line']
|
||||
return Timesheet.create({
|
||||
'name': 'timesheet employee on project_so_1 only',
|
||||
'account_id': project.analytic_account_id.id,
|
||||
'project_id': project.id,
|
||||
'employee_id': self.employee_manager.id,
|
||||
'unit_amount': unit_amount,
|
||||
'task_id': task.id if task else False,
|
||||
})
|
||||
class TestReporting(TestCommonReporting):
|
||||
|
||||
def test_profitability_report(self):
|
||||
|
||||
@@ -125,31 +23,38 @@ class TestReporting(TestCommonSaleTimesheet):
|
||||
self.assertTrue(float_is_zero(project_global_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the global project should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_global_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the global project should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_global_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the global project should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_global_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the global project should be 0.0")
|
||||
|
||||
# confirm sales orders
|
||||
self.sale_order_1.action_confirm()
|
||||
self.sale_order_2.action_confirm()
|
||||
self.sale_order_3.action_confirm()
|
||||
self.env['project.profitability.report'].flush()
|
||||
|
||||
project_so_1 = self.so_line_deliver_project.project_id
|
||||
project_so_2 = self.so_line_order_project.project_id
|
||||
project_so_3 = self.so_line_deliver_manual_project.project_id
|
||||
|
||||
task_so_1 = self.so_line_deliver_project.task_id
|
||||
task_so_2 = self.so_line_order_project.task_id
|
||||
task_so_3 = self.so_line_deliver_manual_project.task_id
|
||||
|
||||
task_in_global_1 = self.so_line_deliver_task.task_id
|
||||
task_in_global_2 = self.so_line_order_task.task_id
|
||||
|
||||
# deliver project should not be impacted, as no timesheet are logged yet
|
||||
project_so_1_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_1.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced'], ['project_id'])[0]
|
||||
project_so_1_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_1.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
|
||||
self.assertTrue(float_is_zero(project_so_1_stat['amount_untaxed_invoiced'], precision_rounding=rounding), "The invoiced amount of the project from SO1 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_1_stat['amount_untaxed_to_invoice'], precision_rounding=rounding), "The amount to invoice of the project from SO1 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_1_stat['timesheet_unit_amount'], precision_rounding=rounding), "The timesheet unit amount of the project from SO1 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_1_stat['timesheet_cost'], precision_rounding=rounding), "The timesheet cost of the global project from SO1 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_1_stat['timesheet_cost'], precision_rounding=rounding), "The timesheet cost of the project from SO1 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_1_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the project from SO1 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_1_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the global project should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_1_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO1 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_1_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the project from SO1 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_1_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the project from SO1 should be 0.0")
|
||||
|
||||
# order project should be to invoice, but nothing has been delivered yet
|
||||
project_so_2_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_2.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced'], ['project_id'])[0]
|
||||
project_so_2_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_2.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
|
||||
self.assertTrue(float_is_zero(project_so_2_stat['amount_untaxed_invoiced'], precision_rounding=rounding), "The invoiced amount of the project from SO2 should be 0.0")
|
||||
self.assertEqual(float_compare(project_so_2_stat['amount_untaxed_to_invoice'], self.so_line_order_project.price_unit * self.so_line_order_project.qty_to_invoice, precision_rounding=rounding), 0, "The amount to invoice should be the one from the SO line, as we are in ordered quantity")
|
||||
self.assertTrue(float_is_zero(project_so_2_stat['timesheet_unit_amount'], precision_rounding=rounding), "The timesheet unit amount of the project from SO2 should be 0.0")
|
||||
@@ -157,9 +62,21 @@ class TestReporting(TestCommonSaleTimesheet):
|
||||
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the project from SO2 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the global project should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_2_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the project from SO2 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_2_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the project from SO2 should be 0.0")
|
||||
|
||||
# global project now contain 2 tasks: 'deliver' one which has no effect (no timesheet yet), and the 'order' one which should update the 'to invoice' amount
|
||||
project_global_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', self.project_global.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced'], ['project_id'])[0]
|
||||
# fixed price project should not be impacted, as no delivered quantity are logged yet
|
||||
project_so_3_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_3.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
|
||||
self.assertTrue(float_is_zero(project_so_3_stat['amount_untaxed_invoiced'], precision_rounding=rounding), "The invoiced amount of the project from SO3 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_3_stat['amount_untaxed_to_invoice'], precision_rounding=rounding), "The amount to invoice of the project from SO3 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_3_stat['timesheet_unit_amount'], precision_rounding=rounding), "The timesheet unit amount of the project from SO3 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_3_stat['timesheet_cost'], precision_rounding=rounding), "The timesheet cost of the project from SO3 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_3_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the project from SO3 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_3_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO3 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_3_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the project from SO3 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_3_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the project from SO3 should be 0.0")
|
||||
|
||||
# global project now contain 3 tasks: 'deliver' ones which have no effect (no timesheet or delivered yet), and the 'order' one which should update the 'to invoice' amount
|
||||
project_global_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', self.project_global.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
|
||||
self.assertTrue(float_is_zero(project_global_stat['amount_untaxed_invoiced'], precision_rounding=rounding), "The invoiced amount of the global project should be 0.0")
|
||||
self.assertEqual(float_compare(project_global_stat['amount_untaxed_to_invoice'], self.so_line_order_task.price_unit * self.so_line_order_task.qty_to_invoice, precision_rounding=rounding), 0, "The amount to invoice of global project should take the task in 'oredered qty' into account")
|
||||
self.assertTrue(float_is_zero(project_global_stat['timesheet_unit_amount'], precision_rounding=rounding), "The timesheet unit amount of the global project should be 0.0")
|
||||
@@ -167,21 +84,22 @@ class TestReporting(TestCommonSaleTimesheet):
|
||||
self.assertTrue(float_is_zero(project_global_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the global project should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_global_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the global project should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_global_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the global project should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_global_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the global project should be 0.0")
|
||||
|
||||
# logged some timesheets: on project only, then on tasks with different employees
|
||||
timesheet1 = self._log_timesheet_user(project_so_1, 2)
|
||||
timesheet2 = self._log_timesheet_user(project_so_2, 2)
|
||||
timesheet3 = self._log_timesheet_user(project_so_1, 3, task_so_1)
|
||||
timesheet4 = self._log_timesheet_user(project_so_2, 3, task_so_2)
|
||||
|
||||
timesheet5 = self._log_timesheet_manager(project_so_1, 1, task_so_1)
|
||||
timesheet6 = self._log_timesheet_manager(project_so_2, 1, task_so_2)
|
||||
timesheet7 = self._log_timesheet_manager(self.project_global, 3, task_in_global_1)
|
||||
timesheet8 = self._log_timesheet_manager(self.project_global, 3, task_in_global_2)
|
||||
timesheet9 = self._log_timesheet_user(project_so_3, 4, task_so_3)
|
||||
self.env['project.profitability.report'].flush()
|
||||
|
||||
# deliver project should now have cost and something to invoice
|
||||
project_so_1_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_1.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced'], ['project_id'])[0]
|
||||
project_so_1_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_1.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
|
||||
project_so_1_timesheet_cost = timesheet1.amount + timesheet3.amount + timesheet5.amount
|
||||
project_so_1_timesheet_sold_unit = timesheet1.unit_amount + timesheet3.unit_amount + timesheet5.unit_amount
|
||||
self.assertTrue(float_is_zero(project_so_1_stat['amount_untaxed_invoiced'], precision_rounding=rounding), "The invoiced amount of the project from SO1 should be 0.0")
|
||||
@@ -191,9 +109,10 @@ class TestReporting(TestCommonSaleTimesheet):
|
||||
self.assertTrue(float_is_zero(project_so_1_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the project from SO1 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_1_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO1 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_1_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the project from SO1 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_1_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the deliver project should be 0.0")
|
||||
|
||||
# order project still have something to invoice but has costs now
|
||||
project_so_2_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_2.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced'], ['project_id'])[0]
|
||||
project_so_2_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_2.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
|
||||
project_so_2_timesheet_cost = timesheet2.amount + timesheet4.amount + timesheet6.amount
|
||||
project_so_2_timesheet_sold_unit = timesheet2.unit_amount + timesheet4.unit_amount + timesheet6.unit_amount
|
||||
self.assertTrue(float_is_zero(project_so_2_stat['amount_untaxed_invoiced'], precision_rounding=rounding), "The invoiced amount of the project from SO2 should be 0.0")
|
||||
@@ -203,9 +122,23 @@ class TestReporting(TestCommonSaleTimesheet):
|
||||
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the project from SO2 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO1 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_2_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the project from SO2 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_2_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the project from SO2 should be 0.0")
|
||||
|
||||
# fixed price project should ow have cost and nothing to invoice as no delivered quantity are logged yet
|
||||
project_so_3_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_3.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
|
||||
project_so_3_timesheet_cost = timesheet9.amount
|
||||
project_so_3_timesheet_sold_unit = timesheet9.unit_amount
|
||||
self.assertTrue(float_is_zero(project_so_3_stat['amount_untaxed_invoiced'], precision_rounding=rounding), "The invoiced amount of the project from SO3 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_3_stat['amount_untaxed_to_invoice'], precision_rounding=rounding), "The amount to invoice of the project from SO3 should be 0.0")
|
||||
self.assertEqual(float_compare(project_so_3_stat['timesheet_unit_amount'], project_so_3_timesheet_sold_unit, precision_rounding=rounding), 0, "The timesheet unit amount of the project from SO3")
|
||||
self.assertEqual(float_compare(project_so_3_stat['timesheet_cost'], project_so_3_timesheet_cost, precision_rounding=rounding), 0, "The timesheet cost of the project from SO3")
|
||||
self.assertTrue(float_is_zero(project_so_3_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the project from SO3 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_3_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO3 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_3_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the project from SO3 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_3_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the project from SO3 should be 0.0")
|
||||
|
||||
# global project should have more to invoice, and should now have costs
|
||||
project_global_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', self.project_global.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced'], ['project_id'])[0]
|
||||
project_global_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', self.project_global.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
|
||||
project_global_timesheet_cost = timesheet7.amount + timesheet8.amount
|
||||
project_global_timesheet_unit = timesheet7.unit_amount + timesheet8.unit_amount
|
||||
project_global_to_invoice = (self.so_line_order_task.price_unit * self.so_line_order_task.product_uom_qty) + (self.so_line_deliver_task.price_unit * timesheet7.unit_amount)
|
||||
@@ -216,6 +149,63 @@ class TestReporting(TestCommonSaleTimesheet):
|
||||
self.assertTrue(float_is_zero(project_global_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the global project should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_global_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO1 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_global_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the global project should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_global_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the global project should be 0.0")
|
||||
|
||||
self.so_line_deliver_manual_project.write({'qty_delivered': 7.0})
|
||||
self.env['project.profitability.report'].flush()
|
||||
|
||||
# deliver project should now have cost and something to invoice
|
||||
project_so_1_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_1.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
|
||||
project_so_1_timesheet_cost = timesheet1.amount + timesheet3.amount + timesheet5.amount
|
||||
project_so_1_timesheet_sold_unit = timesheet1.unit_amount + timesheet3.unit_amount + timesheet5.unit_amount
|
||||
self.assertTrue(float_is_zero(project_so_1_stat['amount_untaxed_invoiced'], precision_rounding=rounding), "The invoiced amount of the project from SO1 should be 0.0")
|
||||
self.assertEqual(float_compare(project_so_1_stat['amount_untaxed_to_invoice'], self.so_line_deliver_project.price_unit * project_so_1_timesheet_sold_unit, precision_rounding=rounding), 0, "The amount to invoice of the project from SO1 should only include timesheet linked to task")
|
||||
self.assertEqual(float_compare(project_so_1_stat['timesheet_unit_amount'], project_so_1_timesheet_sold_unit, precision_rounding=rounding), 0, "The timesheet unit amount of the project from SO1 should include all timesheet in project")
|
||||
self.assertEqual(float_compare(project_so_1_stat['timesheet_cost'], project_so_1_timesheet_cost, precision_rounding=rounding), 0, "The timesheet cost of the project from SO1 should include all timesheet")
|
||||
self.assertTrue(float_is_zero(project_so_1_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the project from SO1 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_1_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO1 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_1_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the project from SO1 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_1_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the deliver project should be 0.0")
|
||||
|
||||
# order project still have something to invoice but has costs now
|
||||
project_so_2_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_2.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
|
||||
project_so_2_timesheet_cost = timesheet2.amount + timesheet4.amount + timesheet6.amount
|
||||
project_so_2_timesheet_sold_unit = timesheet2.unit_amount + timesheet4.unit_amount + timesheet6.unit_amount
|
||||
self.assertTrue(float_is_zero(project_so_2_stat['amount_untaxed_invoiced'], precision_rounding=rounding), "The invoiced amount of the project from SO2 should be 0.0")
|
||||
self.assertEqual(float_compare(project_so_2_stat['amount_untaxed_to_invoice'], self.so_line_order_project.price_unit * self.so_line_order_project.qty_to_invoice, precision_rounding=rounding), 0, "The amount to invoice should be the one from the SO line, as we are in ordered quantity")
|
||||
self.assertEqual(float_compare(project_so_2_stat['timesheet_unit_amount'], project_so_2_timesheet_sold_unit, precision_rounding=rounding), 0, "The timesheet unit amount of the project from SO2 should include all timesheet")
|
||||
self.assertEqual(float_compare(project_so_2_stat['timesheet_cost'], project_so_2_timesheet_cost, precision_rounding=rounding), 0, "The timesheet cost of the project from SO2 should include all timesheet")
|
||||
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the project from SO2 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO1 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_2_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the project from SO2 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_2_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the project from SO2 should be 0.0")
|
||||
|
||||
# fixed price project should ow have cost and something to invoice as some delivered quantity are logged in
|
||||
project_so_3_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_3.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
|
||||
project_so_3_timesheet_cost = timesheet9.amount
|
||||
project_so_3_timesheet_sold_unit = timesheet9.unit_amount
|
||||
self.assertTrue(float_is_zero(project_so_3_stat['amount_untaxed_invoiced'], precision_rounding=rounding), "The invoiced amount of the project from SO3 should be 0.0")
|
||||
self.assertEqual(float_compare(project_so_3_stat['amount_untaxed_to_invoice'], self.so_line_deliver_manual_project.price_unit * self.so_line_deliver_manual_project.qty_to_invoice, precision_rounding=rounding), 0, "The amount to invoice of the project from SO3 should be 0.0")
|
||||
self.assertEqual(float_compare(project_so_3_stat['timesheet_unit_amount'], project_so_3_timesheet_sold_unit, precision_rounding=rounding), 0, "The timesheet unit amount of the project from SO3")
|
||||
self.assertEqual(float_compare(project_so_3_stat['timesheet_cost'], project_so_3_timesheet_cost, precision_rounding=rounding), 0, "The timesheet cost of the project from SO3")
|
||||
self.assertTrue(float_is_zero(project_so_3_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the project from SO3 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_3_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO3 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_3_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the project from SO3 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_3_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the project from SO3 should be 0.0")
|
||||
|
||||
# global project should have more to invoice, and should now have costs
|
||||
project_global_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', self.project_global.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
|
||||
project_global_timesheet_cost = timesheet7.amount + timesheet8.amount
|
||||
project_global_timesheet_unit = timesheet7.unit_amount + timesheet8.unit_amount
|
||||
project_global_to_invoice = (self.so_line_order_task.price_unit * self.so_line_order_task.product_uom_qty) + (self.so_line_deliver_task.price_unit * timesheet7.unit_amount)
|
||||
self.assertTrue(float_is_zero(project_global_stat['amount_untaxed_invoiced'], precision_rounding=rounding), "The invoiced amount of the global project should be 0.0")
|
||||
self.assertEqual(float_compare(project_global_stat['amount_untaxed_to_invoice'], project_global_to_invoice, precision_rounding=rounding), 0, "The amount to invoice of global project should take the task in 'oredered qty' and the delivered timesheets into account")
|
||||
self.assertEqual(float_compare(project_global_stat['timesheet_unit_amount'], project_global_timesheet_unit, precision_rounding=rounding), 0, "The timesheet unit amount of the global project should include all timesheet")
|
||||
self.assertEqual(float_compare(project_global_stat['timesheet_cost'], project_global_timesheet_cost, precision_rounding=rounding), 0, "The timesheet cost of the global project should include all timesheet")
|
||||
self.assertTrue(float_is_zero(project_global_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the global project should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_global_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO1 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_global_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the global project should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_global_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the global project should be 0.0")
|
||||
|
||||
InvoiceWizard = self.env['sale.advance.payment.inv'].with_context(mail_notrack=True)
|
||||
|
||||
@@ -236,7 +226,7 @@ class TestReporting(TestCommonSaleTimesheet):
|
||||
self.env['project.profitability.report'].flush()
|
||||
|
||||
# deliver project should now have cost and something invoiced
|
||||
project_so_1_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_1.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced'], ['project_id'])[0]
|
||||
project_so_1_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_1.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
|
||||
project_so_1_timesheet_cost = timesheet1.amount + timesheet3.amount + timesheet5.amount
|
||||
project_so_1_timesheet_sold_unit = timesheet1.unit_amount + timesheet3.unit_amount + timesheet5.unit_amount
|
||||
|
||||
@@ -247,9 +237,10 @@ class TestReporting(TestCommonSaleTimesheet):
|
||||
self.assertTrue(float_is_zero(project_so_1_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the project from SO1 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_1_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO1 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_1_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the project from SO1 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_1_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the deliver project should be 0.0")
|
||||
|
||||
# order project has still nothing invoiced
|
||||
project_so_2_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_2.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced'], ['project_id'])[0]
|
||||
project_so_2_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_2.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
|
||||
project_so_2_timesheet_cost = timesheet2.amount + timesheet4.amount + timesheet6.amount
|
||||
project_so_2_timesheet_sold_unit = timesheet2.unit_amount + timesheet4.unit_amount + timesheet6.unit_amount
|
||||
self.assertTrue(float_is_zero(project_so_2_stat['amount_untaxed_invoiced'], precision_rounding=rounding), "The invoiced amount of the project from SO2 should be 0.0")
|
||||
@@ -259,9 +250,23 @@ class TestReporting(TestCommonSaleTimesheet):
|
||||
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the project from SO2 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO1 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_2_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the project from SO2 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_2_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the project from SO2 should be 0.0")
|
||||
|
||||
# fixed price project has still nothing invoice
|
||||
project_so_3_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_3.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
|
||||
project_so_3_timesheet_cost = timesheet9.amount
|
||||
project_so_3_timesheet_sold_unit = timesheet9.unit_amount
|
||||
self.assertTrue(float_is_zero(project_so_3_stat['amount_untaxed_invoiced'], precision_rounding=rounding), "The invoiced amount of the project from SO3 should be 0.0")
|
||||
self.assertEqual(float_compare(project_so_3_stat['amount_untaxed_to_invoice'], self.so_line_deliver_manual_project.price_unit * self.so_line_deliver_manual_project.qty_to_invoice, precision_rounding=rounding), 0, "The amount to invoice of the project from SO3 should be 0.0")
|
||||
self.assertEqual(float_compare(project_so_3_stat['timesheet_unit_amount'], project_so_3_timesheet_sold_unit, precision_rounding=rounding), 0, "The timesheet unit amount of the project from SO3")
|
||||
self.assertEqual(float_compare(project_so_3_stat['timesheet_cost'], project_so_3_timesheet_cost, precision_rounding=rounding), 0, "The timesheet cost of the project from SO3")
|
||||
self.assertTrue(float_is_zero(project_so_3_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the project from SO3 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_3_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO3 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_3_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the project from SO3 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_3_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the project from SO3 should be 0.0")
|
||||
|
||||
# global project should have more to invoice, and should now have costs
|
||||
project_global_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', self.project_global.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced'], ['project_id'])[0]
|
||||
project_global_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', self.project_global.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
|
||||
project_global_timesheet_cost = timesheet7.amount + timesheet8.amount
|
||||
project_global_timesheet_unit = timesheet7.unit_amount + timesheet8.unit_amount
|
||||
project_global_to_invoice = self.so_line_order_task.price_unit * self.so_line_order_task.product_uom_qty
|
||||
@@ -273,6 +278,7 @@ class TestReporting(TestCommonSaleTimesheet):
|
||||
self.assertTrue(float_is_zero(project_global_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the global project should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_global_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO1 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_global_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the global project should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_global_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the global project should be 0.0")
|
||||
|
||||
# invoice the Sales Order SO2
|
||||
context = {
|
||||
@@ -291,7 +297,7 @@ class TestReporting(TestCommonSaleTimesheet):
|
||||
self.env['project.profitability.report'].flush()
|
||||
|
||||
# deliver project should not be impacted by the invoice of the other SO
|
||||
project_so_1_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_1.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced'], ['project_id'])[0]
|
||||
project_so_1_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_1.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
|
||||
project_so_1_timesheet_cost = timesheet1.amount + timesheet3.amount + timesheet5.amount
|
||||
project_so_1_timesheet_sold_unit = timesheet1.unit_amount + timesheet3.unit_amount + timesheet5.unit_amount
|
||||
self.assertEqual(float_compare(project_so_1_stat['amount_untaxed_invoiced'], self.so_line_deliver_project.price_unit * project_so_1_timesheet_sold_unit, precision_rounding=rounding), 0, "The invoiced amount of the project from SO1 should only include timesheet linked to task")
|
||||
@@ -301,9 +307,10 @@ class TestReporting(TestCommonSaleTimesheet):
|
||||
self.assertTrue(float_is_zero(project_so_1_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the project from SO1 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_1_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO1 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_1_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the project from SO1 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_1_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the deliver project should be 0.0")
|
||||
|
||||
# order project is now totally invoiced, as we are in ordered qty
|
||||
project_so_2_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_2.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced'], ['project_id'])[0]
|
||||
project_so_2_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_2.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
|
||||
project_so_2_timesheet_cost = timesheet2.amount + timesheet4.amount + timesheet6.amount
|
||||
project_so_2_timesheet_sold_unit = timesheet2.unit_amount + timesheet4.unit_amount + timesheet6.unit_amount
|
||||
self.assertEqual(float_compare(project_so_2_stat['amount_untaxed_invoiced'], self.so_line_order_project.price_unit * self.so_line_order_project.product_uom_qty, precision_rounding=rounding), 0, "The invoiced amount should be the one from the SO line, as we are in ordered quantity")
|
||||
@@ -313,9 +320,23 @@ class TestReporting(TestCommonSaleTimesheet):
|
||||
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the project from SO2 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO1 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_2_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the project from SO2 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_2_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the project from SO2 should be 0.0")
|
||||
|
||||
# fixed price project has still nothing invoice
|
||||
project_so_3_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_3.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
|
||||
project_so_3_timesheet_cost = timesheet9.amount
|
||||
project_so_3_timesheet_sold_unit = timesheet9.unit_amount
|
||||
self.assertTrue(float_is_zero(project_so_3_stat['amount_untaxed_invoiced'], precision_rounding=rounding), "The invoiced amount of the project from SO3 should be 0.0")
|
||||
self.assertEqual(float_compare(project_so_3_stat['amount_untaxed_to_invoice'], self.so_line_deliver_manual_project.price_unit * self.so_line_deliver_manual_project.qty_to_invoice, precision_rounding=rounding), 0, "The amount to invoice of the project from SO3 should be 0.0")
|
||||
self.assertEqual(float_compare(project_so_3_stat['timesheet_unit_amount'], project_so_3_timesheet_sold_unit, precision_rounding=rounding), 0, "The timesheet unit amount of the project from SO3")
|
||||
self.assertEqual(float_compare(project_so_3_stat['timesheet_cost'], project_so_3_timesheet_cost, precision_rounding=rounding), 0, "The timesheet cost of the project from SO3")
|
||||
self.assertTrue(float_is_zero(project_so_3_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the project from SO3 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_3_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO3 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_3_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the project from SO3 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_3_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the project from SO3 should be 0.0")
|
||||
|
||||
# global project should be totally invoiced
|
||||
project_global_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', self.project_global.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced'], ['project_id'])[0]
|
||||
project_global_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', self.project_global.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
|
||||
project_global_timesheet_cost = timesheet7.amount + timesheet8.amount
|
||||
project_global_timesheet_unit = timesheet7.unit_amount + timesheet8.unit_amount
|
||||
project_global_invoiced = self.so_line_order_task.price_unit * self.so_line_order_task.product_uom_qty + self.so_line_deliver_task.price_unit * timesheet7.unit_amount
|
||||
@@ -326,6 +347,76 @@ class TestReporting(TestCommonSaleTimesheet):
|
||||
self.assertTrue(float_is_zero(project_global_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the global project should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_global_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO1 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_global_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the global project should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_global_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the global project should be 0.0")
|
||||
|
||||
# invoice the Sales Order SO3
|
||||
context = {
|
||||
"active_model": 'sale.order',
|
||||
"active_ids": [self.sale_order_3.id],
|
||||
"active_id": self.sale_order_3.id,
|
||||
'open_invoices': True,
|
||||
}
|
||||
payment = InvoiceWizard.create({
|
||||
'advance_payment_method': 'delivered',
|
||||
})
|
||||
action_invoice = payment.with_context(context).create_invoices()
|
||||
invoice_id = action_invoice['res_id']
|
||||
invoice_3 = self.env['account.move'].browse(invoice_id)
|
||||
invoice_3.action_post()
|
||||
self.env['project.profitability.report'].flush()
|
||||
|
||||
# deliver project should not be impacted by the invoice of the other SO
|
||||
project_so_1_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_1.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
|
||||
project_so_1_timesheet_cost = timesheet1.amount + timesheet3.amount + timesheet5.amount
|
||||
project_so_1_timesheet_sold_unit = timesheet1.unit_amount + timesheet3.unit_amount + timesheet5.unit_amount
|
||||
self.assertEqual(float_compare(project_so_1_stat['amount_untaxed_invoiced'], self.so_line_deliver_project.price_unit * project_so_1_timesheet_sold_unit, precision_rounding=rounding), 0, "The invoiced amount of the project from SO1 should only include timesheet linked to task")
|
||||
self.assertTrue(float_is_zero(project_so_1_stat['amount_untaxed_to_invoice'], precision_rounding=rounding), "The amount to invoice of the project from SO1 should be 0.0")
|
||||
self.assertEqual(float_compare(project_so_1_stat['timesheet_unit_amount'], project_so_1_timesheet_sold_unit, precision_rounding=rounding), 0, "The timesheet unit amount of the project from SO1 should include all timesheet in project")
|
||||
self.assertEqual(float_compare(project_so_1_stat['timesheet_cost'], project_so_1_timesheet_cost, precision_rounding=rounding), 0, "The timesheet cost of the project from SO1 should include all timesheet")
|
||||
self.assertTrue(float_is_zero(project_so_1_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the project from SO1 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_1_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO1 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_1_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the project from SO1 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_1_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the deliver project should be 0.0")
|
||||
|
||||
# order project should not be impacted by the invoice of the other SO
|
||||
project_so_2_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_2.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
|
||||
project_so_2_timesheet_cost = timesheet2.amount + timesheet4.amount + timesheet6.amount
|
||||
project_so_2_timesheet_sold_unit = timesheet2.unit_amount + timesheet4.unit_amount + timesheet6.unit_amount
|
||||
self.assertEqual(float_compare(project_so_2_stat['amount_untaxed_invoiced'], self.so_line_order_project.price_unit * self.so_line_order_project.product_uom_qty, precision_rounding=rounding), 0, "The invoiced amount should be the one from the SO line, as we are in ordered quantity")
|
||||
self.assertTrue(float_is_zero(project_so_2_stat['amount_untaxed_to_invoice'], precision_rounding=rounding), "The amount to invoice should be the one 0.0, as all ordered quantity is invoiced")
|
||||
self.assertEqual(float_compare(project_so_2_stat['timesheet_unit_amount'], project_so_2_timesheet_sold_unit, precision_rounding=rounding), 0, "The timesheet unit amount of the project from SO2 should include all timesheet")
|
||||
self.assertEqual(float_compare(project_so_2_stat['timesheet_cost'], project_so_2_timesheet_cost, precision_rounding=rounding), 0, "The timesheet cost of the project from SO2 should include all timesheet")
|
||||
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the project from SO2 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO1 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_2_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the project from SO2 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_2_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the project from SO2 should be 0.0")
|
||||
|
||||
# fixed price project is now partially invoiced, as we are in delivered qty
|
||||
project_so_3_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_3.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
|
||||
project_so_3_timesheet_cost = timesheet9.amount
|
||||
project_so_3_timesheet_sold_unit = timesheet9.unit_amount
|
||||
self.assertEqual(float_compare(project_so_3_stat['amount_untaxed_invoiced'], self.so_line_deliver_manual_project.price_unit * self.so_line_deliver_manual_project.qty_delivered, precision_rounding=rounding), 0, "The invoiced amount of the project from SO3 should be the delivered quantity * the unit price")
|
||||
self.assertTrue(float_is_zero(project_so_3_stat['amount_untaxed_to_invoice'], precision_rounding=rounding), "The amount to invoice of the project from SO3 should be 0.0")
|
||||
self.assertEqual(float_compare(project_so_3_stat['timesheet_unit_amount'], project_so_3_timesheet_sold_unit, precision_rounding=rounding), 0, "The timesheet unit amount of the project from SO3")
|
||||
self.assertEqual(float_compare(project_so_3_stat['timesheet_cost'], project_so_3_timesheet_cost, precision_rounding=rounding), 0, "The timesheet cost of the project from SO3")
|
||||
self.assertTrue(float_is_zero(project_so_3_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the project from SO3 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_3_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO3 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_3_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the project from SO3 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_3_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the project from SO3 should be 0.0")
|
||||
|
||||
# global project should be totally invoiced
|
||||
project_global_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', self.project_global.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
|
||||
project_global_timesheet_cost = timesheet7.amount + timesheet8.amount
|
||||
project_global_timesheet_unit = timesheet7.unit_amount + timesheet8.unit_amount
|
||||
project_global_invoiced = self.so_line_order_task.price_unit * self.so_line_order_task.product_uom_qty + self.so_line_deliver_task.price_unit * timesheet7.unit_amount
|
||||
self.assertEqual(float_compare(project_global_stat['amount_untaxed_invoiced'], project_global_invoiced, precision_rounding=rounding), 0, "The invoiced amount of the global project should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_global_stat['amount_untaxed_to_invoice'], precision_rounding=rounding), "The amount to invoice of global project should take the task in 'oredered qty' and the delivered timesheets into account")
|
||||
self.assertEqual(float_compare(project_global_stat['timesheet_unit_amount'], project_global_timesheet_unit, precision_rounding=rounding), 0, "The timesheet unit amount of the global project should include all timesheet")
|
||||
self.assertEqual(float_compare(project_global_stat['timesheet_cost'], project_global_timesheet_cost, precision_rounding=rounding), 0, "The timesheet cost of the global project should include all timesheet")
|
||||
self.assertTrue(float_is_zero(project_global_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the global project should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_global_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO1 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_global_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the global project should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_global_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the global project should be 0.0")
|
||||
|
||||
# simulate the auto creation of the SO line for expense, like we confirm a vendor bill.
|
||||
so_line_expense = self.env['sale.order.line'].create({
|
||||
@@ -362,7 +453,7 @@ class TestReporting(TestCommonSaleTimesheet):
|
||||
self.env['project.profitability.report'].flush()
|
||||
|
||||
# deliver project should now have expense cost, and expense to reinvoice as there is a still open sales order linked to the AA1
|
||||
project_so_1_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_1.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced'], ['project_id'])[0]
|
||||
project_so_1_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_1.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
|
||||
project_so_1_timesheet_cost = timesheet1.amount + timesheet3.amount + timesheet5.amount
|
||||
project_so_1_timesheet_sold_unit = timesheet1.unit_amount + timesheet3.unit_amount + timesheet5.unit_amount
|
||||
self.assertEqual(float_compare(project_so_1_stat['amount_untaxed_invoiced'], self.so_line_deliver_project.price_unit * project_so_1_timesheet_sold_unit, precision_rounding=rounding), 0, "The invoiced amount of the project from SO1 should only include timesheet linked to task")
|
||||
@@ -372,9 +463,10 @@ class TestReporting(TestCommonSaleTimesheet):
|
||||
self.assertEqual(float_compare(project_so_1_stat['expense_amount_untaxed_to_invoice'], -1 * expense1.amount, precision_rounding=rounding), 0, "The expense cost to reinvoice of the project from SO1 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_1_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO1 should be 0.0")
|
||||
self.assertEqual(float_compare(project_so_1_stat['expense_cost'], expense1.amount, precision_rounding=rounding), 0, "The expense cost of the project from SO1 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_1_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the deliver project should be 0.0")
|
||||
|
||||
# order project is not impacted by the expenses
|
||||
project_so_2_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_2.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced'], ['project_id'])[0]
|
||||
project_so_2_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', project_so_2.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
|
||||
project_so_2_timesheet_cost = timesheet2.amount + timesheet4.amount + timesheet6.amount
|
||||
project_so_2_timesheet_sold_unit = timesheet2.unit_amount + timesheet4.unit_amount + timesheet6.unit_amount
|
||||
self.assertEqual(float_compare(project_so_2_stat['amount_untaxed_invoiced'], self.so_line_order_project.price_unit * self.so_line_order_project.product_uom_qty, precision_rounding=rounding), 0, "The invoiced amount should be the one from the SO line, as we are in ordered quantity")
|
||||
@@ -384,9 +476,10 @@ class TestReporting(TestCommonSaleTimesheet):
|
||||
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the project from SO2 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO1 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_2_stat['expense_cost'], precision_rounding=rounding), "The expense cost of the project from SO2 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_2_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the project from SO2 should be 0.0")
|
||||
|
||||
# global project should have an expense, but not reinvoiceable
|
||||
project_global_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', self.project_global.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced'], ['project_id'])[0]
|
||||
project_global_stat = self.env['project.profitability.report'].read_group([('project_id', 'in', self.project_global.ids)], ['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'], ['project_id'])[0]
|
||||
project_global_timesheet_cost = timesheet7.amount + timesheet8.amount
|
||||
project_global_timesheet_unit = timesheet7.unit_amount + timesheet8.unit_amount
|
||||
project_global_invoiced = self.so_line_order_task.price_unit * self.so_line_order_task.product_uom_qty + self.so_line_deliver_task.price_unit * timesheet7.unit_amount
|
||||
@@ -397,3 +490,338 @@ class TestReporting(TestCommonSaleTimesheet):
|
||||
self.assertTrue(float_is_zero(project_global_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding), "The expense cost to reinvoice of the global project should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_global_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding), "The expense invoiced amount of the project from SO1 should be 0.0")
|
||||
self.assertEqual(float_compare(project_global_stat['expense_cost'], expense2.amount, precision_rounding=rounding), 0, "The expense cost of the global project should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_global_stat['other_revenues'], precision_rounding=rounding), "The other revenues of the global project should be 0.0")
|
||||
|
||||
def test_reversed_downpayment(self):
|
||||
# this test suppose everything is in the same currency as the current one
|
||||
currency = self.env.company.currency_id
|
||||
rounding = currency.rounding
|
||||
|
||||
self.sale_order_2.action_confirm()
|
||||
context = {
|
||||
'active_model': 'sale.order',
|
||||
'active_ids': self.sale_order_2.ids,
|
||||
'active_id': self.sale_order_2.id,
|
||||
'default_journal_id': self.company_data['default_journal_sale'].id,
|
||||
'open_invoices': True,
|
||||
}
|
||||
# Let's do an invoice for a deposit of 100
|
||||
downpayment = self.env['sale.advance.payment.inv'].with_context(context).create({
|
||||
'advance_payment_method': 'percentage',
|
||||
'amount': 10,
|
||||
})
|
||||
action_invoice = downpayment.create_invoices()
|
||||
invoice_id = action_invoice['res_id']
|
||||
invoice_downpayment = self.env['account.move'].browse(invoice_id)
|
||||
invoice_downpayment.action_post()
|
||||
posted_invoice_res_ids = [invoice_id]
|
||||
downpayment2 = self.env['sale.advance.payment.inv'].with_context(context).create({
|
||||
'advance_payment_method': 'percentage',
|
||||
'amount': 25,
|
||||
})
|
||||
action_invoice = downpayment2.create_invoices()
|
||||
invoice_downpayment2 = self.env['account.move'].search(expression.AND([action_invoice['domain'], [('id', 'not in', posted_invoice_res_ids)]]))
|
||||
invoice_downpayment2.action_post()
|
||||
posted_invoice_res_ids += invoice_downpayment2.ids
|
||||
milestone_to_invoice = self.so_line_order_project.price_unit * self.so_line_order_project.qty_to_invoice
|
||||
timesheets_to_invoice = self.so_line_order_task.price_unit * self.so_line_order_task.qty_to_invoice
|
||||
total_product_price = milestone_to_invoice + timesheets_to_invoice
|
||||
credit_note_wizard = self.env['account.move.reversal'].with_context({
|
||||
'active_ids': invoice_downpayment2.ids,
|
||||
'active_id': invoice_downpayment2.id,
|
||||
'active_model': 'account.move'
|
||||
}).create({
|
||||
'refund_method': 'refund',
|
||||
'reason': 'reason test create',
|
||||
})
|
||||
action_moves = credit_note_wizard.reverse_moves()
|
||||
credit_id = action_moves['res_id']
|
||||
invoice_credit = self.env['account.move'].browse(credit_id)
|
||||
invoice_credit.action_post()
|
||||
posted_invoice_res_ids += invoice_credit.ids
|
||||
project_so_2 = self.so_line_order_project.project_id
|
||||
task_so_2 = self.so_line_order_project.task_id
|
||||
task_in_global_2 = self.so_line_order_task.task_id
|
||||
|
||||
self.env['project.profitability.report'].flush()
|
||||
project_so_2_stat = self.env['project.profitability.report'].read_group(
|
||||
[('project_id', 'in', project_so_2.ids)],
|
||||
['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'],
|
||||
['project_id']
|
||||
)[0]
|
||||
self.assertEqual(float_compare(project_so_2_stat['amount_untaxed_invoiced'], 0.1 * total_product_price, precision_rounding=rounding), 0,
|
||||
"The invoiced amount is the amount of downpayments not reversed")
|
||||
self.assertEqual(float_compare(project_so_2_stat['amount_untaxed_to_invoice'], milestone_to_invoice - 0.1 * total_product_price, precision_rounding=rounding), 0,
|
||||
"The amount to invoice is the milestone product minus the downpayment not reversed")
|
||||
self.assertTrue(float_is_zero(project_so_2_stat['timesheet_unit_amount'], precision_rounding=rounding),
|
||||
"The timesheet unit amount of the project from SO2 is 0")
|
||||
self.assertTrue(float_is_zero(project_so_2_stat['timesheet_cost'], precision_rounding=rounding),
|
||||
"The timesheet cost of the project from SO2 is 0")
|
||||
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding),
|
||||
"The expense cost to reinvoice of the project from SO2 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding),
|
||||
"The expense invoiced amount of the project from SO2 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_2_stat['expense_cost'], precision_rounding=rounding),
|
||||
"The expense cost of the project from SO2 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_2_stat['other_revenues'], precision_rounding=rounding),
|
||||
"The other revenues of the project from SO2 should be 0.0")
|
||||
|
||||
# logged some timesheets: on project only, then on tasks with different employees
|
||||
timesheet2 = self._log_timesheet_user(project_so_2, 2)
|
||||
timesheet4 = self._log_timesheet_user(project_so_2, 3, task_so_2)
|
||||
timesheet6 = self._log_timesheet_manager(project_so_2, 2, task_so_2)
|
||||
timesheet8 = self._log_timesheet_manager(self.project_global, 3, task_in_global_2)
|
||||
|
||||
# simulate the auto creation of the SO line for expense, like we confirm a vendor bill.
|
||||
so_line_expense = self.env['sale.order.line'].create({
|
||||
'name': self.product_expense.name,
|
||||
'product_id': self.product_expense.id,
|
||||
'product_uom_qty': 0.0,
|
||||
'product_uom': self.product_expense.uom_id.id,
|
||||
'price_unit': self.product_expense.list_price, # reinvoice at sales price
|
||||
'order_id': self.sale_order_2.id,
|
||||
'is_expense': True,
|
||||
})
|
||||
|
||||
expense1 = self.env['account.analytic.line'].create({
|
||||
'name': 'expense on project_so_2',
|
||||
'account_id': project_so_2.analytic_account_id.id,
|
||||
'so_line': so_line_expense.id,
|
||||
'employee_id': self.employee_user.id,
|
||||
'unit_amount': 4,
|
||||
'amount': 4 * self.product_expense.list_price * -1,
|
||||
'product_id': self.product_expense.id,
|
||||
'product_uom_id': self.product_expense.uom_id.id,
|
||||
})
|
||||
|
||||
self.env['project.profitability.report'].flush()
|
||||
|
||||
project_so_2_stat = self.env['project.profitability.report'].read_group(
|
||||
[('project_id', 'in', project_so_2.ids)],
|
||||
['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'],
|
||||
['project_id']
|
||||
)[0]
|
||||
project_so_2_timesheet_cost = timesheet2.amount + timesheet4.amount + timesheet6.amount
|
||||
project_so_2_timesheet_sold_unit = timesheet2.unit_amount + timesheet4.unit_amount + timesheet6.unit_amount
|
||||
self.assertEqual(float_compare(project_so_2_stat['amount_untaxed_invoiced'], 0.1 * total_product_price, precision_rounding=rounding), 0,
|
||||
"The invoiced amount of the project from SO2 should only include downpayment not reversed")
|
||||
self.assertEqual(float_compare(project_so_2_stat['amount_untaxed_to_invoice'], milestone_to_invoice - 0.1 * total_product_price, precision_rounding=rounding), 0,
|
||||
"The amount to invoice of the project from SO2 should include the milestone to invoice minus the downpayment")
|
||||
self.assertEqual(float_compare(project_so_2_stat['timesheet_unit_amount'], project_so_2_timesheet_sold_unit, precision_rounding=rounding), 0,
|
||||
"The timesheet unit amount of the project from SO2 should include all timesheet in project")
|
||||
self.assertEqual(float_compare(project_so_2_stat['timesheet_cost'], project_so_2_timesheet_cost, precision_rounding=rounding), 0,
|
||||
"The timesheet cost of the project from SO2 should include all timesheet")
|
||||
self.assertEqual(float_compare(project_so_2_stat['expense_amount_untaxed_to_invoice'], -expense1.amount, precision_rounding=rounding), 0,
|
||||
"The expense cost to reinvoice of the project from SO2 should be the expense amount")
|
||||
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding),
|
||||
"The expense invoiced amount of the project from SO2 should be 0.0")
|
||||
self.assertEqual(float_compare(project_so_2_stat['expense_cost'], expense1.amount, precision_rounding=rounding), 0,
|
||||
"The expense cost of the project from SO1 should be expense amount")
|
||||
self.assertTrue(float_is_zero(project_so_2_stat['other_revenues'], precision_rounding=rounding),
|
||||
"The other revenues of the project from SO2 should be 0.0")
|
||||
|
||||
# invoice the Sales Order SO2
|
||||
context = {
|
||||
"active_model": 'sale.order',
|
||||
"active_ids": self.sale_order_2.ids,
|
||||
"active_id": self.sale_order_2.id,
|
||||
'open_invoices': True,
|
||||
'mail_notrack': True,
|
||||
}
|
||||
payment = self.env['sale.advance.payment.inv'].with_context(mail_notrack=True).create({
|
||||
'advance_payment_method': 'delivered',
|
||||
})
|
||||
action_invoice = payment.with_context(context).create_invoices()
|
||||
invoice_payment = self.env['account.move'].search(expression.AND([action_invoice['domain'], [('id', 'not in', posted_invoice_res_ids)]]))
|
||||
invoice_payment.action_post()
|
||||
self.env['project.profitability.report'].flush()
|
||||
|
||||
project_so_2_stat = self.env['project.profitability.report'].read_group(
|
||||
[('project_id', 'in', project_so_2.ids)],
|
||||
['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'],
|
||||
['project_id']
|
||||
)[0]
|
||||
self.assertEqual(float_compare(project_so_2_stat['amount_untaxed_invoiced'], milestone_to_invoice, precision_rounding=rounding), 0,
|
||||
"The invoiced amount of the project from SO2 should only include timesheet linked to task")
|
||||
self.assertTrue(float_is_zero(project_so_2_stat['amount_untaxed_to_invoice'], precision_rounding=rounding),
|
||||
"The amount to invoice of the project from SO2 should be 0.0")
|
||||
self.assertEqual(float_compare(project_so_2_stat['timesheet_unit_amount'], project_so_2_timesheet_sold_unit, precision_rounding=rounding), 0,
|
||||
"The timesheet unit amount of the project from SO2 should include all timesheet in project")
|
||||
self.assertEqual(float_compare(project_so_2_stat['timesheet_cost'], project_so_2_timesheet_cost, precision_rounding=rounding), 0,
|
||||
"The timesheet cost of the project from SO2 should include all timesheet")
|
||||
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding),
|
||||
"The expense to invoice amount of the project from SO2 should be 0.0")
|
||||
self.assertEqual(float_compare(project_so_2_stat['expense_amount_untaxed_invoiced'], -1 * expense1.amount, precision_rounding=rounding), 0,
|
||||
"The expense cost reinvoiced of the project from SO2 should be the expense amount")
|
||||
self.assertEqual(float_compare(project_so_2_stat['expense_cost'], expense1.amount, precision_rounding=rounding), 0,
|
||||
"The expense cost of the project from SO2 should be the expense amount")
|
||||
self.assertTrue(float_is_zero(project_so_2_stat['other_revenues'], precision_rounding=rounding),
|
||||
"The other revenues of the project from SO2 should be 0.0")
|
||||
|
||||
credit_note_wizard = self.env['account.move.reversal'].with_context({
|
||||
'active_ids': invoice_payment.ids,
|
||||
'active_id': invoice_payment.id,
|
||||
'active_model': 'account.move'
|
||||
}).create({
|
||||
'refund_method': 'refund',
|
||||
'reason': 'reason test create',
|
||||
})
|
||||
action_moves = credit_note_wizard.reverse_moves()
|
||||
credit_id = action_moves['res_id']
|
||||
invoice_credit = self.env['account.move'].browse(credit_id)
|
||||
invoice_credit.action_post()
|
||||
self.env['project.profitability.report'].flush()
|
||||
|
||||
project_so_2_stat = self.env['project.profitability.report'].read_group(
|
||||
[('project_id', 'in', project_so_2.ids)],
|
||||
['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'],
|
||||
['project_id']
|
||||
)[0]
|
||||
self.assertEqual(float_compare(project_so_2_stat['amount_untaxed_invoiced'], 0.1 * total_product_price, precision_rounding=rounding), 0,
|
||||
"The invoiced amount of the project from SO2 should only include downpayment not reversed")
|
||||
self.assertEqual(float_compare(project_so_2_stat['amount_untaxed_to_invoice'], milestone_to_invoice - 0.1 * total_product_price, precision_rounding=rounding), 0,
|
||||
"The amount to invoice of the project from SO2 should include the milestone to invoice minus the downpayment")
|
||||
self.assertEqual(float_compare(project_so_2_stat['timesheet_unit_amount'], project_so_2_timesheet_sold_unit, precision_rounding=rounding), 0,
|
||||
"The timesheet unit amount of the project from SO2 should include all timesheet in project")
|
||||
self.assertEqual(float_compare(project_so_2_stat['timesheet_cost'], project_so_2_timesheet_cost, precision_rounding=rounding), 0,
|
||||
"The timesheet cost of the project from SO2 should include all timesheet")
|
||||
self.assertEqual(float_compare(project_so_2_stat['expense_amount_untaxed_to_invoice'], -expense1.amount, precision_rounding=rounding), 0,
|
||||
"The expense cost to reinvoice of the project from SO2 should be the expense amount")
|
||||
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding),
|
||||
"The expense invoiced amount of the project from SO2 should be 0.0")
|
||||
self.assertEqual(float_compare(project_so_2_stat['expense_cost'], expense1.amount, precision_rounding=rounding), 0,
|
||||
"The expense cost of the project from SO1 should be expense amount")
|
||||
self.assertTrue(float_is_zero(project_so_2_stat['other_revenues'], precision_rounding=rounding),
|
||||
"The other revenues of the project from SO2 should be 0.0")
|
||||
|
||||
def test_milestone_no_tracking(self):
|
||||
# this test suppose everything is in the same currency as the current one
|
||||
currency = self.env.company.currency_id
|
||||
rounding = currency.rounding
|
||||
so_line_deliver_no_task = self.env['sale.order.line'].create({
|
||||
'name': self.product_delivery_manual1.name,
|
||||
'product_id': self.product_delivery_manual1.id,
|
||||
'product_uom_qty': 50,
|
||||
'product_uom': self.product_delivery_manual1.uom_id.id,
|
||||
'price_unit': self.product_delivery_manual1.list_price,
|
||||
'order_id': self.sale_order_2.id,
|
||||
})
|
||||
so_line_deliver_no_task.write({'qty_delivered': 1.0})
|
||||
self.sale_order_2.action_confirm()
|
||||
milestone_to_invoice = self.so_line_order_project.price_unit * self.so_line_order_project.qty_to_invoice
|
||||
milestone_no_task = so_line_deliver_no_task.price_unit * so_line_deliver_no_task.qty_to_invoice
|
||||
self.env['project.profitability.report'].flush()
|
||||
|
||||
project_so_2 = self.so_line_order_project.project_id
|
||||
project_so_2_stat = self.env['project.profitability.report'].read_group(
|
||||
[('project_id', 'in', project_so_2.ids)],
|
||||
['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'],
|
||||
['project_id']
|
||||
)[0]
|
||||
self.assertTrue(float_is_zero(project_so_2_stat['amount_untaxed_invoiced'], precision_rounding=rounding),
|
||||
"The invoiced amount of the project from SO2 should be 0.0")
|
||||
self.assertEqual(float_compare(project_so_2_stat['amount_untaxed_to_invoice'], milestone_to_invoice + milestone_no_task, precision_rounding=rounding), 0,
|
||||
"The amount to invoice of the project from SO2 should include the milestone to invoice")
|
||||
self.assertTrue(float_is_zero(project_so_2_stat['timesheet_unit_amount'], precision_rounding=rounding),
|
||||
"The timesheet unit amount of the project from SO2 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_2_stat['timesheet_cost'], precision_rounding=rounding),
|
||||
"The timesheet cost of the project from SO2 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding),
|
||||
"The expense cost to reinvoice of the project from SO2 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding),
|
||||
"The expense invoiced amount of the project from SO2 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_2_stat['expense_cost'], precision_rounding=rounding),
|
||||
"The expense cost of the project from SO2 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_2_stat['other_revenues'], precision_rounding=rounding),
|
||||
"The other revenues of the project from SO2 should be 0.0")
|
||||
|
||||
task_using_milestone_not_tracked = self.env['project.task'].create({
|
||||
'name': 'Task with milestone not tracked',
|
||||
'project_id': project_so_2.id,
|
||||
'partner_id': project_so_2.partner_id.id,
|
||||
'sale_line_id': so_line_deliver_no_task.id,
|
||||
})
|
||||
self.env['project.profitability.report'].flush()
|
||||
project_so_2_stat = self.env['project.profitability.report'].read_group(
|
||||
[('project_id', 'in', project_so_2.ids)],
|
||||
['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'],
|
||||
['project_id']
|
||||
)[0]
|
||||
self.assertTrue(float_is_zero(project_so_2_stat['amount_untaxed_invoiced'], precision_rounding=rounding),
|
||||
"The invoiced amount of the project from SO2 should be 0.0")
|
||||
self.assertEqual(float_compare(project_so_2_stat['amount_untaxed_to_invoice'], milestone_to_invoice + milestone_no_task, precision_rounding=rounding), 0,
|
||||
"The amount to invoice of the project from SO2 should include the milestone to invoice linked to the project or project's task")
|
||||
self.assertTrue(float_is_zero(project_so_2_stat['timesheet_unit_amount'], precision_rounding=rounding),
|
||||
"The timesheet unit amount of the project from SO2 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_2_stat['timesheet_cost'], precision_rounding=rounding),
|
||||
"The timesheet cost of the project from SO2 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding),
|
||||
"The expense cost to reinvoice of the project from SO2 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding),
|
||||
"The expense invoiced amount of the project from SO2 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_2_stat['expense_cost'], precision_rounding=rounding),
|
||||
"The expense cost of the project from SO2 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_2_stat['other_revenues'], precision_rounding=rounding),
|
||||
"The other revenues of the project from SO2 should be 0.0")
|
||||
|
||||
timesheet = self._log_timesheet_user(project_so_2, 3, task_using_milestone_not_tracked)
|
||||
self.env['project.profitability.report'].flush()
|
||||
project_so_2_stat = self.env['project.profitability.report'].read_group(
|
||||
[('project_id', 'in', project_so_2.ids)],
|
||||
['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'],
|
||||
['project_id']
|
||||
)[0]
|
||||
project_so_2_timesheet_cost = timesheet.amount
|
||||
project_so_2_timesheet_sold_unit = timesheet.unit_amount
|
||||
self.assertTrue(float_is_zero(project_so_2_stat['amount_untaxed_invoiced'], precision_rounding=rounding),
|
||||
"The invoiced amount of the project from SO2 should only include downpayment not reversed")
|
||||
self.assertEqual(float_compare(project_so_2_stat['amount_untaxed_to_invoice'], milestone_to_invoice + milestone_no_task, precision_rounding=rounding), 0,
|
||||
"The amount to invoice of the project from SO2 should include the milestone to invoice minus the downpayment")
|
||||
self.assertEqual(float_compare(project_so_2_stat['timesheet_unit_amount'], project_so_2_timesheet_sold_unit, precision_rounding=rounding), 0,
|
||||
"The timesheet unit amount of the project from SO2 should include all timesheet in project")
|
||||
self.assertEqual(float_compare(project_so_2_stat['timesheet_cost'], project_so_2_timesheet_cost, precision_rounding=rounding), 0,
|
||||
"The timesheet cost of the project from SO2 should include all timesheet")
|
||||
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding),
|
||||
"The expense cost to reinvoice of the project from SO2 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding),
|
||||
"The expense invoiced amount of the project from SO2 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_2_stat['expense_cost'], precision_rounding=rounding),
|
||||
"The expense cost of the project from SO2 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_2_stat['other_revenues'], precision_rounding=rounding),
|
||||
"The other revenues of the project from SO2 should be 0.0")
|
||||
# invoice the Sales Order SO2
|
||||
context = {
|
||||
"active_model": 'sale.order',
|
||||
"active_ids": self.sale_order_2.ids,
|
||||
"active_id": self.sale_order_2.id,
|
||||
'open_invoices': True,
|
||||
'mail_notrack': True,
|
||||
}
|
||||
payment = self.env['sale.advance.payment.inv'].with_context(mail_notrack=True).create({
|
||||
'advance_payment_method': 'delivered',
|
||||
})
|
||||
action_invoice = payment.with_context(context).create_invoices()
|
||||
invoice_payment = self.env['account.move'].browse(action_invoice['res_id'])
|
||||
invoice_payment.action_post()
|
||||
self.env['project.profitability.report'].flush()
|
||||
project_so_2_stat = self.env['project.profitability.report'].read_group(
|
||||
[('project_id', 'in', project_so_2.ids)],
|
||||
['project_id', 'amount_untaxed_to_invoice', 'amount_untaxed_invoiced', 'timesheet_unit_amount', 'timesheet_cost', 'expense_cost', 'expense_amount_untaxed_to_invoice', 'expense_amount_untaxed_invoiced', 'other_revenues'],
|
||||
['project_id']
|
||||
)[0]
|
||||
self.assertEqual(float_compare(project_so_2_stat['amount_untaxed_invoiced'], milestone_to_invoice + milestone_no_task, precision_rounding=rounding), 0,
|
||||
"The invoiced amount of the project from SO2 should include the milestone invoiced")
|
||||
self.assertTrue(float_is_zero(project_so_2_stat['amount_untaxed_to_invoice'], precision_rounding=rounding),
|
||||
"The amount to invoice of the project from SO2 should include the milestone to invoice")
|
||||
self.assertEqual(float_compare(project_so_2_stat['timesheet_unit_amount'], project_so_2_timesheet_sold_unit, precision_rounding=rounding), 0,
|
||||
"The timesheet unit amount of the project from SO2 should include all timesheet in project")
|
||||
self.assertEqual(float_compare(project_so_2_stat['timesheet_cost'], project_so_2_timesheet_cost, precision_rounding=rounding), 0,
|
||||
"The timesheet cost of the project from SO2 should include all timesheet")
|
||||
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_to_invoice'], precision_rounding=rounding),
|
||||
"The expense cost to reinvoice of the project from SO2 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_2_stat['expense_amount_untaxed_invoiced'], precision_rounding=rounding),
|
||||
"The expense invoiced amount of the project from SO2 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_2_stat['expense_cost'], precision_rounding=rounding),
|
||||
"The expense cost of the project from SO2 should be 0.0")
|
||||
self.assertTrue(float_is_zero(project_so_2_stat['other_revenues'], precision_rounding=rounding),
|
||||
"The other revenues of the project from SO2 should be 0.0")
|
||||
@@ -241,6 +241,14 @@
|
||||
Re-invoiced costs
|
||||
</td>
|
||||
</tr>
|
||||
<tr t-if="display_cost & (dashboard['profit']['expense_amount_untaxed_to_invoice'] != 0)">
|
||||
<td class="o_timesheet_plan_dashboard_cell">
|
||||
<t t-esc="dashboard['profit']['expense_amount_untaxed_to_invoice']" t-options='{"widget": "monetary", "display_currency": currency}'/>
|
||||
</td>
|
||||
<td title="Costs from expenses that still need to be reinvoiced to your customer (provided that the Analytic Account of the Project was set on the Expense).">
|
||||
To re-invoice costs
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="o_timesheet_plan_dashboard_total">
|
||||
<b>
|
||||
|
||||
@@ -1270,17 +1270,16 @@ class StockMove(models.Model):
|
||||
|
||||
# Find a candidate move line to update or create a new one.
|
||||
for reserved_quant, quantity in quants:
|
||||
to_update = self.move_line_ids.filtered(lambda ml: ml._reservation_is_updatable(quantity, reserved_quant))
|
||||
to_update = next((line for line in self.move_line_ids if line._reservation_is_updatable(quantity, reserved_quant)), False)
|
||||
if to_update:
|
||||
uom_quantity = self.product_id.uom_id._compute_quantity(quantity, to_update[0].product_uom_id, rounding_method='HALF-UP')
|
||||
uom_quantity = self.product_id.uom_id._compute_quantity(quantity, to_update.product_uom_id, rounding_method='HALF-UP')
|
||||
uom_quantity = float_round(uom_quantity, precision_digits=rounding)
|
||||
uom_quantity_back_to_product_uom = to_update[0].product_uom_id._compute_quantity(uom_quantity, self.product_id.uom_id, rounding_method='HALF-UP')
|
||||
uom_quantity_back_to_product_uom = to_update.product_uom_id._compute_quantity(uom_quantity, self.product_id.uom_id, rounding_method='HALF-UP')
|
||||
if to_update and float_compare(quantity, uom_quantity_back_to_product_uom, precision_digits=rounding) == 0:
|
||||
to_update[0].with_context(bypass_reservation_update=True).product_uom_qty += uom_quantity
|
||||
to_update.with_context(bypass_reservation_update=True).product_uom_qty += uom_quantity
|
||||
else:
|
||||
if self.product_id.tracking == 'serial':
|
||||
for i in range(0, int(quantity)):
|
||||
self.env['stock.move.line'].create(self._prepare_move_line_vals(quantity=1, reserved_quant=reserved_quant))
|
||||
self.env['stock.move.line'].create([self._prepare_move_line_vals(quantity=1, reserved_quant=reserved_quant) for i in range(int(quantity))])
|
||||
else:
|
||||
self.env['stock.move.line'].create(self._prepare_move_line_vals(quantity=quantity, reserved_quant=reserved_quant))
|
||||
return taken_quantity
|
||||
@@ -1652,7 +1651,7 @@ class StockMove(models.Model):
|
||||
else:
|
||||
moves_state_to_write['confirmed'].add(move.id)
|
||||
for state, moves_ids in moves_state_to_write.items():
|
||||
self.browse(moves_ids).write({'state': state})
|
||||
self.browse(moves_ids).filtered(lambda m: m.state != state).state = state
|
||||
|
||||
@api.model
|
||||
def _consuming_picking_types(self):
|
||||
@@ -1818,5 +1817,6 @@ class StockMove(models.Model):
|
||||
for move in self:
|
||||
domains.append([('product_id', '=', move.product_id.id), ('location_id', '=', move.location_dest_id.id)])
|
||||
static_domain = [('state', 'in', ['confirmed', 'partially_available']), ('procure_method', '=', 'make_to_stock')]
|
||||
moves_to_reserve = self.env['stock.move'].search(expression.AND([static_domain, expression.OR(domains)]))
|
||||
moves_to_reserve = self.env['stock.move'].search(expression.AND([static_domain, expression.OR(domains)]),
|
||||
order='priority desc, date asc, id asc')
|
||||
moves_to_reserve._action_assign()
|
||||
@@ -77,7 +77,7 @@ class ProductionLot(models.Model):
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
self._check_create()
|
||||
return super(ProductionLot, self).create(vals_list)
|
||||
return super(ProductionLot, self.with_context(mail_create_nosubscribe=True)).create(vals_list)
|
||||
|
||||
def write(self, vals):
|
||||
if 'company_id' in vals:
|
||||
|
||||
@@ -521,7 +521,7 @@ class ProcurementGroup(models.Model):
|
||||
# Search all confirmed stock_moves and try to assign them
|
||||
domain = self._get_moves_to_assign_domain(company_id)
|
||||
moves_to_assign = self.env['stock.move'].search(domain, limit=None,
|
||||
order='priority desc, date asc')
|
||||
order='priority desc, date asc, id asc')
|
||||
for moves_chunk in split_every(100, moves_to_assign.ids):
|
||||
self.env['stock.move'].browse(moves_chunk).sudo()._action_assign()
|
||||
if use_new_cursor:
|
||||
|
||||
@@ -2092,3 +2092,50 @@ class TestStockFlow(TestStockCommon):
|
||||
# Validates the two receipts => It should raise an error as there is duplicate SN.
|
||||
with self.assertRaises(ValidationError):
|
||||
(receipt_1 | receipt_2).button_validate()
|
||||
|
||||
def test_assign_qty_to_first_move(self):
|
||||
""" Suppose two out picking waiting for an available quantity. When receiving such
|
||||
a quantity, the latter should be assign to the picking with the highest priority
|
||||
and the earliest scheduled date. """
|
||||
def create_picking(type, from_loc, to_loc, sequence=10, delay=0):
|
||||
picking = self.PickingObj.create({
|
||||
'picking_type_id': type,
|
||||
'location_id': from_loc,
|
||||
'location_dest_id': to_loc,
|
||||
})
|
||||
self.MoveObj.create({
|
||||
'name': self.productA.name,
|
||||
'sequence': sequence,
|
||||
'date': fields.Datetime.add(fields.Datetime.now(), second=delay),
|
||||
'product_id': self.productA.id,
|
||||
'product_uom_qty': 1,
|
||||
'product_uom': self.productA.uom_id.id,
|
||||
'picking_id': picking.id,
|
||||
'location_id': from_loc,
|
||||
'location_dest_id': to_loc,
|
||||
})
|
||||
picking.action_confirm()
|
||||
return picking
|
||||
|
||||
def validate_picking(picking):
|
||||
res_dict = picking.button_validate()
|
||||
wizard = Form(self.env[(res_dict.get('res_model'))].with_context(res_dict['context'])).save()
|
||||
wizard.process()
|
||||
|
||||
out01 = create_picking(self.picking_type_out, self.stock_location, self.customer_location)
|
||||
out02 = create_picking(self.picking_type_out, self.stock_location, self.customer_location, sequence=2, delay=1)
|
||||
in01 = create_picking(self.picking_type_in, self.supplier_location, self.stock_location, delay=2)
|
||||
|
||||
validate_picking(in01)
|
||||
self.assertEqual(out01.state, 'assigned')
|
||||
self.assertEqual(out02.state, 'confirmed')
|
||||
|
||||
validate_picking(out01)
|
||||
|
||||
out03 = create_picking(self.picking_type_out, self.stock_location, self.customer_location, delay=3)
|
||||
out03.priority = "1"
|
||||
in02 = create_picking(self.picking_type_in, self.supplier_location, self.stock_location, delay=4)
|
||||
|
||||
validate_picking(in02)
|
||||
self.assertEqual(out02.state, 'confirmed')
|
||||
self.assertEqual(out03.state, 'assigned')
|
||||
@@ -460,7 +460,7 @@
|
||||
<field name="name" string="Transfer" filter_domain="['|', ('name', 'ilike', self), ('origin', 'ilike', self)]"/>
|
||||
<field name="partner_id" filter_domain="[('partner_id', 'child_of', self)]"/>
|
||||
<field name="origin"/>
|
||||
<field name="product_id"/>
|
||||
<field name="product_id" filter_domain="['|', '|', ('product_id.default_code', 'ilike', self), ('product_id.name', 'ilike', self), ('product_id.barcode', 'ilike', self)]"/>
|
||||
<field name="picking_type_id"/>
|
||||
<filter name="my_transfers" string="My Transfers" domain="[('user_id', '=', uid)]"/>
|
||||
<filter string="Starred" name="starred" domain="[('priority', '=', '1')]"/>
|
||||
|
||||
@@ -1660,8 +1660,11 @@ var SnippetsMenu = Widget.extend({
|
||||
* when the DOM elements must be in an editable environment to be
|
||||
* considered (@see noCheck), this is true if the DOM elements'
|
||||
* parent must also be in an editable environment to be considered.
|
||||
* @param {string} excludeParent
|
||||
* jQuery selector that the parents of DOM elements must *not* match
|
||||
* to be considered as potential snippet.
|
||||
*/
|
||||
_computeSelectorFunctions: function (selector, exclude, target, noCheck, isChildren) {
|
||||
_computeSelectorFunctions: function (selector, exclude, target, noCheck, isChildren, excludeParent) {
|
||||
var self = this;
|
||||
|
||||
exclude += `${exclude && ', '}.o_snippet_not_selectable`;
|
||||
@@ -1675,6 +1678,12 @@ var SnippetsMenu = Widget.extend({
|
||||
return oldFilter.apply(this) && $(this).find(target).length !== 0;
|
||||
};
|
||||
}
|
||||
if (excludeParent) {
|
||||
const oldFilter = filterFunc;
|
||||
filterFunc = function () {
|
||||
return oldFilter.apply(this) && !$(this).parent().is(excludeParent);
|
||||
};
|
||||
}
|
||||
|
||||
// Prepare the functions
|
||||
var functions = {
|
||||
@@ -1735,10 +1744,12 @@ var SnippetsMenu = Widget.extend({
|
||||
this.templateOptions = [];
|
||||
var selectors = [];
|
||||
var $styles = $html.find('[data-selector]');
|
||||
const snippetAdditionDropIn = $styles.filter('#so_snippet_addition').data('drop-in');
|
||||
$styles.each(function () {
|
||||
var $style = $(this);
|
||||
var selector = $style.data('selector');
|
||||
var exclude = $style.data('exclude') || '';
|
||||
const excludeParent = $style.attr('id') === "so_content_addition" ? snippetAdditionDropIn : '';
|
||||
var target = $style.data('target');
|
||||
var noCheck = $style.data('no-check');
|
||||
var optionID = $style.data('js') || $style.data('option-name'); // used in tour js as selector
|
||||
@@ -1749,7 +1760,7 @@ var SnippetsMenu = Widget.extend({
|
||||
'base_target': target,
|
||||
'selector': self._computeSelectorFunctions(selector, exclude, target, noCheck),
|
||||
'$el': $style,
|
||||
'drop-near': $style.data('drop-near') && self._computeSelectorFunctions($style.data('drop-near'), '', false, noCheck, true),
|
||||
'drop-near': $style.data('drop-near') && self._computeSelectorFunctions($style.data('drop-near'), '', false, noCheck, true, excludeParent),
|
||||
'drop-in': $style.data('drop-in') && self._computeSelectorFunctions($style.data('drop-in'), '', false, noCheck),
|
||||
'data': _.extend({string: $style.attr('string')}, $style.data()),
|
||||
};
|
||||
|
||||
@@ -173,7 +173,7 @@ class Slide(models.Model):
|
||||
slide_resource_ids = fields.One2many('slide.slide.resource', 'slide_id', string="Additional Resource for this slide")
|
||||
slide_resource_downloadable = fields.Boolean('Allow Download', default=True, help="Allow the user to download the content of the slide.")
|
||||
mime_type = fields.Char('Mime-type')
|
||||
html_content = fields.Html("HTML Content", help="Custom HTML content for slides of type 'Web Page'.", translate=True, sanitize_form=False)
|
||||
html_content = fields.Html("HTML Content", help="Custom HTML content for slides of type 'Web Page'.", translate=True, sanitize_attributes=False, sanitize_form=False)
|
||||
# website
|
||||
website_id = fields.Many2one(related='channel_id.website_id', readonly=True)
|
||||
date_published = fields.Datetime('Publish Date', readonly=True, tracking=1)
|
||||
|
||||
Reference in new issue
Block a user