[PATCH] Upstream patch - 15012023

This commit is contained in:
Parthiv Patel
2023-01-15 08:35:25 +00:00
parent 426219349c
commit 1af23cc366
17 changed files with 357 additions and 34 deletions

View File

@@ -564,7 +564,7 @@ class AccountReconcileModel(models.Model):
or (self.match_amount == 'between' and (abs(st_line.amount) > self.match_amount_max or abs(st_line.amount) < self.match_amount_min))
or (self.match_partner and not partner)
or (self.match_partner and self.match_partner_ids and partner not in self.match_partner_ids)
or (self.match_partner and self.match_partner_category_ids and partner.category_id not in self.match_partner_category_ids)
or (self.match_partner and self.match_partner_category_ids and not (partner.category_id & self.match_partner_category_ids))
):
return False

View File

@@ -455,7 +455,9 @@ class TestReconciliationMatchingRules(AccountTestInvoicingCommon):
def test_matching_fields_match_partner_category_ids(self):
test_category = self.env['res.partner.category'].create({'name': 'Consulting Services'})
self.partner_2.category_id = test_category
test_category2 = self.env['res.partner.category'].create({'name': 'Consulting Services2'})
self.partner_2.category_id = test_category + test_category2
self.rule_1.match_partner_category_ids |= test_category
self._check_statement_matching(self.rule_1, {
self.bank_line_1.id: {'aml_ids': []},

View File

@@ -116,5 +116,7 @@ class User(models.Model):
_logger.info("Calendar Synchro - Starting synchronization for %s", user)
try:
user.with_user(user).sudo()._sync_google_calendar(google)
self.env.cr.commit()
except Exception as e:
_logger.exception("[%s] Calendar Synchro - Exception : %s !", user, exception_to_unicode(e))
self.env.cr.rollback()

View File

@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<flectra>
<record id="view_move_form_inherit_l10n_cl" model="ir.ui.view">
<field name="name">account.move.form.inherit.l10n.cl</field>
<field name="model">account.move</field>
@@ -9,9 +8,6 @@
<form>
<field name="l10n_latam_internal_type" invisible="1"/>
</form>
<field name="ref" position="attributes">
<attribute name="attrs">{'invisible': [('move_type', '=', 'out_invoice'), ('l10n_latam_internal_type', '!=', 'debit_note')], 'required': [('l10n_latam_internal_type', '=', 'debit_note')]}</attribute>
</field>
</field>
</record>

View File

@@ -2,11 +2,51 @@
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
from flectra import models
from flectra.tools import float_compare
class StockPicking(models.Model):
_inherit = 'stock.picking'
def _action_done(self):
""" If needed, create a compensation layer, so we add the MO cost
to the dropship one
"""
res = super()._action_done()
for move in self.move_lines:
if not (move.is_subcontract and move._is_dropshipped() and move.state == 'done'):
continue
dropship_svls = move.stock_valuation_layer_ids
if not dropship_svls:
continue
subcontract_svls = move.move_orig_ids.stock_valuation_layer_ids
subcontract_value = sum(subcontract_svls.mapped('value'))
dropship_value = abs(sum(dropship_svls.mapped('value')))
diff = subcontract_value - dropship_value
if float_compare(diff, 0, precision_rounding=move.company_id.currency_id.rounding) <= 0:
continue
svl_vals = move._prepare_common_svl_vals()
svl_vals.update({
'remaining_value': 0,
'remaining_qty': 0,
'value': -diff,
'quantity': 0,
'unit_cost': 0,
'stock_valuation_layer_id': dropship_svls[0].id,
'stock_move_id': False,
})
svl = self.env['stock.valuation.layer'].create(svl_vals)
move = move.with_company(move.company_id)
if move.product_id.valuation != 'real_time':
continue
move._account_entry_move(svl.quantity, svl.description, svl.id, svl.value)
return res
def _get_warehouse(self, subcontract_move):
if subcontract_move.sale_line_id:
return subcontract_move.sale_line_id.order_id.warehouse_id

View File

@@ -3,3 +3,4 @@
from . import test_purchase_subcontracting
from . import test_sale_dropshipping
from . import test_anglo_saxon_valuation

View File

@@ -0,0 +1,134 @@
# -*- coding: utf-8 -*-
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
from flectra.addons.stock_account.tests.test_anglo_saxon_valuation_reconciliation_common import ValuationReconciliationTestCommon
from flectra.tests import tagged, Form
@tagged('post_install', '-at_install')
class TestSubcontractingDropshippingValuation(ValuationReconciliationTestCommon):
@classmethod
def setUpClass(cls, chart_template_ref=None):
super().setUpClass(chart_template_ref=chart_template_ref)
categ_form = Form(cls.env['product.category'])
categ_form.name = 'fifo auto'
categ_form.parent_id = cls.env.ref('product.product_category_all')
categ_form.property_cost_method = 'fifo'
categ_form.property_valuation = 'real_time'
cls.categ_fifo_auto = categ_form.save()
(cls.product_a | cls.product_b).type = 'product'
cls.bom_a = cls.env['mrp.bom'].create({
'product_tmpl_id': cls.product_a.product_tmpl_id.id,
'type': 'subcontract',
'subcontractor_ids': [(6, 0, cls.partner_a.ids)],
'bom_line_ids': [
(0, 0, {'product_id': cls.product_b.id, 'product_qty': 1.0}),
],
})
def test_valuation_subcontracted_and_dropshipped(self):
"""
Product:
- FIFO + Auto
- Subcontracted
Purchase 2 from Subcontractor to a customer (dropship).
Then return 1 to subcontractor and one to stock
It should generate the correct valuations AMLs
"""
# pylint: disable=bad-whitespace
all_amls_ids = self.env['account.move.line'].search_read([], ['id'])
grp_multi_loc = self.env.ref('stock.group_stock_multi_locations')
self.env.user.write({'groups_id': [(4, grp_multi_loc.id)]})
(self.product_a | self.product_b).categ_id = self.categ_fifo_auto
self.product_b.standard_price = 10
dropship_picking_type = self.env['stock.picking.type'].search([
('company_id', '=', self.env.company.id),
('default_location_src_id.usage', '=', 'supplier'),
('default_location_dest_id.usage', '=', 'customer'),
], limit=1, order='sequence')
po = self.env['purchase.order'].create({
"partner_id": self.partner_a.id,
"picking_type_id": dropship_picking_type.id,
"dest_address_id": self.partner_b.id,
"order_line": [(0, 0, {
'product_id': self.product_a.id,
'name': self.product_a.name,
'product_qty': 2.0,
'price_unit': 100,
'taxes_id': False,
})],
})
po.button_confirm()
delivery = po.picking_ids
res = delivery.button_validate()
Form(self.env['stock.immediate.transfer'].with_context(res['context'])).save().process()
stock_in_acc_id = self.categ_fifo_auto.property_stock_account_input_categ_id.id
stock_out_acc_id = self.categ_fifo_auto.property_stock_account_output_categ_id.id
stock_valu_acc_id = self.categ_fifo_auto.property_stock_valuation_account_id.id
amls = self.env['account.move.line'].search([('id', 'not in', all_amls_ids)])
all_amls_ids += amls.ids
self.assertRecordValues(amls, [
# Compensation of dropshipping value
{'account_id': stock_valu_acc_id, 'product_id': self.product_a.id, 'debit': 0.0, 'credit': 20.0},
{'account_id': stock_out_acc_id, 'product_id': self.product_a.id, 'debit': 20.0, 'credit': 0.0},
# Receipt from subcontractor
{'account_id': stock_in_acc_id, 'product_id': self.product_a.id, 'debit': 0.0, 'credit': 220.0},
{'account_id': stock_valu_acc_id, 'product_id': self.product_a.id, 'debit': 220.0, 'credit': 0.0},
# Delivery to subcontractor
{'account_id': stock_valu_acc_id, 'product_id': self.product_b.id, 'debit': 0.0, 'credit': 20.0},
{'account_id': stock_out_acc_id, 'product_id': self.product_b.id, 'debit': 20.0, 'credit': 0.0},
# Initial dropshipped value
{'account_id': stock_valu_acc_id, 'product_id': self.product_a.id, 'debit': 0.0, 'credit': 200.0},
{'account_id': stock_out_acc_id, 'product_id': self.product_a.id, 'debit': 200.0, 'credit': 0.0},
])
# return to subcontracting location
sbc_location = self.env.company.subcontracting_location_id
return_form = Form(self.env['stock.return.picking'].with_context(active_id=delivery.id, active_model='stock.picking'))
return_form.location_id = sbc_location
with return_form.product_return_moves.edit(0) as line:
line.quantity = 1
return_wizard = return_form.save()
return_id, _ = return_wizard._create_returns()
return_picking = self.env['stock.picking'].browse(return_id)
return_picking.move_lines.quantity_done = 1
return_picking.button_validate()
amls = self.env['account.move.line'].search([('id', 'not in', all_amls_ids)])
all_amls_ids += amls.ids
self.assertRecordValues(amls, [
{'account_id': stock_valu_acc_id, 'product_id': self.product_a.id, 'debit': 0.0, 'credit': 110.0},
{'account_id': stock_in_acc_id, 'product_id': self.product_a.id, 'debit': 110.0, 'credit': 0.0},
])
# return to stock location
warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.env.company.id)], limit=1)
stock_location = warehouse.lot_stock_id
stock_location.return_location = True
return_form = Form(self.env['stock.return.picking'].with_context(active_id=delivery.id, active_model='stock.picking'))
return_form.location_id = stock_location
with return_form.product_return_moves.edit(0) as line:
line.quantity = 1
return_wizard = return_form.save()
return_id, _ = return_wizard._create_returns()
return_picking = self.env['stock.picking'].browse(return_id)
return_picking.move_lines.quantity_done = 1
return_picking.button_validate()
amls = self.env['account.move.line'].search([('id', 'not in', all_amls_ids)])
all_amls_ids += amls.ids
self.assertRecordValues(amls, [
{'account_id': stock_out_acc_id, 'product_id': self.product_a.id, 'debit': 0.0, 'credit': 110.0},
{'account_id': stock_valu_acc_id, 'product_id': self.product_a.id, 'debit': 110.0, 'credit': 0.0},
])

