mirror of
https://gitlab.com/flectra-hq/flectra.git
synced 2025-02-25 18:55:21 -06:00
[PATCH] Upstream patch - 31032022
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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)"),
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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'))
|
||||
|
||||
|
||||
@@ -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.')
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user