mirror of
https://gitlab.com/flectra-hq/flectra.git
synced 2025-02-25 18:55:21 -06:00
[PATCH] Upstream patch - 19012023
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from flectra import api, fields, models, _
|
||||
from flectra.tools import html_escape
|
||||
|
||||
class ChannelPartner(models.Model):
|
||||
_inherit = 'mail.channel.partner'
|
||||
@@ -180,7 +181,7 @@ class MailChannel(models.Model):
|
||||
def _send_history_message(self, pid, page_history):
|
||||
message_body = _('No history found')
|
||||
if page_history:
|
||||
html_links = ['<li><a href="%s" target="_blank">%s</a></li>' % (page, page) for page in page_history]
|
||||
html_links = ['<li><a href="%s" target="_blank">%s</a></li>' % (html_escape(page), html_escape(page)) for page in page_history]
|
||||
message_body = '<span class="o_mail_notification"><ul>%s</ul></span>' % (''.join(html_links))
|
||||
self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', pid), {
|
||||
'body': message_body,
|
||||
|
||||
@@ -157,7 +157,7 @@
|
||||
<tree string="Restaurant Order Printers">
|
||||
<field name="name" />
|
||||
<field name="proxy_ip" />
|
||||
<field name="product_categories_ids" />
|
||||
<field name="product_categories_ids" widget="many2many_tags"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
@@ -875,7 +875,7 @@ class Task(models.Model):
|
||||
|
||||
@api.depends('project_id.allowed_user_ids', 'project_id.privacy_visibility')
|
||||
def _compute_allowed_user_ids(self):
|
||||
for task in self:
|
||||
for task in self.with_context(prefetch_fields=False):
|
||||
portal_users = task.allowed_user_ids.filtered('share')
|
||||
internal_users = task.allowed_user_ids - portal_users
|
||||
if task.project_id.privacy_visibility == 'followers':
|
||||
|
||||
@@ -119,10 +119,11 @@ class AccountMoveLine(models.Model):
|
||||
if so_line:
|
||||
is_line_reversing = bool(self.move_id.reversed_entry_id)
|
||||
qty_to_invoice = self.product_uom_id._compute_quantity(self.quantity, self.product_id.uom_id)
|
||||
posted_invoice_lines = so_line.invoice_lines.filtered(lambda l: l.move_id.state == 'posted' and bool(l.move_id.reversed_entry_id) == is_line_reversing)
|
||||
qty_invoiced = sum([x.product_uom_id._compute_quantity(x.quantity, x.product_id.uom_id) for x in posted_invoice_lines])
|
||||
posted_cogs = so_line.invoice_lines.move_id.line_ids.filtered(lambda l: l.is_anglo_saxon_line and l.product_id == self.product_id and l.balance > 0)
|
||||
qty_invoiced = sum([line.product_uom_id._compute_quantity(line.quantity, line.product_id.uom_id) for line in posted_cogs])
|
||||
value_invoiced = sum(posted_cogs.mapped('balance'))
|
||||
|
||||
product = self.product_id.with_company(self.company_id).with_context(is_returned=is_line_reversing)
|
||||
product = self.product_id.with_company(self.company_id).with_context(is_returned=is_line_reversing, value_invoiced=value_invoiced)
|
||||
average_price_unit = product._compute_average_price(qty_invoiced, qty_to_invoice, so_line.move_ids)
|
||||
if average_price_unit:
|
||||
price_unit = self.product_id.uom_id.with_company(self.company_id)._compute_price(average_price_unit, self.product_uom_id)
|
||||
|
||||
@@ -1381,4 +1381,78 @@ class TestAngloSaxonValuation(ValuationReconciliationTestCommon):
|
||||
self.assertEqual(stock_out_aml.credit, 0)
|
||||
cogs_aml = amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_expense'])
|
||||
self.assertEqual(cogs_aml.debit, 0)
|
||||
self.assertEqual(cogs_aml.credit, 20, 'Should be to the value of the returned product')
|
||||
self.assertEqual(cogs_aml.credit, 20, 'Should be to the value of the returned product')
|
||||
|
||||
def test_fifo_several_invoices_reset_repost(self):
|
||||
self.product.categ_id.property_cost_method = 'fifo'
|
||||
|
||||
svl_values = [10, 15, 65]
|
||||
total_value = sum(svl_values)
|
||||
in_moves = self.env['stock.move'].create([{
|
||||
'name': 'IN move @%s' % p,
|
||||
'product_id': self.product.id,
|
||||
'location_id': self.env.ref('stock.stock_location_suppliers').id,
|
||||
'location_dest_id': self.company_data['default_warehouse'].lot_stock_id.id,
|
||||
'product_uom': self.product.uom_id.id,
|
||||
'product_uom_qty': 1,
|
||||
'price_unit': p,
|
||||
} for p in svl_values])
|
||||
in_moves._action_confirm()
|
||||
in_moves.quantity_done = 1
|
||||
in_moves._action_done()
|
||||
|
||||
so = self.env['sale.order'].create({
|
||||
'partner_id': self.partner_a.id,
|
||||
'order_line': [
|
||||
(0, 0, {
|
||||
'name': self.product.name,
|
||||
'product_id': self.product.id,
|
||||
'product_uom_qty': 3.0,
|
||||
'product_uom': self.product.uom_id.id,
|
||||
'price_unit': 100,
|
||||
'tax_id': False,
|
||||
})],
|
||||
})
|
||||
so.action_confirm()
|
||||
|
||||
# Deliver one by one, so it creates an out-SVL each time.
|
||||
# Then invoice the delivered quantity
|
||||
invoices = self.env['account.move']
|
||||
picking = so.picking_ids
|
||||
while picking:
|
||||
picking.move_lines.quantity_done = 1
|
||||
action = picking.button_validate()
|
||||
if isinstance(action, dict):
|
||||
wizard = Form(self.env[action['res_model']].with_context(action['context'])).save()
|
||||
wizard.process()
|
||||
picking = picking.backorder_ids
|
||||
|
||||
invoice = so._create_invoices()
|
||||
invoice.action_post()
|
||||
invoices |= invoice
|
||||
|
||||
out_account = self.product.categ_id.property_stock_account_output_categ_id
|
||||
invoice01, _invoice02, invoice03 = invoices
|
||||
cogs = invoices.line_ids.filtered(lambda l: l.account_id == out_account)
|
||||
self.assertEqual(cogs.mapped('credit'), svl_values)
|
||||
|
||||
# Reset and repost each invoice
|
||||
for i, inv in enumerate(invoices):
|
||||
inv.button_draft()
|
||||
inv.action_post()
|
||||
cogs = invoices.line_ids.filtered(lambda l: l.account_id == out_account)
|
||||
self.assertEqual(cogs.mapped('credit'), svl_values, 'Incorrect values while posting again invoice %s' % (i + 1))
|
||||
|
||||
# Reset and repost all invoices (we only check the total value as the
|
||||
# distribution changes but does not really matter)
|
||||
invoices.button_draft()
|
||||
invoices.action_post()
|
||||
cogs = invoices.line_ids.filtered(lambda l: l.account_id == out_account)
|
||||
self.assertEqual(sum(cogs.mapped('credit')), total_value)
|
||||
|
||||
# Reset and repost few invoices (we only check the total value as the
|
||||
# distribution changes but does not really matter)
|
||||
(invoice01 | invoice03).button_draft()
|
||||
(invoice01 | invoice03).action_post()
|
||||
cogs = invoices.line_ids.filtered(lambda l: l.account_id == out_account)
|
||||
self.assertEqual(sum(cogs.mapped('credit')), total_value)
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
<flectra>
|
||||
<template id="sale_stock_report_invoice_document" inherit_id="account.report_invoice_document">
|
||||
<xpath expr="//div[@id='total']" position="after">
|
||||
<t groups="sale_stock.group_lot_on_invoice">
|
||||
<t t-set="lot_values" t-value="o._get_invoiced_lot_values()"/>
|
||||
<t t-if="lot_values">
|
||||
<br/>
|
||||
<table groups="sale_stock.group_lot_on_invoice" class="table table-sm" style="width: 50%;" name="invoice_snln_table">
|
||||
<table class="table table-sm" style="width: 50%;" name="invoice_snln_table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><span>Product</span></th>
|
||||
@@ -27,6 +28,7 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</t>
|
||||
</t>
|
||||
</xpath>
|
||||
</template>
|
||||
</flectra>
|
||||
|
||||
@@ -669,46 +669,25 @@ class ProductProduct(models.Model):
|
||||
# if True, consider the incoming moves
|
||||
is_returned = self.env.context.get('is_returned', False)
|
||||
|
||||
returned_quantities = defaultdict(float)
|
||||
for move in stock_moves:
|
||||
if move.origin_returned_move_id:
|
||||
returned_quantities[move.origin_returned_move_id.id] += abs(sum(move.sudo().stock_valuation_layer_ids.mapped('quantity')))
|
||||
candidates = stock_moves\
|
||||
.sudo()\
|
||||
.filtered(lambda m: is_returned == bool(m.origin_returned_move_id and sum(m.stock_valuation_layer_ids.mapped('quantity')) >= 0))\
|
||||
.mapped('stock_valuation_layer_ids')\
|
||||
.sorted()
|
||||
qty_to_take_on_candidates = qty_to_invoice
|
||||
tmp_value = 0 # to accumulate the value taken on the candidates
|
||||
for candidate in candidates:
|
||||
if not candidate.quantity:
|
||||
continue
|
||||
candidate_quantity = abs(candidate.quantity)
|
||||
if candidate.stock_move_id.id in returned_quantities:
|
||||
candidate_quantity -= returned_quantities[candidate.stock_move_id.id]
|
||||
if float_is_zero(candidate_quantity, precision_rounding=candidate.uom_id.rounding):
|
||||
continue # correction entries
|
||||
if not float_is_zero(qty_invoiced, precision_rounding=candidate.uom_id.rounding):
|
||||
qty_ignored = min(qty_invoiced, candidate_quantity)
|
||||
qty_invoiced -= qty_ignored
|
||||
candidate_quantity -= qty_ignored
|
||||
if float_is_zero(candidate_quantity, precision_rounding=candidate.uom_id.rounding):
|
||||
continue
|
||||
qty_taken_on_candidate = min(qty_to_take_on_candidates, candidate_quantity)
|
||||
|
||||
qty_to_take_on_candidates -= qty_taken_on_candidate
|
||||
tmp_value += qty_taken_on_candidate * \
|
||||
((candidate.value + sum(candidate.stock_valuation_layer_ids.mapped('value'))) / candidate.quantity)
|
||||
if float_is_zero(qty_to_take_on_candidates, precision_rounding=candidate.uom_id.rounding):
|
||||
break
|
||||
value_invoiced = self.env.context.get('value_invoiced', 0)
|
||||
if 'value_invoiced' in self.env.context:
|
||||
qty_valued, valuation = candidates._consume_all(qty_invoiced, value_invoiced, qty_to_invoice)
|
||||
else:
|
||||
qty_valued, valuation = candidates._consume_specific_qty(qty_invoiced, qty_to_invoice)
|
||||
|
||||
# If there's still quantity to invoice but we're out of candidates, we chose the standard
|
||||
# price to estimate the anglo saxon price unit.
|
||||
if not float_is_zero(qty_to_take_on_candidates, precision_rounding=self.uom_id.rounding):
|
||||
negative_stock_value = self.standard_price * qty_to_take_on_candidates
|
||||
tmp_value += negative_stock_value
|
||||
missing = qty_to_invoice - qty_valued
|
||||
if float_compare(missing, 0, precision_rounding=self.uom_id.rounding) > 0:
|
||||
valuation += self.standard_price * missing
|
||||
|
||||
return tmp_value / qty_to_invoice
|
||||
return valuation / qty_to_invoice
|
||||
|
||||
|
||||
class ProductCategory(models.Model):
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from flectra import fields, models, tools
|
||||
from flectra.tools import float_compare, float_is_zero
|
||||
|
||||
|
||||
class StockValuationLayer(models.Model):
|
||||
@@ -36,3 +37,72 @@ class StockValuationLayer(models.Model):
|
||||
self._table, ['product_id', 'remaining_qty', 'stock_move_id', 'company_id', 'create_date']
|
||||
)
|
||||
|
||||
def _consume_specific_qty(self, qty_valued, qty_to_value):
|
||||
"""
|
||||
Iterate on the SVL to first skip the qty already valued. Then, keep
|
||||
iterating to consume `qty_to_value` and stop
|
||||
The method returns the valued quantity and its valuation
|
||||
"""
|
||||
if not self:
|
||||
return 0, 0
|
||||
|
||||
rounding = self.product_id.uom_id.rounding
|
||||
qty_to_take_on_candidates = qty_to_value
|
||||
tmp_value = 0 # to accumulate the value taken on the candidates
|
||||
for candidate in self:
|
||||
if float_is_zero(candidate.quantity, precision_rounding=rounding):
|
||||
continue
|
||||
candidate_quantity = abs(candidate.quantity)
|
||||
returned_qty = sum([sm.product_uom._compute_quantity(sm.quantity_done, self.uom_id)
|
||||
for sm in candidate.stock_move_id.returned_move_ids if sm.state == 'done'])
|
||||
candidate_quantity -= returned_qty
|
||||
if float_is_zero(candidate_quantity, precision_rounding=rounding):
|
||||
continue
|
||||
if not float_is_zero(qty_valued, precision_rounding=rounding):
|
||||
qty_ignored = min(qty_valued, candidate_quantity)
|
||||
qty_valued -= qty_ignored
|
||||
candidate_quantity -= qty_ignored
|
||||
if float_is_zero(candidate_quantity, precision_rounding=rounding):
|
||||
continue
|
||||
qty_taken_on_candidate = min(qty_to_take_on_candidates, candidate_quantity)
|
||||
|
||||
qty_to_take_on_candidates -= qty_taken_on_candidate
|
||||
tmp_value += qty_taken_on_candidate * ((candidate.value + sum(candidate.stock_valuation_layer_ids.mapped('value'))) / candidate.quantity)
|
||||
if float_is_zero(qty_to_take_on_candidates, precision_rounding=rounding):
|
||||
break
|
||||
|
||||
return qty_to_value - qty_to_take_on_candidates, tmp_value
|
||||
|
||||
def _consume_all(self, qty_valued, valued, qty_to_value):
|
||||
"""
|
||||
The method consumes all svl to get the total qty/value. Then it deducts
|
||||
the already consumed qty/value. Finally, it tries to consume the `qty_to_value`
|
||||
The method returns the valued quantity and its valuation
|
||||
"""
|
||||
if not self:
|
||||
return 0, 0
|
||||
|
||||
rounding = self.product_id.uom_id.rounding
|
||||
qty_total = -qty_valued
|
||||
value_total = -valued
|
||||
new_valued_qty = 0
|
||||
new_valuation = 0
|
||||
|
||||
for svl in self:
|
||||
if float_is_zero(svl.quantity, precision_rounding=rounding):
|
||||
continue
|
||||
relevant_qty = abs(svl.quantity)
|
||||
returned_qty = sum([sm.product_uom._compute_quantity(sm.quantity_done, self.uom_id)
|
||||
for sm in svl.stock_move_id.returned_move_ids if sm.state == 'done'])
|
||||
relevant_qty -= returned_qty
|
||||
if float_is_zero(relevant_qty, precision_rounding=rounding):
|
||||
continue
|
||||
qty_total += relevant_qty
|
||||
value_total += relevant_qty * ((svl.value + sum(svl.stock_valuation_layer_ids.mapped('value'))) / svl.quantity)
|
||||
|
||||
if float_compare(qty_total, 0, precision_rounding=rounding) > 0:
|
||||
unit_cost = value_total / qty_total
|
||||
new_valued_qty = min(qty_total, qty_to_value)
|
||||
new_valuation = unit_cost * new_valued_qty
|
||||
|
||||
return new_valued_qty, new_valuation
|
||||
|
||||
Reference in New Issue
Block a user