mirror of
https://gitlab.com/flectra-hq/flectra.git
synced 2025-02-25 18:55:21 -06:00
[PATCH] Upstream patch - 23092022
This commit is contained in:
@@ -1484,10 +1484,12 @@ class AccountMove(models.Model):
|
||||
if new_pmt_state == 'paid' and move.move_type in ('in_invoice', 'out_invoice', 'entry'):
|
||||
reverse_type = move.move_type == 'in_invoice' and 'in_refund' or move.move_type == 'out_invoice' and 'out_refund' or 'entry'
|
||||
reverse_moves = self.env['account.move'].search([('reversed_entry_id', '=', move.id), ('state', '=', 'posted'), ('move_type', '=', reverse_type)])
|
||||
caba_moves = self.env['account.move'].search([('tax_cash_basis_move_id', 'in', move.ids + reverse_moves.ids), ('state', '=', 'posted')])
|
||||
|
||||
# We only set 'reversed' state in cas of 1 to 1 full reconciliation with a reverse entry; otherwise, we use the regular 'paid' state
|
||||
# We ignore potentials cash basis moves reconciled because the transition account of the tax is reconcilable
|
||||
reverse_moves_full_recs = reverse_moves.mapped('line_ids.full_reconcile_id')
|
||||
if reverse_moves_full_recs.mapped('reconciled_line_ids.move_id').filtered(lambda x: x not in (reverse_moves + reverse_moves_full_recs.mapped('exchange_move_id'))) == move:
|
||||
if reverse_moves_full_recs.mapped('reconciled_line_ids.move_id').filtered(lambda x: x not in (caba_moves + reverse_moves + reverse_moves_full_recs.mapped('exchange_move_id'))) == move:
|
||||
new_pmt_state = 'reversed'
|
||||
|
||||
move.payment_state = new_pmt_state
|
||||
|
||||
@@ -711,7 +711,7 @@ class Meeting(models.Model):
|
||||
current_attendees = self.filtered('active').attendee_ids
|
||||
if 'partner_ids' in values:
|
||||
(current_attendees - previous_attendees)._send_mail_to_attendees('calendar.calendar_template_meeting_invitation')
|
||||
if 'start' in values:
|
||||
if not self.env.context.get('is_calendar_event_new') and 'start' in values:
|
||||
start_date = fields.Datetime.to_datetime(values.get('start'))
|
||||
# Only notify on future events
|
||||
if start_date and start_date >= fields.Datetime.now():
|
||||
@@ -721,6 +721,9 @@ class Meeting(models.Model):
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
# Prevent sending update notification when _inverse_dates is called
|
||||
self = self.with_context(is_calendar_event_new=True)
|
||||
|
||||
vals_list = [ # Else bug with quick_create when we are filter on an other user
|
||||
dict(vals, user_id=self.env.user.id) if not 'user_id' in vals else vals
|
||||
for vals in vals_list
|
||||
@@ -785,7 +788,7 @@ class Meeting(models.Model):
|
||||
if len(event.alarm_ids) > 0:
|
||||
self.env['calendar.alarm_manager']._notify_next_alarm(event.partner_ids.ids)
|
||||
|
||||
return events
|
||||
return events.with_context(is_calendar_event_new=False)
|
||||
|
||||
def _read(self, fields):
|
||||
if self.env.is_system():
|
||||
|
||||
@@ -30,6 +30,20 @@ class TestEventNotifications(SavepointCase, MailCase):
|
||||
}):
|
||||
self.event.partner_ids = self.partner
|
||||
|
||||
def test_message_invite_allday(self):
|
||||
with self.assertSinglePostNotifications([{'partner': self.partner, 'type': 'inbox'}], {
|
||||
'message_type': 'user_notification',
|
||||
'subtype': 'mail.mt_note',
|
||||
}):
|
||||
self.env['calendar.event'].with_context(mail_create_nolog=True).create([{
|
||||
'name': 'Meeting',
|
||||
'allday': True,
|
||||
'start_date': fields.Date.today() + relativedelta(days=7),
|
||||
'stop_date': fields.Date.today() + relativedelta(days=8),
|
||||
'partner_ids': [(4, self.partner.id)],
|
||||
}])
|
||||
|
||||
|
||||
def test_message_invite_self(self):
|
||||
with self.assertNoNotifications():
|
||||
self.event.with_user(self.user).partner_ids = self.partner
|
||||
|
||||
@@ -1163,6 +1163,40 @@
|
||||
}),
|
||||
]"/>
|
||||
</record>
|
||||
<record id="account_tax_template_oss_s_iva_ns" model="account.tax.template">
|
||||
<field name="description">No sujeto y acogidas a la OSS (Servicios)</field>
|
||||
<field name="type_tax_use">sale</field>
|
||||
<field name="name">No sujeto y acogidas a la OSS (Servicios)</field>
|
||||
<field name="chart_template_id" ref="l10n_es.account_chart_template_common"/>
|
||||
<field name="amount" eval="0"/>
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="tax_group_id" ref="tax_group_iva_0"/>
|
||||
<field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
|
||||
(0,0, {
|
||||
'factor_percent': 100,
|
||||
'repartition_type': 'base',
|
||||
'tag_ids': [ref('mod_303_123')],
|
||||
}),
|
||||
|
||||
(0,0, {
|
||||
'factor_percent': 100,
|
||||
'repartition_type': 'tax',
|
||||
}),
|
||||
|
||||
]"/>
|
||||
<field name="refund_repartition_line_ids" eval="[(5, 0, 0),
|
||||
(0,0, {
|
||||
'factor_percent': 100,
|
||||
'repartition_type': 'base',
|
||||
'tag_ids': [ref('mod_303_123')],
|
||||
}),
|
||||
|
||||
(0,0, {
|
||||
'factor_percent': 100,
|
||||
'repartition_type': 'tax',
|
||||
}),
|
||||
]"/>
|
||||
</record>
|
||||
<record id="account_tax_template_s_iva_ns_b" model="account.tax.template">
|
||||
<field name="description">No sujeto (Bienes)</field>
|
||||
<field name="type_tax_use">sale</field>
|
||||
@@ -1197,6 +1231,40 @@
|
||||
}),
|
||||
]"/>
|
||||
</record>
|
||||
<record id="account_tax_template_oss_s_iva_ns_b" model="account.tax.template">
|
||||
<field name="description">No sujeto y acogidas a la OSS (Bienes)</field>
|
||||
<field name="type_tax_use">sale</field>
|
||||
<field name="name">No sujeto y acogidas a la OSS (Bienes)</field>
|
||||
<field name="chart_template_id" ref="l10n_es.account_chart_template_common"/>
|
||||
<field name="amount" eval="0"/>
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="tax_group_id" ref="tax_group_iva_0"/>
|
||||
<field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
|
||||
(0,0, {
|
||||
'factor_percent': 100,
|
||||
'repartition_type': 'base',
|
||||
'tag_ids': [ref('mod_303_123')],
|
||||
}),
|
||||
|
||||
(0,0, {
|
||||
'factor_percent': 100,
|
||||
'repartition_type': 'tax',
|
||||
}),
|
||||
|
||||
]"/>
|
||||
<field name="refund_repartition_line_ids" eval="[(5, 0, 0),
|
||||
(0,0, {
|
||||
'factor_percent': 100,
|
||||
'repartition_type': 'base',
|
||||
'tag_ids': [ref('mod_303_123')],
|
||||
}),
|
||||
|
||||
(0,0, {
|
||||
'factor_percent': 100,
|
||||
'repartition_type': 'tax',
|
||||
}),
|
||||
]"/>
|
||||
</record>
|
||||
<record id="account_tax_template_s_iva_e" model="account.tax.template">
|
||||
<field name="description">Extracomunitario (Servicios)</field>
|
||||
<field name="type_tax_use">sale</field>
|
||||
|
||||
@@ -199,7 +199,7 @@ EU_TAG_MAP = {
|
||||
},
|
||||
# Spain
|
||||
'l10n_es.account_chart_template_common': {
|
||||
'invoice_base_tag': None,
|
||||
'invoice_base_tag': "l10n_es.mod_303_124",
|
||||
'invoice_tax_tag': None,
|
||||
'refund_base_tag': None,
|
||||
'refund_tax_tag': None,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from flectra import api, fields, models, _
|
||||
from .eu_tax_map import EU_TAX_MAP
|
||||
from flectra import api, models
|
||||
from .eu_tag_map import EU_TAG_MAP
|
||||
from .eu_tax_map import EU_TAX_MAP
|
||||
|
||||
|
||||
class Company(models.Model):
|
||||
@@ -117,7 +117,7 @@ class Company(models.Model):
|
||||
return self.env.ref(f'l10n_eu_service.oss_tax_account_company_{self.id}')
|
||||
|
||||
def _get_oss_tags(self):
|
||||
[chart_template_xml_id] = self.chart_template_id.get_xml_id().values()
|
||||
[chart_template_xml_id] = self.chart_template_id.parent_id.get_xml_id().values() or self.chart_template_id.get_xml_id().values()
|
||||
tag_for_country = EU_TAG_MAP.get(chart_template_xml_id, {
|
||||
'invoice_base_tag': None,
|
||||
'invoice_tax_tag': None,
|
||||
@@ -125,11 +125,11 @@ class Company(models.Model):
|
||||
'refund_tax_tag': None,
|
||||
})
|
||||
|
||||
return {
|
||||
repartition_line_key: (
|
||||
self.env.ref(tag_xml_id).tag_ids.filtered(lambda t: not t.tax_negate)
|
||||
if tag_xml_id
|
||||
else None
|
||||
)
|
||||
for repartition_line_key, tag_xml_id in tag_for_country.items()
|
||||
}
|
||||
mapping = {}
|
||||
for repartition_line_key, tag_xml_id in tag_for_country.items():
|
||||
tag = self.env.ref(tag_xml_id) if tag_xml_id else None
|
||||
if tag and tag._name == "account.tax.report.line":
|
||||
tag = tag.tag_ids.filtered(lambda t: not t.tax_negate)
|
||||
mapping[repartition_line_key] = tag
|
||||
|
||||
return mapping
|
||||
|
||||
@@ -21,6 +21,10 @@ class TestOSSBelgium(AccountTestInvoicingCommon):
|
||||
self.company_data['company']._map_eu_taxes()
|
||||
|
||||
def test_country_tag_from_belgium(self):
|
||||
"""
|
||||
This test ensure that xml_id from `account.tax.report.line` in the EU_TAG_MAP are processed correctly by the oss
|
||||
tax creation mechanism.
|
||||
"""
|
||||
# get an eu country which isn't the current one:
|
||||
another_eu_country_code = (self.env.ref('base.europe').country_ids - self.company_data['company'].country_id)[0].code
|
||||
tax_oss = self.env['account.tax'].search([('name', 'ilike', f'%{another_eu_country_code}%')], limit=1)
|
||||
@@ -41,6 +45,43 @@ class TestOSSBelgium(AccountTestInvoicingCommon):
|
||||
self.assertIn(expected_tag_id, oss_tag_id, f"{doc_type} tag from Belgian CoA not correctly linked")
|
||||
|
||||
|
||||
@tagged('post_install', 'post_install_l10n', '-at_install')
|
||||
class TestOSSSpain(AccountTestInvoicingCommon):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls, chart_template_ref='l10n_es.account_chart_template_common'):
|
||||
try:
|
||||
super().setUpClass(chart_template_ref=chart_template_ref)
|
||||
except ValueError as e:
|
||||
if e.args[0] == "External ID not found in the system: l10n_es.account_chart_template_data":
|
||||
cls.skipTest(cls, reason="Spanish CoA is required for this testSuite but l10n_es isn't installed")
|
||||
else:
|
||||
raise e
|
||||
cls.company_data['company'].country_id = cls.env.ref('base.es')
|
||||
cls.company_data['company']._map_eu_taxes()
|
||||
|
||||
def test_country_tag_from_spain(self):
|
||||
"""
|
||||
This test ensure that xml_id from `account.account.tag` in the EU_TAG_MAP are processed correctly by the oss
|
||||
tax creation mechanism.
|
||||
"""
|
||||
# get an eu country which isn't the current one:
|
||||
another_eu_country_code = (self.env.ref('base.europe').country_ids - self.company_data['company'].country_id)[0].code
|
||||
tax_oss = self.env['account.tax'].search([('name', 'ilike', f'%{another_eu_country_code}%')], limit=1)
|
||||
|
||||
for doc_type, tag_xml_id in (
|
||||
("invoice", "l10n_es.mod_303_124"),
|
||||
):
|
||||
with self.subTest(doc_type=doc_type, report_line_xml_id=tag_xml_id):
|
||||
oss_tag_id = tax_oss[f"{doc_type}_repartition_line_ids"]\
|
||||
.filtered(lambda x: x.repartition_type == 'base')\
|
||||
.tag_ids
|
||||
|
||||
expected_tag_id = self.env.ref(tag_xml_id)
|
||||
|
||||
self.assertIn(expected_tag_id, oss_tag_id, f"{doc_type} tag from Spanish CoA not correctly linked")
|
||||
|
||||
|
||||
@tagged('post_install', 'post_install_l10n', '-at_install')
|
||||
class TestOSSUSA(AccountTestInvoicingCommon):
|
||||
|
||||
@@ -58,7 +99,6 @@ class TestOSSUSA(AccountTestInvoicingCommon):
|
||||
self.assertFalse(len(tax_oss), "OSS tax shouldn't be instanced on a US company")
|
||||
|
||||
|
||||
|
||||
@tagged('post_install', 'post_install_l10n', '-at_install')
|
||||
class TestOSSMap(AccountTestInvoicingCommon):
|
||||
|
||||
|
||||
@@ -94,14 +94,13 @@ class AccountMove(models.Model):
|
||||
lines = self.invoice_line_ids.filtered(lambda l: not l.display_type)
|
||||
|
||||
for num, line in enumerate(lines):
|
||||
price_subtotal = line.balance if convert_to_euros else line.price_subtotal
|
||||
# The price_subtotal should be negative when:
|
||||
sign = -1 if line.move_id.is_inbound() else 1
|
||||
price_subtotal = (line.balance * sign) if convert_to_euros else line.price_subtotal
|
||||
# The price_subtotal should be inverted when:
|
||||
# The line has downpayment lines, but is not a downpayment (i.e. the final invoice, from which downpayment lines are subtracted) or,
|
||||
# the line is a reverse charge refund.
|
||||
if (line._get_downpayment_lines() and not is_downpayment) or reverse_charge_refund:
|
||||
price_subtotal = -abs(price_subtotal)
|
||||
else:
|
||||
price_subtotal = abs(price_subtotal)
|
||||
price_subtotal = -price_subtotal
|
||||
|
||||
# Unit price
|
||||
price_unit = 0
|
||||
|
||||
@@ -309,6 +309,22 @@ class TestItEdi(AccountEdiTestCommon):
|
||||
],
|
||||
})
|
||||
|
||||
cls.negative_price_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,
|
||||
}),
|
||||
(0, 0, {
|
||||
**cls.standard_line,
|
||||
'name': 'negative_line',
|
||||
'price_unit': -100.0,
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
# post the invoices
|
||||
cls.price_included_invoice._post()
|
||||
cls.partial_discount_invoice._post()
|
||||
@@ -318,6 +334,7 @@ class TestItEdi(AccountEdiTestCommon):
|
||||
cls.total_400_VAT_simplified_invoice._post()
|
||||
cls.pa_partner_invoice._post()
|
||||
cls.zero_tax_invoice._post()
|
||||
cls.negative_price_invoice._post()
|
||||
|
||||
cls.edi_basis_xml = cls._get_test_file_content('IT00470550013_basis.xml')
|
||||
cls.edi_simplified_basis_xml = cls._get_test_file_content('IT00470550013_simpl.xml')
|
||||
@@ -668,3 +685,44 @@ class TestItEdi(AccountEdiTestCommon):
|
||||
)
|
||||
invoice_etree = self.with_applied_xpath(invoice_etree, "<xpath expr='.//Allegati' position='replace'/>")
|
||||
self.assertXmlTreeEqual(invoice_etree, expected_etree)
|
||||
|
||||
def test_negative_price_invoice(self):
|
||||
invoice_etree = etree.fromstring(self.negative_price_invoice._export_as_xml())
|
||||
expected_etree = self.with_applied_xpath(
|
||||
etree.fromstring(self.edi_basis_xml),
|
||||
'''
|
||||
<xpath expr="//FatturaElettronicaBody//DatiBeniServizi" position="replace">
|
||||
<DatiBeniServizi>
|
||||
<DettaglioLinee>
|
||||
<NumeroLinea>1</NumeroLinea>
|
||||
<Descrizione>standard_line</Descrizione>
|
||||
<Quantita>1.00</Quantita>
|
||||
<PrezzoUnitario>800.400000</PrezzoUnitario>
|
||||
<PrezzoTotale>800.40</PrezzoTotale>
|
||||
<AliquotaIVA>22.00</AliquotaIVA>
|
||||
</DettaglioLinee>
|
||||
<DettaglioLinee>
|
||||
<NumeroLinea>2</NumeroLinea>
|
||||
<Descrizione>negative_line</Descrizione>
|
||||
<Quantita>1.00</Quantita>
|
||||
<PrezzoUnitario>-100.000000</PrezzoUnitario>
|
||||
<PrezzoTotale>-100.00</PrezzoTotale>
|
||||
<AliquotaIVA>22.00</AliquotaIVA>
|
||||
</DettaglioLinee>
|
||||
<DatiRiepilogo>
|
||||
<AliquotaIVA>22.00</AliquotaIVA>
|
||||
<ImponibileImporto>700.40</ImponibileImporto>
|
||||
<Imposta>154.09</Imposta>
|
||||
<EsigibilitaIVA>I</EsigibilitaIVA>
|
||||
</DatiRiepilogo>
|
||||
</DatiBeniServizi>
|
||||
</xpath>
|
||||
<xpath expr="//DettaglioPagamento//ImportoPagamento" position="inside">
|
||||
854.49
|
||||
</xpath>
|
||||
<xpath expr="//DatiGeneraliDocumento//ImportoTotaleDocumento" position="inside">
|
||||
854.49
|
||||
</xpath>
|
||||
''')
|
||||
invoice_etree = self.with_applied_xpath(invoice_etree, "<xpath expr='.//Allegati' position='replace'/>")
|
||||
self.assertXmlTreeEqual(invoice_etree, expected_etree)
|
||||
|
||||
@@ -31,11 +31,11 @@ class AccountMove(models.Model):
|
||||
invoice_line_pickings.setdefault(done_moves_related.picking_id, []).append(line_count)
|
||||
else:
|
||||
total_invoices = done_moves_related.mapped('sale_line_id.invoice_lines').filtered(
|
||||
lambda l: l.move_id.state == 'posted' and l.move_id.move_type == 'out_invoice').sorted(lambda l: l.move_id.invoice_date)
|
||||
lambda l: l.move_id.state == 'posted' and l.move_id.move_type == 'out_invoice').sorted(lambda l: (l.move_id.invoice_date, l.move_id.id))
|
||||
total_invs = [(i.product_uom_id._compute_quantity(i.quantity, i.product_id.uom_id), i) for i in total_invoices]
|
||||
inv = total_invs.pop(0)
|
||||
# Match all moves and related invoice lines FIFO looking for when the matched invoice_line matches line
|
||||
for move in done_moves_related.sorted(lambda m: m.date):
|
||||
for move in done_moves_related.sorted(lambda m: (m.date, m.id)):
|
||||
rounding = move.product_uom.rounding
|
||||
move_qty = move.product_qty
|
||||
while (float_compare(move_qty, 0, precision_rounding=rounding) > 0):
|
||||
|
||||
@@ -88,3 +88,49 @@ class TestDDT(TestSaleCommon):
|
||||
self.inv2 = self.so._create_invoices()
|
||||
self.inv2.action_post()
|
||||
self.assertEqual(self.inv2.l10n_it_ddt_ids.ids, (pickx1 | pickx2).ids, 'DDTs should be linked to the invoice')
|
||||
|
||||
def test_ddt_flow_2(self):
|
||||
"""
|
||||
Test that the link between the invoice lines and the deliveries linked to the invoice
|
||||
through the link with the sale order is calculated correctly.
|
||||
"""
|
||||
so = self.env['sale.order'].create({
|
||||
'partner_id': self.partner_a.id,
|
||||
'order_line': [(0, 0, {
|
||||
'product_id': self.product_a.id,
|
||||
'product_uom_qty': 3,
|
||||
'product_uom': self.product_a.uom_id.id,
|
||||
'price_unit': self.product_a.list_price,
|
||||
'tax_id': self.company_data['default_tax_sale']
|
||||
}
|
||||
)],
|
||||
'pricelist_id': self.company_data['default_pricelist'].id,
|
||||
'picking_policy': 'direct',
|
||||
})
|
||||
so.action_confirm()
|
||||
|
||||
# deliver partially
|
||||
picking_1 = so.picking_ids
|
||||
picking_1.move_lines.write({'quantity_done': 1})
|
||||
wiz_act = picking_1.button_validate()
|
||||
wiz = Form(self.env[wiz_act['res_model']].with_context(wiz_act['context'])).save()
|
||||
wiz.process()
|
||||
|
||||
invoice_1 = so._create_invoices()
|
||||
invoice_form = Form(invoice_1)
|
||||
with invoice_form.invoice_line_ids.edit(0) as line:
|
||||
line.quantity = 1.0
|
||||
invoice_1 = invoice_form.save()
|
||||
invoice_1.action_post()
|
||||
|
||||
picking_2 = so.picking_ids.filtered(lambda p: p.state != 'done')
|
||||
picking_2.move_lines.write({'quantity_done': 2})
|
||||
picking_2.button_validate()
|
||||
|
||||
invoice_2 = so._create_invoices()
|
||||
invoice_2.action_post()
|
||||
|
||||
# Invalidate the cache to ensure the lines will be fetched in the right order.
|
||||
picking_2.invalidate_cache()
|
||||
self.assertEqual(invoice_1.l10n_it_ddt_ids.ids, picking_1.ids, 'DDT picking_1 should be linked to the invoice_1')
|
||||
self.assertEqual(invoice_2.l10n_it_ddt_ids.ids, picking_2.ids, 'DDT picking_2 should be linked to the invoice_2')
|
||||
|
||||
@@ -374,15 +374,6 @@ class MrpWorkorder(models.Model):
|
||||
for workorder in self:
|
||||
workorder.scrap_count = count_data.get(workorder.id, 0)
|
||||
|
||||
@api.onchange('date_planned_finished')
|
||||
def _onchange_date_planned_finished(self):
|
||||
if self.date_planned_start and self.date_planned_finished:
|
||||
interval = self.workcenter_id.resource_calendar_id.get_work_duration_data(
|
||||
self.date_planned_start, self.date_planned_finished,
|
||||
domain=[('time_type', 'in', ['leave', 'other'])]
|
||||
)
|
||||
self.duration_expected = interval['hours'] * 60
|
||||
|
||||
@api.onchange('operation_id')
|
||||
def _onchange_operation_id(self):
|
||||
if self.operation_id:
|
||||
@@ -392,10 +383,25 @@ class MrpWorkorder(models.Model):
|
||||
@api.onchange('date_planned_start', 'duration_expected', 'workcenter_id')
|
||||
def _onchange_date_planned_start(self):
|
||||
if self.date_planned_start and self.duration_expected and self.workcenter_id:
|
||||
self.date_planned_finished = self.workcenter_id.resource_calendar_id.plan_hours(
|
||||
self.duration_expected / 60.0, self.date_planned_start,
|
||||
compute_leaves=True, domain=[('time_type', 'in', ['leave', 'other'])]
|
||||
)
|
||||
self.date_planned_finished = self._calculate_date_planned_finished()
|
||||
|
||||
def _calculate_date_planned_finished(self, date_planned_start=False):
|
||||
return self.workcenter_id.resource_calendar_id.plan_hours(
|
||||
self.duration_expected / 60.0, date_planned_start or self.date_planned_start,
|
||||
compute_leaves=True, domain=[('time_type', 'in', ['leave', 'other'])]
|
||||
)
|
||||
|
||||
@api.onchange('date_planned_finished')
|
||||
def _onchange_date_planned_finished(self):
|
||||
if self.date_planned_start and self.date_planned_finished:
|
||||
self.duration_expected = self._calculate_duration_expected()
|
||||
|
||||
def _calculate_duration_expected(self, date_planned_start=False, date_planned_finished=False):
|
||||
interval = self.workcenter_id.resource_calendar_id.get_work_duration_data(
|
||||
date_planned_start or self.date_planned_start, date_planned_finished or self.date_planned_finished,
|
||||
domain=[('time_type', 'in', ['leave', 'other'])]
|
||||
)
|
||||
return interval['hours'] * 60
|
||||
|
||||
@api.onchange('operation_id', 'workcenter_id', 'qty_production')
|
||||
def _onchange_expected_duration(self):
|
||||
@@ -416,6 +422,16 @@ class MrpWorkorder(models.Model):
|
||||
end_date = fields.Datetime.to_datetime(values.get('date_planned_finished')) or workorder.date_planned_finished
|
||||
if start_date and end_date and start_date > end_date:
|
||||
raise UserError(_('The planned end date of the work order cannot be prior to the planned start date, please correct this to save the work order.'))
|
||||
if 'duration_expected' not in values and not self.env.context.get('bypass_duration_calculation'):
|
||||
if values.get('date_planned_start') and values.get('date_planned_finished'):
|
||||
computed_finished_time = self._calculate_date_planned_finished(start_date)
|
||||
values['date_planned_finished'] = computed_finished_time
|
||||
elif values.get('date_planned_start'):
|
||||
computed_duration = self._calculate_duration_expected(date_planned_start=start_date)
|
||||
values['duration_expected'] = computed_duration
|
||||
elif values.get('date_planned_finished'):
|
||||
computed_duration = self._calculate_duration_expected(date_planned_finished=end_date)
|
||||
values['duration_expected'] = computed_duration
|
||||
# Update MO dates if the start date of the first WO or the
|
||||
# finished date of the last WO is update.
|
||||
if workorder == workorder.production_id.workorder_ids[0] and 'date_planned_start' in values:
|
||||
@@ -582,7 +598,7 @@ class MrpWorkorder(models.Model):
|
||||
vals['date_planned_start'] = start_date
|
||||
if self.date_planned_finished and self.date_planned_finished < start_date:
|
||||
vals['date_planned_finished'] = start_date
|
||||
return self.write(vals)
|
||||
return self.with_context(bypass_duration_calculation=True).write(vals)
|
||||
|
||||
def button_finish(self):
|
||||
end_date = datetime.now()
|
||||
|
||||
@@ -351,6 +351,7 @@
|
||||
<field name="qty_production"/>
|
||||
<field name="product_uom_id" force_save="1"/>
|
||||
<field name="consumption"/>
|
||||
<field name="operation_id"/>
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div class="oe_kanban_global_click">
|
||||
|
||||
@@ -56,7 +56,7 @@ class ProductProduct(models.Model):
|
||||
for move in moves_list:
|
||||
value += move.product_qty * move.product_id._compute_average_price(qty_invoiced * move.product_qty, qty_to_invoice * move.product_qty, move)
|
||||
continue
|
||||
line_qty = bom_line.product_uom_id._compute_quantity(bom_line.product_qty, bom_line.product_id.uom_id)
|
||||
line_qty = bom_line.product_uom_id._compute_quantity(bom_lines[bom_line]['qty'], bom_line.product_id.uom_id)
|
||||
moves = self.env['stock.move'].concat(*moves_list)
|
||||
value += line_qty * bom_line.product_id._compute_average_price(qty_invoiced * line_qty, qty_to_invoice * line_qty, moves)
|
||||
return value
|
||||
|
||||
@@ -2233,3 +2233,46 @@ class TestSaleMrpFlow(ValuationReconciliationTestCommon):
|
||||
|
||||
price = line.product_id.with_company(line.company_id)._compute_average_price(0, line.product_uom_qty, line.move_ids)
|
||||
self.assertEqual(price, 10)
|
||||
|
||||
def test_kit_cost_calculation(self):
|
||||
""" Check that the average cost price is computed correctly after SO confirmation:
|
||||
BOM 1:
|
||||
- 1 unit of “super kit”:
|
||||
- 2 units of “component a”
|
||||
BOM 2:
|
||||
- 1 unit of “component a”:
|
||||
- 3 units of "component b"
|
||||
1 unit of "component b" = $10
|
||||
1 unit of "super kit" = 2 * 3 * $10 = *$60
|
||||
"""
|
||||
super_kit = self._cls_create_product('Super Kit', self.uom_unit)
|
||||
(super_kit + self.component_a + self.component_b).categ_id.property_cost_method = 'average'
|
||||
self.env['mrp.bom'].create({
|
||||
'product_tmpl_id': self.component_a.product_tmpl_id.id,
|
||||
'product_qty': 1.0,
|
||||
'type': 'phantom',
|
||||
'bom_line_ids': [(0, 0, {
|
||||
'product_id': self.component_b.id,
|
||||
'product_qty': 3.0,
|
||||
})]
|
||||
})
|
||||
self.env['mrp.bom'].create({
|
||||
'product_tmpl_id': super_kit.product_tmpl_id.id,
|
||||
'product_qty': 1.0,
|
||||
'type': 'phantom',
|
||||
'bom_line_ids': [(0, 0, {
|
||||
'product_id': self.component_a.id,
|
||||
'product_qty': 2.0,
|
||||
})]
|
||||
})
|
||||
self.component_b.standard_price = 10
|
||||
self.component_a.button_bom_cost()
|
||||
super_kit.button_bom_cost()
|
||||
so_form = Form(self.env['sale.order'])
|
||||
so_form.partner_id = self.partner_a
|
||||
with so_form.order_line.new() as line:
|
||||
line.product_id = super_kit
|
||||
so = so_form.save()
|
||||
self.assertEqual(so.order_line.purchase_price, 60)
|
||||
so.action_confirm()
|
||||
self.assertEqual(so.order_line.purchase_price, 60)
|
||||
|
||||
@@ -122,6 +122,9 @@ tour.register('sale_matrix_tour', {
|
||||
extra_trigger: '.oe_subtotal_footer_separator:contains("65.32")',
|
||||
}, {
|
||||
trigger: '.o_form_button_save:contains("Save")',
|
||||
}, {
|
||||
trigger: '.o_form_button_edit:contains("Edit")',
|
||||
run: function () {}, // Ensure the form is saved before closing the browser
|
||||
},
|
||||
]);
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ class AccountMove(models.Model):
|
||||
if self.state == 'draft' or not self.invoice_date or self.move_type not in ('out_invoice', 'out_refund'):
|
||||
return []
|
||||
|
||||
current_invoice_amls = self.invoice_line_ids.filtered(lambda aml: not aml.display_type and aml.product_id and aml.quantity)
|
||||
current_invoice_amls = self.invoice_line_ids.filtered(lambda aml: not aml.display_type and aml.product_id and aml.product_id.type in ('consu', 'product') and aml.quantity)
|
||||
all_invoices_amls = current_invoice_amls.sale_line_ids.invoice_lines.filtered(lambda aml: aml.move_id.state == 'posted').sorted(lambda aml: (aml.date, aml.move_name, aml.id))
|
||||
index = all_invoices_amls.ids.index(current_invoice_amls[:1].id) if current_invoice_amls[:1] in all_invoices_amls else 0
|
||||
previous_amls = all_invoices_amls[:index]
|
||||
|
||||
@@ -18,7 +18,7 @@ options.registry.gallery = options.Class.extend({
|
||||
var self = this;
|
||||
|
||||
// Make sure image previews are updated if images are changed
|
||||
this.$target.on('image_changed', 'img', function (ev) {
|
||||
this.$target.on('image_changed.gallery', 'img', function (ev) {
|
||||
var $img = $(ev.currentTarget);
|
||||
var index = self.$target.find('.carousel-item.active').index();
|
||||
self.$('.carousel:first li[data-target]:eq(' + index + ')')
|
||||
@@ -27,12 +27,12 @@ options.registry.gallery = options.Class.extend({
|
||||
|
||||
// When the snippet is empty, an edition button is the default content
|
||||
// TODO find a nicer way to do that to have editor style
|
||||
this.$target.on('click', '.o_add_images', function (e) {
|
||||
this.$target.on('click.gallery', '.o_add_images', function (e) {
|
||||
e.stopImmediatePropagation();
|
||||
self.addImages(false);
|
||||
});
|
||||
|
||||
this.$target.on('dropped', 'img', function (ev) {
|
||||
this.$target.on('dropped.gallery', 'img', function (ev) {
|
||||
self.mode(null, self.getMode());
|
||||
if (!ev.target.height) {
|
||||
$(ev.target).one('load', function () {
|
||||
@@ -74,6 +74,13 @@ options.registry.gallery = options.Class.extend({
|
||||
this.$target.removeAttr('style');
|
||||
}
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
destroy() {
|
||||
this._super(...arguments);
|
||||
this.$target.off('.gallery');
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Options
|
||||
|
||||
@@ -718,7 +718,7 @@ class WebsiteSale(http.Controller):
|
||||
return request.redirect('/shop/checkout')
|
||||
|
||||
# IF POSTED
|
||||
if 'submitted' in kw:
|
||||
if 'submitted' in kw and request.httprequest.method == "POST":
|
||||
pre_values = self.values_preprocess(order, mode, kw)
|
||||
errors, error_msg = self.checkout_form_validate(mode, kw, pre_values)
|
||||
post, errors, error_msg = self.values_postprocess(order, mode, pre_values, errors, error_msg)
|
||||
|
||||
@@ -142,7 +142,8 @@ class TestWebsiteSaleCheckoutAddress(TransactionCaseWithUserDemo):
|
||||
p = self.env.user.partner_id
|
||||
so = self._create_so(p.id)
|
||||
|
||||
with MockRequest(self.env, website=self.website, sale_order_id=so.id):
|
||||
with MockRequest(self.env, website=self.website, sale_order_id=so.id) as req:
|
||||
req.httprequest.method = "POST"
|
||||
self.WebsiteSaleController.address(**self.default_address_values)
|
||||
self.assertFalse(self._get_last_address(p).website_id, "New shipping address should not have a website set on it (no specific_user_account).")
|
||||
|
||||
@@ -186,7 +187,9 @@ class TestWebsiteSaleCheckoutAddress(TransactionCaseWithUserDemo):
|
||||
|
||||
env = api.Environment(self.env.cr, self.demo_user.id, {})
|
||||
# change also website env for `sale_get_order` to not change order partner_id
|
||||
with MockRequest(env, website=self.website.with_env(env), sale_order_id=so.id):
|
||||
with MockRequest(env, website=self.website.with_env(env), sale_order_id=so.id) as req:
|
||||
req.httprequest.method = "POST"
|
||||
|
||||
# 1. Logged in user, new shipping
|
||||
self.WebsiteSaleController.address(**self.default_address_values)
|
||||
new_shipping = self._get_last_address(self.demo_partner)
|
||||
@@ -207,7 +210,9 @@ class TestWebsiteSaleCheckoutAddress(TransactionCaseWithUserDemo):
|
||||
|
||||
env = api.Environment(self.env.cr, self.website.user_id.id, {})
|
||||
# change also website env for `sale_get_order` to not change order partner_id
|
||||
with MockRequest(env, website=self.website.with_env(env), sale_order_id=so.id):
|
||||
with MockRequest(env, website=self.website.with_env(env), sale_order_id=so.id) as req:
|
||||
req.httprequest.method = "POST"
|
||||
|
||||
# 1. Public user, new billing
|
||||
self.default_address_values['partner_id'] = -1
|
||||
self.WebsiteSaleController.address(**self.default_address_values)
|
||||
|
||||
Reference in New Issue
Block a user