mirror of
https://gitlab.com/flectra-hq/flectra.git
synced 2026-08-17 16:54:42 -05:00
[PATCH] Upstream patch - 09042022
This commit is contained in:
@@ -3644,8 +3644,9 @@ class AccountMoveLine(models.Model):
|
||||
'discount': 0.0,
|
||||
'price_unit': amount_currency / (quantity or 1.0),
|
||||
}
|
||||
elif not discount_factor:
|
||||
# balance of line is 0, but discount == 100% so we display the normal unit_price
|
||||
elif not discount_factor or not amount_currency:
|
||||
# balance of line is 0, but discount == 100% or taxes (price included) == 100%,
|
||||
# so we display the normal unit_price
|
||||
vals = {}
|
||||
else:
|
||||
# balance is 0, so unit price is 0 as well
|
||||
@@ -4300,6 +4301,17 @@ class AccountMoveLine(models.Model):
|
||||
result.append((line.id, name))
|
||||
return result
|
||||
|
||||
@api.model
|
||||
def invalidate_cache(self, fnames=None, ids=None):
|
||||
# Invalidate cache of related moves
|
||||
if fnames is None or 'move_id' in fnames:
|
||||
field = self._fields['move_id']
|
||||
lines = self.env.cache.get_records(self, field) if ids is None else self.browse(ids)
|
||||
move_ids = {id_ for id_ in self.env.cache.get_values(lines, field) if id_}
|
||||
if move_ids:
|
||||
self.env['account.move'].invalidate_cache(ids=move_ids)
|
||||
return super().invalidate_cache(fnames=fnames, ids=ids)
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# TRACKING METHODS
|
||||
# -------------------------------------------------------------------------
|
||||
@@ -4346,6 +4358,12 @@ class AccountMoveLine(models.Model):
|
||||
|
||||
:return: A recordset of account.partial.reconcile.
|
||||
'''
|
||||
def fix_remaining_cent(currency, abs_residual, partial_amount):
|
||||
if abs_residual - currency.rounding <= partial_amount <= abs_residual + currency.rounding:
|
||||
return abs_residual
|
||||
else:
|
||||
return partial_amount
|
||||
|
||||
debit_lines = iter(self.filtered(lambda line: line.balance > 0.0 or line.amount_currency > 0.0))
|
||||
credit_lines = iter(self.filtered(lambda line: line.balance < 0.0 or line.amount_currency < 0.0))
|
||||
debit_line = None
|
||||
@@ -4436,12 +4454,22 @@ class AccountMoveLine(models.Model):
|
||||
credit_line.company_id,
|
||||
credit_line.date,
|
||||
)
|
||||
min_debit_amount_residual_currency = fix_remaining_cent(
|
||||
debit_line.currency_id,
|
||||
debit_amount_residual_currency,
|
||||
min_debit_amount_residual_currency,
|
||||
)
|
||||
min_credit_amount_residual_currency = debit_line.company_currency_id._convert(
|
||||
min_amount_residual,
|
||||
credit_line.currency_id,
|
||||
debit_line.company_id,
|
||||
debit_line.date,
|
||||
)
|
||||
min_credit_amount_residual_currency = fix_remaining_cent(
|
||||
credit_line.currency_id,
|
||||
-credit_amount_residual_currency,
|
||||
min_credit_amount_residual_currency,
|
||||
)
|
||||
|
||||
debit_amount_residual -= min_amount_residual
|
||||
debit_amount_residual_currency -= min_debit_amount_residual_currency
|
||||
|
||||
@@ -646,3 +646,18 @@ class TestAccountMove(AccountTestInvoicingCommon):
|
||||
'credit': value['debit'],
|
||||
})
|
||||
self.assertRecordValues(reversed_caba_move.line_ids, expected_values)
|
||||
|
||||
def _get_cache_count(self, model_name='account.move', field_name='name'):
|
||||
model = self.env[model_name]
|
||||
field = model._fields[field_name]
|
||||
return len(self.env.cache.get_records(model, field))
|
||||
|
||||
def test_cache_invalidation(self):
|
||||
self.env['account.move'].invalidate_cache()
|
||||
lines = self.test_move.line_ids
|
||||
# prefetch
|
||||
lines.mapped('move_id.name')
|
||||
# check account.move cache
|
||||
self.assertEqual(self._get_cache_count(), 1)
|
||||
self.env['account.move.line'].invalidate_cache(ids=lines.ids)
|
||||
self.assertEqual(self._get_cache_count(), 0)
|
||||
|
||||
@@ -1192,6 +1192,39 @@ class TestAccountMoveReconcile(AccountTestInvoicingCommon):
|
||||
},
|
||||
])
|
||||
|
||||
def test_reconcile_rounding_issue(self):
|
||||
rate = 1/1.5289
|
||||
currency = self.setup_multi_currency_data(default_values={
|
||||
'name': 'XXX',
|
||||
'symbol': 'XXX',
|
||||
'currency_unit_label': 'XX',
|
||||
'currency_subunit_label': 'X',
|
||||
'rounding': 0.01,
|
||||
}, rate2016=rate, rate2017=rate)['currency']
|
||||
|
||||
# Create an invoice 26.45 XXX = 40.43 USD
|
||||
invoice = self.env['account.move'].create({
|
||||
'move_type': 'out_invoice',
|
||||
'partner_id': self.partner_a.id,
|
||||
'currency_id': currency.id,
|
||||
'date': '2017-01-01',
|
||||
'invoice_date': '2017-01-01',
|
||||
'invoice_line_ids': [(0, 0, {
|
||||
'product_id': self.product_a.id,
|
||||
'price_unit': 23.0,
|
||||
'tax_ids': [(6, 0, self.company_data['default_tax_sale'].ids)],
|
||||
})],
|
||||
})
|
||||
invoice.action_post()
|
||||
|
||||
# Pay it with 100.0 USD
|
||||
self.env['account.payment.register']\
|
||||
.with_context(active_model='account.move', active_ids=invoice.ids)\
|
||||
.create({'amount': 100.0, 'currency_id': self.company_data['currency'].id})\
|
||||
._create_payments()
|
||||
|
||||
self.assertTrue(invoice.payment_state in ('in_payment', 'paid'))
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Test creation of extra journal entries during the reconciliation to
|
||||
# deal with taxes that are exigible on payment (cash basis).
|
||||
|
||||
@@ -1012,9 +1012,6 @@ class TestReconciliationExec(TestAccountReconciliationCommon):
|
||||
self.assertEqual(inv1_receivable.full_reconcile_id, inv2_receivable.full_reconcile_id)
|
||||
self.assertEqual(inv1_receivable.full_reconcile_id, payment_receivable.full_reconcile_id)
|
||||
|
||||
exchange_rcv = inv1_receivable.full_reconcile_id.exchange_move_id.line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable')
|
||||
self.assertEqual(exchange_rcv.amount_currency, 0.01)
|
||||
|
||||
self.assertTrue(inv1.payment_state in ('in_payment', 'paid'), "Invoice should be paid")
|
||||
self.assertEqual(inv2.payment_state, 'paid')
|
||||
|
||||
@@ -1082,14 +1079,6 @@ class TestReconciliationExec(TestAccountReconciliationCommon):
|
||||
self.assertEqual(inv1_receivable.full_reconcile_id, inv2_receivable.full_reconcile_id)
|
||||
self.assertEqual(inv1_receivable.full_reconcile_id, payment_receivable.full_reconcile_id)
|
||||
|
||||
# Before saas-13.4, there was no exchange difference entry generated because the amount was
|
||||
# wrongly converted in the _amount_residual method at the invoice date like this:
|
||||
# 315.15 * (600.0 / 540.25) = 515.15 * 1.110596946 = 350.004627487 ~= 350.0
|
||||
# Now, the conversion is made using the payment rate using the _convert method and the
|
||||
# encoded currency rate:
|
||||
# 315.15 * 1.1106 = 350.00559 ~= 350.01
|
||||
self.assertTrue(inv1_receivable.full_reconcile_id.exchange_move_id)
|
||||
|
||||
self.assertTrue(inv1.payment_state in ('in_payment', 'paid'), "Invoice should be paid")
|
||||
self.assertEqual(inv2.payment_state, 'paid')
|
||||
|
||||
@@ -1202,7 +1191,4 @@ class TestReconciliationExec(TestAccountReconciliationCommon):
|
||||
self.assertEqual(inv1_receivable.full_reconcile_id, payment_receivable.full_reconcile_id)
|
||||
self.assertEqual(move_balance_receiv.full_reconcile_id, inv1_receivable.full_reconcile_id)
|
||||
|
||||
exchange_rcv = inv1_receivable.full_reconcile_id.exchange_move_id.line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable')
|
||||
self.assertEqual(exchange_rcv.amount_currency, 0.01)
|
||||
|
||||
self.assertTrue(inv1.payment_state in ('in_payment', 'paid'), "Invoice should be paid")
|
||||
|
||||
@@ -77,12 +77,6 @@ class AccountJournal(models.Model):
|
||||
rec._create_check_sequence()
|
||||
return rec
|
||||
|
||||
@api.returns('self', lambda value: value.id)
|
||||
def copy(self, default=None):
|
||||
rec = super(AccountJournal, self).copy(default)
|
||||
rec._create_check_sequence()
|
||||
return rec
|
||||
|
||||
def _create_check_sequence(self):
|
||||
""" Create a check sequence for the journal """
|
||||
for journal in self:
|
||||
|
||||
@@ -74,7 +74,7 @@ class HrExpense(models.Model):
|
||||
domain="[('company_id', '=', company_id), ('type_tax_use', '=', 'purchase')]", string='Taxes')
|
||||
untaxed_amount = fields.Float("Subtotal", store=True, compute='_compute_amount', digits='Account')
|
||||
total_amount = fields.Monetary("Total", compute='_compute_amount', store=True, currency_field='currency_id', tracking=True)
|
||||
amount_residual = fields.Monetary(string='Amount Due', compute='_compute_amount_residual')
|
||||
amount_residual = fields.Monetary(string='Amount Due', compute='_compute_amount_residual', compute_sudo=True)
|
||||
company_currency_id = fields.Many2one('res.currency', string="Report Company Currency", related='sheet_id.currency_id', store=True, readonly=False)
|
||||
total_amount_company = fields.Monetary("Total (Company Currency)", compute='_compute_total_amount_company', store=True, currency_field='company_currency_id')
|
||||
company_id = fields.Many2one('res.company', string='Company', required=True, readonly=True, states={'draft': [('readonly', False)], 'refused': [('readonly', False)]}, default=lambda self: self.env.company)
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import logging
|
||||
import math
|
||||
|
||||
from collections import namedtuple
|
||||
from collections import defaultdict, namedtuple
|
||||
|
||||
from datetime import datetime, date, timedelta, time
|
||||
from dateutil.rrule import rrule, DAILY
|
||||
@@ -884,20 +884,24 @@ class HolidaysRequest(models.Model):
|
||||
holidays = self.filtered(lambda request: request.holiday_type == 'employee')
|
||||
holidays._create_resource_leave()
|
||||
meeting_holidays = holidays.filtered(lambda l: l.holiday_status_id.create_calendar_meeting)
|
||||
meetings = self.env['calendar.event']
|
||||
if meeting_holidays:
|
||||
meeting_values = meeting_holidays._prepare_holidays_meeting_values()
|
||||
meetings = self.env['calendar.event'].with_context(
|
||||
no_mail_to_attendees=True,
|
||||
active_model=self._name
|
||||
).create(meeting_values)
|
||||
for holiday, meeting in zip(meeting_holidays, meetings):
|
||||
holiday.meeting_id = meeting
|
||||
meeting_values_for_user_id = meeting_holidays._prepare_holidays_meeting_values()
|
||||
for user_id, meeting_values in meeting_values_for_user_id.items():
|
||||
meetings += self.env['calendar.event'].with_user(user_id or self.env.uid).with_context(
|
||||
no_mail_to_attendees=True,
|
||||
active_model=self._name
|
||||
).create(meeting_values)
|
||||
Holiday = self.env['hr.leave']
|
||||
for meeting in meetings:
|
||||
Holiday.browse(meeting.res_id).meeting_id = meeting
|
||||
|
||||
def _prepare_holidays_meeting_values(self):
|
||||
result = []
|
||||
result = defaultdict(list)
|
||||
company_calendar = self.env.company.resource_calendar_id
|
||||
for holiday in self:
|
||||
calendar = holiday.employee_id.resource_calendar_id or company_calendar
|
||||
user = holiday.user_id
|
||||
if holiday.leave_type_request_unit == 'hour':
|
||||
meeting_name = _("%s on Time Off : %.2f hour(s)") % (holiday.employee_id.name or holiday.category_id.name, holiday.number_of_hours_display)
|
||||
else:
|
||||
@@ -906,19 +910,19 @@ class HolidaysRequest(models.Model):
|
||||
'name': meeting_name,
|
||||
'duration': holiday.number_of_days * (calendar.hours_per_day or HOURS_PER_DAY),
|
||||
'description': holiday.notes,
|
||||
'user_id': holiday.user_id.id,
|
||||
'user_id': user.id,
|
||||
'start': holiday.date_from,
|
||||
'stop': holiday.date_to,
|
||||
'allday': False,
|
||||
'privacy': 'confidential',
|
||||
'event_tz': holiday.user_id.tz,
|
||||
'event_tz': user.tz,
|
||||
'activity_ids': [(5, 0, 0)],
|
||||
}
|
||||
# Add the partner_id (if exist) as an attendee
|
||||
if holiday.user_id and holiday.user_id.partner_id:
|
||||
if user and user.partner_id:
|
||||
meeting_values['partner_ids'] = [
|
||||
(4, holiday.user_id.partner_id.id)]
|
||||
result.append(meeting_values)
|
||||
(4, user.partner_id.id)]
|
||||
result[user.id].append(meeting_values)
|
||||
return result
|
||||
|
||||
# YTI TODO: Remove me in master
|
||||
|
||||
@@ -327,7 +327,7 @@ class AccountFrFec(models.TransientModel):
|
||||
ELSE REGEXP_REPLACE(replace(am.ref, '|', '/'), '[\\t\\r\\n]', ' ', 'g')
|
||||
END
|
||||
AS PieceRef,
|
||||
TO_CHAR(am.date, 'YYYYMMDD') AS PieceDate,
|
||||
TO_CHAR(COALESCE(am.invoice_date, am.date), 'YYYYMMDD') AS PieceDate,
|
||||
CASE WHEN aml.name IS NULL OR aml.name = '' THEN '/'
|
||||
WHEN aml.name SIMILAR TO '[\\t|\\s|\\n]*' THEN '/'
|
||||
ELSE REGEXP_REPLACE(replace(aml.name, '|', '/'), '[\\t\\n\\r]', ' ', 'g') END AS EcritureLib,
|
||||
|
||||
@@ -30,8 +30,22 @@ class AccountEdiFormat(models.Model):
|
||||
'''Returns a name conform to the Fattura pa Specifications:
|
||||
See ES documentation 2.2
|
||||
'''
|
||||
a = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
n = invoice.id
|
||||
a = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
||||
# Each company should have its own filename sequence. If it does not exist, create it
|
||||
n = self.env['ir.sequence'].with_company(invoice.company_id).next_by_code('l10n_it_edi.fattura_filename')
|
||||
if not n:
|
||||
# The offset is used to avoid conflicts with existing filenames
|
||||
offset = 62 ** 4
|
||||
sequence = self.env['ir.sequence'].sudo().create({
|
||||
'name': 'FatturaPA Filename Sequence',
|
||||
'code': 'l10n_it_edi.fattura_filename',
|
||||
'company_id': invoice.company_id.id,
|
||||
'number_next': offset,
|
||||
})
|
||||
n = sequence._next()
|
||||
# The n is returned as a string, but we require an int
|
||||
n = int(''.join(filter(lambda c: c.isdecimal(), n)))
|
||||
|
||||
progressive_number = ""
|
||||
while n:
|
||||
(n, m) = divmod(n, len(a))
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
<p:FatturaElettronica xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://ivaservizi.agenziaentrate.gov.it/docs/xsd/fatture/v1.2" xsi:schemaLocation="http://ivaservizi.agenziaentrate.gov.it/docs/xsd/fatture/v1.2 http://www.fatturapa.gov.it/export/fatturazione/sdi/fatturapa/v1.2/Schema_del_file_xml_FatturaPA_versione_1.2.xsd" versione="FPR12">
|
||||
<FatturaElettronicaHeader>
|
||||
<DatiTrasmissione>
|
||||
<IdTrasmittente>
|
||||
<IdPaese>IT</IdPaese>
|
||||
<IdCodice>01234560157</IdCodice>
|
||||
</IdTrasmittente>
|
||||
<ProgressivoInvio>___ignore___</ProgressivoInvio>
|
||||
<FormatoTrasmissione>FPR12</FormatoTrasmissione>
|
||||
<CodiceDestinatario>0000000</CodiceDestinatario>
|
||||
<ContattiTrasmittente>
|
||||
</ContattiTrasmittente>
|
||||
</DatiTrasmissione>
|
||||
<CedentePrestatore>
|
||||
<DatiAnagrafici>
|
||||
<IdFiscaleIVA>
|
||||
<IdPaese>IT</IdPaese>
|
||||
<IdCodice>01234560157</IdCodice>
|
||||
</IdFiscaleIVA>
|
||||
<CodiceFiscale>01234560157</CodiceFiscale>
|
||||
<Anagrafica>
|
||||
<Denominazione>company_2_data</Denominazione>
|
||||
</Anagrafica>
|
||||
</DatiAnagrafici>
|
||||
<Sede>
|
||||
<Indirizzo> </Indirizzo>
|
||||
<CAP>00000</CAP>
|
||||
</Sede>
|
||||
</CedentePrestatore>
|
||||
<CessionarioCommittente>
|
||||
<DatiAnagrafici>
|
||||
<IdFiscaleIVA>
|
||||
<IdPaese>IT</IdPaese>
|
||||
<IdCodice>00465840031</IdCodice>
|
||||
</IdFiscaleIVA>
|
||||
<Anagrafica>
|
||||
<Nome>Alessi</Nome>
|
||||
<Cognome></Cognome>
|
||||
</Anagrafica>
|
||||
</DatiAnagrafici>
|
||||
<Sede>
|
||||
<Indirizzo>Via Privata Alessi 6 </Indirizzo>
|
||||
<CAP>28887</CAP>
|
||||
<Nazione>IT</Nazione>
|
||||
</Sede>
|
||||
</CessionarioCommittente>
|
||||
</FatturaElettronicaHeader>
|
||||
<FatturaElettronicaBody>
|
||||
<DatiGenerali>
|
||||
<DatiGeneraliDocumento>
|
||||
<TipoDocumento>TD01</TipoDocumento>
|
||||
<Divisa>EUR</Divisa>
|
||||
<Data>2022-03-24</Data>
|
||||
<Numero>___ignore___</Numero>
|
||||
</DatiGeneraliDocumento>
|
||||
</DatiGenerali>
|
||||
<DatiBeniServizi>
|
||||
</DatiBeniServizi>
|
||||
<DatiPagamento>
|
||||
<CondizioniPagamento>TP02</CondizioniPagamento>
|
||||
<DettaglioPagamento>
|
||||
<DataScadenzaPagamento>2022-03-24</DataScadenzaPagamento>
|
||||
<ImportoPagamento></ImportoPagamento>
|
||||
<CodicePagamento>___ignore___</CodicePagamento>
|
||||
</DettaglioPagamento>
|
||||
</DatiPagamento>
|
||||
</FatturaElettronicaBody>
|
||||
</p:FatturaElettronica>
|
||||
@@ -5,15 +5,17 @@ import datetime
|
||||
import logging
|
||||
from collections import namedtuple
|
||||
from unittest.mock import patch
|
||||
import freezegun
|
||||
from lxml import etree
|
||||
from freezegun import freeze_time
|
||||
|
||||
from flectra import tools
|
||||
from flectra.tests import tagged
|
||||
from flectra.addons.account_edi.tests.common import AccountEdiTestCommon
|
||||
from flectra.addons.l10n_it_edi.tools.remove_signature import remove_signature
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@tagged('post_install_l10n', 'post_install', '-at_install')
|
||||
class PecMailServerTests(AccountEdiTestCommon):
|
||||
""" Main test class for the l10n_it_edi vendor bills XML import from a PEC mail account"""
|
||||
|
||||
@@ -49,7 +51,8 @@ class PecMailServerTests(AccountEdiTestCommon):
|
||||
cls.company = cls.company_data_2['company']
|
||||
|
||||
# Initialize the company's codice fiscale
|
||||
cls.company.l10n_it_codice_fiscale = 'IT01234560157'
|
||||
cls.company.l10n_it_codice_fiscale = '01234560157'
|
||||
cls.company.vat = 'IT01234560157'
|
||||
|
||||
# Build test data.
|
||||
# invoice_filename1 is used for vendor bill receipts tests
|
||||
@@ -82,6 +85,99 @@ class PecMailServerTests(AccountEdiTestCommon):
|
||||
'server_type': 'imap',
|
||||
'l10n_it_is_pec': True})
|
||||
|
||||
cls.price_included_tax = cls.env['account.tax'].create({
|
||||
'name': '22% price included tax',
|
||||
'amount': 22.0,
|
||||
'amount_type': 'percent',
|
||||
'price_include': True,
|
||||
'include_base_amount': True,
|
||||
'company_id': cls.company.id,
|
||||
})
|
||||
|
||||
cls.italian_partner_a = cls.env['res.partner'].create({
|
||||
'name': 'Alessi',
|
||||
'vat': 'IT00465840031',
|
||||
'l10n_it_codice_fiscale': '00465840031',
|
||||
'country_id': cls.env.ref('base.it').id,
|
||||
'street': 'Via Privata Alessi 6',
|
||||
'zip': '28887',
|
||||
'company_id': cls.company.id,
|
||||
})
|
||||
|
||||
cls.standard_line = {
|
||||
'name': 'standard_line',
|
||||
'quantity': 1,
|
||||
'price_unit': 800.40,
|
||||
'tax_ids': [(6, 0, [cls.company.account_sale_tax_id.id])]
|
||||
}
|
||||
|
||||
cls.price_included_invoice = cls.env['account.move'].with_company(cls.company).create({
|
||||
'move_type': 'out_invoice',
|
||||
'invoice_date': datetime.date(2022, 3, 24),
|
||||
'partner_id': cls.italian_partner_a.id,
|
||||
'invoice_line_ids': [
|
||||
(0, 0, {
|
||||
**cls.standard_line,
|
||||
'name': 'something price included',
|
||||
'tax_ids': [(6, 0, [cls.price_included_tax.id])]
|
||||
}),
|
||||
(0, 0, {
|
||||
**cls.standard_line,
|
||||
'name': 'something else price included',
|
||||
'tax_ids': [(6, 0, [cls.price_included_tax.id])]
|
||||
}),
|
||||
(0, 0, {
|
||||
**cls.standard_line,
|
||||
'name': 'something not price included',
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
cls.partial_discount_invoice = cls.env['account.move'].with_company(cls.company).create({
|
||||
'move_type': 'out_invoice',
|
||||
'invoice_date': datetime.date(2022, 3, 24),
|
||||
'partner_id': cls.italian_partner_a.id,
|
||||
'invoice_line_ids': [
|
||||
(0, 0, {
|
||||
**cls.standard_line,
|
||||
'name': 'no discount',
|
||||
}),
|
||||
(0, 0, {
|
||||
**cls.standard_line,
|
||||
'name': 'special discount',
|
||||
'discount': 50,
|
||||
}),
|
||||
(0, 0, {
|
||||
**cls.standard_line,
|
||||
'name': "an offer you can't refuse",
|
||||
'discount': 100,
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
cls.full_discount_invoice = cls.env['account.move'].with_company(cls.company).create({
|
||||
'move_type': 'out_invoice',
|
||||
'invoice_date': datetime.date(2022, 3, 24),
|
||||
'partner_id': cls.italian_partner_a.id,
|
||||
'invoice_line_ids': [
|
||||
(0, 0, {
|
||||
**cls.standard_line,
|
||||
'name': 'nothing shady just a gift for my friend',
|
||||
'discount': 100,
|
||||
}),
|
||||
],
|
||||
})
|
||||
# post the invoices
|
||||
cls.price_included_invoice._post()
|
||||
cls.partial_discount_invoice._post()
|
||||
cls.full_discount_invoice._post()
|
||||
|
||||
cls.test_invoice_xmls = {k: cls._get_test_file_content(v) for k, v in [
|
||||
('normal_1', 'IT01234567890_FPR01.xml'),
|
||||
('signed', 'IT01234567890_FPR01.xml.p7m'),
|
||||
('export_basis', 'IT00470550013_basis.xml'),
|
||||
]}
|
||||
|
||||
@classmethod
|
||||
def _get_test_file_content(cls, filename):
|
||||
""" Get the content of a test file inside this module """
|
||||
@@ -109,7 +205,7 @@ class PecMailServerTests(AccountEdiTestCommon):
|
||||
|
||||
def test_receive_signed_vendor_bill(self):
|
||||
""" Test a signed (P7M) sample e-invoice file from https://www.fatturapa.gov.it/export/documenti/fatturapa/v1.2/IT01234567890_FPR01.xml """
|
||||
with freezegun.freeze_time('2020-04-06'):
|
||||
with freeze_time('2020-04-06'):
|
||||
invoices = self._create_invoice(self.signed_invoice_content, self.signed_invoice_filename)
|
||||
self.assertRecordValues(invoices, [{
|
||||
'company_id': self.company.id,
|
||||
@@ -158,3 +254,173 @@ class PecMailServerTests(AccountEdiTestCommon):
|
||||
def test_decorrenza_termini(self):
|
||||
""" Test a receipt adapted from https://www.fatturapa.gov.it/export/documenti/messaggi/v1.0/IT01234567890_11111_DT_001.xml """
|
||||
self._test_receipt('DT', 'delivered', 'delivered_expired')
|
||||
|
||||
@freeze_time('2020-03-24')
|
||||
def test_price_included_taxes(self):
|
||||
""" When the tax is price included, there should be a rounding value added to the xml, if the sum(subtotals) * tax_rate is not
|
||||
equal to taxable base * tax rate (there is a constraint in the edi where taxable base * tax rate = tax amount, but also
|
||||
taxable base = sum(subtotals) + rounding amount)
|
||||
"""
|
||||
|
||||
# In this case, the first two lines use a price_include tax the
|
||||
# subtotals should be 800.40 / (100 + 22.0) * 100 = 656.065564..,
|
||||
# where 22.0 is the tax rate.
|
||||
#
|
||||
# Since the subtotals are rounded we actually have 656.07
|
||||
lines = self.price_included_invoice.line_ids
|
||||
price_included_lines = lines.filtered(lambda line: line.tax_ids == self.price_included_tax)
|
||||
self.assertEqual([line.price_subtotal for line in price_included_lines], [656.07, 656.07])
|
||||
# So the taxable a base the edi expects (for this tax) is actually 1312.14
|
||||
price_included_tax_line = lines.filtered(lambda line: line.tax_line_id == self.price_included_tax)
|
||||
self.assertEqual(price_included_tax_line.tax_base_amount, 1312.14)
|
||||
|
||||
# The tax amount of the price included tax should be:
|
||||
# per line: 800.40 - (800.40 / (100 + 22) * 100) = 144.33
|
||||
# tax amount: 144.33 * 2 = 288.66
|
||||
self.assertEqual(price_included_tax_line.price_total, 288.66)
|
||||
|
||||
expected_etree = self.with_applied_xpath(
|
||||
etree.fromstring(self.test_invoice_xmls['export_basis']),
|
||||
'''
|
||||
<xpath expr="//FatturaElettronicaBody//DatiBeniServizi" position="replace">
|
||||
<DatiBeniServizi>
|
||||
<DettaglioLinee>
|
||||
<NumeroLinea>1</NumeroLinea>
|
||||
<Descrizione>something price included</Descrizione>
|
||||
<Quantita>1.00</Quantita>
|
||||
<PrezzoUnitario>656.070000</PrezzoUnitario>
|
||||
<PrezzoTotale>656.07</PrezzoTotale>
|
||||
<AliquotaIVA>22.00</AliquotaIVA>
|
||||
</DettaglioLinee>
|
||||
<DettaglioLinee>
|
||||
<NumeroLinea>2</NumeroLinea>
|
||||
<Descrizione>something else price included</Descrizione>
|
||||
<Quantita>1.00</Quantita>
|
||||
<PrezzoUnitario>656.070000</PrezzoUnitario>
|
||||
<PrezzoTotale>656.07</PrezzoTotale>
|
||||
<AliquotaIVA>22.00</AliquotaIVA>
|
||||
</DettaglioLinee>
|
||||
<DettaglioLinee>
|
||||
<NumeroLinea>3</NumeroLinea>
|
||||
<Descrizione>something not price included</Descrizione>
|
||||
<Quantita>1.00</Quantita>
|
||||
<PrezzoUnitario>800.400000</PrezzoUnitario>
|
||||
<PrezzoTotale>800.40</PrezzoTotale>
|
||||
<AliquotaIVA>22.00</AliquotaIVA>
|
||||
</DettaglioLinee>
|
||||
<DatiRiepilogo>
|
||||
<AliquotaIVA>22.00</AliquotaIVA>
|
||||
<Arrotondamento>-0.04909091</Arrotondamento>
|
||||
<ImponibileImporto>1312.09</ImponibileImporto>
|
||||
<Imposta>288.66</Imposta>
|
||||
<EsigibilitaIVA>I</EsigibilitaIVA>
|
||||
</DatiRiepilogo>
|
||||
<DatiRiepilogo>
|
||||
<AliquotaIVA>22.00</AliquotaIVA>
|
||||
<ImponibileImporto>800.40</ImponibileImporto>
|
||||
<Imposta>176.09</Imposta>
|
||||
<EsigibilitaIVA>I</EsigibilitaIVA>
|
||||
</DatiRiepilogo>
|
||||
</DatiBeniServizi>
|
||||
</xpath>
|
||||
<xpath expr="//DettaglioPagamento//ImportoPagamento" position="inside">
|
||||
2577.29
|
||||
</xpath>
|
||||
''')
|
||||
invoice_etree = etree.fromstring(self.price_included_invoice._export_as_xml())
|
||||
# Remove the attachment and its details
|
||||
invoice_etree = self.with_applied_xpath(invoice_etree, "<xpath expr='.//Allegati' position='replace'/>")
|
||||
self.assertXmlTreeEqual(invoice_etree, expected_etree)
|
||||
|
||||
@freeze_time('2020-03-24')
|
||||
def test_partially_discounted_invoice(self):
|
||||
# The EDI can account for discounts, but a line with, for example, a 100% discount should still have
|
||||
# a corresponding tax with a base amount of 0
|
||||
|
||||
invoice_etree = etree.fromstring(self.partial_discount_invoice._export_as_xml())
|
||||
expected_etree = self.with_applied_xpath(
|
||||
etree.fromstring(self.test_invoice_xmls['export_basis']),
|
||||
'''
|
||||
<xpath expr="//FatturaElettronicaBody//DatiBeniServizi" position="replace">
|
||||
<DatiBeniServizi>
|
||||
<DettaglioLinee>
|
||||
<NumeroLinea>1</NumeroLinea>
|
||||
<Descrizione>no discount</Descrizione>
|
||||
<Quantita>1.00</Quantita>
|
||||
<PrezzoUnitario>800.400000</PrezzoUnitario>
|
||||
<PrezzoTotale>800.40</PrezzoTotale>
|
||||
<AliquotaIVA>22.00</AliquotaIVA>
|
||||
</DettaglioLinee>
|
||||
<DettaglioLinee>
|
||||
<NumeroLinea>2</NumeroLinea>
|
||||
<Descrizione>special discount</Descrizione>
|
||||
<Quantita>1.00</Quantita>
|
||||
<PrezzoUnitario>800.400000</PrezzoUnitario>
|
||||
<ScontoMaggiorazione>
|
||||
<Tipo>SC</Tipo>
|
||||
<Percentuale>50.00</Percentuale>
|
||||
</ScontoMaggiorazione>
|
||||
<PrezzoTotale>400.20</PrezzoTotale>
|
||||
<AliquotaIVA>22.00</AliquotaIVA>
|
||||
</DettaglioLinee>
|
||||
<DettaglioLinee>
|
||||
<NumeroLinea>3</NumeroLinea>
|
||||
<Descrizione>an offer you can't refuse</Descrizione>
|
||||
<Quantita>1.00</Quantita>
|
||||
<PrezzoUnitario>800.400000</PrezzoUnitario>
|
||||
<ScontoMaggiorazione>
|
||||
<Tipo>SC</Tipo>
|
||||
<Percentuale>100.00</Percentuale>
|
||||
</ScontoMaggiorazione>
|
||||
<PrezzoTotale>0.00</PrezzoTotale>
|
||||
<AliquotaIVA>22.00</AliquotaIVA>
|
||||
</DettaglioLinee>
|
||||
<DatiRiepilogo>
|
||||
<AliquotaIVA>22.00</AliquotaIVA>
|
||||
<ImponibileImporto>1200.60</ImponibileImporto>
|
||||
<Imposta>264.13</Imposta>
|
||||
<EsigibilitaIVA>I</EsigibilitaIVA>
|
||||
</DatiRiepilogo>
|
||||
</DatiBeniServizi>
|
||||
</xpath>
|
||||
<xpath expr="//DettaglioPagamento//ImportoPagamento" position="inside">
|
||||
1464.73
|
||||
</xpath>
|
||||
''')
|
||||
invoice_etree = self.with_applied_xpath(invoice_etree, "<xpath expr='.//Allegati' position='replace'/>")
|
||||
self.assertXmlTreeEqual(invoice_etree, expected_etree)
|
||||
|
||||
@freeze_time('2020-03-24')
|
||||
def test_fully_discounted_inovice(self):
|
||||
invoice_etree = etree.fromstring(self.full_discount_invoice._export_as_xml())
|
||||
expected_etree = self.with_applied_xpath(
|
||||
etree.fromstring(self.test_invoice_xmls['export_basis']),
|
||||
'''
|
||||
<xpath expr="//FatturaElettronicaBody//DatiBeniServizi" position="replace">
|
||||
<DatiBeniServizi>
|
||||
<DettaglioLinee>
|
||||
<NumeroLinea>1</NumeroLinea>
|
||||
<Descrizione>nothing shady just a gift for my friend</Descrizione>
|
||||
<Quantita>1.00</Quantita>
|
||||
<PrezzoUnitario>800.400000</PrezzoUnitario>
|
||||
<ScontoMaggiorazione>
|
||||
<Tipo>SC</Tipo>
|
||||
<Percentuale>100.00</Percentuale>
|
||||
</ScontoMaggiorazione>
|
||||
<PrezzoTotale>0.00</PrezzoTotale>
|
||||
<AliquotaIVA>22.00</AliquotaIVA>
|
||||
</DettaglioLinee>
|
||||
<DatiRiepilogo>
|
||||
<AliquotaIVA>22.00</AliquotaIVA>
|
||||
<ImponibileImporto>0.00</ImponibileImporto>
|
||||
<Imposta>0.00</Imposta>
|
||||
<EsigibilitaIVA>I</EsigibilitaIVA>
|
||||
</DatiRiepilogo>
|
||||
</DatiBeniServizi>
|
||||
</xpath>
|
||||
<xpath expr="//DettaglioPagamento//ImportoPagamento" position="inside">
|
||||
0.00
|
||||
</xpath>
|
||||
''')
|
||||
invoice_etree = self.with_applied_xpath(invoice_etree, "<xpath expr='.//Allegati' position='replace'/>")
|
||||
self.assertXmlTreeEqual(invoice_etree, expected_etree)
|
||||
|
||||
@@ -191,6 +191,14 @@ msgid ""
|
||||
" it cannot change."
|
||||
msgstr ""
|
||||
|
||||
#. module: l10n_it_edi_sdicoop
|
||||
#: code:addons/l10n_it_edi_sdicoop/models/account_edi_format.py:0
|
||||
#, python-format
|
||||
msgid ""
|
||||
"The filename is duplicated. Try again (or adjust the FatturaPA Filename "
|
||||
"sequence). Original message from the SDI: %s"
|
||||
msgstr ""
|
||||
|
||||
#. module: l10n_it_edi_sdicoop
|
||||
#: code:addons/l10n_it_edi_sdicoop/models/account_edi_format.py:0
|
||||
#, python-format
|
||||
@@ -236,6 +244,15 @@ msgstr ""
|
||||
msgid "The invoice was refused by the addressee."
|
||||
msgstr ""
|
||||
|
||||
#. module: l10n_it_edi_sdicoop
|
||||
#: code:addons/l10n_it_edi_sdicoop/models/account_edi_format.py:0
|
||||
#: code:addons/l10n_it_edi_sdicoop/models/account_edi_format.py:0
|
||||
#, python-format
|
||||
msgid ""
|
||||
"The invoice was sent to FatturaPA, but we are still awaiting a response. "
|
||||
"Click the link above to check for an update."
|
||||
msgstr ""
|
||||
|
||||
#. module: l10n_it_edi_sdicoop
|
||||
#: code:addons/l10n_it_edi_sdicoop/models/account_edi_format.py:0
|
||||
#, python-format
|
||||
|
||||
@@ -177,7 +177,7 @@ class AccountEdiFormat(models.Model):
|
||||
if 'id_transaction' in response:
|
||||
invoice.l10n_it_edi_transaction = response['id_transaction']
|
||||
to_return[invoice].update({
|
||||
'error': _('The invoice was successfully transmitted to the Public Administration and we are waiting for confirmation.'),
|
||||
'error': _('The invoice was sent to FatturaPA, but we are still awaiting a response. Click the link above to check for an update.'),
|
||||
'blocking_level': 'info',
|
||||
})
|
||||
return to_return
|
||||
@@ -214,7 +214,7 @@ class AccountEdiFormat(models.Model):
|
||||
state = response['state']
|
||||
if state == 'awaiting_outcome':
|
||||
to_return[invoice] = {
|
||||
'error': _('The invoice was successfully transmitted to the Public Administration and we are waiting for confirmation'),
|
||||
'error': _('The invoice was sent to FatturaPA, but we are still awaiting a response. Click the link above to check for an update.'),
|
||||
'blocking_level': 'info',
|
||||
}
|
||||
proxy_acks.append(id_transaction)
|
||||
@@ -251,6 +251,13 @@ class AccountEdiFormat(models.Model):
|
||||
' Original message from the SDI: %s', errors[idx]))
|
||||
to_return[invoice] = {'attachment': invoice.l10n_it_edi_attachment_id, 'success': True}
|
||||
else:
|
||||
# Add helpful text if duplicated filename error
|
||||
if '00002' in error_codes:
|
||||
idx = error_codes.index('00002')
|
||||
errors[idx] = _(
|
||||
'The filename is duplicated. Try again (or adjust the FatturaPA Filename sequence).'
|
||||
' Original message from the SDI: %s', [errors[idx]]
|
||||
)
|
||||
to_return[invoice] = {'error': self._format_error_message(_('The invoice has been refused by the Exchange System'), errors), 'blocking_level': 'error'}
|
||||
invoice.l10n_it_edi_transaction = False
|
||||
elif state == 'notificaMancataConsegna':
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
<field name="name">[BOX 7] The amount in Box 6 is subracted from Box 5</field>
|
||||
<field name="parent_id" ref="tax_report_sale_and_income"/>
|
||||
<field name="sequence" eval="3"/>
|
||||
<field name="code">NZBOX7</field>
|
||||
<field name="formula">NZBOX5 - NZBOX6</field>
|
||||
<field name="report_id" ref="tax_report"/>
|
||||
</record>
|
||||
@@ -42,7 +43,8 @@
|
||||
<field name="name">[BOX 8] Multiply the amount in Box 7 by 3 and then divide by 23</field>
|
||||
<field name="parent_id" ref="tax_report_sale_and_income"/>
|
||||
<field name="sequence" eval="4"/>
|
||||
<field name="formula">((NZBOX5 - NZBOX6) * 3)/23</field>
|
||||
<field name="code">NZBOX8</field>
|
||||
<field name="formula">(NZBOX7 * 3)/23</field>
|
||||
<field name="report_id" ref="tax_report"/>
|
||||
</record>
|
||||
|
||||
@@ -60,7 +62,8 @@
|
||||
<field name="name">[BOX 10] Total GST collected on sales and income</field>
|
||||
<field name="parent_id" ref="tax_report_sale_and_income"/>
|
||||
<field name="sequence" eval="6"/>
|
||||
<field name="formula">(((NZBOX5 - NZBOX6) * 3)/23) + NZBOX9</field>
|
||||
<field name="code">NZBOX10</field>
|
||||
<field name="formula">NZBOX8 + NZBOX9</field>
|
||||
<field name="report_id" ref="tax_report"/>
|
||||
</record>
|
||||
|
||||
@@ -83,6 +86,7 @@
|
||||
<field name="name">[BOX 12] Multiply BOX11 by 3 and then divide by 23</field>
|
||||
<field name="parent_id" ref="tax_report_purchases_and_expenses"/>
|
||||
<field name="sequence" eval="2"/>
|
||||
<field name="code">NZBOX12</field>
|
||||
<field name="formula">(NZBOX11 * 3)/23</field>
|
||||
<field name="report_id" ref="tax_report"/>
|
||||
</record>
|
||||
@@ -91,6 +95,7 @@
|
||||
<field name="name">[BOX 13] Credit adjustments from your calculation sheet</field>
|
||||
<field name="parent_id" ref="tax_report_purchases_and_expenses"/>
|
||||
<field name="sequence" eval="3"/>
|
||||
<field name="code">NZBOX13</field>
|
||||
<field name="report_id" ref="tax_report"/>
|
||||
</record>
|
||||
|
||||
@@ -98,14 +103,15 @@
|
||||
<field name="name">[BOX 14] Total GST credit for purchases and expenses</field>
|
||||
<field name="parent_id" ref="tax_report_purchases_and_expenses"/>
|
||||
<field name="sequence" eval="4"/>
|
||||
<field name="formula">((NZBOX11 * 3)/23)</field>
|
||||
<field name="code">NZBOX14</field>
|
||||
<field name="formula">NZBOX12 + NZBOX13</field>
|
||||
<field name="report_id" ref="tax_report"/>
|
||||
</record>
|
||||
|
||||
<record id="tax_report_box15" model="account.tax.report.line">
|
||||
<field name="name">[BOX 15] Difference between BOX10 and BOX14</field>
|
||||
<field name="sequence" eval="3"/>
|
||||
<field name="formula">((((NZBOX5 - NZBOX6) * 3)/23) + NZBOX9) - ((NZBOX11 * 3)/23)</field>
|
||||
<field name="formula">NZBOX10 - NZBOX14</field>
|
||||
<field name="report_id" ref="tax_report"/>
|
||||
</record>
|
||||
|
||||
|
||||
@@ -50,6 +50,9 @@ function factory(dependencies) {
|
||||
* @param {integer} ui.item.id
|
||||
*/
|
||||
async handleAddChannelAutocompleteSelect(ev, ui) {
|
||||
// Necessary in order to prevent AutocompleteSelect event's default
|
||||
// behaviour as html tags visible for a split second in text area
|
||||
ev.preventDefault();
|
||||
const name = this.addingChannelValue;
|
||||
this.clearIsAddingItem();
|
||||
if (ui.item.special) {
|
||||
|
||||
@@ -158,13 +158,17 @@ class ProductProduct(models.Model):
|
||||
if not qty_per_kit:
|
||||
continue
|
||||
rounding = component.uom_id.rounding
|
||||
component_res = res.get(component.id, {
|
||||
"virtual_available": float_round(component.virtual_available, precision_rounding=rounding),
|
||||
"qty_available": float_round(component.qty_available, precision_rounding=rounding),
|
||||
"incoming_qty": float_round(component.incoming_qty, precision_rounding=rounding),
|
||||
"outgoing_qty": float_round(component.outgoing_qty, precision_rounding=rounding),
|
||||
"free_qty": float_round(component.free_qty, precision_rounding=rounding),
|
||||
})
|
||||
component_res = (
|
||||
res.get(component.id)
|
||||
if component.id in res
|
||||
else {
|
||||
"virtual_available": float_round(component.virtual_available, precision_rounding=rounding),
|
||||
"qty_available": float_round(component.qty_available, precision_rounding=rounding),
|
||||
"incoming_qty": float_round(component.incoming_qty, precision_rounding=rounding),
|
||||
"outgoing_qty": float_round(component.outgoing_qty, precision_rounding=rounding),
|
||||
"free_qty": float_round(component.free_qty, precision_rounding=rounding),
|
||||
}
|
||||
)
|
||||
ratios_virtual_available.append(component_res["virtual_available"] / qty_per_kit)
|
||||
ratios_qty_available.append(component_res["qty_available"] / qty_per_kit)
|
||||
ratios_incoming_qty.append(component_res["incoming_qty"] / qty_per_kit)
|
||||
|
||||
@@ -10,6 +10,7 @@ class MrpProduction(models.Model):
|
||||
def _cal_price(self, consumed_moves):
|
||||
finished_move = self.move_finished_ids.filtered(lambda x: x.product_id == self.product_id and x.state not in ('done', 'cancel') and x.quantity_done > 0)
|
||||
# Take the price unit of the reception move
|
||||
if finished_move.move_dest_ids.is_subcontract:
|
||||
self.extra_cost = finished_move.move_dest_ids._get_price_unit()
|
||||
last_done_receipt = finished_move.move_dest_ids.filtered(lambda m: m.state == 'done')[-1:]
|
||||
if last_done_receipt.is_subcontract:
|
||||
self.extra_cost = last_done_receipt._get_price_unit()
|
||||
return super()._cal_price(consumed_moves=consumed_moves)
|
||||
|
||||
@@ -47,9 +47,11 @@ class TestAccountSubcontractingFlows(TestMrpSubcontractingCommon):
|
||||
move.product_id = self.finished
|
||||
move.product_uom_qty = 1
|
||||
picking_receipt = picking_form.save()
|
||||
picking_receipt.move_lines.price_unit = 30.0
|
||||
picking_receipt.move_lines.price_unit = 15.0
|
||||
|
||||
picking_receipt.action_confirm()
|
||||
# Suppose the additional cost changes:
|
||||
picking_receipt.move_lines.price_unit = 30.0
|
||||
picking_receipt.move_lines.quantity_done = 1.0
|
||||
picking_receipt._action_done()
|
||||
|
||||
@@ -150,3 +152,55 @@ class TestAccountSubcontractingFlows(TestMrpSubcontractingCommon):
|
||||
self.assertEqual(len(f_layers), 4)
|
||||
for layer in f_layers:
|
||||
self.assertEqual(layer.value, 100 + 50)
|
||||
|
||||
def test_tracked_compo_and_backorder(self):
|
||||
"""
|
||||
Suppose a subcontracted product P with two tracked components, P is FIFO
|
||||
Create a receipt for 10 x P, receive 5, then 3 and then 2
|
||||
"""
|
||||
self.env.ref('product.product_category_all').property_cost_method = 'fifo'
|
||||
self.comp1.tracking = 'lot'
|
||||
self.comp1.standard_price = 10
|
||||
self.comp2.tracking = 'lot'
|
||||
self.comp2.standard_price = 20
|
||||
|
||||
lot01, lot02 = self.env['stock.production.lot'].create([{
|
||||
'name': "Lot of %s" % product.name,
|
||||
'product_id': product.id,
|
||||
'company_id': self.env.company.id,
|
||||
} for product in (self.comp1, self.comp2)])
|
||||
|
||||
receipt_form = Form(self.env['stock.picking'])
|
||||
receipt_form.picking_type_id = self.env.ref('stock.picking_type_in')
|
||||
receipt_form.partner_id = self.subcontractor_partner1
|
||||
with receipt_form.move_ids_without_package.new() as move:
|
||||
move.product_id = self.finished
|
||||
move.product_uom_qty = 10
|
||||
receipt = receipt_form.save()
|
||||
# add an extra cost
|
||||
receipt.move_lines.price_unit = 50
|
||||
receipt.action_confirm()
|
||||
|
||||
for qty_producing in (5, 3, 2):
|
||||
action = 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_producing
|
||||
with mo_form.move_line_raw_ids.edit(0) as ml:
|
||||
ml.lot_id = lot01
|
||||
with mo_form.move_line_raw_ids.edit(1) as ml:
|
||||
ml.lot_id = lot02
|
||||
mo = mo_form.save()
|
||||
mo.subcontracting_record_component()
|
||||
|
||||
action = receipt.button_validate()
|
||||
if isinstance(action, dict):
|
||||
wizard = Form(self.env[action['res_model']].with_context(action['context'])).save()
|
||||
wizard.process()
|
||||
receipt = receipt.backorder_ids
|
||||
|
||||
self.assertRecordValues(self.finished.stock_valuation_layer_ids, [
|
||||
{'quantity': 5, 'value': 5 * (10 + 20 + 50)},
|
||||
{'quantity': 3, 'value': 3 * (10 + 20 + 50)},
|
||||
{'quantity': 2, 'value': 2 * (10 + 20 + 50)},
|
||||
])
|
||||
|
||||
@@ -219,7 +219,9 @@ class Pricelist(models.Model):
|
||||
price = product.price_compute(rule.base)[product.id]
|
||||
|
||||
if price is not False:
|
||||
price = rule._compute_price(price, price_uom, product, quantity=qty, partner=partner)
|
||||
# pass the date through the context for further currency conversions
|
||||
rule_with_date_context = rule.with_context(date=date)
|
||||
price = rule_with_date_context._compute_price(price, price_uom, product, quantity=qty, partner=partner)
|
||||
suitable_rule = rule
|
||||
break
|
||||
# Final price conversion into pricelist currency
|
||||
@@ -576,6 +578,7 @@ class PricelistItem(models.Model):
|
||||
The unused parameters are there to make the full context available for overrides.
|
||||
"""
|
||||
self.ensure_one()
|
||||
date = self.env.context.get('date') or fields.Date.today()
|
||||
convert_to_price_uom = (lambda price: product.uom_id._compute_price(price, price_uom))
|
||||
if self.compute_price == 'fixed':
|
||||
price = convert_to_price_uom(self.fixed_price)
|
||||
@@ -585,18 +588,30 @@ class PricelistItem(models.Model):
|
||||
# complete formula
|
||||
price_limit = price
|
||||
price = (price - (price * (self.price_discount / 100))) or 0.0
|
||||
if self.base == 'standard_price':
|
||||
price_currency = product.cost_currency_id
|
||||
elif self.base == 'pricelist':
|
||||
price_currency = self.currency_id # Already converted before to the pricelist currency
|
||||
else:
|
||||
price_currency = product.currency_id
|
||||
if self.price_round:
|
||||
price = tools.float_round(price, precision_rounding=self.price_round)
|
||||
|
||||
def convert_to_base_price_currency(amount):
|
||||
return self.currency_id._convert(amount, price_currency, self.env.company, date, round=False)
|
||||
|
||||
if self.price_surcharge:
|
||||
price_surcharge = convert_to_price_uom(self.price_surcharge)
|
||||
price_surcharge = convert_to_base_price_currency(self.price_surcharge)
|
||||
price_surcharge = convert_to_price_uom(price_surcharge)
|
||||
price += price_surcharge
|
||||
|
||||
if self.price_min_margin:
|
||||
price_min_margin = convert_to_price_uom(self.price_min_margin)
|
||||
price_min_margin = convert_to_base_price_currency(self.price_min_margin)
|
||||
price_min_margin = convert_to_price_uom(price_min_margin)
|
||||
price = max(price, price_limit + price_min_margin)
|
||||
|
||||
if self.price_max_margin:
|
||||
price_max_margin = convert_to_price_uom(self.price_max_margin)
|
||||
price_max_margin = convert_to_base_price_currency(self.price_max_margin)
|
||||
price_max_margin = convert_to_price_uom(price_max_margin)
|
||||
price = min(price, price_limit + price_max_margin)
|
||||
return price
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from datetime import datetime
|
||||
import time
|
||||
|
||||
from flectra.tests.common import TransactionCase
|
||||
from flectra.tools import float_compare, test_reports
|
||||
@@ -83,6 +84,12 @@ class TestProductPricelist(TransactionCase):
|
||||
self.uom_unit_id = self.ref('uom.product_uom_unit')
|
||||
self.list0 = self.ref('product.list0')
|
||||
|
||||
self.new_currency = self.env['res.currency'].create({
|
||||
'name': 'Wonderful Currency',
|
||||
'symbol': ':)',
|
||||
'rate_ids': [(0, 0, {'rate': 10, 'name': time.strftime('%Y-%m-%d')})],
|
||||
})
|
||||
|
||||
self.ipad_retina_display.write({'uom_id': self.uom_unit_id, 'categ_id': self.category_5_id})
|
||||
self.customer_pricelist = self.ProductPricelist.create({
|
||||
'name': 'Customer Pricelist',
|
||||
@@ -201,4 +208,52 @@ class TestProductPricelist(TransactionCase):
|
||||
float_compare(monitor.price, monitor.lst_price/2, precision_digits=2), 0,
|
||||
msg)
|
||||
|
||||
def test_20_price_different_currency_pricelist(self):
|
||||
pricelist = self.ProductPricelist.create({
|
||||
'name': 'Currency Pricelist',
|
||||
'currency_id': self.new_currency.id,
|
||||
'item_ids': [(0, 0, {
|
||||
'compute_price': 'formula',
|
||||
'base': 'list_price',
|
||||
'price_surcharge': 100
|
||||
})]
|
||||
})
|
||||
product = self.monitor.with_context({
|
||||
'pricelist': pricelist.id, 'quantity': 1
|
||||
})
|
||||
# product price use the currency of the pricelist
|
||||
self.assertEqual(product.price, 10100)
|
||||
|
||||
def test_21_price_diff_cur_min_margin_pricelist(self):
|
||||
pricelist = self.ProductPricelist.create({
|
||||
'name': 'Currency with Margin Pricelist',
|
||||
'currency_id': self.new_currency.id,
|
||||
'item_ids': [(0, 0, {
|
||||
'compute_price': 'formula',
|
||||
'base': 'list_price',
|
||||
'price_min_margin': 10,
|
||||
'price_max_margin': 100,
|
||||
})]
|
||||
})
|
||||
product = self.monitor.with_context({
|
||||
'pricelist': pricelist.id, 'quantity': 1
|
||||
})
|
||||
# product price use the currency of the pricelist
|
||||
self.assertEqual(product.price, 10010)
|
||||
|
||||
def test_22_price_diff_cur_max_margin_pricelist(self):
|
||||
pricelist = self.ProductPricelist.create({
|
||||
'name': 'Currency with Margin Pricelist',
|
||||
'currency_id': self.new_currency.id,
|
||||
'item_ids': [(0, 0, {
|
||||
'compute_price': 'formula',
|
||||
'base': 'list_price',
|
||||
'price_surcharge': 100,
|
||||
'price_max_margin': 90
|
||||
})]
|
||||
})
|
||||
product = self.monitor.with_context({
|
||||
'pricelist': pricelist.id, 'quantity': 1
|
||||
})
|
||||
# product price use the currency of the pricelist
|
||||
self.assertEqual(product.price, 10090)
|
||||
|
||||
@@ -14,10 +14,12 @@ class StockMoveLine(models.Model):
|
||||
help='This is the date on which the goods with this Serial Number may'
|
||||
' become dangerous and must not be consumed.')
|
||||
|
||||
@api.depends('product_id', 'picking_type_use_create_lots')
|
||||
@api.depends('product_id', 'picking_type_use_create_lots', 'lot_id.expiration_date')
|
||||
def _compute_expiration_date(self):
|
||||
for move_line in self:
|
||||
if move_line.picking_type_use_create_lots:
|
||||
if not move_line.expiration_date and move_line.lot_id.expiration_date:
|
||||
move_line.expiration_date = move_line.lot_id.expiration_date
|
||||
elif move_line.picking_type_use_create_lots:
|
||||
if move_line.product_id.use_expiration_date:
|
||||
if not move_line.expiration_date:
|
||||
move_line.expiration_date = fields.Datetime.today() + datetime.timedelta(days=move_line.product_id.expiration_time)
|
||||
|
||||
@@ -499,3 +499,29 @@ class TestStockProductionLot(TestStockCommon):
|
||||
new_date = datetime.today() + timedelta(days=15)
|
||||
quant.with_user(self.demo_user).with_context(inventory_mode=True).write({'removal_date': new_date})
|
||||
self.assertEqual(quant.removal_date, new_date)
|
||||
|
||||
def test_apply_lot_date_on_sml(self):
|
||||
"""
|
||||
When assigning a lot to a SML, if the lot has an expiration date,
|
||||
the latter should be applied on the SML
|
||||
"""
|
||||
exp_date = fields.Datetime.today() + relativedelta(days=15)
|
||||
|
||||
lot = self.env['stock.production.lot'].create({
|
||||
'name': 'Lot 1',
|
||||
'product_id': self.apple_product.id,
|
||||
'expiration_date': fields.Datetime.to_string(exp_date),
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
|
||||
sml = self.env['stock.move.line'].create({
|
||||
'location_id': self.supplier_location,
|
||||
'location_dest_id': self.stock_location,
|
||||
'product_id': self.apple_product.id,
|
||||
'qty_done': 3,
|
||||
'product_uom_id': self.apple_product.uom_id.id,
|
||||
'lot_id': lot.id,
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
|
||||
self.assertEqual(sml.expiration_date, exp_date)
|
||||
|
||||
@@ -156,7 +156,8 @@ class SaleOrder(models.Model):
|
||||
}]
|
||||
reward_dict = {}
|
||||
lines = self._get_paid_order_lines()
|
||||
amount_total = sum(self._get_base_order_lines(program).mapped('price_subtotal'))
|
||||
amount_total = sum([any(line.tax_id.mapped('price_include')) and line.price_total or line.price_subtotal
|
||||
for line in self._get_base_order_lines(program)])
|
||||
if program.discount_apply_on == 'cheapest_product':
|
||||
line = self._get_cheapest_line()
|
||||
if line:
|
||||
|
||||
@@ -751,7 +751,7 @@ class TestSaleCouponProgramNumbers(TestSaleCouponCommon):
|
||||
})
|
||||
|
||||
order = self.empty_order
|
||||
orderline = self.env['sale.order.line'].create([
|
||||
self.env['sale.order.line'].create([
|
||||
{
|
||||
'product_id': self.conferenceChair.id,
|
||||
'name': 'Conference Chair',
|
||||
@@ -793,10 +793,7 @@ class TestSaleCouponProgramNumbers(TestSaleCouponCommon):
|
||||
}).process_coupon()
|
||||
self.assertEqual(order.amount_total, 0.0, "The promotion program should not make the order total go below 0")
|
||||
order.recompute_coupon_lines()
|
||||
#TODO fix numbers
|
||||
self.assertEqual(order.amount_total, 9.09, "The promotion program should not be altered after recomputation")
|
||||
self.assertEqual(order.amount_tax, 8.18)
|
||||
self.assertEqual(order.amount_untaxed, 0.91)
|
||||
self.assertEqual(order.amount_total, 0.0, "The promotion program should not be altered after recomputation")
|
||||
|
||||
order.order_line[3:].unlink() #remove all coupon
|
||||
|
||||
@@ -811,10 +808,38 @@ class TestSaleCouponProgramNumbers(TestSaleCouponCommon):
|
||||
'coupon_code': 'test_10pc'
|
||||
}).process_coupon()
|
||||
order.recompute_coupon_lines()
|
||||
#TODO fix numbers
|
||||
self.assertEqual(order.amount_tax, 9.01)
|
||||
self.assertEqual(order.amount_untaxed, 0.08)
|
||||
self.assertEqual(order.amount_total, 9.09, "The promotion program should not be altered after recomputation")
|
||||
self.assertEqual(order.amount_total, 0.0, "The promotion program should not be altered after recomputation")
|
||||
|
||||
def test_program_percentage_discount_on_product_included_tax(self):
|
||||
# test 100% percentage discount (tax included)
|
||||
|
||||
program = self.env['coupon.program'].create({
|
||||
'name': '100% discount',
|
||||
'promo_code_usage': 'no_code_needed',
|
||||
'program_type': 'promotion_program',
|
||||
'discount_percentage': 100.0,
|
||||
'rule_minimum_amount_tax_inclusion': 'tax_included',
|
||||
})
|
||||
self.tax_10pc_incl.price_include = True
|
||||
|
||||
self.drawerBlack.taxes_id = self.tax_10pc_incl
|
||||
order = self.empty_order
|
||||
order.order_line = self.env['sale.order.line'].create({
|
||||
'product_id': self.drawerBlack.id,
|
||||
'product_uom_qty': 1.0,
|
||||
'order_id': order.id,
|
||||
})
|
||||
order.recompute_coupon_lines()
|
||||
self.assertEqual(len(order.order_line.ids), 2, "The discount should be applied")
|
||||
self.assertEqual(order.amount_total, 0.0, "Order should be 0 as it is a 100% discount")
|
||||
|
||||
# test 95% percentage discount (tax included)
|
||||
program.discount_percentage = 95
|
||||
order.recompute_coupon_lines()
|
||||
# lst_price is 25$ so total now should be 1.25$ (1.14$ + 0.11$ taxes)
|
||||
self.assertEqual(len(order.order_line.ids), 2, "The discount should be applied")
|
||||
self.assertAlmostEqual(order.amount_tax, 0.11, places=2)
|
||||
self.assertAlmostEqual(order.amount_untaxed, 1.14, places=2)
|
||||
|
||||
def test_program_discount_on_multiple_specific_products(self):
|
||||
""" Ensure a discount on multiple specific products is correctly computed.
|
||||
|
||||
@@ -39,7 +39,9 @@ class StockMove(models.Model):
|
||||
precision = self.env['decimal.precision'].precision_get('Product Price')
|
||||
# If the move is a return, use the original move's price unit.
|
||||
if self.origin_returned_move_id and self.origin_returned_move_id.sudo().stock_valuation_layer_ids:
|
||||
return self.origin_returned_move_id.sudo().stock_valuation_layer_ids[-1].unit_cost
|
||||
layers = self.origin_returned_move_id.sudo().stock_valuation_layer_ids
|
||||
quantity = sum(layers.mapped("quantity"))
|
||||
return layers.currency_id.round(sum(layers.mapped("value")) / quantity) if not float_is_zero(quantity, layers.uom_id.rounding) else 0
|
||||
return price_unit if not float_is_zero(price_unit, precision) or self._should_force_price_unit() else self.product_id.standard_price
|
||||
|
||||
@api.model
|
||||
|
||||
@@ -515,6 +515,16 @@ class TestStockValuationAVCO(TestStockValuationCommon):
|
||||
self.assertEqual(self.product1.quantity_svl, 0)
|
||||
self.assertEqual(self.product1.standard_price, 1.01)
|
||||
|
||||
def test_return_delivery_2(self):
|
||||
self.product1.write({"standard_price": 1})
|
||||
move1 = self._make_out_move(self.product1, 10, create_picking=True, force_assign=True)
|
||||
self._make_in_move(self.product1, 10, unit_cost=2)
|
||||
self._make_return(move1, 10)
|
||||
|
||||
self.assertEqual(self.product1.value_svl, 20)
|
||||
self.assertEqual(self.product1.quantity_svl, 10)
|
||||
self.assertEqual(self.product1.standard_price, 2)
|
||||
|
||||
|
||||
class TestStockValuationFIFO(TestStockValuationCommon):
|
||||
def setUp(self):
|
||||
@@ -685,6 +695,15 @@ class TestStockValuationFIFO(TestStockValuationCommon):
|
||||
returned = self._make_return(out_move02, 1)
|
||||
self.assertEqual(returned.stock_valuation_layer_ids.value, 0)
|
||||
|
||||
def test_return_delivery_3(self):
|
||||
self.product1.write({"standard_price": 1})
|
||||
move1 = self._make_out_move(self.product1, 10, create_picking=True, force_assign=True)
|
||||
self._make_in_move(self.product1, 10, unit_cost=2)
|
||||
self._make_return(move1, 10)
|
||||
|
||||
self.assertEqual(self.product1.value_svl, 20)
|
||||
self.assertEqual(self.product1.quantity_svl, 10)
|
||||
|
||||
|
||||
class TestStockValuationChangeCostMethod(TestStockValuationCommon):
|
||||
def test_standard_to_fifo_1(self):
|
||||
|
||||
Reference in New Issue
Block a user