View File

@@ -6,14 +6,6 @@
-webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
}
.oe_form_gantt_avatars:after {
font-family: "mnmliconsRegular" !important;
font-size: 21px;
font-weight: 300 !important;
content: "y";
top: 3px;
position: relative;
}
.openerp .oe_kanban_view .oe_kanban_project {
width: 250px;

View File

@@ -37,7 +37,7 @@ class StockMove(models.Model):
def _get_price_unit(self):
""" Returns the unit price for the move"""
self.ensure_one()
if self.purchase_line_id and self.product_id.id == self.purchase_line_id.product_id.id:
if not self.origin_returned_move_id and self.purchase_line_id and self.product_id.id == self.purchase_line_id.product_id.id:
price_unit_prec = self.env['decimal.precision'].precision_get('Product Price')
line = self.purchase_line_id
order = line.order_id

View File

@@ -555,6 +555,8 @@ class Product(models.Model):
'route_ids': route_ids,
'warehouse_id': location.get_warehouse()
})
if rule in seen_rules:
raise UserError(_("Invalid rule's configuration, the following rule causes an endless loop: %s", rule.display_name))
if not rule:
return seen_rules
if rule.procure_method == 'make_to_stock' or rule.action not in ('pull_push', 'pull'):

View File

@@ -5,6 +5,7 @@ from datetime import date, datetime, timedelta
from flectra.tests.common import Form, TransactionCase
from flectra.tools import mute_logger
from flectra.exceptions import UserError
class TestProcRule(TransactionCase):
@@ -19,6 +20,40 @@ class TestProcRule(TransactionCase):
})
self.partner = self.env['res.partner'].create({'name': 'Partner'})
def test_endless_loop_rules_from_location(self):
""" Creates and configure a rule the way, when trying to get rules from
location, it goes in a state where the found rule tries to trigger another
rule but finds nothing else than itself and so get stuck in a recursion error."""
warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.env.company.id)], limit=1)
reception_route = warehouse.reception_route_id
self.product.type = 'product'
# Creates a delivery for this product, that way, this product will be to resupply.
picking_form = Form(self.env['stock.picking'])
picking_form.picking_type_id = warehouse.out_type_id
with picking_form.move_ids_without_package.new() as move_line:
move_line.product_id = self.product
move_line.product_uom_qty = 10
delivery = picking_form.save()
delivery.action_confirm()
self.product._compute_quantities() # Computes `outgoing_qty` to have the orderpoint.
# Then, creates a rule and adds it into the route's rules.
reception_route.rule_ids.action_archive()
self.env['stock.rule'].create({
'name': 'Looping Rule',
'route_id': reception_route.id,
'location_id': warehouse.lot_stock_id.id,
'location_src_id': warehouse.lot_stock_id.id,
'action': 'pull_push',
'procure_method': 'make_to_order',
'picking_type_id': warehouse.int_type_id.id,
})
# Tries to open the Replenishment view -> It should raise an UserError.
with self.assertRaises(UserError):
self.env['stock.warehouse.orderpoint'].action_open_orderpoints()
def test_proc_rule(self):
# Create a product route containing a stock rule that will
# generate a move from Stock for every procurement created in Output

