[PATCH] Upstream patch - 31032022

This commit is contained in:
Parthiv Patel
2022-03-31 08:35:36 +00:00
parent 36279f8d01
commit f2086f755d
23 changed files with 479 additions and 48 deletions

View File

@@ -23,8 +23,6 @@ class FetchmailServer(models.Model):
self.is_ssl = True
self.port = 993
else:
self.server_type = 'pop'
self.is_ssl = False
self.google_gmail_authorization_code = False
self.google_gmail_refresh_token = False
self.google_gmail_access_token = False

View File

@@ -3,6 +3,7 @@
<record id="fetchmail_server_view_form" model="ir.ui.view">
<field name="name">fetchmail.server.view.form.inherit.gmail</field>
<field name="model">fetchmail.server</field>
<field name="priority">100</field>
<field name="inherit_id" ref="fetchmail.view_email_server_form"/>
<field name="arch" type="xml">
<field name="server" position="before">

View File

@@ -57,7 +57,13 @@ class Meeting(models.Model):
if google_event.is_cancelled():
return {'active': False}
alarm_commands = self._flectra_reminders_commands(google_event.reminders.get('overrides') or default_reminders)
# default_reminders is never () it is set to google's default reminder (30 min before)
# we need to check 'useDefault' for the event to determine if we have to use google's
# default reminder or not
reminder_command = google_event.reminders.get('overrides')
if not reminder_command:
reminder_command = google_event.reminders.get('useDefault') and default_reminders or ()
alarm_commands = self._flectra_reminders_commands(reminder_command)
attendee_commands, partner_commands = self._flectra_attendee_commands(google_event)
values = {
'name': google_event.summary or _("(No title)"),

View File

@@ -17,10 +17,10 @@ class GoogleGmailMixin(models.AbstractModel):
_SERVICE_SCOPE = 'https://mail.google.com/'
use_google_gmail_service = fields.Boolean('Gmail Authentication')
google_gmail_authorization_code = fields.Char(string='Authorization Code', groups='base.group_system')
google_gmail_refresh_token = fields.Char(string='Refresh Token', groups='base.group_system')
google_gmail_access_token = fields.Char(string='Access Token', groups='base.group_system')
google_gmail_access_token_expiration = fields.Integer(string='Access Token Expiration Timestamp', groups='base.group_system')
google_gmail_authorization_code = fields.Char(string='Authorization Code', groups='base.group_system', copy=False)
google_gmail_refresh_token = fields.Char(string='Refresh Token', groups='base.group_system', copy=False)
google_gmail_access_token = fields.Char(string='Access Token', groups='base.group_system', copy=False)
google_gmail_access_token_expiration = fields.Integer(string='Access Token Expiration Timestamp', groups='base.group_system', copy=False)
google_gmail_uri = fields.Char(compute='_compute_gmail_uri', string='URI', help='The URL to generate the authorization code from Google', groups='base.group_system')
@api.depends('google_gmail_authorization_code')

View File

@@ -27,7 +27,6 @@ class IrMailServer(models.Model):
self.smtp_encryption = 'starttls'
self.smtp_port = 587
else:
self.smtp_encryption = 'none'
self.google_gmail_authorization_code = False
self.google_gmail_refresh_token = False
self.google_gmail_access_token = False

View File

@@ -96,18 +96,12 @@ class AccountFrFec(models.TransientModel):
"""
dom_tom_group = self.env.ref('l10n_fr.dom-tom')
is_dom_tom = company.country_id.code in dom_tom_group.country_ids.mapped('code')
if is_dom_tom:
if not company.vat or is_dom_tom:
return ''
elif company.country_id.code == 'FR':
if not company.vat:
raise UserError(_("Missing VAT number for company %s") % company.display_name)
elif len(company.vat) < 13 or not siren.is_valid(company.vat[4:13]):
raise UserError(_("Invalid VAT number for company %s") % company.display_name)
else:
return company.vat[4:13]
elif company.country_id.code == 'FR' and len(company.vat) >= 13 and siren.is_valid(company.vat[4:13]):
return company.vat[4:13]
else:
return '' if not company.vat else company.vat
return company.vat
def generate_fec(self):
self.ensure_one()

View File

@@ -1329,6 +1329,7 @@ class MrpProduction(models.Model):
def action_cancel(self):
""" Cancels production order, unfinished stock moves and set procurement
orders in exception """
self.workorder_ids.filtered(lambda x: x.state not in ['done', 'cancel']).action_cancel()
if not self.move_raw_ids:
self.state = 'cancel'
return True
@@ -1352,7 +1353,6 @@ class MrpProduction(models.Model):
if finish_moves:
production._log_downside_manufactured_quantity({finish_move: (production.product_uom_qty, 0.0) for finish_move in finish_moves}, cancel=True)
self.workorder_ids.filtered(lambda x: x.state not in ['done', 'cancel']).action_cancel()
finish_moves = self.move_finished_ids.filtered(lambda x: x.state not in ('done', 'cancel'))
raw_moves = self.move_raw_ids.filtered(lambda x: x.state not in ('done', 'cancel'))

View File

@@ -101,3 +101,39 @@ class TestMrpCancelMO(TestMrpCommon):
self.assertEqual(manufacturing_order.exists().state, 'progress')
with self.assertRaises(UserError):
manufacturing_order.unlink()
def test_cancel_mo_with_workorder(self):
"""
Create a manufacturing order without component and with a work order
and check that when you cancel the MO, the WO is also canceled.
"""
bom = self.env['mrp.bom'].create({
'product_id': self.product_2.id,
'product_tmpl_id': self.product_2.product_tmpl_id.id,
'product_uom_id': self.product_2.uom_id.id,
'consumption': 'flexible',
'product_qty': 1.0,
'operation_ids': [
(0, 0, {'name': 'test_wo', 'workcenter_id': self.workcenter_1.id, 'time_cycle': 15, 'sequence': 1}),
],
'type': 'normal',
'sequence': 2,
'bom_line_ids': []
})
# Create MO
production_form = Form(self.env['mrp.production'])
production_form.product_id = self.product_2
production_form.bom_id = bom
manufacturing_order = production_form.save()
# Check that there is no component
self.assertFalse(manufacturing_order.move_raw_ids.id)
# Cancel the MO
manufacturing_order.action_cancel()
# Check that MO and WO are canceled
self.assertEqual(manufacturing_order.state, 'cancel', "MO should be in cancel state.")
self.assertEqual(manufacturing_order.workorder_ids.state, 'cancel', 'MO work orders must be cancelled as well.')

View File

@@ -79,15 +79,15 @@
attrs="{'invisible': [('production_state','=', 'draft')], 'readonly': [('is_user_working', '=', True)]}"/>
<field name="state" widget="badge" decoration-success="state == 'done'" decoration-info="state not in ('done', 'cancel')" attrs="{'invisible': [('production_state', 'in', ('draft', 'done'))]}"/>
<button name="button_start" type="object" string="Start" class="btn-success"
attrs="{'invisible': ['|', '|', '|', ('production_state','in', ('draft', 'done')), ('working_state', '=', 'blocked'), ('state', '=', 'done'), ('is_user_working', '!=', False)]}"/>
attrs="{'invisible': ['|', '|', '|', ('production_state','in', ('draft', 'done', 'cancel')), ('working_state', '=', 'blocked'), ('state', '=', 'done'), ('is_user_working', '!=', False)]}"/>
<button name="button_pending" type="object" string="Pause" class="btn-warning"
attrs="{'invisible': ['|', '|', ('production_state', 'in', ('draft', 'done')), ('working_state', '=', 'blocked'), ('is_user_working', '=', False)]}"/>
attrs="{'invisible': ['|', '|', ('production_state', 'in', ('draft', 'done', 'cancel')), ('working_state', '=', 'blocked'), ('is_user_working', '=', False)]}"/>
<button name="button_finish" type="object" string="Done" class="btn-success"
attrs="{'invisible': ['|', '|', ('production_state', 'in', ('draft', 'done')), ('working_state', '=', 'blocked'), ('is_user_working', '=', False)]}"/>
attrs="{'invisible': ['|', '|', ('production_state', 'in', ('draft', 'done', 'cancel')), ('working_state', '=', 'blocked'), ('is_user_working', '=', False)]}"/>
<button name="%(mrp.act_mrp_block_workcenter_wo)d" type="action" string="Block" context="{'default_workcenter_id': workcenter_id}" class="btn-danger"
attrs="{'invisible': ['|', ('production_state', 'in', ('draft', 'done')), ('working_state', '=', 'blocked')]}"/>
attrs="{'invisible': ['|', ('production_state', 'in', ('draft', 'done', 'cancel')), ('working_state', '=', 'blocked')]}"/>
<button name="button_unblock" type="object" string="Unblock" context="{'default_workcenter_id': workcenter_id}" class="btn-danger"
attrs="{'invisible': ['|', ('production_state', 'in', ('draft', 'done')), ('working_state', '!=', 'blocked')]}"/>
attrs="{'invisible': ['|', ('production_state', 'in', ('draft', 'done', 'cancel')), ('working_state', '!=', 'blocked')]}"/>
<button name="action_open_wizard" type="object" icon="fa-external-link" class="oe_edit_only"
context="{'default_workcenter_id': workcenter_id}"/>
<field name="show_json_popover" invisible="1"/>

View File

@@ -5,7 +5,7 @@
<field name="model">product.template</field>
<field name="priority">3</field>
<field name="inherit_id" ref="product.product_template_only_form_view" />
<field name="groups_id" eval="[(4, ref('mrp.group_mrp_user'))]"/>
<field name="groups_id" eval="[(4, ref('mrp.group_mrp_manager'))]"/>
<field name="arch" type="xml">
<xpath expr="//div[@name='standard_price_uom']" position="inside">
<field name="cost_method" invisible="1"/>
@@ -24,7 +24,7 @@
<field name="model">product.product</field>
<field name="priority">4</field>
<field name="inherit_id" ref="product.product_normal_form_view"/>
<field name="groups_id" eval="[(4, ref('mrp.group_mrp_user'))]"/>
<field name="groups_id" eval="[(4, ref('mrp.group_mrp_manager'))]"/>
<field name="arch" type="xml">
<xpath expr="//div[@name='standard_price_uom']" position="inside">
<field name="cost_method" invisible="1"/>
@@ -43,6 +43,7 @@
<field name="name">product.product.product.view.form.easy.bom.inherit</field>
<field name="model">product.product</field>
<field name="inherit_id" ref="product.product_variant_easy_edit_view"/>
<field name="groups_id" eval="[(4, ref('mrp.group_mrp_manager'))]"/>
<field name="arch" type="xml">
<data>
<xpath expr="//field[@name='standard_price']" position="replace">

View File

@@ -12,9 +12,9 @@ class StockLandedCost(models.Model):
], ondelete={'manufacturing': 'set default'})
mrp_production_ids = fields.Many2many(
'mrp.production', string='Manufacturing order',
copy=False, states={'done': [('readonly', True)]}, groups='mrp.group_mrp_user')
copy=False, states={'done': [('readonly', True)]}, groups='stock.group_stock_manager')
allowed_mrp_production_ids = fields.Many2many(
'mrp.production', compute='_compute_allowed_mrp_production_ids', groups='mrp.group_mrp_user')
'mrp.production', compute='_compute_allowed_mrp_production_ids', groups='stock.group_stock_manager')
@api.depends('company_id')
def _compute_allowed_mrp_production_ids(self):

View File

@@ -139,3 +139,43 @@ class TestStockLandedCostsMrp(ValuationReconciliationTestCommon):
self.assertEqual(len(landed_cost.stock_valuation_layer_ids), 1)
self.assertEqual(landed_cost.stock_valuation_layer_ids.product_id, self.product_refrigerator)
self.assertEqual(landed_cost.stock_valuation_layer_ids.value, 5.0)
def test_landed_cost_on_mrp_02(self):
"""
Test that a user who has manager access to stock can create and validate a landed cost linked
to a Manufacturing order without the need for MRP access
"""
# Create a user with only manager access to stock
stock_manager = self.env['res.users'].with_context({'no_reset_password': True}).create({
'name': "Stock Manager",
'login': "test",
'email': "test@test.com",
'groups_id': [(6, 0, [self.env.ref('stock.group_stock_manager').id])]
})
# Make some stock and reserve
self.env['stock.quant']._update_available_quantity(self.product_component1, self.warehouse_1.lot_stock_id, 10)
self.env['stock.quant']._update_available_quantity(self.product_component2, self.warehouse_1.lot_stock_id, 10)
# Create and confirm a MO with a user who has access to MRP
man_order_form = Form(self.env['mrp.production'].with_user(self.allow_user))
man_order_form.product_id = self.product_refrigerator
man_order_form.bom_id = self.bom_refri
man_order_form.product_qty = 1.0
man_order = man_order_form.save()
man_order.action_confirm()
# produce product
man_order_form.qty_producing = 1
man_order_form.save()
man_order.button_mark_done()
# Create the landed cost with the stock_manager user
landed_cost = Form(self.env['stock.landed.cost'].with_user(stock_manager)).save()
landed_cost.target_model = 'manufacturing'
# Check that the MO can be selected by the stock_manger user
self.assertTrue(man_order.id in landed_cost.allowed_mrp_production_ids.ids)
landed_cost.mrp_production_ids = [(6, 0, [man_order.id])]
# Check that he can validate the landed cost without an access error
landed_cost.with_user(stock_manager).button_validate()
self.assertEqual(landed_cost.state, 'done')

View File

@@ -7,7 +7,7 @@
<field name="arch" type="xml">
<field name="target_model" position="attributes">
<attribute name="invisible">0</attribute>
<attribute name="groups">mrp.group_mrp_user</attribute>
<attribute name="groups">stock.group_stock_manager</attribute>
</field>
<field name="picking_ids" position="after">
<field name="allowed_mrp_production_ids" invisible="1"/>

View File

@@ -1,7 +1,5 @@
# -*- coding: utf-8 -*-
# -*- coding: utf-8 -*-
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
from . import test_purchase_subcontracting
from . import test_sale_dropshipping

View File

@@ -0,0 +1,252 @@
# -*- coding: utf-8 -*-
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
from flectra.tests import Form
from flectra.addons.mrp_subcontracting.tests.common import TestMrpSubcontractingCommon
class TestSaleDropshippingFlows(TestMrpSubcontractingCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.supplier = cls.env["res.partner"].create({"name": "Supplier"})
cls.customer = cls.env["res.partner"].create({"name": "Customer"})
cls.dropship_route = cls.env.ref('stock_dropshipping.route_drop_shipping')
def test_dropship_with_different_suppliers(self):
"""
Suppose a kit with 3 components supplied by 3 vendors
When dropshipping this kit, if 2 components are delivered and if the last
picking is cancelled, we should consider the kit as fully delivered.
"""
partners = self.env['res.partner'].create([{'name': 'Vendor %s' % i} for i in range(4)])
compo01, compo02, compo03, kit = self.env['product.product'].create([{
'name': name,
'type': 'consu',
'route_ids': [(6, 0, [self.dropship_route.id])],
'seller_ids': [(0, 0, {'name': seller.id})],
} for name, seller in zip(['Compo01', 'Compo02', 'Compo03', 'Kit'], partners)])
self.env['mrp.bom'].create({
'product_tmpl_id': kit.product_tmpl_id.id,
'product_qty': 1,
'type': 'phantom',
'bom_line_ids': [
(0, 0, {'product_id': compo01.id, 'product_qty': 1}),
(0, 0, {'product_id': compo02.id, 'product_qty': 1}),
(0, 0, {'product_id': compo03.id, 'product_qty': 1}),
],
})
sale_order = self.env['sale.order'].create({
'partner_id': self.customer.id,
'picking_policy': 'direct',
'order_line': [
(0, 0, {'name': kit.name, 'product_id': kit.id, 'product_uom_qty': 1}),
],
})
sale_order.action_confirm()
self.assertEqual(sale_order.order_line.qty_delivered, 0)
purchase_orders = self.env['purchase.order'].search([('partner_id', 'in', partners.ids)])
purchase_orders.button_confirm()
self.assertEqual(sale_order.order_line.qty_delivered, 0)
# Deliver the first one
picking = sale_order.picking_ids.filtered(lambda p: p.partner_id == partners[0])
action = picking.button_validate()
wizard = Form(self.env[action['res_model']].with_context(action['context'])).save()
wizard.process()
self.assertEqual(sale_order.order_line.qty_delivered, 0)
# Deliver the third one
picking = sale_order.picking_ids.filtered(lambda p: p.partner_id == partners[2])
action = picking.button_validate()
wizard = Form(self.env[action['res_model']].with_context(action['context'])).save()
wizard.process()
self.assertEqual(sale_order.order_line.qty_delivered, 0)
# Cancel the second one
sale_order.picking_ids[1].action_cancel()
self.assertEqual(sale_order.order_line.qty_delivered, 1)
def test_return_kit_and_delivered_qty(self):
"""
Sell a kit thanks to the dropshipping route, return it then deliver it again
The delivered quantity should be correctly computed
"""
compo, kit = self.env['product.product'].create([{
'name': n,
'type': 'consu',
'route_ids': [(6, 0, [self.dropship_route.id])],
'seller_ids': [(0, 0, {'name': self.supplier.id})],
} for n in ['Compo', 'Kit']])
self.env['mrp.bom'].create({
'product_tmpl_id': kit.product_tmpl_id.id,
'product_qty': 1,
'type': 'phantom',
'bom_line_ids': [
(0, 0, {'product_id': compo.id, 'product_qty': 1}),
],
})
sale_order = self.env['sale.order'].create({
'partner_id': self.customer.id,
'picking_policy': 'direct',
'order_line': [
(0, 0, {'name': kit.name, 'product_id': kit.id, 'product_uom_qty': 1}),
],
})
sale_order.action_confirm()
self.env['purchase.order'].search([], order='id desc', limit=1).button_confirm()
self.assertEqual(sale_order.order_line.qty_delivered, 0.0)
picking = sale_order.picking_ids
action = picking.button_validate()
wizard = Form(self.env[action['res_model']].with_context(action['context'])).save()
wizard.process()
self.assertEqual(sale_order.order_line.qty_delivered, 1.0)
for case in ['return', 'deliver again']:
delivered_before_case = 1.0 if case == 'return' else 0.0
delivered_after_case = 0.0 if case == 'return' else 1.0
return_form = Form(self.env['stock.return.picking'].with_context(active_ids=[picking.id], active_id=picking.id, active_model='stock.picking'))
return_wizard = return_form.save()
action = return_wizard.create_returns()
picking = self.env['stock.picking'].browse(action['res_id'])
self.assertEqual(sale_order.order_line.qty_delivered, delivered_before_case, "Incorrect delivered qty for case '%s'" % case)
action = picking.button_validate()
wizard = Form(self.env[action['res_model']].with_context(action['context'])).save()
wizard.process()
self.assertEqual(sale_order.order_line.qty_delivered, delivered_after_case, "Incorrect delivered qty for case '%s'" % case)
def test_partial_return_kit_and_delivered_qty(self):
"""
Suppose a kit with 4x the same dropshipped component
Suppose a complex delivery process:
- Deliver 2 (with backorder)
- Return 2
- Deliver 1 (with backorder)
- Deliver 1 (process "done")
- Deliver 1 (from the return)
- Deliver 1 (from the return)
The test checks the all-or-nothing policy of the delivered quantity
This quantity should be 1.0 after the last delivery
"""
compo, kit = self.env['product.product'].create([{
'name': n,
'type': 'consu',
'route_ids': [(6, 0, [self.dropship_route.id])],
'seller_ids': [(0, 0, {'name': self.supplier.id})],
} for n in ['Compo', 'Kit']])
self.env['mrp.bom'].create({
'product_tmpl_id': kit.product_tmpl_id.id,
'product_qty': 1,
'type': 'phantom',
'bom_line_ids': [
(0, 0, {'product_id': compo.id, 'product_qty': 4}),
],
})
sale_order = self.env['sale.order'].create({
'partner_id': self.customer.id,
'picking_policy': 'direct',
'order_line': [
(0, 0, {'name': kit.name, 'product_id': kit.id, 'product_uom_qty': 1}),
],
})
sale_order.action_confirm()
self.env['purchase.order'].search([], order='id desc', limit=1).button_confirm()
self.assertEqual(sale_order.order_line.qty_delivered, 0.0, "Delivered components: 0/4")
picking01 = sale_order.picking_ids
picking01.move_lines.quantity_done = 2
action = picking01.button_validate()
wizard = Form(self.env[action['res_model']].with_context(action['context'])).save()
wizard.process()
self.assertEqual(sale_order.order_line.qty_delivered, 0.0, "Delivered components: 2/4")
# Create a return of picking01 (with both components)
return_form = Form(self.env['stock.return.picking'].with_context(active_id=picking01.id, active_model='stock.picking'))
wizard = return_form.save()
wizard.product_return_moves.write({'quantity': 2.0})
res = wizard.create_returns()
return01 = self.env['stock.picking'].browse(res['res_id'])
return01.move_lines.quantity_done = 2
return01.button_validate()
self.assertEqual(sale_order.order_line.qty_delivered, 0.0, "Delivered components: 0/4")
picking02 = picking01.backorder_ids
picking02.move_lines.quantity_done = 1
action = picking02.button_validate()
wizard = Form(self.env[action['res_model']].with_context(action['context'])).save()
wizard.process()
self.assertEqual(sale_order.order_line.qty_delivered, 0.0, "Delivered components: 1/4")
picking03 = picking02.backorder_ids
picking03.move_lines.quantity_done = 1
picking03.button_validate()
self.assertEqual(sale_order.order_line.qty_delivered, 0.0, "Delivered components: 2/4")
# Create a return of return01 (with 1 component)
return_form = Form(self.env['stock.return.picking'].with_context(active_id=return01.id, active_model='stock.picking'))
wizard = return_form.save()
wizard.product_return_moves.write({'quantity': 1.0})
res = wizard.create_returns()
picking04 = self.env['stock.picking'].browse(res['res_id'])
picking04.move_lines.quantity_done = 1
picking04.button_validate()
self.assertEqual(sale_order.order_line.qty_delivered, 0.0, "Delivered components: 3/4")
# Create a second return of return01 (with 1 component, the last one)
return_form = Form(self.env['stock.return.picking'].with_context(active_id=return01.id, active_model='stock.picking'))
wizard = return_form.save()
wizard.product_return_moves.write({'quantity': 1.0})
res = wizard.create_returns()
picking04 = self.env['stock.picking'].browse(res['res_id'])
picking04.move_lines.quantity_done = 1
picking04.button_validate()
self.assertEqual(sale_order.order_line.qty_delivered, 1, "Delivered components: 4/4")
def test_cancelled_picking_and_delivered_qty(self):
"""
The delivered quantity should be zero if all SM are cancelled
"""
compo, kit = self.env['product.product'].create([{
'name': n,
'type': 'consu',
'route_ids': [(6, 0, [self.dropship_route.id])],
'seller_ids': [(0, 0, {'name': self.supplier.id})],
} for n in ['Compo', 'Kit']])
self.env['mrp.bom'].create({
'product_tmpl_id': kit.product_tmpl_id.id,
'product_qty': 1,
'type': 'phantom',
'bom_line_ids': [
(0, 0, {'product_id': compo.id, 'product_qty': 1}),
],
})
sale_order = self.env['sale.order'].create({
'partner_id': self.customer.id,
'picking_policy': 'direct',
'order_line': [
(0, 0, {'name': kit.name, 'product_id': kit.id, 'product_uom_qty': 1}),
],
})
sale_order.action_confirm()
self.env['purchase.order'].search([], order='id desc', limit=1).button_confirm()
self.assertEqual(sale_order.order_line.qty_delivered, 0.0)
sale_order.picking_ids.action_cancel()
self.assertEqual(sale_order.order_line.qty_delivered, 0.0)

View File

@@ -209,8 +209,9 @@ class SaleOrder(models.Model):
currency_id = fields.Many2one(related='pricelist_id.currency_id', depends=["pricelist_id"], store=True)
analytic_account_id = fields.Many2one(
'account.analytic.account', 'Analytic Account',
readonly=True, copy=False, check_company=True, # Unrequired company
states={'draft': [('readonly', False)], 'sent': [('readonly', False)]},
compute='_compute_analytic_account_id', store=True,
readonly=False, copy=False, check_company=True, # Unrequired company
states={'sale': [('readonly', True)], 'done': [('readonly', True)], 'cancel': [('readonly', True)]},
domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]",
help="The analytic account related to a sales order.")
@@ -325,6 +326,18 @@ class SaleOrder(models.Model):
else:
order.expected_date = False
@api.depends('partner_id', 'date_order')
def _compute_analytic_account_id(self):
for order in self:
if not order.analytic_account_id:
default_analytic_account = order.env['account.analytic.default'].sudo().account_get(
partner_id=order.partner_id.id,
user_id=order.env.uid,
date=order.date_order,
company_id=order.company_id.id,
)
order.analytic_account_id = default_analytic_account.analytic_id
@api.onchange('expected_date')
def _onchange_commitment_date(self):
self.commitment_date = self.expected_date
@@ -1417,6 +1430,7 @@ class SaleOrderLine(models.Model):
order_partner_id = fields.Many2one(related='order_id.partner_id', store=True, string='Customer', readonly=False)
analytic_tag_ids = fields.Many2many(
'account.analytic.tag', string='Analytic Tags',
compute='_compute_analytic_tag_ids', store=True, readonly=False,
domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]")
analytic_line_ids = fields.One2many('account.analytic.line', 'so_line', string="Analytic lines")
is_expense = fields.Boolean('Is expense', help="Is true if the sales order line comes from an expense or a vendor bills")
@@ -1593,6 +1607,19 @@ class SaleOrderLine(models.Model):
line.untaxed_amount_to_invoice = amount_to_invoice
@api.depends('product_id', 'order_id.date_order', 'order_id.partner_id')
def _compute_analytic_tag_ids(self):
for line in self:
if not line.analytic_tag_ids:
default_analytic_account = line.env['account.analytic.default'].sudo().account_get(
product_id=line.product_id.id,
partner_id=line.order_id.partner_id.id,
user_id=self.env.uid,
date=line.order_id.date_order,
company_id=line.company_id.id,
)
line.analytic_tag_ids = default_analytic_account.analytic_tag_ids
def _get_invoice_line_sequence(self, new=0, old=0):
"""
Method intended to be overridden in third-party module if we want to prevent the resequencing

View File

@@ -249,6 +249,7 @@ class SaleOrderOption(models.Model):
product = self.product_id.with_context(
lang=self.order_id.partner_id.lang,
)
self.uom_id = self.uom_id or product.uom_id
self.name = product.get_product_multiline_description_sale()
self._update_price_and_discount()

View File

@@ -2,7 +2,7 @@
# Part of Odoo, Flectra. See LICENSE file for full copyright and licensing details.
from flectra.addons.sale.tests.common import TestSaleCommon
from flectra.tests import tagged
from flectra.tests import Form, tagged
@tagged('-at_install', 'post_install')
@@ -341,3 +341,10 @@ class TestSaleOrder(TestSaleCommon):
self.pl_option_discount,
"If a pricelist is set without discount included,"
" the discount should be correctly computed.")
def test_option_creation(self):
"""Make sure the product uom is automatically added to the option when the product is specified"""
order_form = Form(self.sale_order)
with order_form.sale_order_option_ids.new() as option:
option.product_id = self.product_1
self.assertTrue(bool(option.uom_id))

