[PATCH] Upstream patch - 19022023

This commit is contained in:
Parthiv Patel
2023-02-19 08:34:50 +00:00
parent 0b8e6c3abb
commit 807f032906
14 changed files with 364 additions and 30 deletions

View File

@@ -17,9 +17,7 @@ import json
import re
import warnings
#forbidden fields
INTEGRITY_HASH_MOVE_FIELDS = ('date', 'journal_id', 'company_id')
INTEGRITY_HASH_LINE_FIELDS = ('debit', 'credit', 'account_id', 'partner_id')
MAX_HASH_VERSION = 2
def calc_check_digits(number):
@@ -203,6 +201,7 @@ class AccountMove(models.Model):
help='Bank Account Number to which the invoice will be paid. A Company bank account if this is a Customer Invoice or Vendor Credit Note, otherwise a Partner bank account number.',
check_company=True)
payment_reference = fields.Char(string='Payment Reference', index=True, copy=False,
compute='_compute_payment_reference', store=True, readonly=False,
help="The payment reference to set on journal items.")
payment_id = fields.Many2one(
index=True,
@@ -1300,6 +1299,14 @@ class AccountMove(models.Model):
'message': "%s\n\n%s" % (changed, detected)
}}
@api.depends('state')
def _compute_payment_reference(self):
for move in self.filtered(lambda m: (
m.state == 'posted'
and m._auto_compute_invoice_reference()
)):
move.payment_reference = move._get_invoice_computed_reference()
def _get_last_sequence_domain(self, relaxed=False):
self.ensure_one()
if not self.date or not self.journal_id:
@@ -2057,8 +2064,8 @@ class AccountMove(models.Model):
def write(self, vals):
for move in self:
if (move.restrict_mode_hash_table and move.state == "posted" and set(vals).intersection(INTEGRITY_HASH_MOVE_FIELDS)):
raise UserError(_("You cannot edit the following fields due to restrict mode being activated on the journal: %s.") % ', '.join(INTEGRITY_HASH_MOVE_FIELDS))
if (move.restrict_mode_hash_table and move.state == "posted" and set(vals).intersection(move._get_integrity_hash_fields())):
raise UserError(_("You cannot edit the following fields due to restrict mode being activated on the journal: %s.") % ', '.join(move._get_integrity_hash_fields()))
if (move.restrict_mode_hash_table and move.inalterable_hash and 'inalterable_hash' in vals) or (move.secure_sequence_number and 'secure_sequence_number' in vals):
raise UserError(_('You cannot overwrite the values ensuring the inalterability of the accounting.'))
if (move.posted_before and 'journal_id' in vals and move.journal_id.id != vals['journal_id']):
@@ -2742,15 +2749,6 @@ class AccountMove(models.Model):
for move in to_post:
move.message_subscribe([p.id for p in [move.partner_id] if p not in move.sudo().message_partner_ids])
# Compute 'ref' for 'out_invoice'.
if move._auto_compute_invoice_reference():
to_write = {
'payment_reference': move._get_invoice_computed_reference(),
'line_ids': []
}
for line in move.line_ids.filtered(lambda line: line.account_id.user_type_id.type in ('receivable', 'payable')):
to_write['line_ids'].append((1, line.id, {'name': to_write['payment_reference']}))
move.write(to_write)
for move in to_post:
if move.is_sale_document() \
@@ -2904,6 +2902,18 @@ class AccountMove(models.Model):
'context': ctx,
}
def _get_integrity_hash_fields(self):
# Use the latest hash version by default, but keep the old one for backward compatibility when generating the integrity report.
hash_version = self._context.get('hash_version', MAX_HASH_VERSION)
if hash_version == 1:
return ['date', 'journal_id', 'company_id']
elif hash_version == MAX_HASH_VERSION:
return ['name', 'date', 'journal_id', 'company_id']
raise NotImplementedError(f"hash_version={hash_version} doesn't exist")
def _get_integrity_hash_fields_and_subfields(self):
return self._get_integrity_hash_fields() + [f'line_ids.{subfield}' for subfield in self.line_ids._get_integrity_hash_fields()]
def _get_new_hash(self, secure_seq_number):
""" Returns the hash to write on journal entries when they get posted"""
self.ensure_one()
@@ -2927,6 +2937,8 @@ class AccountMove(models.Model):
hash_string = sha256((previous_hash + self.string_to_hash).encode('utf-8'))
return hash_string.hexdigest()
@api.depends(lambda self: self._get_integrity_hash_fields_and_subfields())
@api.depends_context('hash_version')
def _compute_string_to_hash(self):
def _getattrstring(obj, field_str):
field_value = obj[field_str]
@@ -2936,11 +2948,11 @@ class AccountMove(models.Model):
for move in self:
values = {}
for field in INTEGRITY_HASH_MOVE_FIELDS:
for field in move._get_integrity_hash_fields():
values[field] = _getattrstring(move, field)
for line in move.line_ids:
for field in INTEGRITY_HASH_LINE_FIELDS:
for field in line._get_integrity_hash_fields():
k = 'line_%d_%s' % (line.id, field)
values[k] = _getattrstring(line, field)
#make the json serialization canonical
@@ -3246,7 +3258,7 @@ class AccountMoveLine(models.Model):
account_internal_group = fields.Selection(related='account_id.user_type_id.internal_group', string="Internal Group", readonly=True)
account_root_id = fields.Many2one(related='account_id.root_id', string="Account Root", store=True, readonly=True)
sequence = fields.Integer(default=10)
name = fields.Char(string='Label', tracking=True)
name = fields.Char(string='Label', tracking=True, compute='_compute_name', store=True, readonly=False)
quantity = fields.Float(string='Quantity',
default=lambda self: 0 if self._context.get('default_display_type') else 1.0, digits='Product Unit of Measure',
help="The optional quantity expressed by this line, eg: number of product sold. "
@@ -3425,6 +3437,15 @@ class AccountMoveLine(models.Model):
account = repartition_line.account_id
return account
def _get_integrity_hash_fields(self):
# Use the new hash version by default, but keep the old one for backward compatibility when generating the integrity report.
hash_version = self._context.get('hash_version', MAX_HASH_VERSION)
if hash_version == 1:
return ['debit', 'credit', 'account_id', 'partner_id']
elif hash_version == MAX_HASH_VERSION:
return ['name', 'debit', 'credit', 'account_id', 'partner_id']
raise NotImplementedError(f"hash_version={hash_version} doesn't exist")
def _get_computed_name(self):
self.ensure_one()
@@ -3577,6 +3598,11 @@ class AccountMoveLine(models.Model):
if rec:
record.analytic_tag_ids = rec.analytic_tag_ids
@api.depends('move_id.payment_reference')
def _compute_name(self):
for line in self.filtered(lambda l: not l.name and l.account_id.user_type_id.type in ('receivable', 'payable')):
line.name = line.move_id.payment_reference
def _get_price_total_and_subtotal(self, price_unit=None, quantity=None, discount=None, currency=None, product=None, partner=None, taxes=None, move_type=None):
self.ensure_one()
return self._get_price_total_and_subtotal_model(
@@ -4220,10 +4246,18 @@ class AccountMoveLine(models.Model):
if account_to_write and account_to_write.deprecated:
raise UserError(_('You cannot use a deprecated account.'))
inalterable_fields = set(self._get_integrity_hash_fields()).union({'inalterable_hash', 'secure_sequence_number'})
hashed_moves = self.move_id.filtered('inalterable_hash')
violated_fields = set(vals) & inalterable_fields
if hashed_moves and violated_fields:
raise UserError(_(
"You cannot edit the following fields: %s.\n"
"The following entries are already hashed:\n%s",
', '.join(f['string'] for f in self.fields_get(violated_fields).values()),
'\n'.join(hashed_moves.mapped('name')),
))
for line in self:
if line.parent_state == 'posted':
if line.move_id.restrict_mode_hash_table and set(vals).intersection(INTEGRITY_HASH_LINE_FIELDS):
raise UserError(_("You cannot edit the following fields due to restrict mode being activated on the journal: %s.") % ', '.join(INTEGRITY_HASH_LINE_FIELDS))
if any(key in vals for key in ('tax_ids', 'tax_line_id')):
raise UserError(_('You cannot modify the taxes related to a posted journal item, you should reset the journal entry to draft to do so.'))

View File

@@ -4,6 +4,7 @@ from datetime import timedelta, datetime, date
import calendar
from dateutil.relativedelta import relativedelta
from flectra.addons.account.models.account_move import MAX_HASH_VERSION
from flectra import fields, models, api, _
from flectra.exceptions import ValidationError, UserError, RedirectWarning
from flectra.tools.misc import format_date
@@ -515,8 +516,13 @@ class ResCompany(models.Model):
previous_hash = u''
start_move_info = []
hash_corrupted = False
current_hash_version = 1
for move in moves:
if move.inalterable_hash != move._compute_hash(previous_hash=previous_hash):
computed_hash = move.with_context(hash_version=current_hash_version)._compute_hash(previous_hash=previous_hash)
while move.inalterable_hash != computed_hash and current_hash_version < MAX_HASH_VERSION:
current_hash_version += 1
computed_hash = move.with_context(hash_version=current_hash_version)._compute_hash(previous_hash=previous_hash)
if move.inalterable_hash != computed_hash:
rslt.update({'msg_cover': _('Corrupted data on journal entry with id %s.', move.id)})
results_by_journal['results'].append(rslt)
hash_corrupted = True

View File

@@ -8,6 +8,7 @@ from . import test_account_move_in_invoice
from . import test_account_move_in_refund
from . import test_account_move_entry
from . import test_invoice_tax_amount_by_group
from . import test_account_inalterable_hash
from . import test_account_journal
from . import test_account_account
from . import test_account_tax

View File

@@ -0,0 +1,186 @@
from flectra.addons.account.tests.common import AccountTestInvoicingCommon
from flectra.models import Model
from flectra.tests import tagged
from flectra import fields
from flectra.exceptions import UserError
from flectra.tools import format_date
@tagged('post_install', '-at_install')
class TestAccountMoveInalterableHash(AccountTestInvoicingCommon):
@classmethod
def setUpClass(cls, chart_template_ref=None):
super().setUpClass(chart_template_ref=chart_template_ref)
def test_account_move_inalterable_hash(self):
"""Test that we cannot alter a field used for the computation of the inalterable hash"""
self.company_data['default_journal_sale'].restrict_mode_hash_table = True
move = self.init_invoice("out_invoice", self.partner_a, "2023-01-01", amounts=[1000], post=True)
with self.assertRaisesRegex(UserError, "You cannot overwrite the values ensuring the inalterability of the accounting."):
move.inalterable_hash = 'fake_hash'
with self.assertRaisesRegex(UserError, "You cannot overwrite the values ensuring the inalterability of the accounting."):
move.secure_sequence_number = 666
with self.assertRaisesRegex(UserError, "You cannot edit the following fields due to restrict mode being activated.*"):
move.name = "fake name"
with self.assertRaisesRegex(UserError, "You cannot edit the following fields due to restrict mode being activated.*"):
move.date = fields.Date.from_string('2023-01-02')
with self.assertRaisesRegex(UserError, "You cannot edit the following fields due to restrict mode being activated.*"):
move.company_id = 666
with self.assertRaisesRegex(UserError, "You cannot edit the following fields due to restrict mode being activated.*"):
move.write({
'company_id': 666,
'date': fields.Date.from_string('2023-01-03')
})
with self.assertRaisesRegex(UserError, "You cannot edit the following fields.*Account.*"):
move.line_ids[0].account_id = move.line_ids[1]['account_id']
with self.assertRaisesRegex(UserError, "You cannot edit the following fields.*Partner.*"):
move.line_ids[0].partner_id = 666
# The following fields are not part of the hash so they can be modified
move.invoice_date_due = fields.Date.from_string('2023-01-02')
move.line_ids[0].date_maturity = fields.Date.from_string('2023-01-02')
def test_account_move_hash_integrity_report(self):
"""Test the hash integrity report"""
moves = (
self.init_invoice("out_invoice", self.partner_a, "2023-01-01", amounts=[1000, 2000])
| self.init_invoice("out_invoice", self.partner_b, "2023-01-02", amounts=[1000, 2000])
)
moves.action_post()
# No records to be hashed because the restrict mode is not activated yet
integrity_check = moves.company_id._check_hash_integrity()['results'][0] # First journal
self.assertEqual(integrity_check['msg_cover'], 'This journal is not in strict mode.')
# No records to be hashed even if the restrict mode is activated because the hashing is not retroactive
self.company_data['default_journal_sale'].restrict_mode_hash_table = True
integrity_check = moves.company_id._check_hash_integrity()['results'][0]
self.assertEqual(integrity_check['msg_cover'], 'There isn\'t any journal entry flagged for data inalterability yet for this journal.')
# Everything should be correctly hashed and verified
new_moves = (
self.init_invoice("out_invoice", self.partner_a, "2023-01-03", amounts=[1000, 2000])
| self.init_invoice("out_invoice", self.partner_b, "2023-01-04", amounts=[1000, 2000])
| self.init_invoice("out_invoice", self.partner_a, "2023-01-05", amounts=[1000, 2000])
| self.init_invoice("out_invoice", self.partner_b, "2023-01-06", amounts=[1000, 2000])
| self.init_invoice("out_invoice", self.partner_a, "2023-01-07", amounts=[1000, 2000])
)
new_moves.action_post()
moves |= new_moves
integrity_check = moves.company_id._check_hash_integrity()['results'][0]
self.assertRegex(integrity_check['msg_cover'], f'Entries are hashed from {moves[2].name}.*')
self.assertEqual(integrity_check['first_move_date'], format_date(self.env, fields.Date.to_string(moves[2].date)))
self.assertEqual(integrity_check['last_move_date'], format_date(self.env, fields.Date.to_string(moves[-1].date)))
# Let's change one of the fields used by the hash. It should be detected by the integrity report.
# We need to bypass the write method of account.move to do so.
Model.write(moves[4], {'date': fields.Date.from_string('2023-01-07')})
integrity_check = moves.company_id._check_hash_integrity()['results'][0]
self.assertEqual(integrity_check['msg_cover'], f'Corrupted data on journal entry with id {moves[4].id}.')
# Let's try with the one of the subfields
Model.write(moves[4], {'date': fields.Date.from_string("2023-01-05")}) # Revert the previous change
Model.write(moves[-1].line_ids[0], {'partner_id': self.partner_b.id})
integrity_check = moves.company_id._check_hash_integrity()['results'][0]
self.assertEqual(integrity_check['msg_cover'], f'Corrupted data on journal entry with id {moves[-1].id}.')
# Let's try with the inalterable_hash field itself
Model.write(moves[-1].line_ids[0], {'partner_id': self.partner_a.id}) # Revert the previous change
Model.write(moves[-1], {'inalterable_hash': 'fake_hash'})
integrity_check = moves.company_id._check_hash_integrity()['results'][0]
self.assertEqual(integrity_check['msg_cover'], f'Corrupted data on journal entry with id {moves[-1].id}.')
def test_account_move_hash_versioning_1(self):
"""We are updating the hash algorithm. We want to make sure that we do not break the integrity report.
This test focuses on the case where the user has only moves with the old hash algorithm."""
self.init_invoice("out_invoice", self.partner_a, "2023-01-01", amounts=[1000, 2000], post=True) # Not hashed
self.company_data['default_journal_sale'].restrict_mode_hash_table = True
moves = (
self.init_invoice("out_invoice", self.partner_a, "2023-01-02", amounts=[1000, 2000])
| self.init_invoice("out_invoice", self.partner_b, "2023-01-03", amounts=[1000, 2000])
| self.init_invoice("out_invoice", self.partner_b, "2023-01-04", amounts=[1000, 2000])
)
moves.with_context(hash_version=1).action_post()
integrity_check = moves.company_id._check_hash_integrity()['results'][0]
self.assertRegex(integrity_check['msg_cover'], f'Entries are hashed from {moves[0].name}.*')
self.assertEqual(integrity_check['first_move_date'], format_date(self.env, fields.Date.to_string(moves[0].date)))
self.assertEqual(integrity_check['last_move_date'], format_date(self.env, fields.Date.to_string(moves[-1].date)))
# Let's change one of the fields used by the hash. It should be detected by the integrity report
# independently of the hash version used. I.e. we first try the v1 hash, then the v2 hash and neither should work.
# We need to bypass the write method of account.move to do so.
Model.write(moves[1], {'date': fields.Date.from_string('2023-01-07')})
integrity_check = moves.company_id._check_hash_integrity()['results'][0]
self.assertEqual(integrity_check['msg_cover'], f'Corrupted data on journal entry with id {moves[1].id}.')
def test_account_move_hash_versioning_2(self):
"""We are updating the hash algorithm. We want to make sure that we do not break the integrity report.
This test focuses on the case where the user has only moves with the new hash algorithm."""
self.init_invoice("out_invoice", self.partner_a, "2023-01-01", amounts=[1000, 2000], post=True) # Not hashed
self.company_data['default_journal_sale'].restrict_mode_hash_table = True
moves = (
self.init_invoice("out_invoice", self.partner_a, "2023-01-01", amounts=[1000, 2000])
| self.init_invoice("out_invoice", self.partner_b, "2023-01-02", amounts=[1000, 2000])
| self.init_invoice("out_invoice", self.partner_b, "2023-01-03", amounts=[1000, 2000])
)
moves.action_post()
integrity_check = moves.company_id._check_hash_integrity()['results'][0]
self.assertRegex(integrity_check['msg_cover'], f'Entries are hashed from {moves[0].name}.*')
self.assertEqual(integrity_check['first_move_date'], format_date(self.env, fields.Date.to_string(moves[0].date)))
self.assertEqual(integrity_check['last_move_date'], format_date(self.env, fields.Date.to_string(moves[-1].date)))
# Let's change one of the fields used by the hash. It should be detected by the integrity report
# independently of the hash version used. I.e. we first try the v1 hash, then the v2 hash and neither should work.
# We need to bypass the write method of account.move to do so.
Model.write(moves[1], {'date': fields.Date.from_string('2023-01-07')})
integrity_check = moves.company_id._check_hash_integrity()['results'][0]
self.assertEqual(integrity_check['msg_cover'], f'Corrupted data on journal entry with id {moves[1].id}.')
def test_account_move_hash_versioning_v1_to_v2(self):
"""We are updating the hash algorithm. We want to make sure that we do not break the integrity report.
This test focuses on the case where the user has moves with both hash algorithms."""
self.init_invoice("out_invoice", self.partner_a, "2023-01-01", amounts=[1000, 2000], post=True) # Not hashed
self.company_data['default_journal_sale'].restrict_mode_hash_table = True
moves_v1 = (
self.init_invoice("out_invoice", self.partner_a, "2023-01-01", amounts=[1000, 2000])
| self.init_invoice("out_invoice", self.partner_b, "2023-01-02", amounts=[1000, 2000])
| self.init_invoice("out_invoice", self.partner_b, "2023-01-03", amounts=[1000, 2000])
)
moves_v1.with_context(hash_version=1).action_post()
fields_v1 = moves_v1.with_context(hash_version=1)._get_integrity_hash_fields()
moves_v2 = (
self.init_invoice("out_invoice", self.partner_a, "2023-01-01", amounts=[1000, 2000])
| self.init_invoice("out_invoice", self.partner_b, "2023-01-02", amounts=[1000, 2000])
| self.init_invoice("out_invoice", self.partner_b, "2023-01-03", amounts=[1000, 2000])
)
moves_v2.with_context(hash_version=2).action_post()
fields_v2 = moves_v2._get_integrity_hash_fields()
self.assertNotEqual(fields_v1, fields_v2) # Make sure two different hash algorithms were used
moves = moves_v1 | moves_v2
integrity_check = moves.company_id._check_hash_integrity()['results'][0]
self.assertRegex(integrity_check['msg_cover'], f'Entries are hashed from {moves[0].name}.*')
self.assertEqual(integrity_check['first_move_date'], format_date(self.env, fields.Date.to_string(moves[0].date)))
self.assertEqual(integrity_check['last_move_date'], format_date(self.env, fields.Date.to_string(moves[-1].date)))
# Let's change one of the fields used by the hash. It should be detected by the integrity report
# independently of the hash version used. I.e. we first try the v1 hash, then the v2 hash and neither should work.
# We need to bypass the write method of account.move to do so.
Model.write(moves[4], {'date': fields.Date.from_string('2023-01-07')})
integrity_check = moves.company_id._check_hash_integrity()['results'][0]
self.assertEqual(integrity_check['msg_cover'], f'Corrupted data on journal entry with id {moves[4].id}.')
# Let's revert the change and make sure that we cannot use the v1 after the v2.
# This means we don't simply check whether the move is correctly hashed with either algorithms,
# but that we can only use v2 after v1 and not go back to v1 afterwards.
Model.write(moves[4], {'date': fields.Date.from_string("2023-01-02")}) # Revert the previous change
moves_v1_bis = (
self.init_invoice("out_invoice", self.partner_a, "2023-01-10", amounts=[1000, 2000])
| self.init_invoice("out_invoice", self.partner_b, "2023-01-11", amounts=[1000, 2000])
| self.init_invoice("out_invoice", self.partner_b, "2023-01-12", amounts=[1000, 2000])
)
moves_v1_bis.with_context(hash_version=1).action_post()
integrity_check = moves.company_id._check_hash_integrity()['results'][0]
self.assertEqual(integrity_check['msg_cover'], f'Corrupted data on journal entry with id {moves_v1_bis[0].id}.')

View File

@@ -40,6 +40,7 @@ class Partner(models.Model):
replacement_xml = """
<div>
<field name="country_enforce_cities" invisible="1"/>
<field name="type" invisible="1"/>
<field name="parent_id" invisible="1"/>
<field name='city' placeholder="%(placeholder)s" class="o_address_city"
attrs="{

View File

@@ -249,12 +249,11 @@ class StockPicking(models.Model):
sale_order = self.sale_id
if sale_order and self.carrier_id.invoice_policy == 'real' and self.carrier_price:
delivery_lines = sale_order.order_line.filtered(lambda l: l.is_delivery and l.currency_id.is_zero(l.price_unit) and l.product_id == self.carrier_id.product_id)
carrier_price = self.carrier_price * (1.0 + (float(self.carrier_id.margin) / 100.0))
if not delivery_lines:
delivery_lines = [sale_order._create_delivery_line(self.carrier_id, carrier_price)]
delivery_lines = [sale_order._create_delivery_line(self.carrier_id, self.carrier_price)]
delivery_line = delivery_lines[0]
delivery_line[0].write({
'price_unit': carrier_price,
'price_unit': self.carrier_price,
# remove the estimated price from the description
'name': self.carrier_id.with_context(lang=self.partner_id.lang).name,
})

View File

@@ -78,7 +78,7 @@ class GoogleService(models.AbstractModel):
}
get_param = self.env['ir.config_parameter'].sudo().get_param
base_url = get_param('web.base.url', default='http://www.flectrahq.com?NoBaseUrl')
base_url = self._context.get('base_url') or self.env.user.get_base_url()
client_id = get_param('google_%s_client_id' % (service,), default=False)
encoded_params = urls.url_encode({

View File

@@ -18,7 +18,8 @@ class GoogleCalendarController(http.Controller):
this URL for authorization for example
"""
if model == 'calendar.event':
GoogleCal = GoogleCalendarService(request.env['google.service'])
base_url = request.httprequest.url_root.strip('/')
GoogleCal = GoogleCalendarService(request.env['google.service'].with_context(base_url=base_url))
# Checking that admin have already configured Google API for google synchronization !
client_id = request.env['ir.config_parameter'].sudo().get_param('google_calendar_client_id')

View File

@@ -10,6 +10,7 @@ const {
afterNextRender,
beforeEach,
createRootComponent,
nextTick,
start,
} = require('mail/static/src/utils/test_utils.js');
@@ -151,6 +152,45 @@ QUnit.test("suggested recipient without partner are unchecked by default", async
);
});
QUnit.test("suggested recipient without partner are unchecked when closing the dialog without creating partner", async function (assert) {
assert.expect(1);
this.data['res.fake'].records.push({
id: 10,
email_cc: "john@test.be",
});
const params = {
archs: {},
};
params.archs["res.partner,false,form"] = `
<form>
<field name="name"/>
</form>
`;
await this.start(params);
const chatter = this.env.models['mail.chatter'].create({
threadId: 10,
threadModel: 'res.fake',
});
await this.createChatterComponent({ chatter });
await afterNextRender(() =>
document.querySelector(`.o_ChatterTopbar_buttonSendMessage`).click()
);
// click on checkbox to open dialog
document.querySelector('.o_ComposerSuggestedRecipient:not([data-partner-id]) input[type=checkbox]').click();
await nextTick();
// close dialog without changing anything
document.querySelector('.modal-dialog .close').click();
assert.notOk(
document.querySelector('.o_ComposerSuggestedRecipient:not([data-partner-id]) input[type=checkbox]').checked,
"suggested recipient without partner must be unchecked",
);
});
QUnit.test("suggested recipient with partner are checked by default", async function (assert) {
assert.expect(1);

View File

@@ -129,6 +129,7 @@ class ComposerSuggestedRecipient extends Component {
this._isDialogOpen = true;
widget.on('closed', this, () => {
this._isDialogOpen = false;
this._checkboxRef.el.checked = !!this.suggestedRecipientInfo.partner;
});
widget.context = Object.assign({}, widget.context, session.user_context)
widget.open();

View File

@@ -64,6 +64,8 @@ class StockPicking(models.Model):
for picking in self:
productions_to_done = picking._get_subcontracted_productions()._subcontracting_filter_to_done()
if not productions_to_done:
continue
production_ids_backorder = []
if not self.env.context.get('cancel_backorder'):
production_ids_backorder = productions_to_done.filtered(lambda mo: mo.state == "progress").ids

View File

@@ -54,14 +54,14 @@ class StockRule(models.Model):
group_id = fields.Many2one('procurement.group', 'Fixed Procurement Group')
action = fields.Selection(
selection=[('pull', 'Pull From'), ('push', 'Push To'), ('pull_push', 'Pull & Push')], string='Action',
required=True)
required=True, index=True)
sequence = fields.Integer('Sequence', default=20)
company_id = fields.Many2one('res.company', 'Company',
default=lambda self: self.env.company,
domain="[('id', '=?', route_company_id)]")
location_id = fields.Many2one('stock.location', 'Destination Location', required=True, check_company=True)
domain="[('id', '=?', route_company_id)]", index=True)
location_id = fields.Many2one('stock.location', 'Destination Location', required=True, check_company=True, index=True)
location_src_id = fields.Many2one('stock.location', 'Source Location', check_company=True)
route_id = fields.Many2one('stock.location.route', 'Route', required=True, ondelete='cascade')
route_id = fields.Many2one('stock.location.route', 'Route', required=True, ondelete='cascade', index=True)
route_company_id = fields.Many2one(related='route_id.company_id', string='Route Company')
procure_method = fields.Selection([
('make_to_stock', 'Take From Stock'),
@@ -85,7 +85,7 @@ class StockRule(models.Model):
propagate_cancel = fields.Boolean(
'Cancel Next Move', default=False,
help="When ticked, if the move created by this rule is cancelled, the next move will be cancelled too.")
warehouse_id = fields.Many2one('stock.warehouse', 'Warehouse', check_company=True)
warehouse_id = fields.Many2one('stock.warehouse', 'Warehouse', check_company=True, index=True)
propagate_warehouse_id = fields.Many2one(
'stock.warehouse', 'Warehouse to Propagate',
help="The warehouse to propagate on the created move/procurement, which can be different of the warehouse this rule is for (e.g for resupplying rules from another warehouse)")

View File

@@ -369,6 +369,11 @@ class TestBatchPicking02(TransactionCase):
'type': 'product',
'categ_id': self.env.ref('product.product_category_all').id,
})
self.productB = self.env['product.product'].create({
'name': 'Product B',
'type': 'product',
'categ_id': self.env.ref('product.product_category_all').id,
})
def test_same_package_several_pickings(self):
"""
@@ -416,3 +421,53 @@ class TestBatchPicking02(TransactionCase):
{'state': 'done', 'quantity_done': 7},
])
self.assertEqual(pickings.move_line_ids.result_package_id, package)
def test_batch_validation_without_backorder(self):
loc1, loc2 = self.stock_location.child_ids
self.env['stock.quant']._update_available_quantity(self.productA, loc1, 10)
self.env['stock.quant']._update_available_quantity(self.productB, loc1, 10)
picking_1 = self.env['stock.picking'].create({
'location_id': loc1.id,
'location_dest_id': loc2.id,
'picking_type_id': self.picking_type_internal.id,
'company_id': self.env.company.id,
})
self.env['stock.move'].create({
'name': self.productA.name,
'product_id': self.productA.id,
'product_uom_qty': 1,
'product_uom': self.productA.uom_id.id,
'picking_id': picking_1.id,
'location_id': loc1.id,
'location_dest_id': loc2.id,
})
picking_2 = self.env['stock.picking'].create({
'location_id': loc1.id,
'location_dest_id': loc2.id,
'picking_type_id': self.picking_type_internal.id,
'company_id': self.env.company.id,
})
self.env['stock.move'].create({
'name': self.productB.name,
'product_id': self.productB.id,
'product_uom_qty': 5,
'product_uom': self.productB.uom_id.id,
'picking_id': picking_2.id,
'location_id': loc1.id,
'location_dest_id': loc2.id,
})
(picking_1 | picking_2).action_confirm()
(picking_1 | picking_2).action_assign()
picking_2.move_lines.move_line_ids.write({'qty_done': 1})
batch = self.env['stock.picking.batch'].create({
'name': 'Batch 1',
'company_id': self.env.company.id,
'picking_ids': [(4, picking_1.id), (4, picking_2.id)]
})
batch.action_confirm()
action = batch.action_done()
Form(self.env[action['res_model']].with_context(action['context'])).save().process_cancel_backorder()
self.assertEqual(batch.state, 'done')

View File

@@ -20,6 +20,14 @@ options.registry.TableOfContent = options.Class.extend({
this.observer.observe(targetNode, config);
return this._super(...arguments);
},
/**
* @override
*/
destroy: function () {
// The observer needs to be disconnected first.
this.observer.disconnect();
this._super(...arguments);
},
/**
* @override
*/