View File

@@ -40,6 +40,7 @@ class StockMove(models.Model):
# 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:
layers = self.origin_returned_move_id.sudo().stock_valuation_layer_ids
layers |= layers.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, precision_rounding=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
@@ -80,7 +81,7 @@ class StockMove(models.Model):
:rtype: bool
"""
self.ensure_one()
if self._get_in_move_lines():
if self._get_in_move_lines() and not self._is_dropshipped_returned():
return True
return False
@@ -108,7 +109,7 @@ class StockMove(models.Model):
:rtype: bool
"""
self.ensure_one()
if self._get_out_move_lines():
if self._get_out_move_lines() and not self._is_dropshipped():
return True
return False
@@ -214,23 +215,26 @@ class StockMove(models.Model):
common_vals = dict(move._prepare_common_svl_vals(), remaining_qty=0)
# create the in
in_vals = {
'unit_cost': unit_cost,
'value': unit_cost * quantity,
'quantity': quantity,
}
in_vals.update(common_vals)
svl_vals_list.append(in_vals)
# create the in if it does not come from a valued location (eg subcontract -> customer)
if not move.location_id._should_be_valued():
in_vals = {
'unit_cost': unit_cost,
'value': unit_cost * quantity,
'quantity': quantity,
}
in_vals.update(common_vals)
svl_vals_list.append(in_vals)
# create the out if it does not go to a valued location (eg customer -> subcontract)
if not move.location_dest_id._should_be_valued():
out_vals = {
'unit_cost': unit_cost,
'value': unit_cost * quantity * -1,
'quantity': quantity * -1,
}
out_vals.update(common_vals)
svl_vals_list.append(out_vals)
# create the out
out_vals = {
'unit_cost': unit_cost,
'value': unit_cost * quantity * -1,
'quantity': quantity * -1,
}
out_vals.update(common_vals)
svl_vals_list.append(out_vals)
return self.env['stock.valuation.layer'].sudo().create(svl_vals_list)
def _create_dropshipped_returned_svl(self, forced_quantity=None):