View File

@@ -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 float_compare
class SaleOrder(models.Model):
@@ -89,17 +90,24 @@ class SaleOrderLine(models.Model):
(b.product_tmpl_id == order_line.product_id.product_tmpl_id and not b.product_id)))
if relevant_bom:
# In case of dropship, we use a 'all or nothing' policy since 'bom_line_id' was
# not written on a move coming from a PO.
# not written on a move coming from a PO: all moves (to customer) must be done
# and the returns must be delivered back to the customer
# FIXME: if the components of a kit have different suppliers, multiple PO
# are generated. If one PO is confirmed and all the others are in draft, receiving
# the products for this PO will set the qty_delivered. We might need to check the
# state of all PO as well... but sale_mrp doesn't depend on purchase.
if dropship:
moves = order_line.move_ids.filtered(lambda m: m.state != 'cancel')
if moves and all(m.state == 'done' for m in moves):
order_line.qty_delivered = order_line.product_uom_qty
if any((m.location_dest_id.usage == 'customer' and m.state != 'done')
or (m.location_dest_id.usage != 'customer'
and m.state == 'done'
and float_compare(m.quantity_done,
sum(sub_m.product_uom._compute_quantity(sub_m.quantity_done, m.product_uom) for sub_m in m.returned_move_ids if sub_m.state == 'done'),
precision_rounding=m.product_uom.rounding) > 0)
for m in moves) or not moves:
order_line.qty_delivered = 0
else:
order_line.qty_delivered = 0.0
order_line.qty_delivered = order_line.product_uom_qty
continue
moves = order_line.move_ids.filtered(lambda m: m.state == 'done' and not m.scrapped)
filters = {

View File

@@ -433,7 +433,7 @@ class Project(models.Model):
action_data = _to_action_data('project.project', res_id=self.id,
views=[[self.env.ref('project.edit_project').id, 'form']])
else:
action_data = _to_action_data(action=self.env.ref('project.open_view_project_all_config').sudo(),
action_data = _to_action_data(action=self.env.ref('project.open_view_project_all_config'),
domain=[('id', 'in', self.ids)])
stat_buttons.append({
@@ -463,7 +463,7 @@ class Project(models.Model):
'count': sum(self.mapped('task_count')),
'icon': 'fa fa-tasks',
'action': _to_action_data(
action=self.env.ref('project.action_view_task').sudo(),
action=self.env.ref('project.action_view_task'),
domain=tasks_domain,
context=tasks_context
)
@@ -473,7 +473,7 @@ class Project(models.Model):
'count': self.env['project.task'].search_count(late_tasks_domain),
'icon': 'fa fa-tasks',
'action': _to_action_data(
action=self.env.ref('project.action_view_task').sudo(),
action=self.env.ref('project.action_view_task'),
domain=late_tasks_domain,
context=tasks_context,
),
@@ -483,7 +483,7 @@ class Project(models.Model):
'count': self.env['project.task'].search_count(overtime_tasks_domain),
'icon': 'fa fa-tasks',
'action': _to_action_data(
action=self.env.ref('project.action_view_task').sudo(),
action=self.env.ref('project.action_view_task'),
domain=overtime_tasks_domain,
context=tasks_context,
),
@@ -503,7 +503,7 @@ class Project(models.Model):
'count': len(sale_orders),
'icon': 'fa fa-dollar',
'action': _to_action_data(
action=self.env.ref('sale.action_orders').sudo(),
action=self.env.ref('sale.action_orders'),
domain=[('id', 'in', sale_orders.ids)],
context={'create': False, 'edit': False, 'delete': False}
)
@@ -520,7 +520,7 @@ class Project(models.Model):
'count': len(invoice_ids),
'icon': 'fa fa-pencil-square-o',
'action': _to_action_data(
action=self.env.ref('account.action_move_out_invoice_type').sudo(),
action=self.env.ref('account.action_move_out_invoice_type'),
domain=[('id', 'in', invoice_ids), ('move_type', '=', 'out_invoice')],
context={'create': False, 'delete': False}
)
@@ -551,7 +551,12 @@ def _to_action_data(model=None, *, action=None, views=None, res_id=None, domain=
# pass in either action or (model, views)
if action:
assert model is None and views is None
act = clean_action(action.read()[0], env=action.env)
act = {
field: value
for field, value in action.sudo().read()[0].items()
if field in action._get_readable_fields()
}
act = clean_action(act, env=action.env)
model = act['res_model']
views = act['views']
# FIXME: search-view-id, possibly help?

View File

@@ -922,6 +922,7 @@ var FieldDate = InputField.extend({
/**
* Confirm the value on hit enter and re-render
* It will also remove the offset to get the UTC value
*
* @private
* @override
@@ -930,7 +931,12 @@ var FieldDate = InputField.extend({
async _onKeydown(ev) {
this._super(...arguments);
if (ev.which === $.ui.keyCode.ENTER) {
await this._setValue(this.$input.val());
let value = this.$input.val();
try {
value = this._parseValue(value);
value.add(-this.getSession().getTZOffset(value), "minutes");
} catch (err) {}
await this._setValue(value);
this._render();
}
},

View File

@@ -4702,6 +4702,58 @@ QUnit.module('basic_fields', {
form.destroy();
});
QUnit.test('datetime field: hit enter should update value', async function (assert) {
/*
This test verifies that the field datetime is correctly computed when:
- we press enter to validate our entry
- we click outside the field to validate our entry
- we save
*/
assert.expect(3);
const form = await createView({
View: FormView,
model: 'partner',
data: this.data,
arch:'<form string="Partners"><field name="datetime"/></form>',
res_id: 1,
translateParameters: { // Avoid issues due to localization formats
date_format: '%m/%d/%Y',
time_format: '%H:%M:%S',
},
viewOptions: {
mode: 'edit',
},
session: {
getTZOffset: function () {
return 120;
},
},
});
const datetime = form.el.querySelector('input[name="datetime"]');
// Enter a beginning of date and press enter to validate
await testUtils.fields.editInput(datetime, '01/08/22 14:30:40');
await testUtils.fields.triggerKeydown(datetime, 'enter');
const datetimeValue = `01/08/2022 14:30:40`;
assert.strictEqual(datetime.value, datetimeValue);
// Click outside the field to check that the field is not changed
await testUtils.dom.click(form.$el);
assert.strictEqual(datetime.value, datetimeValue);
// Save and check that it's still ok
await testUtils.form.clickSave(form);
const { textContent } = form.el.querySelector('span[name="datetime"]')
assert.strictEqual(textContent, datetimeValue);
form.destroy();
});
QUnit.module('RemainingDays');
QUnit.test('remaining_days widget on a date field in list view', async function (assert) {

View File

@@ -20,7 +20,7 @@ class Lead(models.Model):
JOIN crm_lead_website_visitor_rel lv ON l.id = lv.crm_lead_id
JOIN website_visitor v ON v.id = lv.website_visitor_id
JOIN website_track p ON p.visitor_id = v.id
WHERE l.id in %s
WHERE l.id in %s AND v.active = TRUE
GROUP BY l.id"""
self.env.cr.execute(sql, (tuple(self.ids),))
page_data = self.env.cr.dictfetchall()