View File

@@ -0,0 +1,93 @@
Copyright 2004-2021 The Oooh Baby Project Authors (https://github.com/googlefonts/oooh-baby)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

Binary file not shown.

View File

@@ -11,6 +11,15 @@ from functools import partial
def uninstall_hook(cr, registry):
# Cleanup records which are related to websites and will not be autocleaned
# by the uninstall operation. This must be done here in the uninstall_hook
# as during an uninstallation, `unlink` is not called for records which were
# created by the user (not XML data). Same goes for @api.ondelete available
# from 15.0 and above.
env = api.Environment(cr, SUPERUSER_ID, {})
env['website'].search([])._remove_attachments_on_website_unlink()
# Properly unlink website_id from ir.model.fields
def rem_website_id_null(dbname):
db_registry = flectra.modules.registry.Registry.new(dbname)
with api.Environment.manage(), db_registry.cursor() as cr:

View File

@@ -135,6 +135,15 @@ class Assets(models.AbstractModel):
self = self.sudo()
website = self.env['website'].get_current_website()
res = super(Assets, self)._get_custom_attachment(custom_url, op=op)
# FIXME (?) In website, those attachments should always have been
# created with a website_id. The "not website_id" part in the following
# condition might therefore be useless (especially since the attachments
# do not seem ordered). It was developed in the spirit of served
# attachments which follow this rule of "serve what belongs to the
# current website or all the websites" but it probably does not make
# sense here. It however allowed to discover a bug where attachments
# were left without website_id. This will be kept untouched in stable
# but will be reviewed and made more robust in master.
return res.with_context(website_id=website.id).filtered(lambda x: not x.website_id or x.website_id == website)
def _get_custom_view(self, custom_url, op='='):

View File

@@ -244,6 +244,11 @@ class Website(models.Model):
if not website:
raise UserError(_('You must keep at least one website.'))
self._remove_attachments_on_website_unlink()
return super().unlink()
def _remove_attachments_on_website_unlink(self):
# Do not delete invoices, delete what's strictly necessary
attachments_to_unlink = self.env['ir.attachment'].search([
('website_id', 'in', self.ids),
@@ -253,7 +258,6 @@ class Website(models.Model):
('url', 'ilike', '.assets\\_'),
])
attachments_to_unlink.unlink()
return super(Website, self).unlink()
def create_and_redirect_to_theme(self):
self._force()